Files
ABAP-Indexing/docs/수정사항-적용.md
T

183 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 수정사항 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개 값을 감으로 정한 상태가 현재의 가장 큰 미확정 요소다.