quickwit-oss/tantivy · error

Empty DocOpstamp is forbidden

Error message

Empty DocOpstamp is forbidden

What it means

During merge or apply_deletes, the writer collects per-doc operation stamps and takes their max via iterator .max().expect(...). An empty doc_opstamps list has no max, so this panic fires; the code assumes every segment being processed has at least one document with stamps.

Source

Thrown at src/indexer/index_writer.rs:244

}

/// `doc_opstamps` is required to be non-empty.
fn apply_deletes(
    segment: &Segment,
    delete_cursor: &mut DeleteCursor,
    doc_opstamps: &[Opstamp],
) -> crate::Result<Option<BitSet>> {
    if delete_cursor.get().is_none() {
        // if there are no delete operation in the queue, no need
        // to even open the segment.
        return Ok(None);
    }

    let max_doc_opstamp: Opstamp = doc_opstamps
        .iter()
        .cloned()
        .max()
        .expect("Empty DocOpstamp is forbidden");

    let segment_reader = SegmentReader::open(segment)?;
    let doc_to_opstamps = DocToOpstampMapping::WithMap(doc_opstamps);

    let max_doc = segment.meta().max_doc();
    let mut deleted_bitset = BitSet::with_max_value_and_full(max_doc);
    let may_have_deletes = compute_deleted_bitset(
        &mut deleted_bitset,
        &segment_reader,
        delete_cursor,
        &doc_to_opstamps,
        max_doc_opstamp,
    )?;
    Ok(if may_have_deletes {
        Some(deleted_bitset)
    } else {
        None
    })

View on GitHub (pinned to b5d8deb80c)

Solutions

  1. Ensure apply_deletes/index_documents are not invoked for empty segments — filter segments with max_doc()==0 before merging.
  2. Guard your merge/delete pipeline: skip segments with no documents.
  3. If hit through normal usage, report as a tantivy bug with a reproduction and upgrade to a patched version.
  4. After deleting all docs, avoid triggering merges/delete application on the now-empty segment until new documents exist.

Example fix

// before
let mut segments = reader.searchable_segments()?;
writer.merge(&segments.iter().map(|s| s.id()).collect::<Vec<_>>())?; // panics if a segment is empty
// after
let segments: Vec<_> = reader.searchable_segments()?.into_iter().filter(|s| s.meta().max_doc() > 0).collect();
if !segments.is_empty() { writer.merge(&segments.iter().map(|s| s.id()).collect::<Vec<_>>())?; }
Defensive patterns

Strategy: validation

Validate before calling

// skip empty segments before merge/delete application
let segments: Vec<SegmentId> = reader.searchable_segments()?
    .into_iter()
    .filter(|s| s.meta().max_doc() > 0)
    .map(|s| s.id())
    .collect();
if segments.is_empty() { return Ok(()); }

Type guard

fn mergeable(seg: &Segment) -> bool { seg.meta().max_doc() > 0 }

Try / catch

null

Prevention

When it happens

Trigger: Calling index_documents followed by delete application on a segment that yields an empty doc_opstamps vector — e.g. merging/deleting against an empty segment or a segment with zero matching docs in custom merge/segment-management code.

Common situations: Merging segments where one is empty; custom code manipulating doc_opstamps; indexing zero documents and then triggering delete application; edge cases after purging all documents.

Understand the failure class

Related errors


AI-assisted analysis of quickwit-oss/tantivy@b5d8deb80c (2026-09-05). Data as JSON: /api/errors/5b4725956d4b11aa. Report an issue: GitHub.