fix(retrieval): scope the rule search before ranking it - an in-scope rule is no longer lost behind other owners' nearer rules (#4958)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 20s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m11s
CI & Build / Python tests (push) Successful in 2m0s
CI & Build / Build & push image (push) Successful in 29s
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 20s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m11s
CI & Build / Python tests (push) Successful in 2m0s
CI & Build / Build & push image (push) Successful in 29s
Ordered straight off rule_embeddings, the planner walked the HNSW index, which returns about hnsw.ef_search (40) nearest chunks across every owner and project and filters by home only afterwards. A reader's own rule ranked past the 40th chunk overall was silently dropped - on a shared install, other users' rules fill those 40. This is the likely cause of the intermittent test_integration_rule_scope failures, whose axis-vector fixtures sit far from every real embedding in the graph. The in-scope chunks now go through a MATERIALIZED CTE, which the index cannot order, so the ranking over them is exact. A rulebook is hundreds of chunks; exact is cheap. New test: 80 nearer rules belonging to someone else no longer hide the reader's one rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1550,28 +1550,44 @@ async def semantic_search_rules(
|
||||
# function fails open.
|
||||
home = await rule_home(user_id, project_id, everywhere=everywhere)
|
||||
|
||||
# SCOPE FIRST, THEN RANK — exactly (#4958). Ordered straight off
|
||||
# rule_embeddings, the planner walks the HNSW index, which hands back
|
||||
# about `hnsw.ef_search` (40) nearest chunks from EVERY owner and
|
||||
# project and only then applies the home filter: an in-scope rule
|
||||
# ranked past the 40th chunk overall is silently gone, and on a
|
||||
# shared install other users' rules fill those 40. A MATERIALIZED
|
||||
# CTE cannot be ordered through the index, so the distance is
|
||||
# computed for each in-scope chunk and the ranking is exact. A
|
||||
# rulebook is hundreds of chunks, not millions — exact is cheap here.
|
||||
scoped = (
|
||||
joined_to_homes(
|
||||
select(
|
||||
RuleEmbedding.rule_id.label("rule_id"),
|
||||
distance.label("distance"),
|
||||
RuleEmbedding.chunk_index.label("chunk_index"),
|
||||
RuleEmbedding.chunk_text.label("chunk_text"),
|
||||
)
|
||||
.select_from(RuleEmbedding)
|
||||
.join(Rule, RuleEmbedding.rule_id == Rule.id)
|
||||
)
|
||||
.where(
|
||||
Rule.deleted_at.is_(None),
|
||||
# No threshold predicate — see the note above
|
||||
# semantic_search_notes. Applied below, after the collapse.
|
||||
home,
|
||||
*( [Rule.kind == kind] if kind else [] ),
|
||||
)
|
||||
.cte("scoped_rule_chunks")
|
||||
.prefix_with("MATERIALIZED")
|
||||
)
|
||||
|
||||
async with async_session() as session:
|
||||
rows = (await session.execute(
|
||||
joined_to_homes(
|
||||
select(
|
||||
Rule,
|
||||
distance.label("distance"),
|
||||
RuleEmbedding.chunk_index,
|
||||
RuleEmbedding.chunk_text,
|
||||
)
|
||||
.select_from(RuleEmbedding)
|
||||
.join(Rule, RuleEmbedding.rule_id == Rule.id)
|
||||
)
|
||||
.where(
|
||||
Rule.deleted_at.is_(None),
|
||||
# No threshold predicate — see the note above
|
||||
# semantic_search_notes. Applied below, after the collapse.
|
||||
home,
|
||||
*( [Rule.kind == kind] if kind else [] ),
|
||||
)
|
||||
select(Rule, scoped.c.distance, scoped.c.chunk_index, scoped.c.chunk_text)
|
||||
.join(scoped, scoped.c.rule_id == Rule.id)
|
||||
# Overfetch so collapsing chunks to their best row still fills
|
||||
# the page — the same reason the note search overfetches.
|
||||
.order_by(distance)
|
||||
.order_by(scoped.c.distance)
|
||||
.limit(limit * _CHUNK_OVERFETCH)
|
||||
)).all()
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user