Skip to content

FTS queries permanently fail with "FtsRocksdbReducer: segments not in consecutive doc_id order" after incremental upsert/delete workload (likely interrupted optimize) #587

Description

@xuxiang900

Issue draft for alibaba/zvec

Title: FTS queries permanently fail with "FtsRocksdbReducer: segments not in consecutive doc_id order" after incremental upsert/delete workload (likely interrupted optimize)


Environment

  • @zvec/zvec 0.5.0 (Node.js binding, @zvec/bindings-darwin-arm64 0.5.0)
  • macOS 15 / arm64, running inside an Electron 40 main process (Node 22 ABI)
  • FTS field with jieba tokenizer + lowercase filter; one collection is FTS-only, a second collection has the same FTS field plus an HNSW VECTOR_FP32 field (dim 1536, cosine)

Summary

After weeks of incremental indexing (batched upsertSync + deleteSync followed by optimize() after each sync round), both collections reached a state where every FTS query — query() with fts.matchString as well as multiQuery() (FTS + HNSW with RRF rerank) — permanently fails with:

FtsRocksdbReducer: segments not in consecutive doc_id order. field=body

Once a collection is in this state:

  • Further upsertSync / deleteSync / manifest-level syncs succeed — writes don't notice anything wrong, so the application believes the index is healthy.
  • Every FTS read keeps throwing the error above.
  • The only recovery we found is deleting the collection directory and rebuilding from scratch. Incremental re-indexing cannot repair it, since existing segments are untouched.

What we found on disk

Both affected collection directories contain leftover *.tmp segment directories alongside the committed ones, e.g.:

chunks/
  0.tmp/            <- full segment layout inside: fts.3.rocksdb,
  1.tmp/               embedding.index.2.proxima, scalar.index.1.rocksdb, ...
  2.tmp/
  4
  6
  7
  ...
  8058.tmp/
  8059.tmp/
  12264.tmp/
  LOCK
  del.10
  idmap.0
  manifest.23

The .tmp directories look like segment merges that were staged but never renamed/committed — consistent with optimize() being interrupted (the host application can be quit by the user while a sync+optimize round is running). Segment numbers reaching 12k+ reflect the highly incremental workload.

Where the error comes from

The check is in src/db/index/column/fts_column/fts_rocksdb_reducer.cc (~line 137): the reducer requires every non-empty segment to satisfy segment_stats.min_doc_id == segment_stats_.back().max_doc_id + 1, i.e. strictly consecutive global doc_id ranges, and returns Status::InternalError otherwise.

Our (unverified) hypothesis: an interrupted optimize() — or repeated delete/upsert rounds interleaved with partial merges — leaves a committed segment set whose doc_id ranges have gaps or overlaps. The invariant is then violated forever after, and since writes never re-validate it, the collection is silently wedged for reads.

Questions

  1. Is the "strictly consecutive doc_id ranges" invariant expected to survive a process crash / kill during optimize()? If not, should open/recovery detect and quarantine stale .tmp segments and re-derive a consistent segment set?
  2. Is there (or could there be) a cheaper recovery path than dropping the whole collection — e.g. a repair() / forced full re-merge that rebuilds segment stats from postings?
  3. Should write paths (or optimize()) validate this invariant and surface an error early, instead of writes succeeding while every read fails?

Repro status

We don't have a minimal standalone reproduction yet — this is from a production desktop app where the collection had gone through thousands of incremental sync rounds. Happy to provide the corrupted collection's segment layout/manifest details, or test candidate fixes against the corrupted dataset (we kept a copy before rebuilding).

Possibly related to the segment-lifecycle fixes that landed recently (#566 / #569), though the reducer path itself appears unchanged since FTS was introduced in #408.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions