Skip to content

HUB-043: make the corpus queryable as one knowledge base - #12

Merged
funsaized merged 4 commits into
mainfrom
hub-043-corpus-retrieval
Aug 13, 2026
Merged

HUB-043: make the corpus queryable as one knowledge base#12
funsaized merged 4 commits into
mainfrom
hub-043-corpus-retrieval

Conversation

@funsaized

Copy link
Copy Markdown
Owner

Hybrid dense+BM25+RRF retrieval ran only inside a research job's report synthesis. /query and /rag used a second, dense-only implementation, so 679 documents collected across 62 jobs sat physically in one index and logically in 62 silos — and the FTS5 needle channel that lifted exact-term hit@4 from 0.6923 to 1.0 was unreachable from any corpus-wide query.

ScopedRetrievalService.retrieve now takes job_id=None to mean the whole corpus. The job id is a filter, not a mode. The dense-only path is deleted, not deprecated.

Two things the analysis had not anticipated

  • The filters had to move, not just widen. topic_filter/tags_filter were Qdrant payload conditions. Left there, they would have narrowed the dense channel while the lexical channel searched the whole corpus. They now resolve to a document scope from job_sources, which is also the more correct source: a page found by two jobs on different topics belongs to both, and only job_sources records the second.
  • Unscoping made row width matter. documents_for_job read whole rows; corpus-wide that meant decoding 44 MB of markdown per query to obtain three identity columns. Both accessors are now projected.

Verification

  • Job-scoped retrieval is provably unchanged. Fingerprinted over the ordered (document_id, chunk_index, score, channels, rrf_score) list for the six largest jobs — 360 selected chunks — under the deployed image and the new one. All six SHA-256 digests identical. (The original acceptance wording, "report synthesis is byte-identical", was not checkable: synthesis is LLM-driven. This is the deterministic thing underneath it.)
  • Live against the real corpus, new image on the deployed Qdrant and Ollama: "how does reciprocal rank fusion combine rankings" → 64 chunks from 33 sources; "kubernetes observability tracing" → 65 from 42. Both channels contributing and fusing. Under the old code these could reach one job's sources at most.
  • Prerequisite confirmed before deploying: all 68,072 Qdrant points carry a document_id and every one resolves in SQLite, so corpus-wide retrieval drops nothing.
  • Filters checked on live metadata: topic → 122 documents, tag llm → 9, unmatched topic → 0 (not the whole corpus).
  • 357 tests pass in a throwaway container (Redis DB 15, placeholder key); no deployed container was recreated.

Unblocks HUB-044 through HUB-046.

🤖 Generated with Claude Code

funsaized and others added 4 commits August 13, 2026 16:09
The live Serper contract returns dates as 'Oct 31, 2019', which neither ISO
nor RFC-2822 parsing accepts. Every Serper result therefore parsed as
undated, and any job setting freshness_days would have rejected all of them
as stale_or_undated -- a defect the mocked tests could not expose, found on
the first real API call.

Adds explicit human-readable formats after the existing two, so shapes that
already parsed are untouched.

340 tests green in-container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With SearXNG stopped, a job served all four facet queries from Serper,
retained 17 sources and completed. With SearXNG restored the next job went
back to searxng, so primacy returns automatically and the paid path stays
insurance. The key reaches no log line.

Records the two failed attempts to force the fallback, both instructive:
SEARXNG_ENGINES pointed at a nonexistent engine does nothing because SearXNG
uses category defaults, and overriding SEARXNG_URL in .env does nothing
because docker-compose.yml hardcodes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six items, each grounded in a fetched arXiv abstract and in measurements
from this session.

HUB-043 is the highest-value: ScopedRetrievalService.retrieve takes job_id
as its first argument, so hybrid dense+BM25+RRF and the FTS5 needle channel
run only inside one job's report synthesis while /query and /rag use a
separate dense-only path. Two retrieval implementations, and the better one
cannot see the corpus -- 530 documents physically in one index and logically
in 49 silos.

Also corrects HUB-027: its stated precondition 'hybrid retrieval first' is
not actually met, because hybrid retrieval exists for jobs and not for the
corpus-wide queries a graph layer would serve. HUB-048 replaces its
open-ended evaluation with the RAG-vs-GraphRAG measured protocol.

HUB-047 names a gap the backlog never had: benchmarks exist for exact-term
recall, claim support and source screening, but nothing measures whether the
knowledge base answers real questions well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-043)

Hybrid dense+BM25+RRF retrieval ran only inside a research job's report
synthesis. /query and /rag used a second, dense-only implementation, so 679
documents collected across 62 jobs sat physically in one index and logically
in 62 silos, and the FTS5 needle channel that lifted exact-term hit@4 from
0.6923 to 1.0 was unreachable from any corpus-wide query.

ScopedRetrievalService.retrieve now takes job_id=None to mean the whole
corpus. The job id is a filter, not a mode: the same fusion, per-source caps
and needle channel run either way. The dense-only path is deleted, not
deprecated.

/query's topic_filter and tags_filter survive but relocate. They were Qdrant
payload conditions; left there they would have narrowed the dense channel
while the lexical channel searched the whole corpus. They now resolve to a
document scope from job_sources, which is also the more correct source: a
page found by two jobs on different topics belongs to both, and only
job_sources records the second.

Unscoping made row width matter: documents_for_job read whole rows, which
corpus-wide meant decoding 44MB of markdown per query to obtain three
identity columns. Both accessors are now projected.

Corpus scope sends no identity filter, since enumerating every id would be
equivalent but grows without bound. An empty list stays an error in both
search_evidence and search_chunks -- that means a caller expected a scope and
lost it, and silently widening is the failure the guard exists to catch.

Under fusion /query reports the RRF score rather than a cosine: a similarity
that contradicts the ordering would be worse. hub_retrieval_score still
observes only candidates with a real cosine, and query latency moves off
hub_embedding_duration_seconds -- which timed the whole retrieval under a name
meaning one part of it -- onto hub_retrieval_duration_seconds.

Verified: job-scoped retrieval fingerprinted over the ordered (document_id,
chunk_index, score, channels, rrf_score) list for the six largest jobs (360
chunks) under the deployed image and the new one -- all six digests identical.
Live against the real corpus, "how does reciprocal rank fusion combine
rankings" draws 64 chunks from 33 sources across jobs. All 68,072 Qdrant
points carry a document_id resolving in SQLite, so nothing is dropped.

357 tests pass in a throwaway container.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@funsaized
funsaized merged commit 63aacc6 into main Aug 13, 2026
4 checks passed
@funsaized
funsaized deleted the hub-043-corpus-retrieval branch August 14, 2026 16:51
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