{"record":{"id":"09be4112bfd96f88","repo":"quickwit-oss/tantivy","slug":"phraseprefixscorer-must-have-at-least-two-terms","errorCode":null,"errorMessage":"PhrasePrefixScorer must have at least two terms","messagePattern":"PhrasePrefixScorer must have at least two terms","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/query/phrase_prefix_query/phrase_prefix_scorer.rs","lineNumber":135,"sourceCode":"        let max_offset = term_postings\n            .iter()\n            .map(|(pos, _)| *pos)\n            .chain(std::iter::once(suffix_pos))\n            .max()\n            .unwrap();\n\n        let phrase_scorer = if term_postings.len() > 1 {\n            PhraseKind::MultiPrefix(PhraseScorer::new_with_offset(\n                term_postings,\n                similarity_weight_opt,\n                fieldnorm_reader,\n                0,\n                1,\n            ))\n        } else {\n            let (pos, postings) = term_postings\n                .pop()\n                .expect(\"PhrasePrefixScorer must have at least two terms\");\n            let offset = suffix_pos - pos;\n            PhraseKind::SinglePrefix {\n                position_offset: offset as u32,\n                postings,\n                positions: Vec::with_capacity(100),\n            }\n        };\n        let mut phrase_prefix_scorer = PhrasePrefixScorer {\n            phrase_scorer,\n            suffixes,\n            suffix_offset: (max_offset - suffix_pos) as u32,\n            phrase_count: 0,\n            suffix_position_buffer: Vec::with_capacity(100),\n        };\n        if phrase_prefix_scorer.doc() != TERMINATED && !phrase_prefix_scorer.matches_prefix() {\n            phrase_prefix_scorer.advance();\n        }\n        phrase_prefix_scorer","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/quickwit-oss/tantivy/blob/b5d8deb80c26924e6b007a5b1a7630f35ca64de4/src/query/phrase_prefix_query/phrase_prefix_scorer.rs#L117-L153","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet scorer = phrase_prefix_weight.scorer(reader, 1.0)?; // panics on empty expansion\n\n// after\nif phrase_prefix_weight.term_postings_count(reader) == 0 {\n    return Ok(Box::new(EmptyScorer));\n}\nlet scorer = phrase_prefix_weight.scorer(reader, 1.0)?;","handlingStrategy":"validation","validationCode":"// before running the query: ensure the phrase expansion yields at least one term\n// for this field/segment (e.g. check the term dictionary for the prefix);\n// if empty, return an empty result instead of building the scorer.","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","panic","query","phrase-prefix","invariant"],"backgroundTag":"empty-phrase-terms","analyzedSha":"b5d8deb80c26924e6b007a5b1a7630f35ca64de4","analyzedAt":"2026-09-05T13:20:51.521Z","contentChangedAt":"2026-09-05T13:20:51.521Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}