Files
buzz/schema
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57ccandnpub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d 4d8fe9a5e0 spike: postgres FTS backend behind BUZZ_SEARCH_BACKEND flag
Adds a Postgres full-text search backend as an alternative to Typesense for
NIP-50 search, gated behind BUZZ_SEARCH_BACKEND=typesense|postgres|disabled
(default typesense — no behavior change for existing deployments).

The replacement is structural: NIP-50 search is the only Typesense call site,
and the read path already refetches canonical events from Postgres by id, so
Typesense was just a lookup index in front of the DB that owns the data. A
generated stored tsvector column + GIN index gives the same shape with zero
write-path code change.

Changes
- migrations/0004_search_fts.sql: events.content_tsv GENERATED ALWAYS AS
  to_tsvector('simple', content) STORED, GIN index, cascades to partitions.
- crates/buzz-search: SearchBackend enum (Typesense | Postgres | Disabled),
  SearchService::with_postgres / ::disabled, postgres.rs backend impl,
  backend-neutral SearchQuery (structured kinds/authors/channel_ids/since/until;
  each backend renders its own filter).
- crates/buzz-relay/src/config.rs: BUZZ_SEARCH_BACKEND env wired with strict
  parsing (unknown value → ConfigError::InvalidValue, no silent fallback) +
  3 unit tests.
- crates/buzz-relay/src/main.rs: dispatch on backend; Postgres → with_postgres
  using db.pool(); Disabled → no-op; Typesense → existing path. ensure_collection
  only runs for the Typesense backend.
- crates/buzz-relay/src/{handlers/req.rs, api/bridge.rs}: swap to the new
  SearchService surface. Caller code shrinks — filter parts were already
  structured.
- crates/buzz-db/src/lib.rs: Db::pool() accessor for the PG backend.

Validation (against parent 2e426b2f, PG17 side-deployed):
- buzz-search lib: 29/29 pass.
- buzz-relay config tests: 11/11 (incl. 3 new).
- NIP-50 e2e on Typesense backend: 5/5 pass (regression baseline).
- NIP-50 e2e on Postgres backend: 5/5 pass — including
  test_nip50_search_relevance_order, confirming ts_rank_cd ranks correctly
  for the NIP-50 query shape and the 'simple' tokenizer config is acceptable.
- Wider e2e_nostr_interop sweep on Postgres: 19/23. The 4 failures reproduce
  identically on Typesense backend on this branch — pre-existing test-fixture
  coupling to a hard-coded 'events' collection name, not a regression.

This is additive: Typesense remains default; nothing in the existing path is
removed. Operators flip BUZZ_SEARCH_BACKEND per release to A/B/rollback.

Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
2026-06-24 19:27:35 -04:00
..