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
- 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?
- 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?
- 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.
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/zvec0.5.0 (Node.js binding,@zvec/bindings-darwin-arm640.5.0)jiebatokenizer +lowercasefilter; one collection is FTS-only, a second collection has the same FTS field plus an HNSWVECTOR_FP32field (dim 1536, cosine)Summary
After weeks of incremental indexing (batched
upsertSync+deleteSyncfollowed byoptimize()after each sync round), both collections reached a state where every FTS query —query()withfts.matchStringas well asmultiQuery()(FTS + HNSW with RRF rerank) — permanently fails with:Once a collection is in this state:
upsertSync/deleteSync/ manifest-level syncs succeed — writes don't notice anything wrong, so the application believes the index is healthy.What we found on disk
Both affected collection directories contain leftover
*.tmpsegment directories alongside the committed ones, e.g.:The
.tmpdirectories look like segment merges that were staged but never renamed/committed — consistent withoptimize()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 satisfysegment_stats.min_doc_id == segment_stats_.back().max_doc_id + 1, i.e. strictly consecutive global doc_id ranges, and returnsStatus::InternalErrorotherwise.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
optimize()? If not, should open/recovery detect and quarantine stale.tmpsegments and re-derive a consistent segment set?repair()/ forced full re-merge that rebuilds segment stats from postings?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.