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.