Kosha docs
Documentation menu

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.

POST /v1/namespaces/{namespace}/search
{
  "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. 0 means 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 Text field 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_text empty is fine — the phrase supplies its own terms.
  • Sending both is not additive. With query_text and match_phrase, the phrase's terms drive scoring and query_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.

PhraseslopMatches
"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.