Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 13:23:37 +09:00
co-authored by Claude Fable 5.1
commit 11ae3629b2
453 changed files with 259183 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
# 수정사항 10건 검토·적용 기록 (2026-09-16)
원본: `수정사항.txt`. 이 파일은 **2026-09-16 구조 변경(로직 조각 도입) 이전** 기준으로 작성돼
일부 항목은 이미 해소된 상태였다. 항목별 판정과 적용 내용을 남긴다.
| # | 항목 | 판정 | 적용 |
|---|---|---|---|
| 1 | LLM 요약이 검색 색인에 미반영 | 부분 유효 | ✅ 적용 |
| 2 | 프로그램 단위 요약 미생성 | **이미 완료** | 코드 변경 없음 |
| 3 | 텍스트 심볼 치환 | 프롬프트 완료 / FTS 미반영 | ✅ FTS 반영 |
| 4 | 용어 사전이 질의 확장에 미사용 | 유효 | ✅ 적용 |
| 5 | keywords_en · sap_objects 필드 없음 | **이미 완료** | 코드 변경 없음 |
| 6 | 중복 프로그램 처리 없음 | 유효 | ✅ 적용 |
| 7 | 정답셋 4문항 | 유효 | ⏸ **범위 제외** (사용자 지시) |
| 8 | 엔티티·프로세스 위키 페이지 없음 | 유효 | ✅ 엔티티 적용 / 프로세스 보류 |
| 9 | unit 문서 70만 파일 문제 | **구조 변경으로 해소** | 무효 |
| 10 | 요약 병렬 실행 없음 | 유효 | ✅ 적용 |
## 이미 완료였던 항목 (2·5·9)
- **2번** — `summarize/runner.py:ensure_program_summary()``ProgramSummary` 를 만들어
`program.summary_json` 에 저장하고, `UNIT_PROMPT``program_purpose` · `main_flow` ·
`key_internal_tables` 를 문맥으로 주입한다. DB 에 요약이 0건이었던 것은 미구현이 아니라
OpenRouter 무료 티어의 **429 Too Many Requests** 로 실행이 실패해서였다 (`unit.summary_error` 6건 전부 429).
- **5번** — `schemas.LogicChunk``purpose_en` · `keywords_ko` · `keywords_en` · `sap_objects`
모두 있고 프롬프트가 명시적으로 요구한다.
- **9번** — `units/` 문서 생성은 중단됐다. 로직 조각은 프로그램 문서 안의 H3/H4 섹션이다.
원본 파일의 "units/ 를 git 제외로 둔 것은 좋은 완화책" 서술은 현재 상태와 맞지 않는다
(`.gitignore``units` 항목이 없고 디렉토리도 없다).
## 적용 내용
### 1번 — 요약을 검색 색인에 반영
구조 변경으로 `chunk_fts` 가 신설돼 조각의 `purpose`/`keywords`/`objects` 는 이미 색인되고 있었고,
`extract_unit` 도 요약 후 `unit_fts` 를 갱신하고 있었다. 남아 있던 두 구멍만 막았다.
- **적재 시점 `unit_fts.purpose` 의 장식 문자** — `index/loader.py``header_comment` 를 날것으로
넣어 1,177행 중 **867행이 `'----------------'`** 이었다. `clean_comment()` 를 적용해 0행으로.
- `clean_comment``query/tools.py``index/db.py` 로 이동 (loader 가 써야 하는데
`index → query` 는 의존 역방향). `query.tools.clean_comment` 이름은 하위호환으로 유지.
- **`program_fts` 가 타이틀·패키지명뿐** — `business_purpose_ko` / `main_flow` / `keywords_ko` /
`keywords_en` / `business_tags` / `sap_module` / `related_tcodes` / 조각 키워드 / 조각 SAP 객체 /
테이블명 / 텍스트 심볼을 색인에 넣었다.
- **색인을 두 테이블로 나눴다**: `program_fts`(이름·타이틀·패키지) + `program_desc_fts`(서술).
한 행에 몰아넣었더니 회귀가 났다 — bm25 는 **행 전체 길이로 정규화**하므로 요약이 붙어
길어진 행이 타이틀만 있는 짧은 행보다 낮게 나온다. 실측으로 타이틀이 정확히 `"총계정원장 조회"`
ZFIR10070 이 `"총계정원장 조회하는 프로그램"` 질의에서 **1위 → 33위**로 밀렸다
(행 길이 1,872자 vs 40자). 컬럼 가중치로는 해결되지 않는다(정규화는 행 단위).
나누면 각 테이블 안 행 길이가 비슷해지고, 서술 히트는 `_DESC_WEIGHT=0.6` 으로 **더해질 뿐**
이름·타이틀 일치를 밀어내지 않는다. 회귀 테스트:
`tests/test_dedupe_parallel.py::test_long_summary_does_not_outrank_title_match`
- 세 FTS 모두 `rank`(모든 컬럼 가중치 1) 대신 **가중 bm25**(`index/db.py:bm25_rank`)를 쓴다.
- FTS5 는 컬럼 추가가 불가하므로 `index/db.py:_migrate_fts()` 가 컬럼 구성 변화를 감지해
DROP→재생성한다 (전부 파생 데이터라 안전).
- `refresh_program_fts(con, program)` 단건 경로를 추가했다. 전량 재구축을 `/ingest` 마다 돌리면
1만 본에서 O(N²) 가 된다.
- 검색 결과의 `reason` 이 근거를 구분한다: `이름/타이틀 일치` > `키워드 일치` > `요약 일치` > `동의어 확장`.
### 3번 — 텍스트 심볼을 FTS 에 반영
- `index/db.py:text_symbol_phrases()` — 코드 범위에 등장하는 `TEXT-nnn` 을 한국어 원문으로 치환.
- 조각 저장 시 `chunk_fts.keywords` / `bigrams` 에, 프로그램 색인에는 `program_fts.purpose` 에 넣는다.
- 결과: `"계정코드를 입력하세요"`, `"회계단위 간편선택"` 같은 **화면 문구로도** 검색된다 (LLM 무관).
### 4번 — 용어 사전 질의 확장
- `config/glossary.py` — 사전 로딩을 한 곳으로 모았다. 기존에는 `wiki_out/run.py` 안에
인라인 정규식으로만 있어 검색 쪽에서 쓸 수 없었다. **양방향 동의어 맵**을 만든다
(`입고``GR/MSEG/101` 뿐 아니라 `GR``입고/MSEG` 도 성립).
- `query/expand.py`**2단 검색**. 원질의로 먼저 찾고, 결과가 `top_k` 미만일 때만 동의어 식으로
보충하며 점수에 `EXPANDED_WEIGHT=0.35` 감쇠를 걸고 `matched_by='동의어 확장'` 으로 표시한다.
- 동의어를 원질의와 같은 OR 버킷에 섞지 않은 이유: `fts_or` 는 토큰과 한글 2-gram 을 전부 OR 로
이어 이미 재현율 편향이다. 여기에 동의어까지 같은 가중치로 넣으면 `"총계정원장"` 질의가
`"원장"` 2-gram 하나로 걸린 문서와 동일 점수가 된다.
- `config/domain_glossary.yaml` 에 MM 섹션(입고·출고·자재·구매오더·이동유형 …)을 시드했다.
기존 사전이 FI 전용이라 원본 파일이 예로 든 "입고 → GR/101" 이 실제로는 동작하지 않았다.
### 6번 — 중복 unit 조각 복제
- `summarize/runner.py:_dedupe_plan()` + `clone_unit_chunks()`.
- `code_hash` 가 같은 unit 은 **대표 1건만 LLM 에 보내고** 나머지는 조각을 복제한다.
이번 배치 밖에서 이미 추출된 동일 코드 unit 이 있으면 **LLM 호출 0회**로 복제한다.
- **줄 번호 평행이동이 핵심이다** — 코드는 같아도 include 안 위치가 달라
`target.line_start - rep.line_start` 만큼 밀어야 한다. 테스트가 복제된 줄 범위의 코드 원문이
대표와 동일한지 확인한다.
- 실측(샘플 14본): 중복 그룹 108개 / unit 287개 → **LLM 호출 179회(62%) 절감 가능**.
- `--no-dedupe` 로 끌 수 있다.
### 8번 — 엔티티 위키 페이지
- `wiki_out/entities.py``tables/` `functions/` `tcodes/``table_ref` · `call_edge` 에서
**LLM 없이 결정론적으로** 생성. 실측 산출: 테이블 146건, 펑션 94건.
- `tables/<T>.md` — 읽는 프로그램 / 쓰는 프로그램 / unit 단위 사용 지점
- `functions/<FM>.md` — 호출 프로그램 / 호출 지점 / RFC 여부
- `tcodes/``tcode` 테이블 미확보로 현재 0건 (데이터가 들어오면 그대로 생성됨)
- 프로그램당 파일이 아니라 **엔티티당 파일**이라 9번의 파일 수 폭발이 재발하지 않는다.
- `index.md` 를 계층 진입점으로 재작성 — **SAP 모듈 → 패키지 → 프로그램**.
모듈은 요약의 `sap_module`, 요약 전에는 `Z<모듈><번호>` 관행에서 추정한다.
- **프로세스 페이지는 보류.** 프로그램을 가로지르는 reduce 단계가 필요하고, 조각 데이터가
실제로 쌓인 뒤에 판단하는 게 맞다 (원본 파일도 "별도 작업"으로 인정).
### 10번 — 병렬 실행
- `llm_concurrency` 가 **선언만 있고 읽는 코드가 없는 죽은 설정**이었다. 실제로 연결했다.
- sqlite 커넥션은 스레드 간 공유가 안 되므로 3단으로 갈랐다:
`build_unit_prompts()` (DB 읽기, 메인) → `_run_calls_parallel()` (LLM 호출, 워커) →
`finish_unit()` (검증·저장, 메인).
- **동시성만 올리면 429 가 늘어난다.** `OpenAICompatClient.complete_json` 에 429/5xx 지수 백오프
재시도(`Retry-After` 존중, jitter 포함)를 같이 넣었다. 기존에는 재시도가 없어 429 한 번에
unit 이 `failed` 로 굳었다.
## 함께 고친 것 (직전 분석에서 보고한 버그)
### 파서 토큰화 — 필드심볼·테이블본문 대입
`<ls_fcat>-fieldname = 'X'``['<ls_fcat>', '-', 'fieldname', '=', ...]` 로 쪼개져
일반 대입 판정(`up[1] == "="`)이 깨졌다. 결과로 **쓰기 지점이 누락**되고 미인식 문장으로 잡혔다.
- `parser/tokenizer.py` — 필드심볼이 `-컴포넌트` / `->메서드` 까지 한 토큰. `&1` 매크로 파라미터도 토큰화.
- `parser/statements.py:assignment_eq_index()` — 대입 판정을 한 곳으로. `X = `, `X[] = `,
`X[ key = v ] = ` 형태를 모두 인식한다. `dataflow``run`(미인식 리포트)이 같은 판정을 쓴다.
- 효과 (샘플 14본 / 문장 24,578건. 베이스라인 커밋 `35a15aa` 를 worktree 로 꺼내 직접 측정):
| 지표 | 이전 | 이후 | 차이 |
|---|---|---|---|
| 미인식 문장 | 1,055건 (4.29%) | **30건 (0.12%)** | 1,025 |
| 최악 프로그램 | ZMMR71110_API **12.03%** | ZBACKUP_FI **0.46%** | |
| 쓰기 지점 총계 | 7,327 | **8,251** | **+924** |
| └ 필드심볼 대상 | 207 | **1,067** | **+860** |
| └ `itab[]` 형태 | 230 | **307** | **+77** |
`itab[] = ...``<fs>-comp = ...` 는 내부테이블을 채우는 흔한 관용구라
`trace_variable`("gt_head 가 어디서 채워지나")의 정확도에 직접 영향이 있었다.
이전에도 필드심볼 쓰기가 207건 잡혔던 것은 `READ TABLE ... ASSIGNING <fs>` /
`LOOP AT ... ASSIGNING <fs>` 경로는 별도 분기라 영향을 받지 않았기 때문이다.
### 외부 호출 목록 오염
`CL_GUI_ALV_GRID=>MC_FC_AUF` 같은 **정적 상수 읽기**가 `method_ref` 로 기록돼 호출로 취급됐다.
`query/tools.py:_structure_summary` 는 걸러냈지만 `wiki_out/okf_writer.py` 는 걸르지 않아
`x-calls` 40건이 거의 전부 ALV 상수와 `CALL SCREEN` 의 화면번호(`"100"`)였다.
- `parser/refs.py:EXTERNAL_CALL_KINDS` / `FUNCTION_CALL_KINDS` 로 기준을 한 곳에 두고
요약·위키·엔티티 페이지가 공유한다.
- ZFIR10070 의 `x-calls`: ALV 상수 40건 → **실제 FM·외부 PERFORM·SUBMIT 14건**.
### 설치 명령이 동작하지 않았음
README 가 안내하는 `pip install -e ".[dev]"` 가 실패했다 — flat-layout 자동 탐색이
`data/` `wiki/` 까지 패키지로 오인한다. `pyproject.toml``[tool.setuptools] packages` 를 명시했다.
### 로더 강제 재적재
파서·색인 로직이 바뀌면 소스가 그대로라도 산출물이 달라지는데, `source_hash` 가 같으면 스킵돼
반영할 방법이 없었다. `python -m index.loader --force` 를 추가했다.
## 7번 (정답셋)을 제외한 결과
1·4번의 효과를 숫자로 확인할 수단이 없다. 확인한 것은 **기능이 동작한다는 사실**뿐이다:
- `ACDOCT` / `FAGLL03` 로 질의 → 총계정원장 프로그램이 `동의어 확장` 으로 나온다
- `"회계 담당자가 월 단위로 확인"`(요약 본문) → 프로그램이 나온다
- `"계정코드를 입력하세요"`(텍스트 심볼) → 프로그램이 나온다
정밀도가 좋아졌는지, 아래 계수들이 적절한지, 2단 검색이 베이스라인보다 나은지는
**측정하지 않았다**. 현 정답셋 5문항으로는 개선도 회귀도 보이지 않는다.
정답셋 50~100문항이 생긴 뒤에 튜닝할 값:
| 값 | 위치 | 현재 | 뜻 |
|---|---|---|---|
| `EXPANDED_WEIGHT` | `query/expand.py` | 0.35 | 동의어 히트 점수 감쇠 |
| `MAX_SYNONYMS` | `query/expand.py` | 24 | 질의당 동의어 상한 |
| `_DESC_WEIGHT` | `query/tools.py` | 0.6 | 요약·키워드 색인 히트 가중 |
| `_BM25_WEIGHTS` | `index/db.py` | 표 참조 | FTS 컬럼별 bm25 가중치 |
| 2단 검색 보충 조건 | `query/tools.py:_fts_rows` | `len(결과) < limit` | 동의어 단계 발동 시점 |
**이번 회귀가 이 과제의 필요성을 그대로 보여준다.** `find` recall@5 가 1.0 → 0.833 으로 떨어진 것을
5문항 정답셋이 잡아냈지만, 원인 파악은 수동 디버깅으로 했다. 문항이 50개면 어떤 유형의 질의가
깨졌는지가 바로 드러난다. 위 5개 값을 감으로 정한 상태가 현재의 가장 큰 미확정 요소다.