quickwit-oss/tantivy · error

PhrasePrefixScorer must have at least two terms

Error message

PhrasePrefixScorer must have at least two terms

What it means

PhrasePrefixScorer::new partitions phrase terms into MultiPrefix/SinglePrefix kinds; when only one posting remains it pops it from term_postings and .expects a value, panicking with "PhrasePrefixScorer must have at least two terms" (src/query/phrase_prefix_query/phrase_prefix_scorer.rs:135) if the list is empty. The invariant is that this code path is only reached with at least two postings; reaching it empty means the caller supplied no phrase terms at all.

Solutions

  1. Before executing, check that the query has at least one term with postings for the given field/segment (or return an empty result / EmptyScorer instead of building the scorer).
  2. Validate user input: reject or no-op empty phrases and prefixes before constructing PhrasePrefixQuery.
  3. Ensure the field is indexed with a tokenizer that actually produces terms for your documents.
  4. Use query parsing with validation, or wrap scoring in catch_unwind if queries are untrusted.

Example fix

// before
let scorer = phrase_prefix_weight.scorer(reader, 1.0)?; // panics on empty expansion

// after
if phrase_prefix_weight.term_postings_count(reader) == 0 {
    return Ok(Box::new(EmptyScorer));
}
let scorer = phrase_prefix_weight.scorer(reader, 1.0)?;
Defensive patterns

Strategy: validation

Validate before calling

// before running the query: ensure the phrase expansion yields at least one term
// for this field/segment (e.g. check the term dictionary for the prefix);
// if empty, return an empty result instead of building the scorer.

Prevention

When it happens

Trigger: Constructing a PhrasePrefixQuery (directly or via a custom query plugin) whose term dictionary expansion produces zero terms — e.g. an empty phrase, a prefix that matches no dictionary terms, or a field with no indexed terms — then calling weight(...) -> scorer(...) which drives PhrasePrefixScorer::new.

Common situations: User-supplied search prefixes that match nothing in the dictionary; queries built programmatically with empty term lists; a field indexed with a tokenizer that dropped all tokens; custom queries reusing PhrasePrefixScorer internals with an empty postings vec.

Related errors


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

Appendix: source

Thrown at src/query/phrase_prefix_query/phrase_prefix_scorer.rs:135

        let max_offset = term_postings
            .iter()
            .map(|(pos, _)| *pos)
            .chain(std::iter::once(suffix_pos))
            .max()
            .unwrap();

        let phrase_scorer = if term_postings.len() > 1 {
            PhraseKind::MultiPrefix(PhraseScorer::new_with_offset(
                term_postings,
                similarity_weight_opt,
                fieldnorm_reader,
                0,
                1,
            ))
        } else {
            let (pos, postings) = term_postings
                .pop()
                .expect("PhrasePrefixScorer must have at least two terms");
            let offset = suffix_pos - pos;
            PhraseKind::SinglePrefix {
                position_offset: offset as u32,
                postings,
                positions: Vec::with_capacity(100),
            }
        };
        let mut phrase_prefix_scorer = PhrasePrefixScorer {
            phrase_scorer,
            suffixes,
            suffix_offset: (max_offset - suffix_pos) as u32,
            phrase_count: 0,
            suffix_position_buffer: Vec::with_capacity(100),
        };
        if phrase_prefix_scorer.doc() != TERMINATED && !phrase_prefix_scorer.matches_prefix() {
            phrase_prefix_scorer.advance();
        }
        phrase_prefix_scorer

View on GitHub (pinned to b5d8deb80c)