SD System Design In Depth
0%
lesson system-designsearchconsistency

Search index synchronization

Module 5.4: Information Retrieval & Search Engines Updated Sep 17, 2026
📄 Cheat Sheet
Enabling Full-text Search with Change Data Capture - Frantisek Hartman
Official Elastic Community Enabling Full-text Search with Change Data Capture - Frantisek Hartman Watch on YouTube ↗

Directly about streaming database changes into Elasticsearch with Debezium and Kafka, including ordering and reindexing concerns.

0:00 / --:--

The Synchronization Challenge: Near-Real-Time (NRT) Indexing

Primary transactional records live in relational or NoSQL databases (PostgreSQL, DynamoDB, MongoDB), while read queries target Elasticsearch or OpenSearch. Keeping the search index synchronized with the primary database under high write volumes presents severe race conditions and consistency challenges.

CDC Pipeline with Debezium and Kafka

flowchart TD App["Merchant Service"] -->|"1. ACID Write: UPDATE products"| DB[("PostgreSQL Primary DB")] DB -->|"2. Write-Ahead Log (WAL)"| CDC["Debezium Change Data Capture (CDC)"] CDC -->|"3. Stream Mutation Event"| Kafka[("Kafka Topic: catalog.updates")] Kafka --> SyncWorker["Index Sync Worker"] SyncWorker -->|"4. Batch Bulk Index API (100 items / 500ms)"| ES[("Elasticsearch Cluster")] subgraph LuceneEngine ["Inside Elasticsearch Node"] ES --> Buffer["In-Memory Indexing Buffer"] Buffer -->|"5. refresh_interval: 1s"| Segment["New In-Memory Searchable Segment (NRT)"] Segment -->|"6. Flush: Lucene commit (translog is fsynced per request for durability)"| DiskStorage[("Persistent Disk")] end

Under the Hood: Solving Out-of-Order Updates

Because Kafka partitions are processed by concurrent worker threads, network delays can cause an older update (V₁) to arrive after a newer update (V₂). If the worker naively writes V₁, the search index is left with permanently corrupted stale data.

Source revisions and Elasticsearch concurrency tokens are different tools. A CDC event can carry a source-owned, strictly monotonic revision and use external versioning where supported. Elasticsearch's _seq_no and _primary_term are assigned by Elasticsearch and can guard a read-modify-write against the version already observed there; they are not primary-database sequence numbers. Wall-clock timestamps are unsafe unless the source guarantees monotonic, unique ordering. Whichever mechanism is used, deletes and updates must share one ordering rule.

Worked example: the out-of-order update

Product 42 changes price twice in quick succession. The database commits version 6 (price 20) then version 7 (price 18). Because of a retry, the sync worker receives version 7 first and version 6 second.

StepNaive workerWorker with external versioning
Receive v7, price 18Index price 18Index with version 7; accepted
Receive v6, price 20Index price 20, now stale foreverRejected with a version conflict because 6 is not greater than 7
Final stateWrongCorrect

The version must be source-owned and monotonic per document, for example a row version column, or a log sequence number from the WAL. Elasticsearch supports this with version_type=external. Deletes also need versions; Elasticsearch keeps delete tombstones for index.gc_deletes, 60 seconds by default, so a very late update after that window can still resurrect a deleted document unless the pipeline guards against it.

Sync strategies compared

StrategyConsistencyComplexityWhen it fits
Application dual writeCan diverge permanently on partial failureLowPrototypes only
Transactional outboxEventually consistent, ordered per aggregateMediumWhen you control the app schema
Log-based CDC such as DebeziumEventually consistent, commit orderMedium to highMultiple consumers, legacy apps
Periodic batch reindexStale by the batch intervalLowSmall catalogues, nightly freshness

Rebuilding without downtime

  1. Create a new index, for example products_v2, with the new mapping.
  2. Record the current CDC offset, then bulk-load a snapshot into products_v2.
  3. Replay CDC from the recorded offset until lag is near zero; versioning makes the overlap safe.
  4. Atomically switch the read alias from products_v1 to products_v2, keep v1 for rollback.

Latency math: with a 500 ms bulk batch, Kafka lag near zero and a 1 s refresh interval, new writes typically become searchable in about 1 to 3 seconds. Setting refresh_interval lower increases segment churn and merge cost.

Check yourself Question 1 of 3
Safe version source recall
From: Under the Hood: Solving Out-of-Order Updates

A CDC sync worker uses external versioning to reject stale updates. Which value is appropriate as the version for each document?

Key Takeaways

Use CDC (Debezium + Kafka) to tail database WAL logs and stream updates asynchronously to Elasticsearch.
Elasticsearch refresh_interval (default 1s) delivers Near-Real-Time searchability by creating in-memory Lucene segments.
Use a source-owned monotonic revision or Elasticsearch's own observed concurrency tokens correctly; do not conflate source versions with _seq_no/_primary_term.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue