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.

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)

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.