ADR-0009 — Search as a derived index: Postgres FTS → OpenSearch
V1 Freeze (2026-06-12): Accepted (tiered). Postgres-FTS (name/metadata search) is V1. OpenSearch (content search) is deferred to P3 behind the same query API.
Context
BitVault needs search. The brief lists OpenSearch. But v1 search is over names and metadata/tags — which Postgres does well. OpenSearch is the heaviest, least- justified dependency for that scope (a JVM cluster to search filenames) and clashes with the tiered-dependency goal (Ledger, ADR-0012). Full-text search inside document contents is the only feature that truly justifies OpenSearch, and it is a later capability (FR E3). Any search index is derived and will drift from the source of truth (R7).
Decision
- Search is always a derived, disposable projection of the source of truth
(Postgres) built via domain events (ADR-0006), behind a
Searchquery API. It is never read as authoritative (R7) and is rebuildable by replaying the journal (I6). - v1: Postgres full-text search for names + metadata + tags. No new dependency; ships in the standard tier.
- P3+: OpenSearch for full-text content search and richer relevance/facets, introduced as an event consumer (the indexer worker) in the full tier. The query API’s contract is stable across the backend swap.
- Search is optional. A deployment can run with only Postgres-FTS; turning OpenSearch off degrades to name/metadata search, it does not break the product.
Consequences
Positive
- No heavyweight dependency until a feature (content search) demands it (Ledger).
- Self-host stays light; OpenSearch is opt-in (ADR-0012).
- Derived + rebuildable index makes drift (R7) a recoverable annoyance, not data loss.
- The query API abstracts the backend, so FTS→OpenSearch is invisible to clients.
Negative / costs
- Two search implementations behind one interface (FTS + OpenSearch) to maintain once content search ships; mitigated by the stable query contract + shared event consumer shape.
- Eventual consistency: newly committed files are searchable only after indexing (NFR-5) — UI shows indexing state.
- OpenSearch, when enabled, is operationally heavy (JVM, memory, cluster) — which is exactly why it is gated behind a real feature.
Alternatives considered
- OpenSearch from day one (as briefed): rejected for v1 — overengineering for filename search; violates tiered-deps; Postgres-FTS is sufficient.
- Postgres-FTS forever: rejected as the end state — content search at scale, relevance tuning, and facets are where OpenSearch earns its keep (and is a stated portfolio technology).
- Read search straight from Postgres tables (no projection): rejected — couples search to the write model and the schema; the derived-projection seam is what lets search scale and be swapped/optional.