Schedule icon
Query icon
If icon
Fail icon
Scroll icon
IonToJson icon
Script icon
Log icon
SlackIncomingWebhook icon
Pause icon
Bulk icon
Request icon
Search icon
Loop icon
Queries icon

Backfill OpenSearch Documents Hidden by Bulk Rejections

Detect OpenSearch documents lost behind per-item Bulk 429s, reindex exact Postgres ids and prove the gap closed with Search counts.

Categories
Data

A Bulk API response can be HTTP 200 while individual items carry 429 rejections. A shipper that records only the HTTP status marks the batch delivered, leaving Postgres rows that search cannot find. This flow treats the source window as authoritative: it Scrolls the matching OpenSearch ids, reads the same bounded rows from PostgreSQL, computes the missing set, replays only those rows with the OpenSearch Bulk task and checks the result with a fresh Scroll plus per-bucket Search counts. If any id remains missing it writes the residual ids to a PostgreSQL dead-letter table before failing the execution.

How it works

  1. source_stats and source_rows (jdbc.postgresql.Query) validate a bounded window and store its complete authoritative rows. Null, duplicate or over-limit ids stop before any OpenSearch write.
  2. index_scroll (opensearch.Scroll) walks the complete matching index id set. IonToJson converts both streams to JSONL and compute_gap finds exact missing and unexpected ids plus source bucket counts.
  3. build_bulk renders only missing rows as stable _id index operations. Safe mode reports the measured gap. An armed run pauses for approval, re-reads both systems and refuses a stale candidate set.
  4. reindex_missing (opensearch.Bulk) sends the approved rows in bounded chunks. A second Scroll and verify_gap compute residual ids. verify_bucket_counts (opensearch.Search) logs a size-zero count for every declared bucket.
  5. A residual set is inserted into opensearch_missing_document_dead_letter before the run fails. The errors block alerts Slack on every failure path.

What you get

  • Exact missing ids, not a blind replay of the whole index.
  • A bounded source snapshot and a stale-plan check immediately before the write.
  • Idempotent Bulk indexing with a configurable chunk size.
  • Independent post-write Scroll and Search measurements with conserved counts.
  • Residual ids preserved in PostgreSQL when the gap cannot be closed.

Who it's for

  • Platform teams operating Postgres-backed OpenSearch read models.
  • Search owners whose ingestion client logs only the Bulk HTTP status.
  • On-call engineers repairing a quiet result-count shortfall without rebuilding an index.

Why orchestrate this with Kestra

Kestra keeps the source and index snapshots, approval decision, Bulk file and independent verification in one execution. Retries can safely replay stable ids, while the paused plan is re-read before writing and the dead-letter table gives the next run a durable residual list.

Tested end to end

The evidence packet records the pinned Kestra, OpenSearch and PostgreSQL versions, the degraded fixture, healthy and protected controls, dry-run and approval paths, the measured before and after counts, an ineffective write and a second fresh-fixture audit.

Case Execution Result
healthy control 3ioQg5UE1tiDDaWsmDsouH source 6, index 6, missing 0
degraded dry run 7lJRu3QjNmhw1hWuD7m9P6 source 6, index 4, missing 2, no write
declined approval 1k1BWWBsCqx7ARPBAhGbaM 2 missing, approval false, no Bulk
remediation 7JtOXZy3OTO8mIuo4yEQ6N 2 Bulk records, residual 0
repeat 44cMA5jb4ytQfeUkr2sdBc source 6, index 6, missing 0, no write
stale plan race 2HCflvKAOsYEIvBTfSDsQZ source changed during Pause, failed before Bulk

Prerequisites

  • OpenSearch permissions to search, scroll and bulk index the one declared index.
  • PostgreSQL permissions to SELECT the source window and CREATE/INSERT the dead-letter table.
  • A bounded source table or view with unique id, a comparable bucket field and those fields copied into OpenSearch _source.

Secrets

  • OPENSEARCH_HOST, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD
  • POSTGRES_URL, POSTGRES_USERNAME, POSTGRES_PASSWORD
  • SLACK_WEBHOOK_URL

Inputs

Input Purpose
index_name One target OpenSearch index
source_table Authoritative PostgreSQL relation with id and bucket columns
window_start, window_end Inclusive/exclusive bounded repair window
bucket_values Buckets independently counted by Search
max_rows, bulk_chunk Snapshot and request ceilings
dry_run, auto_remediate Safe-mode and write controls

Quick start

  1. Add the listed secrets and create a source table whose rows are also indexed with the same id and bucket fields.
  2. Set the index, source relation and window inputs. Set bucket_values to the buckets you want independently counted.
  3. Execute with dry_run: true and inspect the missing id list and before counts.
  4. For a repair, execute with auto_remediate: true, dry_run: false, approve the exact plan and inspect the post-write Scroll, Search counts and residual table.
  5. Enable hourly_window only after supplying a windowing convention for the previous hour.

How to extend

  • Populate window_start, window_end and bucket_values from a scheduler or a partition manifest.
  • Add a source version column to the stale-plan comparison when rows can change during approval.
  • Route the dead-letter table to a ticket or a bounded retry flow after the OpenSearch pressure is cleared.
  • Keep max_rows below the worker and indexer's safe memory budget.

Limits: the source view must expose the id and bucket fields in every indexed document. The flow does not delete unexpected index documents. Search bucket totals are logged for the declared bucket list; the exact residual decision comes from the independent Scroll diff.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.