CASE STUDY · POSTGRES CDC + HYBRID SEARCH

POINT IT AT POSTGRES.
SEARCH SYNCS ITSELF.

The founder of daily.dev asked a public question: can XERJ replicate automatically from Postgres? Yes. A small CDC consumer reads the WAL through a logical-replication slot, and every INSERT, UPDATE, and DELETE flows into XERJ by itself — no reindex jobs. One hybrid query then fuses BM25 and vector rankings server-side.

FOR·POSTGRES TEAMS·SEARCH ENGINEERS·PLATFORM TEAMS
FUNCTIONAL PROOF·SYNTHETIC 8-ROW CORPUS·NOT A BENCHMARK
XERJ POSTGRES TSVECTOR + PGVECTOR
Queries per hybrid search 1 fused query 2 queries + app-side merge
App-side score merges 0 · RRF fuses server-side 1 · across two score scales

ARCHITECTURAL COUNT FROM THE RECORDED RUN · SOURCES IN docs/case-studies/daily-dev-postgres-cdc →

SEARCH ON THE PRIMARY

THE XERJ ANSWER

RECORDED RUN·POSTGRES 16 + PGVECTOR 0.8.2·LOGICAL REPLICATION
3/3
MUTATION TYPES REPLICATED
INSERT + UPDATE + DELETE reflected in one slot drain
0
CHANGES LOST IN DOWNTIME
consumer down while Postgres kept writing
2
CHANGES REPLAYED ON RESTART
exactly the missed ones, from the confirmed LSN
0
MANUAL REINDEXES
every mutation applied by upsert or delete-by-id

HOW IT WORKS.

A ~20-line SQL bootstrap turns on logical replication; a small Python consumer drains the slot, re-embeds changed rows (EmbeddingGemma via Ollama, 768-dim cosine), and bulk-writes XERJ. Search is one request: XERJ's hybrid query fuses the BM25 ranking and the kNN ranking with reciprocal-rank fusion — no score normalization, no merge code.

THE ONE REQUEST — BM25 + VECTOR, FUSED SERVER-SIDE

$ curl -s localhost:9200/posts/_search -H 'Content-Type: application/json' -d '{
  "query": { "hybrid": {
    "queries": [
      { "query": { "match": { "summary": "ditching the JVM search stack for something cheaper" } } },
      { "query": { "knn": { "field": "vec", "query_vector": [/* 768-dim */], "num_candidates": 9 } } }
    ],
    "fusion": { "type": "rrf", "k": 60 }
  } }
}'

Recorded top-3 for that query — RRF rank-fusion scores, shown as the fused order:

  1. Why we moved off Elasticsearch RRF 0.03279
  2. The cost of microservices RRF 0.03175
  3. Building a vector search engine from scratch RRF 0.03151

CHANGE POSTGRES, WATCH XERJ FOLLOW — THEN A CRASH-RECOVERY REPLAY

-- in Postgres:  UPDATE p7 · INSERT p9 · DELETE p5
$ python3 cdc_sync.py
[cdc] synced 2 upserts, 1 deletes to XERJ  [('p7','UPDATE'),('p9','INSERT'),('p5','DELETE')]

# consumer killed; UPDATE p1 + INSERT p11 happen while it is down
$ python3 cdc_stream.py
[stream] consuming WAL from confirmed LSN (push, no polling)
[stream] UPDATE p1  -> XERJ  LSN=27863424  [checkpointing]
[stream] INSERT p11 -> XERJ  LSN=27863976  [checkpointing]

WHAT THIS DOES NOT SHOW.

01
TINY SYNTHETIC CORPUS.
Eight seed posts (~11 after live changes), modeled on the open-source daily-api Post schema — not daily.dev's data. A functional proof: no latency, throughput, or cost numbers exist, and "within seconds" is qualitative.
02
DEMO SLOT, NOT PRODUCTION SHAPE.
The consumer reads Postgres' built-in test_decoding slot. The production shape is a pgoutput slot, wal2json, or Debezium in front of the same upsert/delete path.
03
AT-LEAST-ONCE, CONVERGENT.
Delivery is at-least-once, converging to Postgres state through idempotent upsert/delete-by-id plus LSN checkpointing — exactly-once-convergent, not bare exactly-once.
04
BRING YOUR OWN EMBEDDINGS.
XERJ stores and searches vectors; it does not embed. Quality is your model's job (EmbeddingGemma via Ollama here). RRF k and weights need per-corpus tuning; fusion:'learned' does not exist — only rrf and linear.

READ THE WALKTHROUGH.

daily.dev is a motivating party, not a customer. Every output above is recorded from the scripts in the walkthrough — setup.sql, cdc_sync.py, cdc_stream.py — against Postgres 16 + pgvector 0.8.2 in Docker. Four commands reproduce it end to end.

OPEN THE WALKTHROUGH ALL CASE STUDIES
READY?·REQUEST ACCESS

RUN IT ON
YOUR POSTGRES.

Tell us your table shape and change rate. We'll wire the CDC consumer against a copy of your schema and send you the scripts and the binary.

We only use this email to send you the binary. Ever. ✓ THANKS. CHECK YOUR INBOX WITHIN 24 HOURS.