Skip to content

feat(Wave 8 W8-1 #12): activate search_relations vector recall#1767

Merged
earayu merged 1 commit into
mainfrom
w8-1-search-relations-vector-recall
Apr 28, 2026
Merged

feat(Wave 8 W8-1 #12): activate search_relations vector recall#1767
earayu merged 1 commit into
mainfrom
w8-1-search-relations-vector-recall

Conversation

@earayu

@earayu earayu commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Wave 8 W8-1 task #12. 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, architect ratify msg=cf860ae4).

Algorithm

  1. Embed query via embedder.embed_query.
  2. vector_connector.search with Eq("indexer", "graph_relation") filter + the same top_k / score_threshold knobs search_entities uses.
  3. Parse each hit's payload (entity_name="src->tgt" + entity_type=relation_type per task feat: api test #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) in msg=cf860ae4 — preserves compose_context byte-parity rendering required by the task fix: create collection bug #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 (12-item)

# Invariant This PR
1 L1 graph data 不污染 ✅ read-only path
2 L1 → L2 单向派生 n/a (read-only)
3 Compactor 在 sync 末尾 n/a (search-time)
4 Vector store via VectorStoreConnectorAdaptor ✅ uses abstract VectorStoreConnector; no Qdrant-specific imports
5 payload indexer filter ✅ pinned in test_search_relations_uses_graph_relation_filter (Eq("indexer", "graph_relation"))
6 uuid5 deterministic vector point id n/a (reads vectors, doesn't write)
7 snapshot-diff via lineage entity name set n/a
8 alias_map persistence n/a
9 upsert_entity_with_lineage transparent alias redirect n/a (write-side concern)
10 DB 列长度 application-layer cap n/a
11 候选检测仅写不自动合并 (D-3) ✅ read-only; never invokes any mutation
12 命名 grep-zero LightRAG grep -rn 'LightRAG|lightrag' aperag/indexing/graph_search_service.py tests/unit_test/indexing/test_graph_search_service.py → 0 hits in new code

4-pattern pre-check matrix

Pattern 1 v1 — caller-grep:

$ grep -rn 'search_relations' aperag/ tests/ | grep -v __pycache__
aperag/indexing/graph_search_service.py:194  # method definition (this PR)
tests/unit_test/indexing/test_graph_search_service.py  # tests (this PR)

→ 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.

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 via asyncio.to_thread.
  • task feat: api test #3 writer payload shape: 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)

  1. 不无限扩范围 ✅ no Protocol method added; reuses existing get_relation + vector_connector.search. Module constants for the new relation indexer + arrow split.
  2. 先做实尽快上线 ✅ ~50 LOC code change + 10 tests; small surface, ratified scope.
  3. 简单稳定优于复杂 ✅ mirrors search_entities shape for symmetry; failure paths use the same swallow → [] convention.
  4. 私有化部署后免维护 ✅ no operator config; thresholds inherited from existing service kwargs.

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:

  • 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 returns empty
  • embedder failure swallowed
  • vector store failure swallowed

14 pre-existing search_entities / get_subgraph / compose_context tests retained.

ruff check + ruff format --check clean.

References

🤖 Generated with Claude Code

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 earayu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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_relationsget_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 #5 GRAPH_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")。

@earayu earayu merged commit 1091172 into main Apr 28, 2026
7 checks passed
@earayu earayu deleted the w8-1-search-relations-vector-recall branch April 28, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant