Feature
Phrase search
match_phrase requires query terms to appear in order and close
together, not merely to appear. Kosha stores term positions in the inverted
index at write time, so a phrase match is a positional check over postings
rather than a re-scan of the document text.
{
"query_text": "",
"match_phrase": {
"field": "body",
"phrase": "breach notification policy",
"slop": 0
}
}
Parameters
- phrase string · required
- The phrase. It is tokenized with the same analyzer as indexing — whitespace split, punctuation trimmed, lowercased — so punctuation and casing in the phrase do not affect matching.
- slop int · default 0
-
How far each term may sit from its expected position.
0means strictly adjacent and in order. See Slop below for what larger values actually permit. - field string · required by the schema
-
Must be supplied, but does not currently scope the match. Kosha's term
dictionary is per segment rather than per field, so a phrase matches
positions from any
Textfield on the document. Treat this as documentation of intent until per-field postings land.
How it scores
A phrase query is not a filter bolted onto a text query — it replaces the
scoring terms. When match_phrase is present, BM25 scores the
phrase's terms, then every candidate is checked positionally:
documents that do not satisfy the phrase are dropped, and documents that do are
boosted by up to 50% over their BM25 score.
Two consequences:
-
A phrase query stands alone. Leaving
query_textempty is fine — the phrase supplies its own terms. -
Sending both is not additive. With
query_textandmatch_phrase, the phrase's terms drive scoring andquery_text's terms do not contribute. If you want "must contain these words, and prefer this phrase", the honest way to express that today is two requests, or a filter for the hard requirement.
Slop
With slop: 0, term n of the phrase must appear at exactly
the position of term 0 plus n. Raising slop widens the window each
subsequent term may occupy, measured from that expected position.
| Phrase | slop | Matches |
|---|---|---|
"breach notification" |
0 |
"…a breach notification was…" |
"breach notification" |
2 |
Also "…a breach requiring notification…" |
"notification breach" |
2 |
Also matches the original order, since the window is symmetric around the expected position |
That last row is worth internalising: the check is a distance from an expected
position, in either direction, rather than a strict left-to-right walk. A slop
of 2 or more will match transposed terms. If order matters absolutely, use
slop: 0.
Single-term phrases
A phrase that tokenizes to one term matches any document containing that term —
there is no position constraint left to enforce. It is equivalent to a one-word
query_text, with the 50% phrase boost applied.
Cost
Phrase matching decodes each term's position list, not just its document list, which is strictly more work than a plain BM25 query over the same terms — the positions are read once per term per segment, not once per candidate document. Common terms make that meaningfully more expensive, because the position lists are long.
A filter alongside the phrase is the effective lever: it prunes segments through their footer blooms before any positions are decoded.