Skip to content

Commit 9b633a1

Browse files
SonAIengineclaude
andcommitted
docs: rewrite ROADMAP.md + add Backfill section to CONCEPTS.md
## docs/ROADMAP.md — full rewrite The old file was pinned to v0.1.0 → v0.2.0 → v0.3.0 planning from many months ago. Every task in it (PostgreSQL backend, HNSW, MCP server, Hybrid search, PPR, etc.) has been shipped multiple minor releases ago. The file was actively misleading — new contributors would read "current status: v0.1.0" and think half the features don't exist yet. New layout: - **Current status (v0.15.0)** — PyPI version, test count, tool count, plus a compact table of what each v0.14.x → v0.15.0 release shipped. - **v0.16.0 — next minor** — concrete tasks grouped into four priority buckets: - P1: flip `graph.search(engine=)` default from `"legacy"` to `"evidence"`, update tests that depended on legacy stages, bench regression check. - P2: re-measure embedder+reranker and agent baselines (still v0.13.0-era numbers in CLAUDE.md because v0.14.x search path changes broke their meaning). - P3: CDC schema drift detection — `schema_fingerprint` is already persisted but never compared; ALTER TABLE events go unnoticed. - P4: PostgreSQL backend feature parity with SQLite (HNSW persist, CDC tables, etc). - **v0.17.0** — legacy HybridSearch removal, self-calibrating cosine probe (replaces the last magic-number default), Oracle / MSSQL CDC. - **v0.18.0+ — long-term, unconfirmed** — LLM-as-Judge bench mode, CI bench regression gate, Doc2Query++, ColBERT late interaction, observability, cost tracking, multi-tenant. - **Completed (historical)** — brief pointer to the v0.1 → v0.12 history plus the v0.13 → v0.15 detail table that the old file missed entirely. - **Design principles** — 8 rules (was 6), added "LLM-free indexing" and the v0.14.x lesson "silent failure is a bug". The design-principles section now explicitly codifies the lesson the whole v0.14.x series chased: a feature that exists in code but has no wiring is a bug, not a feature. Future reviewers should treat it that way. ## docs/CONCEPTS.md — new section 11 "Backfill" Inserted between the existing CDC section (10) and the limitations section (which renumbers from 11 → 12). Covers: - **Why it exists** — the two silent-failure modes v0.14.x uncovered (MCP PhraseExtractor wiring gap, empty embeddings when embedder is added post-ingest) and why re-ingest from source is often impractical. - **Two passes** — embedding backfill (batch via `embedder.embed_batch`) and phrase-hub backfill (walk nodes with no outgoing CONTAINS, run the extractor, create hub edges). Both idempotent, best-effort, bounded by `max_nodes`. - **BackfillResult** — full dataclass reference. - **Wiring preconditions** — backfill does not fabricate missing components; user has to wire the embedder / phrase_extractor first, then call backfill. Explicitly not a magic "fix everything" button. - **MCP tool** — `knowledge_backfill(scope, batch_size, max_nodes)` with sample request/response. - **Why phrase backfill matters** — explains how the ChunkEntityIndex + PPR dead-path problem manifests and why filling the hubs flips cross-document search back on. - **Limitations** — backfill is not a CDC replacement, embedder swap requires force re-embed (future `force=True` flag mentioned), phrase-hub quality depends on extractor locale. - **Design principle** — explicit note that backfill is a *recovery* tool, not a new feature. Future silent-failure categories can land as additional passes on the same function. No code changes, no test impact. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent f5822aa commit 9b633a1

2 files changed

Lines changed: 300 additions & 95 deletions

File tree

docs/CONCEPTS.md

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,155 @@ dialect별 placeholder 차이 (`?` vs `$1` vs `%s`)는 `_translate_placeholders`
618618

619619
---
620620

621-
## 11. 한계와 향후 방향
621+
## 11. Backfill: 기존 그래프를 in-place로 복구
622+
623+
> v0.14.x 시리즈에서 발견한 "silent failure" 패턴의 회수 경로.
624+
625+
### 왜 필요한가
626+
627+
v0.14.x 시리즈는 "기능은 코드에 있는데 wiring이 빠져서 silent하게
628+
죽어 있던" 버그들을 여러 건 고쳤습니다:
629+
630+
- **v0.14.0 초기**: MCP 서버의 `_ensure_graph()``ChunkEntityIndex`
631+
wire했지만 `PhraseExtractor`를 빼먹어서 문서 간 phrase hub 다리가
632+
아예 안 만들어짐. v0.14.3에서 한 줄 fix.
633+
- **v0.14.0 전체 기간**: 사용자가 embedder 없이 인제스트 → 나중에
634+
`--embed-url`로 다시 띄워도 기존 노드는 `Node.embedding=[]` 상태
635+
그대로. HNSW 인덱스는 비어 있고 vector 검색이 부분적으로 죽음.
636+
637+
두 경우 모두 **신규 인제스트만** 고쳐졌고, 이미 들어 있는 데이터는
638+
재인제스트 말고는 복구할 방법이 없었습니다. 실전에서는 재인제스트가
639+
비싸거나 (수십만 문서) 불가능합니다 (소스 파일이 더 이상 없음). 그래서
640+
in-place 복구 도구가 필요했습니다.
641+
642+
### `graph.backfill()` — 두 가지 복구 경로
643+
644+
```python
645+
result = await graph.backfill(
646+
embeddings=True, # 빈 embedding 채우기
647+
phrases=True, # phrase hub 누락분 재생성
648+
batch_size=64,
649+
max_nodes=None, # None = 전체, int = 처음 N개만
650+
)
651+
print(result.embeddings_filled, result.phrases_linked)
652+
```
653+
654+
**Pass 1 — Embedding backfill** (`embeddings=True`):
655+
모든 노드를 walk하고 `node.embedding == []`인 것만 모아 `embedder.embed_batch()`
656+
batch_size씩 넘김. 성공한 결과를 `backend.update_node()`로 저장.
657+
이미 임베딩 있는 노드는 건너뜀 (멱등성).
658+
659+
**Pass 2 — Phrase hub backfill** (`phrases=True`):
660+
텍스트를 가진 노드 중 outgoing CONTAINS 엣지가 **하나도 없는** 노드만
661+
선별 → `phrase_extractor.extract_and_link()` 재실행 → 결과로 나온
662+
phrase hub 노드에 `CONTAINS` 엣지 생성 → `ChunkEntityIndex`에 등록.
663+
Phrase hub 노드 자신 (태그 `_phrase`)는 건너뜀 — hub of hubs 방지.
664+
665+
두 pass 모두:
666+
667+
- **Best-effort** — 단일 행 실패는 `BackfillResult.errors`에 append만
668+
되고 나머지는 계속 진행. 한 노드의 임베딩 실패가 전체를 abort하지
669+
않음.
670+
- **Idempotent** — 두 번 돌려도 두 번째는 `embeddings_filled=0`,
671+
`phrases_linked=0`. 건강한 노드는 스킵.
672+
- **Bounded**`max_nodes` 파라미터로 점진 처리 가능. 100만 노드
673+
그래프를 한 번에 처리할 수 없을 때 유용.
674+
675+
### `BackfillResult` — 투명한 리포트
676+
677+
```python
678+
@dataclass(slots=True)
679+
class BackfillResult:
680+
scanned: int = 0 # 총 inspect한 노드 수
681+
embeddings_filled: int = 0 # 새로 임베딩 채운 노드 수
682+
phrases_linked: int = 0 # 새로 만든 CONTAINS 엣지 수
683+
skipped_no_text: int = 0 # title/content 없어서 embed 불가
684+
elapsed_ms: float = 0.0 # 벽시계 시간
685+
errors: list[str] = [] # per-node 에러 메시지
686+
```
687+
688+
### Wiring 필요 조건
689+
690+
`backfill()`은 그래프가 이미 필요한 컴포넌트를 wire하고 있어야 동작:
691+
692+
- **Embedding backfill**: `SynapticGraph(embedder=...)` 필요.
693+
없으면 **no-op** (에러 아님). `graph.backfill(embeddings=True)`
694+
`scanned=0`을 반환하고 조용히 넘어감.
695+
- **Phrase backfill**: `SynapticGraph(phrase_extractor=...)` 필요.
696+
없으면 **no-op**.
697+
698+
즉 백필 도구는 "없는 의존성을 상상으로 만들어내지" 않습니다. 사용자가
699+
먼저 누락된 wiring을 고치고 (`--embed-url` 추가, 신규 그래프
700+
생성자에서 `PhraseExtractor()` 전달) 그 다음 backfill을 호출하는
701+
순서입니다.
702+
703+
### MCP tool
704+
705+
```json
706+
// MCP 도구 호출 예시
707+
{
708+
"tool": "knowledge_backfill",
709+
"scope": "all", // "all" | "embeddings" | "phrases"
710+
"batch_size": 64,
711+
"max_nodes": null // 전체 처리
712+
}
713+
```
714+
715+
응답:
716+
```json
717+
{
718+
"success": true,
719+
"scope": "all",
720+
"scanned": 19843,
721+
"embeddings_filled": 19843,
722+
"phrases_linked": 5612,
723+
"skipped_no_text": 0,
724+
"elapsed_ms": 42350.2,
725+
"errors": []
726+
}
727+
```
728+
729+
### Phrase backfill은 검색 품질을 극적으로 개선함
730+
731+
Phrase hub가 없는 그래프는 `GraphExpander``PersonalizedPageRank`
732+
cross-document 탐색이 **사실상 dead path**입니다. 문서 간 연결이
733+
없으니 PPR이 walk할 엣지가 없고, 1-hop 확장도 같은 문서 내부만
734+
맴돕니다. 결과는 "FTS over disjoint files" — 키워드 매칭이 안 되는
735+
의미 기반 질의에 0건 응답.
736+
737+
Backfill을 실행하면 `CONTAINS` 엣지가 대량 생성되면서 `ChunkEntityIndex`
738+
채워지고, 다음 검색부터 PPR이 실제 그래프를 돌기 시작합니다. 이 효과는
739+
벤치마크 수치로도 바로 드러납니다 (특히 multi-hop KRRA Hard / assort Hard).
740+
741+
### 한계
742+
743+
- **재-인제스트의 완전한 대체는 아님**. 원본 소스의 스키마가 바뀌었거나
744+
새 컬럼이 추가됐다면 backfill은 그걸 감지하지 못합니다. 그때는 CDC
745+
(`sync_from_database()`)가 올바른 도구.
746+
- **Embedder / reranker 모델을 바꾸면 재-embed 필요**. Backfill은
747+
"현재 wire된 embedder로 다시 돌려" 동작이기 때문에 임베더 전환 시
748+
`embeddings=True`로 다시 돌려야 기존 벡터가 새 모델 벡터로 교체됩니다.
749+
(단, 현재 구현은 `node.embedding is []`만 체크하므로 강제 재-embed를
750+
원하면 수동으로 비워야 함 — P3에서 `force=True` 옵션 예정.)
751+
- **Phrase hub 품질은 extractor에 의존**. Korean vs English vs mixed
752+
locale에서 extractor 선택이 중요 — `create_phrase_extractor(profile)`
753+
경로 사용 권장.
754+
755+
### 설계 원칙
756+
757+
Backfill은 **"새 기능이 아니라 복구 도구"**입니다. v0.14.x 시리즈에서
758+
발견한 교훈 — "feature가 wiring 누락으로 silent하게 죽어 있으면 안
759+
된다" — 의 후속 조치. 이상적으로는 애초에 wiring이 잘 되어 있어서
760+
backfill을 쓸 일이 없는 게 맞지만, 한번 발생한 과거를 되돌리는 경로는
761+
제공해야 사용자가 재인제스트의 비용/불가능성에 묶이지 않습니다.
762+
763+
향후 발견될 새로운 silent-failure 패턴도 같은 방식으로 backfill 도구의
764+
한 pass로 추가될 수 있습니다. 예: `fingerprint_backfill=True` (스키마
765+
fingerprint 재계산), `category_backfill=True` (카테고리 라벨 재추출) 등.
766+
767+
---
768+
769+
## 12. 한계와 향후 방향
622770

623771
### 현재 한계
624772
- **멀티홉 질의 불안정**: GPT-4o-mini 같은 작은 모델은 2-3홉부터 오판

0 commit comments

Comments
 (0)