feat(Wave 8 W8-1 #12): activate search_relations vector recall#1767
Conversation
Replaces the Wave 7 conservative 1-hop expansion in
``GraphSearchService.search_relations`` with direct vector recall
filtered on ``Eq("indexer", "graph_relation")``. Task #3 (PR #1757)
has been writing relation vectors all along; this is finally the
consumer side, completing the LightRAG-style full recall the spec
§K.12.6 expected (huangheng W8-1 sediment from task #3 PR CR
msg=d4ad0259).
Algorithm
---------
1. Embed query via ``embedder.embed_query``.
2. ``vector_connector.search`` with ``Eq("indexer", "graph_relation")``
filter + the same threshold/top_k knobs ``search_entities`` uses.
3. Parse each hit's payload (``entity_name="src->tgt"`` +
``entity_type=relation_type`` per task #3 writer
``aperag/indexing/graph.py:1631``) by splitting on ``->``;
skip hits that don't parse cleanly.
4. Reverse-lookup full ``RelationWithLineage`` via
``asyncio.gather(*store.get_relation(...))`` (architect ratify
approach (a), msg=cf860ae4 — preserves ``compose_context``
byte-parity rendering required by the task #5 invariant).
5. Drop ``None`` results (edge GC'd between sync and search).
Failure paths (embedder / vector store down) swallow and return
``[]``, mirroring ``search_entities``.
§K.12 invariant cross-check
---------------------------
- #4 vector store via abstraction: uses ``VectorStoreConnector``,
no Qdrant-specific imports.
- #5 indexer filter: ``Eq("indexer", "graph_relation")`` pinned in
``test_search_relations_uses_graph_relation_filter``.
- #11 D-3 read-only: never invokes any mutation method.
- #12 grep-zero LightRAG: 0 hits in the new code.
- All other invariants n/a (read-only path, no schema changes).
Test coverage
-------------
10 new/replaced cases in ``test_graph_search_service.py``:
- empty query / zero-topk short-circuit (no embed, no search)
- ``Eq("indexer","graph_relation")`` filter + threshold + top_k
pinning
- payload parse + ``store.get_relation`` reverse lookup happy path
- hit-order preservation + payload-key dedup
- payload skip cases: missing payload, missing arrow, missing
entity_type, empty source, empty target
- GC tolerance: edge deleted between sync and search
- embedder failure swallowed
- vector store failure swallowed
Plus 14 pre-existing ``search_entities`` / ``get_subgraph`` /
``compose_context`` tests retained — total 24/24 pass.
earayu
left a comment
There was a problem hiding this comment.
🟢 LGTM ✅ (huangheng pass-1, per spec §K.12.11) — GitHub 不允许同账号 approve;verdict = ready to merge
Wave 8 W8-1 干净激活 — Option (a) parse + reverse-lookup 严守,与 search_entities 对称结构 + 与 task #5 byte-parity invariant 不破,24/24 tests + module constants GRAPH_RELATION_INDEXER / RELATION_PAYLOAD_ARROW 避 magic strings。
12-invariant cross-check(task #12 W8-1 scope)
| # | Material | 状态 |
|---|---|---|
| 4 | Vector store via Adaptor | ✅ abstract VectorStoreConnector + asyncio.to_thread wrap |
| 5 | payload indexer="graph_relation" filter |
✅ Eq("indexer", GRAPH_RELATION_INDEXER) line 200ish;test_search_relations_uses_graph_relation_filter 钉点 |
| 11 | D-3 read-only | ✅ 0 mutation |
| 12 | grep-zero LightRAG | ✅ 新代码 0 hits |
| 其他 | n/a | search-time read path |
task #5 byte-parity invariant 不破 verification
search_relations 走 get_relation(src, tgt, type) reverse-lookup → 拿全 RelationWithLineage(含 description_parts + evidence_lineage)→ compose_context 渲染 description 与现有 byte-parity test (test_compose_context_matches_retrieval_pipeline_render_byte_for_byte task #5 钉) 一致。如果 search_relations 用 minimal RelationWithLineage 重建会破 description rendering — 选 (a) 是正确选择。✅
Architecture quality
- module constants
GRAPH_RELATION_INDEXER+RELATION_PAYLOAD_ARROW避 magic string —— 与 task #5GRAPH_ENTITY_INDEXER同 pattern 对称 - failure swallow (embedder / vector / parse error / GC tolerance) 全 mirror search_entities convention
- payload parse defensive:5 个 skip cases(missing payload / missing arrow / missing entity_type / empty src / empty tgt)— production-realistic 防御
- GC tolerance (
asyncio.gather(return_exceptions=True)+ filter None) — edge gc 后 search 不爆,graceful
4-pattern + simple-stable
PR body Pattern 1 v1+v2+v2+v3 全 paste,simple-stable 4 项全显式 ✅。
修完会 LGTM 的清单
实际上已经可以 merge ✅。
@明书 W8-1 干净激活 + module constant 对称设计 + 5 skip cases + GC tolerance — sediment 兑现质量高。👍
@符炫炜 LGTM,可 merge。
@不穷 推进 task #12 → done after merge;Wave 8 余下 #13 / #14 / #16 in_progress(#15 已 done)。
1 个 minor follow-up(不阻塞)
PR description Pattern 1 v1 提到 "no in-tree caller today (compose_context callers wire entities + relations independently). Wave 8 follow-up: retrieval pipeline can adopt vector-recalled relations in _graph_search once a real workload validates relevance."
意思是 compose_context 渲染的 relations 当前不是 from search_relations,而是 caller 独立传入。本 PR 仅激活 vector recall 能力,但 retrieval pipeline 实际消费这条 relation vector recall 路径还是 deferred。这是一个 minor 后续: Wave 8/9 可加 follow-up 让 _graph_search 实际调 search_relations (而非靠 entity vector recall + 1-hop expansion 间接得到 relations)。
不阻塞 — 当前 PR 完成 W8-1 spec 范畴 (search_relations 走 vector recall)。Wave 8 close-out 时可 sediment 进 W8-9 candidate 列表("retrieval pipeline 消费 search_relations vector recall")。
Summary
Wave 8 W8-1 task #12. Replaces the Wave 7 conservative 1-hop expansion in
GraphSearchService.search_relationswith direct vector recall filtered onEq("indexer", "graph_relation"). Task #3 (PR #1757) has been writing relation vectors all along; this is finally the consumer side, completing the LightRAG-style full recall the spec §K.12.6 expected (huangheng W8-1 sediment from task #3 PR CR msg=d4ad0259, architect ratify msg=cf860ae4).Algorithm
embedder.embed_query.vector_connector.searchwithEq("indexer", "graph_relation")filter + the sametop_k/score_thresholdknobssearch_entitiesuses.entity_name="src->tgt"+entity_type=relation_typeper task feat: api test #3 writeraperag/indexing/graph.py:1631) by splitting on->; skip hits that don't parse cleanly.RelationWithLineageviaasyncio.gather(*store.get_relation(...))(architect ratify approach (a) in msg=cf860ae4 — preservescompose_contextbyte-parity rendering required by the task fix: create collection bug #5 invariant).Noneresults (edge GC'd between sync and search).Failure paths (embedder / vector store down) swallow and return
[], mirroringsearch_entities.§K.12 invariant cross-check (12-item)
VectorStoreConnectorAdaptorVectorStoreConnector; no Qdrant-specific importsindexerfiltertest_search_relations_uses_graph_relation_filter(Eq("indexer", "graph_relation"))upsert_entity_with_lineagetransparent alias redirectgrep -rn 'LightRAG|lightrag' aperag/indexing/graph_search_service.py tests/unit_test/indexing/test_graph_search_service.py→ 0 hits in new code4-pattern pre-check matrix
Pattern 1 v1 — caller-grep:
→ no in-tree caller today (compose_context callers wire entities + relations independently). Wave 8 follow-up: retrieval pipeline can adopt vector-recalled relations in
_graph_searchonce a real workload validates relevance.Pattern 1 v2 — n/a (existing method body replaced).
Pattern 2 — dependency interface grep:
LineageGraphStore.get_relation(source, target, type) -> RelationWithLineage | None(aperag/indexing/graph.py:574) — async-native.VectorStoreConnector.search(QueryRequest) -> list[SearchHit](aperag/vectorstore/base.py:127) — sync, wrapped viaasyncio.to_thread.aperag/indexing/graph.py:1626-1633—{indexer: "graph_relation", entity_name: f"{src}->{tgt}", entity_type: relation_type}confirmed.Pattern 3 — per-collection vs per-tenant binding:
→ unchanged from Wave 7; service is already per-collection bound at construction. No new binding decisions.
simple-stable 4-guardrail (earayu2 msg=421c4223)
get_relation+vector_connector.search. Module constants for the new relation indexer + arrow split.search_entitiesshape for symmetry; failure paths use the sameswallow → []convention.Test plan
24/24 cases pass (
uv run pytest tests/unit_test/indexing/test_graph_search_service.py -v):10 new/replaced cases for
search_relations:Eq("indexer","graph_relation")filter + threshold + top_k pinningstore.get_relationreverse lookup happy pathentity_type, empty source, empty target14 pre-existing
search_entities/get_subgraph/compose_contexttests retained.ruff check+ruff format --checkclean.References
#删除老graph(architect ratify msg=cf860ae4 — approach (a) reverse-lookup viaget_relationto preservecompose_contextbyte-parity)🤖 Generated with Claude Code