feat: semantic search via weighted FTS, body backfill, pgvector and MCP server#283
feat: semantic search via weighted FTS, body backfill, pgvector and MCP server#283salmonumbrella wants to merge 1 commit into
Conversation
…CP server Overhaul Mailflow search end to end: - Weighted lexical ranking: subject > sender > body (10:4:1 setweight A-D + ts_rank_cd), prefix matching as you type, NULL-safe ordering during backfill, served by a GIN index on a new trigger-maintained search_fts column filled by a resumable drainer (fast-DDL migrations only — no boot-blocking table rewrite). Free-text ranks by relevance; filter-only queries stay date-ordered. Stopword-safe term matching; quoted phrases. - Body materialization: provider-gated background IMAP drainer fills body_text (apple/yahoo/generic on; Gmail/PurelyMail/Microsoft off per their throttle history) with circuit breakers, snippet-indexer sequencing, poison-message forward progress, and background_jobs progress rows. - Embeddings: any OpenAI-compatible /v1/embeddings endpoint via AI settings (encrypted key, host-validated, masked reads); fingerprinted generations (config change => clean rebuild, generations never mix); crash-safe scan-and-fill worker with CAS stamps, 24h backstop, poison downshift, single-flight lock, activation-on-coverage, and config-fingerprint guards; HNSW per-dimension partial indexes; a content-change trigger re-embeds late-arriving bodies. Admin settings UI with explicit opt-in privacy copy, Test/Build buttons with live progress, and local-embeddings guidance — all in 7 locales. Stock Postgres still boots clean with semantic disabled. - Hybrid search: single-query BM25∪ANN reciprocal-rank fusion behind an explicit in-input Semantic toggle (lexical stays the default; silent lexical fallback while the index builds). Subject boost gated on a live BM25 leg; folder scope, negation, and pagination applied to all modes; hnsw.ef_search sized for the fused candidate pool. Eval-verified on a real 18k-message mailbox (paraphrase Recall@20: lexical 0% -> hybrid 76%; keyword MRR 0.43, best of all modes). - MCP: bearer-tokened Streamable-HTTP /mcp endpoint (SHA-256-hashed tokens minted in Profile, Origin-validated, per-token rate-limited) exposing 12 tools incl. search_in_message (keyword + vector) and semantic_search_messages; every call scoped to the token owner's accounts; staged-only soft deletion with a no-enforceable-filter guard. Deployment: compose ships pgvector/pgvector:pg16 (same PG16 major — the data dir mounts as-is). Run REINDEX DATABASE after the first boot on the new image: the alpine(musl)->Debian(glibc) collation change can misorder existing text btree indexes (the app logs a loud warning when it detects this). Migrations 0035-0041 are fast-DDL, crash-idempotent, boot-safe. The legacy search_vector column survives until a follow-up removes it after the search_fts backfill completes on real installs. Verified: backend 1118 unit tests + gated pgvector integration suites; frontend 1447 tests, lint/build clean; deployed and exercised against a real 18k-message mailbox (search, embeddings build ~27 msg/s, hybrid, MCP round-trips + Origin/rate-limit guards); independent per-phase review gates plus a full-diff audit and three correctness/parity audits — all findings fixed pre-merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4abd208 to
3773150
Compare
|
@maathimself this one is interesting. look forward to your thoughts. |
|
This is great, the security, scoping, and provider gating are all done right. My one ask before merging: could you split it? Right now four independent features land in one 14.7k-line squash commit, and they have very different risk profiles:
The keyword-search improvements benefit everyone with near-zero risk, but they're currently coupled to the database image change and a big new surface most users won't enable. Split into separate PRs, (1) can land almost immediately, and (2)/(3) can be reviewed and opted into on their own. It also makes each far easier to roll back or bisect. |
|
Done — split as requested:
The split is a re-partition of this PR's reviewed tree, not a rewrite: with all three applied, the result is byte-identical to this PR's head ( Happy to close this one in favor of the stack whenever you prefer. |
Summary
Overhauls search end to end: results ranked by relevance instead of date-only, bodies searchable across the whole mailbox, an optional embeddings pipeline adding a hybrid semantic mode, and a token-scoped MCP endpoint exposing search to agents. Everything costly or privacy-sensitive is off by default; stock Postgres still boots clean with semantic disabled.
Changes
setweightA–D +ts_rank_cd), prefix matching as you type, served by a GIN index over a trigger-maintainedsearch_ftscolumn. Backfill runs as a resumable background drainer (fast-DDL migrations only, no boot-blocking rewrite); not-yet-backfilled rows fall back to the previous query path. Filter-only queries stay date-ordered; quoted phrases supported.body_text(apple/yahoo/generic on; Gmail/PurelyMail/Microsoft off given their throttle history), with circuit breakers, poison-message forward progress, andbackground_jobsprogress rows./v1/embeddingsendpoint (key encrypted at rest, host-validated, masked on read). Fingerprinted generations (config change ⇒ clean rebuild), a crash-safe fill worker, per-dimension HNSW partial indexes, re-embedding of late-arriving bodies. Query side is hybrid BM25∪ANN reciprocal-rank fusion behind an in-input Semantic toggle — lexical stays the default, with silent fallback while the index builds. Settings UI with explicit privacy copy and live Test/Build progress, in all 7 locales./mcpendpoint (SHA-256-hashed tokens minted in Profile, Origin-validated, rate-limited) exposing 12 tools includingsemantic_search_messagesandsearch_in_message. Every call is scoped to the token owner's accounts; deletion is staged-only.Evals on a real 18k-message mailbox: paraphrase Recall@20 goes 0% (current search) → 76% (hybrid); keyword MRR 0.43, the best of all modes tested.
Deployment notes
postgres:16-alpine→pgvector/pgvector:pg16(same PG16 major; the data dir mounts as-is). RunREINDEX DATABASEonce after the first boot: the musl → glibc collation change can misorder existing text btree indexes (the app logs a loud warning when it detects this).search_vectorcolumn stays until a follow-up removes it once thesearch_ftsbackfill completes on real installs.search/lexicalRepo.js(same constant, equivalent tests).@modelcontextprotocol/sdk.Testing
--max-warnings 0)Contributor License Agreement
By submitting this pull request I confirm that:
third-party material and confirmed it is compatible with the CLA).
🤖 Generated with Claude Code