Repository navigation
Cover the async retrieval path, and record a de-duplication asymmetry - #227
Merged
Merged
Conversation
HybridRetriever implements retrieval twice -- retrieve_documents and aretrieve_documents, the second gathering coroutines so collections are queried concurrently. Nothing exercised the async one, and that is the one the application serves through. bin/retrieval_baseline, the tool the constitution names for measuring retrieval changes, drives the sync one. If they drift, every measurement is of a path no user takes. They do not drift today: same documents, same order, checked across three query shapes. Two things this test had to be argued out of claiming. It first reported the paths returning different document sets -- that was FakeEmbeddings returning a fresh random vector per call, so the same query embedded differently on the second run. DeterministicFakeEmbedding fixes it, and the near-miss is recorded in the docstring because the false result is more instructive than the true one. And it does not cover the per-collection cap. Deterministic fake vectors carry no relation to the text, so every query retrieves much the same set and fusion lands on exactly the cap however many queries are given -- changing the cap on one path alone stays invisible. Tried at 12 and 60 documents per collection with one, three and seven queries; ten per collection every time. That limit is written into the docstring rather than left as apparent coverage, and covering it needs real embeddings, which is retrieval_baseline's job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new equivalence test failed on GitHub and passed here. BM25 tokenises with word_tokenize(..., language="english"), which needs punkt_tab; the Dockerfile installs it at build time and the CI test job never did. So the suite passed on any machine where a developer had downloaded it once and failed on a clean one -- the classic shape, and it was my test that exposed it rather than caused it. Any future test that drives retrieval would have hit the same wall. CI now fetches the same resource the Dockerfile does, so the async path is genuinely covered there rather than skipped. The test also skips with a message naming the download command when the resource is absent, so a developer without it gets that instead of a LookupError raised from inside nltk. Checked by patching nltk.data.find to raise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An adversarial pass over the retriever — the code every question goes through, and the thing spec 009 proposes changing.
The gap
HybridRetrieverimplements retrieval twice:retrieve_documentsandaretrieve_documents, the second gathering coroutines so collections are queried concurrently. Nothing exercised the async one — and that's the one the application serves through, whilebin/retrieval_baselinedrives the sync one.That asymmetry is worse than ordinary duplication.
retrieval_baselineis the tool the constitution names for measuring retrieval changes, and spec 009's whole verification plan rests on it. If the paths drift, every measurement is of a path no user takes.They don't drift today — same documents, same order, across three query shapes. Now pinned.
Two things this test had to be argued out of claiming
It first reported the two paths returning different document sets. That was
FakeEmbeddings, which returns a fresh random vector per call, so the same query embedded differently on the second run.DeterministicFakeEmbeddingfixes it. The near-miss is in the docstring, because the false result is more instructive than the true one.It does not cover the per-collection cap, and says so. Deterministic fake vectors carry no relation to the text, so every query retrieves much the same set and fusion lands on exactly the cap however many queries are supplied — changing the cap on one path alone stays invisible. I tried 12 and 60 documents per collection with one, three and seven queries: ten per collection every time. Rather than leave apparent coverage, the limit is written down; covering it needs real embeddings, which is
retrieval_baseline's job.A finding I measured and chose not to act on
dedupe_by_entitykeys onst_id, falling back topage_content. But BM25 documents come fromCSVLoader, whose metadata is{source, row}— nost_idat all. Verified against the real bundle: 0 of the first 2,000 reaction documents carry one.So the two halves of the pipeline de-duplicate by different identities. Vector hits collapse to one row per stable ID; BM25 hits don't, and
reactionshas 17,004 rows for 16,107 distinct reactions, so the same reaction appears under several pathways.Measured on real questions rather than argued about:
Worst case one slot in ten. Real, minor, and not worth a blind change to retrieval — "this should be better" is not a finding. Recorded here so spec 009 knows it's there, since routing work touches exactly this code.
329 tests, ruff and mypy clean.
🤖 Generated with Claude Code