ADR 0005: Search: Postgres FTS + pgvector fusion replaces the Meilisearch path¶
Date: 2026-10-02 · Status: accepted · Task: UX-11 · Decision: D-09
Context¶
The spec asked for Meilisearch; D-09 revised it: keep the service count low,
use Postgres FTS + pgvector hybrid first, and only add Meilisearch if FTS
proves insufficient. Search was implemented Meilisearch-only
(utils/search.py, indexed on ingest); nothing degraded when Meilisearch
was absent: the endpoint silently returned zero hits.
Decision¶
content_items.search_tsv(migration 0018): weighted GENERATED tsvector (title A, headline+brief B), GIN-indexed.GET /api/v1/searchruns two legs (websearch_to_tsquery+ts_rankand a pgvector ANN query restricted to rows embedded by the active spec) fused with Reciprocal Rank Fusion (k=60;services/search/hybrid.py).- Filters (source, origin, saved, since) apply inside BOTH legs, so fusion never leaks out-of-scope rows.
- Meilisearch removed (#139): the dormant client (
utils/search.py), the compose service and theMEILISEARCH_*settings were deleted. Search is Postgres-only.
Consequences¶
- One fewer hard runtime dependency; zero-hit mode is gone.
full_textis excluded from the tsvector on purpose: it is large and is pruned by retention; summaries + titles stay. Recall over pruned bodies is a known, documented tradeoff.- Latency budget (p95 < 300 ms at 100k items) relies on the GIN index and HNSW; a load benchmark is deferred to RL-06 (benchmarks page), not yet measured at 100k rows.
- Meilisearch revisit criterion: if ranked-relevance complaints appear that RRF cannot fix (e.g. typo tolerance at scale), re-introduce Meilisearch behind a flag rather than re-implement the fusion.