Migrate docsite embed/search to Postgres + pgvector (WIP) - #606
Conversation
|
Amazing, thanks so much Isaac! I will review this on Monday. |
|
Hi @IZO-Ong this looks brilliant, thank you so much! Great to see an eval set in here. For the final version it would be nice to expand it to ~30 queries, including some keyword-heavy ones (exact function or term lookups where hybrid should shine on), and add hybrid vs semantic on Postgres to the comparison so we have evidence for switching strategies later. Just a clarification on scope from here onward since the original issue was written before some things changed: the prompt.py item is already done (we replaced the adaptor TypeScript definitions with a function list) and adaptor-docs search is now being explored separately. So please treat your scope as Postgres backend + general-docs search only. The hybrid wiring into job_chat's general_docs retrieval and the planner's search tool (the two call sites you already touched) can be a follow-up PR, gated on the eval showing parity or better. One thing to keep in mind: both call sites currently filter results with a similarity threshold, which hybrid intentionally doesn't support, so part of that follow-up is deciding what replaces it (top_k only, an RRF floor, etc.). Don't invest in optimising search methods beyond that; if hybrid needs heavy tuning to beat semantic, flag it rather than iterating. Adaptor docs will land in the same Postgres store later (via your migrations system), but how they're searched is out of scope here. |
There was a problem hiding this comment.
Could we rename this to a timestamped filename to stick to Stuart's request in #305
There was a problem hiding this comment.
Is the convention for the migration filename being YYYYMMDDHHMMSS ok to make the ordering lexical? So for instance the file name might be 20260728000000
|
Hi all, I've updated based on the feedback above:
Full results table + per query breakdown in the Results section above. |
…o match brief spec
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…url for integration testing
Short Description
Migrates
embed_docsite/search_docsitefrom Pinecone to Postgres + pgvector (0.8.1 extension), keeping the legacy Pinecone path available. Both indexing and searching support either backend, selected independently — the write target and read backend are separate settings. WIP — comments are welcome!Addresses #305
Implementation Details
Using both backends
Indexing —
embed_docsite's payload takes atargetparam:Searching —
search_docsitereadsDOCSITE_SEARCH_BACKEND(env, defaultpinecone), overridable per-request via abackendpayload field:job_chat'sretrieve_docs.pyand thesearch_documentationtool both resolve the backend the same way, through a sharedresolve_backend()insearch_docsite.py.No new required env vars for existing setups, Two new optional vars in
.env.example:DOCSITE_SEARCH_BACKEND—pinecone|postgres(Defaults to pinecone if not provided)POSTGRES_TEST_URL— separate DB for the integration suite only (see Tests below)Search strategies
Postgres adds three strategies:
semantic(pgvector cosine),keyword(Postgres full-text search), andhybrid(Reciprocal Rank Fusion of both).Production consumers (
job_chat,search_documentation) usestrategy='semantic'on both backends, at the same0.8cosine threshold Pinecone already used.hybrid/keywordare available via thesearch_docsiteservice directly and viarun_eval, but aren't wired into any production consumer yet.Migrations
Schema lives in services/migrations/20260728000000_docsite_batches_and_chunks.sql (timestamped per @stuartc's request for predictable ordering) — docsite_batches (versioned batch lifecycle: building -> complete/failed) and docsite_chunks (chunk text + vector(1536) embedding + generated tsvector for FTS), plus the vector extension and four indexes.
Applied by services/db_migrations.py, run explicitly from embed_docsite's Postgres write path. Migrations are idempotent (tracked in _migrations_docs) and lexically ordered. No manual step needed, as the first embed_docsite run with target=postgres sets up the schema.
Running
run_evalCompares both backends (
strategy='semantic'on each) over a golden query set: recall@5, p50/p95 latency, and for the Postgres-semantic-vs-Pinecone pairing specifically, per-query doc-title agreement.Setup:
Set OPENAI_API_KEY, PINECONE_API_KEY, POSTGRES_URL in services/.env (this is the file that's actually loaded — see note below).
Populate Pinecone and all three Postgres chunk sizes.
Run the eval (must run from
services/, the Python path root):cd services poetry run python -m search_docsite.tests.eval.run_evalResults
30 golden queries now (15 conceptual, 15 keyword), and the comparison now covers hybrid vs semantic across three chunk sizes (1000/1800/2500 chars). Labels for all 30 queries were derived from the
general_docscorpus via full-text search on each query's key terms.Jaccard similarity between Pinecone and Postgres semantic (1000) is 0.970.
Full run_eval output
Discussions
Hybrid ties semantic on keyword recall everywhere (both hit 1.00 at every chunk size) rather than beating it, and semantic is at least as good as hybrid on conceptual recall at every chunk size tested. Hybrid's p95 is consistently the best in its chunk-size group, but on recall alone this data doesn't support switching the production default.
2500 chars is the only size where Postgres matches Pinecone's 1.00 conceptual recall for both strategies; 1000 and 1800 both dip to 0.93 on hybrid (1800 dips on semantic too).
Tests
Unit — 113 passing, fully mocked, no live services required:
embed_docsite:test_docsite_processor.py,test_docsite_indexer.py,test_embed_docsite.py,test_db_migrations.pysearch_docsite:test_docsite_search.py(new Postgres backend),test_pinecone_legacy_search.py(legacy, preserved),test_search_docsite_main.py,test_run_eval.py(now covers per-category recall aggregation and chunk-size batch resolution too)job_chatandsearch_documentationfor the newresolve_backend()delegationIntegration — 6 passing, against a real ephemeral Postgres + pgvector:
services/embed_docsite/tests/integration/test_postgres_docsite_roundtrip.py— fresh-DB migrate→index→promote, search across all three strategies, reindex prunes the previous batch, reader gets a clear 503 without a schemadocker run -d --name apollo-pgvector-test -e POSTGRES_PASSWORD=postgres -p 5433:5432 pgvector/pgvector:pg16 export POSTGRES_TEST_URL=postgresql://postgres:postgres@127.0.0.1:5433/postgres poetry run pytest services/embed_docsite/tests/integration -qCaution: Do not use
POSTGRES_URLforPOSTGRES_TEST_URL— the fixturesDROPthe docsite tables and thevectorextension between tests, so pointing this at a database would destroy its data.Not done by this PR
hybridintojob_chat's general-docs retrieval and the planner's search tool.AI Usage
Please disclose whether you've used AI in this work (it's cool, we just want to
know!):
You can read more details in our
Responsible AI Policy