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_scorerView on GitHub (pinned to b5d8deb80c)
Solutions
- 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).
- Validate user input: reject or no-op empty phrases and prefixes before constructing PhrasePrefixQuery.
- Ensure the field is indexed with a tokenizer that actually produces terms for your documents.
- 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
- Reject empty phrases/prefixes at the input layer
- Check the term dictionary for the prefix before scoring
- Ensure the field's tokenizer actually produces terms
- Never build PhrasePrefixScorer directly with an empty postings vec
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
- At least one bound must be set
- At least one bound must be set
- expected json type in term
- unknown compressor id {id:?}
- actual doc store version: {doc_store_version}, max_supported
AI-assisted analysis of quickwit-oss/tantivy@b5d8deb80c (2026-09-05).
Data as JSON: /api/errors/09be4112bfd96f88.
Report an issue: GitHub.