Skip to content

Reference

Search API

Executes a search query against an index on the node that receives the request.

POST /v1alpha1/indexes/{name}/search

The endpoint also has a generated page stating every field it accepts and returns. See Search an index.

{
"query": [
{ "type": "text", "text": "silent spr", "fields": { "name": 3, "description": null } },
{ "field": "published", "match": { "value": true } }
],
"filters": [
{ "field": "category", "match": { "type": "in", "values": ["fiction"] } }
],
"facets": [ { "field": "category" } ],
"sort": [ { "type": "score" }, { "field": "name", "order": "asc" } ],
"locale": "sv",
"fields": ["name", "price"],
"highlight": { "fields": { "name": {} } },
"limit": 20,
"offset": 0,
"total": "estimate"
}

All request properties are optional. An empty request matches all documents in the index.

PropertyTypeDefaultDescription
queryArray[]Clauses that a matching document must satisfy. Clauses in the array are combined with an implicit AND. Evaluated clauses narrow all facet counts. If omitted, matches all documents.
filtersArray[]Refinement clauses, specified as field clauses or nested clauses. Filters narrow hits, but facets on the filtered field exclude their own filter entries from counts by default (see Facets). Unsupported clause types return search:filter:clause_unsupported. Clauses that score results return search:filter:scoring_unsupported.
facetsArray[]Fields to aggregate match counts for. See Facets. If omitted, no facet counts are calculated.
sortArray[{"type": "score"}]Order in which results are returned. If omitted, results are sorted by relevance score in descending order.
signalsArrayIndex ranking signalsDocument ranking signals used to adjust relevance scoring. Added to the ranking signals configured on the index unless signalsMode says otherwise. See Signals. If omitted, uses the ranking signals configured on the index.
signalsModeStringaddHow signals meets the ranking configured on the index: add ranks by both, replace ranks by signals alone. Supplying this without signals returns search:signal:mode_without_signals. See Signals.
rescoreObjectNoneReorders the best results of a search in a second pass without changing which documents matched. See Rescoring.
localeStringField defaultsBCP-47 locale tag used to read and return locale-specific fields. Matches the closest declared locale on each field (for example, sv-SE falls back to sv). If no matching variant exists, uses the field default.
fieldsArrayAll stored fieldsDocument fields to return with each result. Fields inside an object are specified by dotted path and returned nested inside the object. Requesting unretrievable fields returns an error (see Document source). The primary key is always included.
highlightObjectNoneFields to return highlighted snippets for. See Highlighting.
matchedObjectNoneNested object fields for which to return matched values with each hit. See Matched values.
hitsObjectNoneSpecifies an object field whose matched values return as individual hits instead of full documents. See What a hit stands for.
limitInteger10Maximum number of results to return, at most EXOFIND_SEARCH_MAX_LIMIT (default 1000). Setting limit to 0 returns the total match count without hits.
offsetInteger0Number of matching results to skip. Specify at most one of offset, after, or before.
afterStringNoneCursor string from the next property of a previous response to fetch the next page.
beforeStringNoneCursor string from the previous property of a previous response to fetch the preceding page.
pagesObjectNoneRequests numbered page metadata. Accepts an optional { "max": n } object to limit the number of page entries (default 9). Implies "total": "exact".
totalString"estimate"Counting mode for the total matching document count: "estimate" counts until exceeding the returned window; "exact" counts every matching document.
freshnessObjectNoneMinimum index state required to answer the search. Accepts an object with property atLeast containing a freshness token. See Freshness.

Clauses represent search conditions. They are structured as a tagged union where type selects the clause type. If type is omitted, the clause defaults to a field clause containing field and match.

Matches documents by the value of a single field:

{ "field": "category", "match": { "value": "fiction" } }

The targeted field must be indexed for the requested matcher usage. If the field is not configured for that usage, the request returns search:usage_unsupported.

Matches query text across one or more fields:

{ "type": "text", "text": "silent spr", "fields": { "name": 3, "description": null } }

The text clause accepts the following options:

  • fields: An object mapping field names to score weights. A field mapped to null uses the weight from its field definition. If omitted, searches all searchable fields.
  • match: Term matching mode: "all" (default), "any", "phrase", or "user". "phrase" requires terms to appear in exact order and adjacent. "user" parses search syntax such as quotes and negation (see Reading what was typed).
  • join: Combination mode for parsed query parts: "all" (default) or "any". Applies only to "match": "user". See Reading what was typed.
  • prefix: Prefix matching behavior on the final query term: "last_token" (default) matches the trailing word as a prefix; "off" requires an exact word match.
  • typos: Typo tolerance handling: "auto" (default) follows each field’s typoTolerance configuration and the typo exclusions in the search settings of the index; "off" disables typo tolerance for the clause.
  • slop: Number of intervening words permitted between terms in a phrase. Defaults to 0 (words must be adjacent).
  • relax: Query relaxation strategy when no documents match: "unmatched" (default), "words", or "off". See Finding something rather than nothing.
  • interpret: Reading of numbers, units, and field values in query text: "auto" (default) or "off". See Reading numbers and units and Reading the values of a field.
  • combine: Scope for multi-field term matching: "term" (default) or "field".

Phrase queries operate within a single field. In phrase queries, combine is ignored and terms are matched exactly as typed, regardless of field typoTolerance. Stopwords removed during text analysis leave empty positions: searching for spring of 1962 matches that sequence, but searching for spring 1962 does not. Fields defined only for autocomplete do not support phrase matching; queries omitting fields skip autocomplete-only fields, and explicitly targeting one returns search:usage_unsupported.

The slop parameter allows intervening words between phrase terms without changing their relative order. For example, silent spring with "slop": 1 matches silent green spring, but does not match spring silent. Closer terms rank higher than terms separated by more words. Setting slop above 0 with "match": "all" or "match": "any" returns search:clause:slop_unsupported.

Under "combine": "term", each term is evaluated across all targeted fields and scored in the highest-scoring field, allowing terms to appear in different fields. Document score is the sum of term scores. Under "combine": "field", a single field must satisfy match on its own, and the document is scored by its single best-matching field.

When fields produce different token counts during analysis (such as through decompounding or stopword removal), fields with identical tokenizations are grouped and evaluated together, scoring the document by the best-matching group.

Setting "match": "user" parses input using search syntax and combines the parsed parts according to join (default "all"):

{ "type": "text", "text": "running shoes \"trail ready\" -leather", "match": "user" }
SyntaxDescription
"apple watch"Matches the enclosed terms as an ordered phrase.
-leatherExcludes documents containing the term.
-"apple watch"Excludes documents containing the phrase.

All other punctuation characters are treated as literal text. Hyphens inside words (e-mail), quotation marks inside words (it"s), and hyphens without a following term remain part of the search string. An unclosed quotation mark extends to the end of the text.

User query syntax does not generate parse errors. Options configured on the clause apply to the parsed terms: fields and combine determine search targets, slop applies to quoted phrases, and prefix applies to the final loose word or the final word of an unclosed quote. Excluded terms do not apply prefix matching or typoTolerance.

If quoted text targets a field configured only for autocomplete, user mode treats the quoted phrase as individual terms rather than returning an error. A query containing only exclusions evaluates against all documents in the index. A query containing no searchable terms matches no documents.

The join option controls how parsed query parts are combined in user mode:

{ "type": "text", "text": "storage layout", "match": "user", "join": "any" }
  • "all" (default): Requires documents to match every loose word and quoted phrase.
  • "any": Matches documents satisfying at least one loose word or quoted phrase.

Excluded terms (-term) always apply regardless of join. Filters read from query text (see Reading numbers and units) are included as parts combined by join. Under "join": "any", a document matching either the remaining text or the read filter matches the query, and excluded terms still remove documents from both.

Setting join with any match mode other than "user" returns search:clause:join_unsupported.

When "join": "any" is set, query relaxation is disabled and the response omits the relaxed object. See Finding something rather than nothing.

A search in user mode reads a number typed next to a unit, or next to a comparative word, as a filter on the number field that declares the unit. The filter is part of the search that runs, so the hits, the total, and the facet counts all reflect it.

Only a text clause with "match": "user" is read. Match modes "all", "any", and "phrase" are never read. A field clause with a text matcher is never read. Quoted phrases and exclusions (-word) are never read.

A text clause is read in any position within query: at the top level, inside and, or, not, boost, or nested clauses, or within rankings of a fuse clause. The clause is replaced in place by a single clause containing the remaining query text and, for each reading, either its filter or its words as text. These parts combine according to join: under "join": "all", all parts must match; under "join": "any", matching any one part is sufficient. Excluded terms (-term) always apply regardless of join. Request filters and the filter properties of knn and fuse clauses are not read.

A search can specify multiple text clauses with the same query text, such as across root-level fields and inside a nested clause over variant fields. Each clause is read and receives the filters that its position supports. If text clauses contain different text or specify different interpret targets, none is read.

Inside a nested clause, a reading is a filter on the same list value that matched the text. Only numeric fields within that nested path can hold the filter. Declared fields outside the path are omitted from that position, and their words remain text. If no position in the search can hold a field, a number typed with its unit remains text. Specifying an interpret target outside the path of the nested clause returns search:nested:field_not_inside. A fallback inside a nested clause is read for a list value that holds nothing on the target it stands in for.

A unit is declared on a numeric field (int32, int64, float, or double; see Numeric fields). A unit can be declared on a root field or on a numeric field inside an object field. Fields containing a wildcard * are not read.

A quantity is a number accompanied by a unit or comparative words. A number alone or inside a word is treated as a text word (for example, size 44, iphone 15, 4k, mp3, or h100). A number must be a single word; numbers containing spaces (such as 1 000) are treated as separate words and are not read.

Shapes read from query text:

TypedRead as
under 100, below 100, less than 100, cheaper than 100lt 100
max 100, maximum 100, at most 100, up to 100lte 100
over 100, above 100, more than 100gt 100
min 100, minimum 100, at least 100, from 100gte 100
100 kr, 100kr, SEK 100, $100, 16 GB, 16gb, 2 gigabytesequals the number, on the field with that unit
under 100 krlt 100 on the field with that unit
100-200 kr, 100 to 200 kr, between 100 and 200, from 100 to 200gte 100 and lte 200, both inclusive

Numbers are parsed according to the search request locale, falling back to the root locale. Words match case-insensitively. Comparative words are supported for specific languages; see Comparative words. A locale without a comparative word list reads numbers written with a unit, but does not read bare comparative expressions. Reading is greedy from left to right.

Unit matching behavior:

  • Currencies: Matched by ISO 4217 code (SEK, USD, EUR), symbol ($, , kr), narrow symbol, or name in the search locale and in English. SEK also matches :-. Multi-word currency names are not matched.
  • CLDR units: Matched by short and long forms in the search locale and in English (for example, GB, gigabyte, and gigabytes for gigabyte; % for percent).
  • Custom text: Matched as exact case-insensitive text strings.

A unit may appear as its own word before or after the number, or attached directly to the number (100 kr, kr 100, 100kr, $100).

Target field selection:

  • A number typed with a unit is read on every field that declares that unit. Multiple fields declaring the same unit generate separate filters for each field.
  • A number typed without a unit (next to comparative words or in a range) is read on the single field whose unit is a currency. If the index has no currency field, or more than one currency field, the number remains text.

To read only on specific targets, set interpret to an object { "fields": [ target, ... ] } on the text clause. The object form always reads; targets cannot be combined with "off". The fields array must contain at least one target (search:clause:interpret_fields_required).

A target is an object with the following properties:

  • field (required): Target number field, as named in the index definition. A field inside a nested object is named by its dotted path (such as prices.amount). A field inside a flattened object or at the root is named the same way a field clause names it.
  • when (optional): Array of clauses that must hold where the number is read. For a field inside a nested list, the clauses must hold in the same value of the list as the number (such as prices.list equals cust-17 alongside prices.amount). For a field at the root or inside a flattened object, the clauses must hold for the document. Accepts field, text, and, or, not, and boost clauses. Specifying a nested, knn, or fuse clause inside when returns search:clause:interpret_when_unsupported. Specifying a when clause naming a field outside the nested list returns search:nested:field_not_inside.
  • fallback (optional): Array of fallback targets, each with the same target shape (including its own optional when and fallback). Fallbacks are read in order for documents that hold no value on earlier targets where when holds. For a nested target, this means the document has no value in the list where when holds and the field is set. Every target in a fallback chain must declare the same unit; specifying a fallback in another unit returns search:interpret:fallback_unit_mismatch.

Targets are validated on every search that names them, whether or not the query text contains a number. Naming a field that does not exist returns search:field_unknown. Naming a field that is not a number field or that declares no unit returns search:interpret:unit_required.

Target selection rules when targets are specified:

  • A number typed with a unit (100 kr, under 2 kg) is read on every target whose field declares that unit. A target with fallbacks counts by the unit of its first field.
  • A number typed without a unit (under 100, between 100 and 200) is read on every target whose unit is a currency, provided all named targets share the same currency. If the named targets span multiple currencies, the number remains text. Unlike the rule without targets (which requires exactly one currency field across the index), naming multiple targets that share a single currency reads the number on all of them, matching documents that satisfy any of those targets.
  • The words of a reading are still searched as text, and documents that satisfy the filter rank first.

A fallback chain evaluates each target in sequence. For example, with target prices.amount where when matches prices.list == cust-17 and fallback prices.amount where when matches prices.list == store, the text under 100 reads as:

  1. A value of prices where list is cust-17 and amount is below 100.
  2. For a document that has no value of prices where list is cust-17 and amount is set, a value of prices where list is store and amount is below 100.

A document that has a customer price of 149 and a store price of 99 does not match because the customer price exists. Flat fields work the same way: a target on price_customer with a fallback on price_store reads under 100 as price_customer < 100, or price_store < 100 for a document with no price_customer.

Example request:

{
"query": [
{
"type": "text", "match": "user", "text": "rain under 100",
"interpret": {
"fields": [
{
"field": "prices.amount",
"when": [ { "field": "prices.list", "match": { "value": "cust-17" } } ],
"fallback": [
{
"field": "prices.amount",
"when": [ { "field": "prices.list", "match": { "value": "store" } } ]
}
]
}
]
}
}
]
}

Example response:

{
"hits": [ ... ],
"interpreted": {
"filters": [
{
"field": "prices.amount",
"when": [ { "field": "prices.list", "match": { "value": "cust-17" } } ],
"match": { "type": "range", "lt": 100 },
"words": ["under", "100"],
"fallback": [
{
"field": "prices.amount",
"when": [ { "field": "prices.list", "match": { "value": "store" } } ]
}
]
}
],
"text": "rain"
}
}

On whole-number fields (int32, int64), a fractional bound rounds to the nearest integer satisfying the condition: under 99.5 becomes lt 100, at least 99.5 becomes gte 100, and max 99.5 becomes lte 99. Exact fractional equality (99.5) is not read on whole-number fields. Numbers outside the range of the integer type are not read.

The words of a filter are still searched as text. A document matches if it satisfies the read filter or contains the words as text. Documents that satisfy the filter receive a score boost and rank first. Read words are removed from the remaining query text, which evaluates using the clause’s fields, prefix, typos, and relax settings. Total counts and facet counts reflect the search with the filters included.

When a query reads quantities, the response includes an interpreted object:

{
"hits": [ ... ],
"interpreted": {
"filters": [
{
"field": "price",
"match": { "type": "range", "lt": 100 },
"words": ["under", "100", "kr"]
}
],
"text": "shoes"
}
}

The interpreted object contains:

  • filters: Array of read filters in the order their words appeared. Each entry contains field (target field name), match (matcher object in field clause format), and words (typed words converted into the filter, in order). Each entry may carry when and fallback in request shape, absent when the target had none.
  • text: Query text remaining after removing filter words. Returns an empty string if all typed text was read.

To disable reading, set "interpret": "off" on the text clause, enclose terms in quotation marks, or remove quantity words from the text. The setting turns off the reading of field values as well.

A search in user mode reads a word or a span of words that matches a stored field value as a filter on that field. For example, red nike shoes reads red as the value Red on a colour field and nike as the value Nike on a brand field, and searches shoes as text. The read filter is part of the query that runs, so hits, totals, and facet counts reflect it.

Fields are opted in through the search settings of the index with interpret. See Field settings. To be eligible, a field must be a string field configured with filter and facet and without hierarchy. A field inside an object field is specified by its dotted path. Without configured settings, no field values are read.

The same clause rules apply as for Reading numbers and units: only a text clause with "match": "user" is read, in any position within query. Quoted phrases and exclusions (-word) are never read. Inside a nested clause, a value is read as a filter on the same list value that matched the text, and only fields within that nested path can hold the filter.

Values are looked up in the index generation that answers the search request. A value can be read as soon as a document containing it is indexed, and is no longer read when the last document containing it is removed.

Matching behavior:

  • Word spans: Spans of one to three words typed adjacent to each other are looked up as a single value (for example, dark red or the north face).
  • Folding: The typed span and stored values are folded by the normalize step of the field’s autocomplete analyzer configuration, or by the default normalizer if none is configured. Case and diacritics are folded, but words are not stemmed (shoes does not match Shoe).
  • Declared labels: A span matching the label of a declared value in the search locale reads as that value. For example, röd in a Swedish search reads as the value Red when its label is Röd. A label is read only while at least one document contains the value. See Declared values.
  • Greedy longest match: Reading uses a greedy longest match from the left. At each word position, the longest span that matches a value in an opted-in field is selected, and reading continues after that span. Words within a longer matched span are not read separately.
  • Multiple matches: A span that matches values in several fields, or matches several stored spellings of a value, is read as every one of them. A document matches if it satisfies any of the read filters.
  • Evaluation order: Numbers are read first. A word that forms part of a quantity (such as 100 in under 100) is never read as a field value.

The words of a filter are still searched as text, with the filter side boosted so documents satisfying the filter rank first. Each entry in interpreted.filters contains field, a match object of { "value": ... } containing the stored spelling, and the typed words:

{
"hits": [ ... ],
"interpreted": {
"filters": [
{
"field": "colour",
"match": { "value": "Red" },
"words": ["red"]
},
{
"field": "brand",
"match": { "value": "Nike" },
"words": ["nike"]
}
],
"text": "shoes"
}
}

Specifying interpret targets on a search request (see Choosing the fields a reading may target) selects the fields a number is read on and does not affect values. The values of all opted-in fields are still read.

When "match": "all" produces zero results, the relax option controls how the engine relaxes terms:

relax valueDescription
unmatched (default)Drops words that do not exist in the index.
wordsDrops unmatched words, then drops the most common remaining words one by one until results are found.
offDoes not relax terms; returns an empty result set.

Relaxation occurs only when the initial query returns zero matches. Only loose, unquoted words are eligible for relaxation; quoted phrases and negated terms (-term) are never dropped. Relaxation stops while at least one word remains. Dropped terms still contribute to scoring, ranking documents containing those terms above documents that do not.

Relaxation does not apply to "match": "any" or to "match": "user" with "join": "any", and the response omits the relaxed object. A single term is enough to match in these modes, so no term kept documents out of the results and dropping one cannot produce more.

When a query is relaxed, the response includes a relaxed object:

{
"hits": [ ... ],
"relaxed": {
"dropped": [
{ "word": "waterproof", "reason": "unmatched" },
{ "word": "mens", "reason": "common" }
],
"text": "running shoes"
}
}

The relaxed object contains:

  • dropped: List of dropped words and the reason each was removed ("unmatched" or "common").
  • text: The effective query string used to execute the search.

Total counts and facet counts reflect the relaxed search. If zero results are caused by query filters, text relaxation is not applied.

Matches the k nearest documents by vector distance in a specified field:

{ "type": "knn", "field": "embedding", "vector": [0.1, 0.2], "k": 10,
"filter": [ { "field": "published", "match": { "value": true } } ] }
  • field: The vector field to search.
  • vector: The query vector array. The array length must match the dimensions declared in the field definition.
  • k: Number of nearest documents to return, at most EXOFIND_SEARCH_MAX_KNN_K (default 1000).
  • filter: Array of filter clauses that documents must satisfy before nearest-neighbor evaluation.

To search a vector field inside a nested object, place the knn clause inside a nested clause for that path. See Searching vectors inside a nested path.

Matches documents across several rankings, scored and merged by rank:

{ "type": "fuse", "depth": 200, "rankConstant": 60,
"rankings": [
{ "clauses": [ { "type": "text", "text": "waterproof jacket", "fields": { "name": null } } ] },
{ "clauses": [ { "type": "knn", "field": "embedding", "vector": [0.1, 0.2], "k": 200 } ], "weight": 0.5 }
],
"filter": [ { "field": "inStock", "match": { "value": true } } ] }
  • rankings: Array of rankings to run and merge. Each entry contains a clauses array combined with an implicit AND, and an optional weight (default 1).
  • depth: Number of results read from each ranking. Defaults to 100, and is at most EXOFIND_SEARCH_MAX_FUSE_DEPTH (default 1000).
  • rankConstant: Constant added to each rank before it is inverted. Defaults to 60.
  • filter: Array of clauses that narrow every ranking before it is cut to depth.

Each ranking runs independently. Documents are scored by the sum of weight / (rankConstant + rank) across the rankings that reached them, where rank 1 is the top result of a ranking. Because the clause reads only result positions, scores from different scales (such as BM25 text relevance and vector similarity) combine without normalization.

Reciprocal rank fusion provides the following behaviors:

  • Documents that rank well across multiple rankings outrank documents placed first in only one ranking.
  • Poorly performing rankings contribute noise rather than dominating results. This makes it safe to fuse rankings from unverified sources, such as vectors generated from user profiles.
  • The fused score is close to 1 / rankConstant. A scoring clause placed beside the fuse clause adds its own score scale on top. Rank with the fuse clause and use clauses beside it for filtering.
  • Lower rankConstant values increase the weight of the highest-ranked results in each ranking. Higher values flatten the difference across ranks, giving more weight to documents found by multiple rankings.
  • The weight property scales a ranking’s contribution relative to other rankings. It cannot reorder results within that ranking, because child clauses determine ranking order.
  • Conditions that every result must satisfy belong in filter rather than beside the fuse clause. A knn clause inside a ranking applies filter entries as a pre-filter, ensuring the vector ranking returns k results. Clauses placed beside the fuse clause filter the merged list after each ranking is cut to depth, which can produce fewer results.

The fuse clause is a top-N clause that matches at most depth results per ranking:

  • Total match counts reflect the merged list, not all matching documents in the index.
  • Facet counts aggregate only documents in the merged list.
  • Pagination cannot exceed the merged list. Set depth high enough to cover all requested result pages.
  • Providing an explicit sort orders the merged list by that sort instead of the fused score.
  • The explain endpoint reports the rank assigned by each ranking and its contributed score.
  • Each pass over a fused query runs its child rankings again. Calculating facet counts on a fused search executes child rankings multiple times.

Fusing rules and error conditions:

  • Specifying fewer than two rankings returns search:clause:rankings_too_few.
  • Specifying a ranking with no clauses returns search:clause:ranking_empty.
  • Setting a ranking weight below 0 or to a non-finite number returns search:clause:weight_out_of_range.
  • Setting depth below 1 returns search:clause:depth_out_of_range.
  • Setting rankConstant to 0, below 0, or to a non-finite number returns search:clause:rank_constant_out_of_range.
  • Specifying a knn clause inside a ranking of a search with hits returns search:hits:knn_unsupported.

Matches documents where a single element of a nested object field satisfies all child clauses:

{ "type": "nested", "path": "variants", "clauses": [
{ "field": "variants.color", "match": { "value": "red" } },
{ "field": "variants.price", "match": { "type": "range", "lt": 20 } }
] }
  • path: Name of the nested object field.
  • clauses: Array of clauses evaluated within a single nested object value. An empty array matches any document where the object field is present.
  • score: Scoring mode for aggregating matching nested values: "max" (default), "min", "avg", or "total".

A nested clause must target a nested object field path. Using a nested clause on a flattened object field returns search:nested:path_not_nested. Using a nested clause on a non-object field returns an error. Child clauses may include field, text, knn, and, or, not, and boost. Including a root-level clause such as another nested or a fuse clause returns search:nested:clause_unsupported.

A text clause inside a nested clause searches across all fields in the nested path when fields is omitted:

{ "type": "nested", "path": "variants", "score": "total", "clauses": [
{ "type": "text", "text": "waterproof leather" }
] }

A knn clause inside a nested clause searches a vector field defined within that nested path:

{ "type": "nested", "path": "chunks", "clauses": [
{ "type": "knn", "field": "chunks.embedding", "vector": [0.1, 0.2], "k": 20,
"filter": [ { "field": "chunks.lang", "match": { "value": "en" } } ] }
] }
  • k counts nested values rather than documents. A document that contains multiple nearest values occupies multiple positions in k.
  • The knn filter names fields inside the path and narrows candidate values before the nearest are picked.
  • Clauses placed beside the nested clause narrow afterwards and can return fewer than k results.
  • Naming a field of the index inside the nested clause returns search:nested:field_not_inside.
  • Naming a nested vector field outside a nested clause returns search:nested:field_outside.

To return matched nested values as individual hits, set hits to the nested path.

Logical conjunction clauses that combine child clauses:

  • and: All child clauses must match.
  • or: At least one child clause must match.
  • not: No child clauses may match.

Each logical clause accepts a clauses array of child clauses.

Increases the relevance score of documents that satisfy child clauses without excluding non-matching documents:

{ "type": "boost", "weight": 2, "clauses": [ { "field": "featured", "match": { "value": true } } ] }
  • weight: Multiplier applied to matching documents. Values greater than 1 increase score; values between 0 and 1 decrease score.
  • clauses: Array of clauses required to apply the boost weight.

Leaving out weight, or setting it below 0 or to a non-finite number, returns search:clause:weight_out_of_range.

Matchers define criteria evaluated against field values in a field clause. A matcher is structured as a tagged union where type selects the matcher type. If type is omitted, the matcher defaults to equals.

MatcherShapeDescription
equals{ "value": v }Matches field values equal to value.
in{ "type": "in", "values": [v, ...] }Matches field values equal to any value in values. An empty array matches no documents.
any{ "type": "any" }Matches any document that contains a value for the field.
prefix{ "type": "prefix", "value": "EX-" }Matches string field values starting with value, evaluated against the entire field value.
under{ "type": "under", "path": "Men/Shoes" }Matches values at or below the specified path in a hierarchical tree. Requires a field configured with hierarchy. Path segments must match complete levels.
range{ "type": "range", "gte": 10, "lt": 20 }Matches values within bounds. Accepts inclusive (gte, lte) and exclusive (gt, lt) bounds. At least one bound is required.
ranges{ "type": "ranges", "values": [ { "gte": 10, "lt": 20 }, { "gte": 50 } ] }Matches values falling within any of the specified range objects. Each range requires at least one bound. An empty array matches no documents.
text{ "type": "text", "text": "..." }Matches text within a single field using field-level analysis. Supports match, prefix, typos, slop, and relax options.
distance{ "type": "distance", "lat": 59.3, "lon": 18.1, "radius": 5000 }Matches geopoint values within radius meters of the specified latitude and longitude coordinates.

Specifying a matcher unsupported by the target field type returns an error.

Sort objects specify the ordering of returned hits. A sort is structured as a tagged union where type defaults to a field sort when omitted. If order is omitted, score sorts default to descending and field sorts default to ascending ("asc").

SortShapeDescription
field{ "field": "name", "order": "asc" }Sorts by field value ("asc" or "desc"). The target field must have sorting enabled.
score{ "type": "score" }Sorts by document relevance score in descending order.
distance{ "type": "distance", "field": "location", "lat": 59.3, "lon": 18.1 }Sorts by distance from the specified geographic coordinate, nearest first. Does not accept an order property.

Configured index tie-breaker sorts are automatically appended after the requested sorts.

To sort by a field inside a nested object, specify the dotted field path:

"query": [
{ "type": "nested", "path": "variants", "clauses": [
{ "field": "variants.color", "match": { "value": "red" } }
] }
],
"sort": [ { "field": "variants.price", "order": "asc" } ]

Sorting uses only the nested values that matched the query’s nested clauses. Clauses inside or, not, or boost clauses do not filter values for sorting. If a query contains no nested clauses on the path, all nested values are considered. For ascending sorts, the document is sorted by its lowest matching value; for descending sorts, by its highest matching value. Documents with no matching nested values use the missing behavior configured in the field definition.

Specifying a distance sort on a nested object field returns search:sort:nested_unsupported.

Ranking signals modify relevance scores by evaluating document field values:

"signals": [
{ "field": "purchases", "saturation": { "pivot": 50 } },
{ "field": "published", "decay": { "halfLife": 604800 }, "weight": 0.5 },
{ "field": "popularity", "linear": { "ceiling": 1 } }
]

Each ranking signal names exactly one shape. saturation and linear read number fields, and decay reads timestamp fields. The shapes and their parameters are described under Signals in the field types reference. A ranking signal reads a field defined for sort, or a signal field whose value is refreshed in place.

A search request adds its ranking signals to the ranking configured on the index. The signalsMode property controls this:

ValueBehavior
add (default)Ranks by the index’s ranking signals and the request’s ranking signals together. A ranking signal in the request that names the same field as one on the index replaces it instead of compounding with it.
replaceRanks by the request’s ranking signals alone. An empty array then ranks results solely by text match score.

Use add to layer a per-request ranking signal, such as user affinity, on top of ranking the index owns. A later change to the index’s ranking then still reaches these searches. Use replace to try out a complete ranking before adopting it:

"signals": [ { "field": "brandAffinity", "saturation": { "pivot": 5 } } ],
"signalsMode": "add"

Omitting signals leaves the search to the index’s ranking, whatever signalsMode says. Supplying signalsMode without signals returns search:signal:mode_without_signals.

Ranking signals apply only when results are ordered by relevance. Providing an explicit sort overrides ranking signal ordering.

Targeting an unknown field returns search:field_unknown. Targeting a field without sorting enabled returns search:usage_unsupported. Specifying a ranking signal function unsupported by the field type returns search:matcher:type_unsupported.

Rescoring reorders the best results of a search in a second pass without changing which documents matched.

The first pass ranks every match by relevance. The best window results of that pass are scored again by the boosts and ranking signals in the rescore block. The final score is first + weight * second. Results below window keep their first-pass relevance score.

"rescore": {
"window": 200,
"boost": [ { "field": "brand", "match": { "value": "aurora" } } ],
"signals": [ { "field": "purchases", "saturation": { "pivot": 50 } } ],
"weight": 0.5
}

The rescore block is configured in the search request only. It is not stored on the index or in search settings. Facets and total counts are computed during the first pass; rescoring does not change facet counts or totals.

OptionTypeDefaultDescription
windowIntegerRequiredNumber of best results to score a second time. Must be at least offset plus limit, and at most EXOFIND_SEARCH_MAX_RESCORE_WINDOW (default 1000).
boostArray[]Clauses that lift results satisfying them. Clauses do not filter or narrow search hits. Wrap a clause in boost to adjust its weight.
signalsArray[]Document values taken into the second score, using the same syntax as top-level signals. Applied to every result in the window.
weightNumber1Multiplier applied to the second-pass score before adding it to the first-pass score. Must be a finite number greater than or equal to 0.

Rescoring rules and error conditions:

  • The rescore block must contain at least one boost or signals entry. An empty block returns search:rescore:empty.
  • Omitting window returns search:rescore:window_required.
  • Setting window below 1 or above EXOFIND_SEARCH_MAX_RESCORE_WINDOW returns search:rescore:window_out_of_range.
  • Setting weight below 0 or to a non-finite number returns search:rescore:weight_out_of_range.
  • Specifying rescore on a search with hits returns search:rescore:hits_unsupported.
  • Rescoring applies only when results are ordered by relevance. Providing an explicit sort overrides rescoring.
  • The explain endpoint ignores rescore and explains only the first-pass score.

The window is ranked from the first result on every request. Paging works differently inside and below the window:

  • Inside the window, the next and previous cursors count results rather than encoding positions. The response reports an offset for these pages.
  • The next cursor from the last page in the window continues below the window. Results there keep the order relevance gave them and receive no second-pass scoring. A search with nothing below the window returns no next, the same as any other last page.
  • Send the same rescore block with each pagination request. Cursors carry positions, not the search that produced them.
  • Numbered pages stop at the window.
  • A request whose offset plus limit reaches past the window returns search:rescore:window_too_small.

Facets compute match counts for distinct values of specified fields. The target field must have facet enabled in its field definition; otherwise, the request returns search:usage_unsupported.

"filters": [
{ "field": "category", "match": { "type": "in", "values": ["fiction"] } }
],
"facets": [
{ "field": "category", "limit": 20 },
{ "name": "years", "field": "published_year", "order": "value" }
]
OptionTypeDefaultDescription
nameStringField nameKey used for the facet in the response. Required when faceting on the same field multiple times. Duplicate facet names return search:facet:name_duplicate.
fieldStringRequiredTarget field to aggregate.
limitInteger10Maximum number of facet values to return, from 1 to EXOFIND_SEARCH_MAX_FACET_VALUES (default 1000). Other values return search:facet:limit_out_of_range.
orderString"count"Sort order of facet values: "count" (descending by count), "value" (ascending by value), or "declared" (the order configured in search settings, followed by undeclared values sorted by count). See Declared values.
rangesArrayNoneArray of range bucket definitions. See Range buckets. Cannot be combined with limit or order (search:facet:ranges_conflicting).
pathStringRootStarting path level for hierarchical fields. See Counting down a tree.
depthInteger1Number of hierarchical levels below path to count (1 to 10).
excludeFiltersArrayFacet fieldList of field paths whose filter entries are excluded from this facet’s calculation. Defaults to the facet’s own field path. An empty array [] disables filter exclusion. A blank path returns search:facet:exclude_filters_invalid.

The response returns facet counts under the facets object:

"facets": {
"category": {
"values": [
{ "value": "fiction", "count": 87, "label": "Fiction" },
{ "value": "poetry", "count": 21, "label": "Poetry" }
],
"totalValues": 14
}
}
  • values: Array of facet value objects containing value and count. Includes label if the search settings declare a label for the value in the search locale. See Declared values.
  • totalValues: Number of distinct values that match the query, counting the ones the limit leaves out. It counts values, not documents.

A facet ordered by declared returns values with a declared order first, sorted by their declared order, followed by undeclared values sorted by count. If the search settings declare no order for the field, values return sorted by count. The limit truncates the list from the top, so a limit smaller than the number of declared values returns no undeclared values.

Setting ranges computes counts across defined ranges on numeric and timestamp fields:

"facets": [
{ "field": "price", "ranges": [
{ "to": 100 },
{ "from": 100, "to": 200 },
{ "from": 200 }
] }
]

Range rules:

  • A range bucket includes values from from (inclusive) up to to (exclusive).
  • Either from or to may be omitted for open-ended ranges, but not both (search:facet:range_empty).
  • to must be greater than from (search:facet:range_invalid).
  • Maximum 1000 buckets per facet (search:facet:ranges_too_many).
  • Using ranges on unsupported field types returns search:matcher:type_unsupported.

The response returns range counts in the buckets array:

"facets": {
"price": {
"buckets": [
{ "to": 100, "count": 41 },
{ "from": 100, "to": 200, "count": 17 },
{ "from": 200, "count": 3 }
]
}
}

Selected buckets can be filtered in subsequent requests using a ranges matcher:

"filters": [
{ "field": "price", "match": { "type": "ranges", "values": [ { "lt": 100 }, { "gte": 200 } ] } }
]
  • query clauses: Narrow all facet counts and all search hits.
  • filters clauses: Narrow search hits and facet counts for all fields except facets configured to exclude those filter paths (by default, facets on the same field).
  • Filter granularity: Filter exclusions apply to whole filter entries. Separate conditions into distinct filter entries to allow independent facet exclusion.
  • Locales: Locale-specific fields are aggregated using the locale variant selected for the search.
  • Totals: Calculating facets always computes an exact total count. Setting "limit": 0 with facets returns facet counts without fetching document hits.
  • No “other” count: A facet returns no count for the values the limit leaves out. Compare totalValues with the length of values to see how many values are missing, and raise the limit to get counts for them.

For fields configured with hierarchy, facets aggregate counts by hierarchy level:

"facets": [ { "field": "category", "path": "Men", "depth": 2 } ]

The response returns nested hierarchy levels:

"facets": {
"category": {
"values": [
{ "value": "Shoes", "path": "Men/Shoes", "count": 42, "totalValues": 2,
"values": [
{ "value": "Running", "path": "Men/Shoes/Running", "count": 28 },
{ "value": "Casual", "path": "Men/Shoes/Casual", "count": 14 }
] },
{ "value": "Outerwear", "path": "Men/Outerwear", "count": 9 }
],
"totalValues": 2
}
}
  • value: The label of the current level.
  • path: The full path to the level, used in under filter matchers.
  • limit, order, and totalValues: Evaluated independently per hierarchy level. A level returns no count for the child values its limit leaves out.

A document is counted once at each ancestor level in its path. Specifying path or depth on non-hierarchical fields returns search:usage_unsupported. Combining path or depth with ranges returns search:facet:ranges_with_tree.

Faceting on a dotted path inside a nested object counts matching parent documents for each value:

"facets": [ { "field": "variants.color" } ]

Counts reflect parent documents; a document with multiple matching nested values counts once. Only nested values that satisfy the query’s nested clauses are included in facet counts.

A filter on a nested field is specified as a nested clause in filters:

"filters": [
{ "type": "nested", "path": "variants",
"clauses": [ { "field": "variants.color", "match": { "value": "red" } } ] },
{ "type": "nested", "path": "variants",
"clauses": [ { "field": "variants.price", "match": { "type": "range", "lte": 20 } } ] }
]

Filter exclusions identify entries by the most specific path covering all clauses within the entry. A filter entry covering both variants.color and variants.price is treated as a filter on variants.

A facet returns at most EXOFIND_SEARCH_MAX_FACET_VALUES values (default 1000), so a field with more values than that cannot be reached from the values a search answers with. A separate endpoint answers the values of one facet field that start with a typed prefix, counted under the query and filters of a search:

POST /v1alpha1/indexes/{name}/facets/{field}/values

The endpoint also has a generated page stating every field it accepts and returns. See Search the values of a facet.

The field must have facet enabled in its field definition; otherwise, the request returns search:usage_unsupported. The request carries the query and filters of the search, and what the values start with:

{
"query": [ { "type": "text", "text": "running shoes" } ],
"filters": [ { "field": "brand", "match": { "type": "in", "values": ["Nike"] } } ],
"prefix": "adi",
"limit": 5
}
OptionTypeDefaultDescription
queryArrayNoneClauses that a counted document must satisfy, in the same shape as the query of a search.
filtersArrayNoneRefinement clauses, in the same shape as the filters of a search. Filter entries on the facet’s own field are left out of the counts.
prefixStringNonePrefix that returned values, or their labels in the request locale, must start with. If omitted or blank, returns all values.
localeStringField defaultBCP-47 locale tag used to read locale-specific fields and select labels for declared values, matching search request behavior.
limitInteger10Maximum number of values to return, from 1 to EXOFIND_SEARCH_MAX_FACET_VALUES (default 1000). Other values return search:facet:limit_out_of_range.
orderString"count"Sort order of the values: "count" (descending by count), "value" (ascending by value), or "declared" (the order configured in search settings, followed by undeclared values sorted by count).

The response returns the values in the same shape as the values of a facet:

{
"values": [
{ "value": "adidas", "count": 87 },
{ "value": "Adidas Originals", "count": 12 }
],
"totalValues": 2,
"generation": "2",
"tookMs": 1.208
}
  • values: Array of facet value objects containing value and count, in the requested order and capped by limit. Includes label if the search settings declare a label for the value in the request locale.
  • totalValues: Number of distinct values that start with the prefix, counting the ones the limit leaves out. It counts values, not documents.
  • generation: Name of the generation that answered. A request that names the index answers from the generation that is live when it arrives, so add @ and this name to the index name to send a later request to the same data. See Names and generations.
  • tookMs: Execution time in milliseconds.

Matching rules:

  • String fields: The prefix and the values are compared folded in case and Unicode form by the normalize filter of the field’s autocomplete chain, or of the chain the engine builds for autocomplete when the field declares none. finds Röd. Accents are folded only when the chain holds an asciiFolding filter. See Analysis.
  • Declared labels: The prefix is also compared, using the same folding rules, against the label of each declared value in the request locale. If the prefix matches a label, the endpoint returns the underlying value. For example, matches the value red when its Swedish label is Röd. See Declared values.
  • Number, boolean, and timestamp fields: The prefix is compared with the value as a search response shows it, ignoring case. 19 finds every year of the nineties, and 2024-06 finds a month of timestamps.
  • Hierarchical fields: A field configured with hierarchy returns search:facet:prefix_unsupported.
  • Counts: The counts are the ones a facet of the same search answers. The query and the filters on other fields narrow them, and the filter entries on the facet’s own field are left out. A search that relaxes its query counts under the relaxed query.
  • Limits: The query and filters count against EXOFIND_SEARCH_MAX_CLAUSES and EXOFIND_SEARCH_MAX_CLAUSE_DEPTH, limit against EXOFIND_SEARCH_MAX_FACET_VALUES, and counting stops at EXOFIND_SEARCH_TIMEOUT, as for a search.
  • Metrics: The duration of these requests is measured by the exofind.facet.values timer, apart from searches. See Metrics.
POST /v1alpha1/indexes/{name}/suggest

The endpoint also has a generated page stating every field it accepts and returns. See Suggest what to search for.

Suggestions come only from the fields the search settings opt in with suggest (see Field settings). An index whose settings suggest no field answers an empty list.

{
"text": "adi",
"filters": [ { "field": "category", "match": { "value": "Shoes" } } ],
"limit": 5
}
FieldTypeDefaultDescription
textStringNoneWhat has been typed so far. If omitted or blank, returns the most common values.
localeStringField defaultBCP-47 locale tag used to read locale-specific fields and to pick the labels of declared values, matching search request behavior.
filtersArrayNoneRefinement clauses, in the same shape as the filters of a search. A filter on a suggested field is left out of that field’s own counts.
limitInteger5Maximum number of suggestions to return, from 1 to EXOFIND_SUGGEST_MAX_LIMIT (default 100). Other values return search:suggest:limit_out_of_range.
typosString"auto"Whether values one mistake away from the text may be suggested: "auto" or "off".

The response returns the suggestions:

{
"suggestions": [
{ "text": "adidas", "typed": 3, "field": "brand", "value": "adidas", "count": 87 },
{ "text": "Adidas Originals", "typed": 3, "field": "brand", "value": "Adidas Originals", "count": 12 }
],
"generation": "2",
"tookMs": 0.412
}
  • text: What to show and to search for. The label of the value in the request locale where the search settings declare one, otherwise the value itself. Where the typed text starts the value but not the label, the value is shown.
  • typed: How many characters at the start of text the typed text covers, so the part typed can be marked apart from the part that completes it. Returns 0 when the suggestion was found a mistake away, and 0 for a blank text.
  • corrected: true when the suggestion was found one mistake away from the text. Omitted otherwise.
  • field: The field holding the value, as the index definition names it (dotted path for a field inside an object).
  • value: The value as the field stores it, which a filter on the field matches.
  • label: The label the search settings declare for the value in the request locale (falling back to the field’s default locale). Omitted when none is declared.
  • count: Number of documents holding the value under the filters.
  • generation: Name of the generation that answered. A request that names the index answers from the generation that is live when it arrives, so add @ and this name to the index name to send a later request to the same data. See Names and generations.
  • tookMs: Execution time in milliseconds, including fractions of one.

Matching rules:

  • Folding: The text and the values of a field are compared folded in case and Unicode form by the normalize filter of the field’s autocomplete analyzer chain, or of the chain the engine builds for autocomplete when the field declares none. finds Röd. Accents are folded only when the chain holds an asciiFolding filter. Words are not stemmed (shoes does not find Shoe).
  • Declared labels: The text is also compared with the label of each declared value in the request locale, so suggests the value red labelled Röd in Swedish. A declared value no document holds is never suggested.
  • Whole-value prefix: The comparison is against the start of the whole value, not of each word: air does not find Nike Air Max. Infix matching is not supported.
  • Ordering: Suggestions are ordered by count descending; ties are broken by field name then by value, so the order is stable.
  • Typo tolerance: When fewer values than limit start with the text, typos is auto, and the text is at least 5 characters long, values within one edit (insertion, deletion, substitution, or transposition of adjacent characters) of the text are suggested after the ones the text starts. The first character of the text is never read as a mistake. Corrected suggestions come after exact suggestions and carry corrected: true and typed: 0.
  • Counts and sideways filters: Counts are calculated under the request filters. A filter on a suggested field is left out of that field’s own counts, so a filter already selected keeps other values of the field suggestable.
  • Limits: The filters count against EXOFIND_SEARCH_MAX_CLAUSES and EXOFIND_SEARCH_MAX_CLAUSE_DEPTH, and limit against EXOFIND_SUGGEST_MAX_LIMIT. A suggest request that runs longer than EXOFIND_SUGGEST_TIMEOUT (default 2s) returns HTTP 503 search:timeout and drops collected counts.

A filter panel that completes the values of one facet uses the facet values endpoint (POST /v1alpha1/indexes/{name}/facets/{field}/values) instead. The suggest endpoint spans every opted-in field and marks the typed part of each suggestion.

The duration of suggest requests is measured by the exofind.suggest timer. See Metrics.

Highlighting returns matched text fragments for specified fields:

"highlight": {
"fields": {
"name": {},
"description": { "fragments": 1, "length": 80, "pre": "<b>", "post": "</b>" }
}
}

Fields must have highlighting enabled in their field definitions (matching or autocomplete). Requesting highlighting on an unconfigured field returns search:usage_unsupported.

OptionTypeDefaultDescription
fragmentsInteger3Maximum number of fragments to return.
lengthInteger150Target character length per fragment (1 to 10000). Fragments break on sentence boundaries.
preString"<em>"Prefix tag inserted before highlighted terms.
postString"</em>"Postfix tag inserted after highlighted terms.

Highlighting rules:

  • Fragments are generated only from scoring clauses. Non-scoring filter clauses do not produce highlights.
  • Prefix-matched terms and typo-corrected terms are highlighted as full matched words in the source text.
  • Locale-specific fields highlight the variant matched by the search.
  • Text beyond the first 10,000 characters of a field value is not evaluated for highlighting.
  • Highlighted text is not HTML-escaped.
  • A field below a nested list highlights only on value hits for that list. Naming a nested field on a search that returns document hits returns search:nested:field_outside. See What a hit stands for.

Returns matched values of a nested object field for each hit:

"matched": {
"fields": {
"variants": { "limit": 3, "fields": ["variants.color"] }
}
}

Targeting a field that is not a nested object returns search:matched:field_not_nested.

OptionTypeDefaultDescription
limitInteger3Maximum number of matched values to return per hit (1 to 100).
fieldsArrayAll object fieldsField paths inside the nested object to include in each returned value.

Field paths in fields must reside under the target object path (search:matched:field_not_inside) and exist in the schema (search:field_unknown). If the index has document source set to none, a named field must be stored (search:usage_unsupported). Naming a child object returns search:source_not_kept.

Response format under each hit:

"matched": {
"variants": {
"values": [ { "color": "red", "size": "M", "price": 19.5 } ],
"totalValues": 3
}
}
  • values: Array of matched nested values, up to limit. If scoring clauses exist within the nested clause, values are ordered by score; otherwise, they appear in document order. If document source is none, each value contains its stored fields, and values is omitted when none of the value’s fields are stored.
  • totalValues: Total count of matched values for that nested field in the document.

Setting hits causes each matched value of a nested object field to return as an individual hit instead of a document hit:

"hits": { "path": "variants" }

When hits is configured, totals count matching nested values, facets count value hits, and pagination cursors step through values. Targeting a field that is not a nested object returns search:hits:path_not_nested.

Adding when narrows expansion to the documents it matches, leaving the rest as document hits. See Expanding only some documents.

OptionTypeDefaultDescription
pathStringRequiredDotted path of the nested object field whose matched values become hits.
fieldsArrayAll object fieldsDotted field paths inside the nested object to return in value.
whenArrayAll matching documentsClauses selecting which documents expand into value hits; other matching documents return as document hits. See Expanding only some documents.

Field names in fields must be prefixed by path (search:hits:field_not_inside) and exist in the index (search:field_unknown). If document source is none, a named field must be stored (search:usage_unsupported). Naming a child object returns search:source_not_kept.

Hit response structure:

{
"id": "9781234567890",
"index": 2,
"key": "V-2",
"score": 8.42,
"value": { "sku": "V-2", "color": "red", "size": "M", "price": 19.5 },
"document": { "name": "Trail Tee", "brand": "Ridge" }
}
  • id: Primary key of the parent document. Multiple hits share an id when a document contains multiple matching values.
  • index: Zero-based array index of the value in the parent document. Present on every value hit.
  • key: Value of the key field when the object field declares a key. Present even when hits.fields does not request the key child field. Omitted for document hits, for fields with no key, and on an index whose source is none when the key child field is not stored. See the object section of the field types reference for how to declare a key.
  • value: The matched nested value object. If document source is none, it contains the value’s stored fields, and is omitted when none of the value’s fields are stored.
  • document: Selected fields of the parent document per the search request’s fields property.

The identity of a value hit is id combined with key where a key is declared, and id combined with index otherwise. The key survives a reindex, while index does not because reindexing can reorder values. Cursors over value hits still step by position.

Value hit scoring combines the parent document score (including ranking signals) with the nested value’s clause score. sort can order by score or by fields within the nested object path. Specifying index root fields in sort returns search:hits:sort_unsupported; specifying distance sort returns search:hits:sort_unsupported. Index tie-breaker sorts are ignored.

Setting hits cannot be combined with:

  • matched (search:hits:matched_unsupported)
  • knn clauses on a field of the index (search:hits:knn_unsupported)

A knn clause inside a nested clause for the same path is allowed, and makes the nearest values the hits. See Searching vectors inside a nested path.

highlight can name fields inside path, and each value hit returns fragments cut from its own value. Naming any other field returns search:hits:highlight_field_not_inside. A document returning as a document hit under when carries no fragments. See Highlight matches inside sub-documents.

Specifying when restricts value expansion to documents that match the when clauses. All other matching documents return as document hits, returning both hit types in a single result page:

"hits": {
"path": "variants",
"when": [ { "field": "splitVariants", "match": { "value": true } } ]
}

The when array accepts field and nested clauses combined with an implicit AND. Scoring clauses are not permitted. Unsupported clause types return search:hits:when_clause_unsupported; clauses that score return search:hits:when_scoring_unsupported. Both errors point to hits.when[<index>] in the request body. If when is omitted, every matching document expands.

When when is configured:

  • Sorting: Mixed result pages can only be sorted by score. Field sorts return search:hits:when_sort_unsupported (pointing to sort[<index>]), or search:hits:when_sort_unsupported when calling the engine directly. Distance sorts return search:hits:sort_unsupported.
  • Scoring: Every hit receives its parent document relevance score. Nested value clause scores are not added.
  • Facets: Facet counts aggregate matching documents rather than hits.
  • Totals: The total property counts hits, counting expanded documents once per matching nested value. The response includes a documents object with count and exact fields reporting the total count of matching documents.
  • Empty nested values: A document that matches when but contains no matching values under path returns no hit. It is not returned as a document hit. The document still contributes to documents and facet counts.
  • Pagination cursors: Cursors are keyed by hit type (document hits, value hits, or mixed hits). A cursor generated for a query using when is rejected by queries without when, and vice versa.
{
"hits": [
{
"id": "9781234567890",
"score": 8.42,
"document": { "name": "Silent Spring", "price": 19.5 },
"highlights": { "name": ["<em>Silent</em> Spring"] }
}
],
"total": { "count": 128, "exact": false },
"page": { "limit": 20, "offset": 0, "next": "AW8..." },
"generation": "2",
"freshness": "AQoIcHJvZHVjdHMSATIYBw",
"tookMs": 7.412
}
PropertyTypeDescription
hitsArrayArray of hit objects matching the query. Each hit contains id, score (omitted if the search computed no scores - a search of plain filters, with no ranking signal and no rescore), document fields, and optional highlights, matched, index, key, or value properties.
totalObjectMatch count object containing count (integer) and exact (boolean indicating whether count is exact or a lower bound). Counted in whatever the search returns, so a document expanded by hits.when counts once per value.
documentsObjectTotal count of matching documents, in the same shape as total. Present only when hits.when is set; omitted otherwise.
facetsObjectMap of facet names to facet results. Omitted if facets was not requested.
pageObjectPagination state containing limit, offset (omitted when navigating via cursor), and optional next and previous cursor strings.
relaxedObjectDetails of dropped terms when query relaxation was applied. Omitted if the query was not relaxed.
interpretedObjectThe filters read out of the query text, and the remaining query text. Omitted when nothing was read. See Reading numbers and units.
generationStringName of the generation that answered. A request that names the index answers from the generation that is live when it arrives, so add @ and this name to the index name to send a later request to the same data. See Names and generations.
freshnessStringFreshness token naming the index state that produced the response. Pass as freshness.atLeast on subsequent requests to evaluate against this state or a later one. See Freshness.
tookMsNumberExecution time for the search request in milliseconds.

A locale specific field returns the single variant matched for the query locale:

"document": {
"id": "1",
"name": { "sv": "röda löparskor" }
}
  • The object key is the declared variant that was read (for example, sv when queried with sv-SE).
  • If a document contains no value for that variant, the field is omitted from document.
  • If the index is configured to backfill unpopulated locales, the backfilled value is returned.

Requesting "pages": {} adds a pages object inside page:

"pages": {
"count": 7,
"previous": { "number": 1, "cursor": "..." },
"next": { "number": 3, "cursor": "..." },
"start": [ { "number": 1, "cursor": "..." }, { "number": 2, "cursor": "...", "current": true } ],
"end": [ { "number": 7, "cursor": "..." } ]
}

Page metadata is divided into start, middle, and end arrays. The current boolean marks the current page. end is omitted if the final page exceeds maximum page depth. Page numbers are 1-based.

Explains how a specific document or value hit scores for a search query.

POST /v1alpha1/indexes/{name}/search/actions/explain?key={key}&index={index}

The endpoint requires the search permission. Any node answers the request using the generation that node last pulled, without forwarding to the indexer.

  • name (path parameter, required): The index name. Can include a generation suffix, such as books@2.
  • key (query parameter, required): The primary key of the document, formatted according to the type of the key field. For value hits, provide the primary key of the parent document.
  • index (query parameter, optional): Zero-based index of the value along the hits.path to explain. Defaults to 0. Read only when searching for value hits.

The request body accepts the same JSON search request as POST /v1alpha1/indexes/{name}/search.

The endpoint compiles the search using the same clauses, locale, and index search settings to produce the exact score reported by a search.

The following request properties are read:

  • query
  • filters
  • locale
  • signals
  • hits.path

The following request properties are ignored: limit, offset, after, before, pages, sort, facets, highlight, matched, fields, rescore, and total. They are dropped before the request is read, so what would refuse a search - a cursor taken under another sort, an offset past EXOFIND_SEARCH_MAX_PAGE_DEPTH, a limit above EXOFIND_SEARCH_MAX_LIMIT, a rescore window too small for the page - still gets an explanation. If the search request specifies a field sort, the endpoint still computes and explains the relevance score. Because rescore is ignored, the explained score is the one the first pass gave, without the second pass.

{
"matched": true,
"score": 7.42,
"detail": {
"matched": true,
"score": 7.42,
"description": "sum of:",
"children": [
{
"matched": true,
"score": 5.10,
"description": "weight(title:bok) [BM25Similarity], result of:",
"clause": "query[0]",
"clauseType": "text",
"field": "title",
"usage": "matching",
"locale": "sv",
"children": []
},
{
"matched": true,
"score": 1.30,
"description": "signals, product of:",
"children": [
{
"matched": true,
"score": 1.24,
"description": "signal popularity (saturation, pivot 10.0, weight 1.0) reads 412.0",
"children": []
}
]
}
]
},
"generation": "2",
"tookMs": 1.208
}

Top-level response properties:

PropertyTypeDescription
matchedBooleanWhether the hit satisfies the search. A hit that does not match appears in no search results.
scoreNumberThe relevance score of the hit. Returns 0 if the hit does not match.
detailObjectRoot score step explaining how the score was calculated.
relaxedObjectRelaxation details containing dropped words and the effective query text. Omitted if query relaxation did not run.
interpretedObjectThe filters read out of the query text, and the remaining query text. Omitted when nothing was read.
generationStringName of the generation that answered. A request that names the index answers from the generation that is live when it arrives, so add @ and this name to the index name to send a later request to the same data. See Names and generations.
tookMsNumberExecution time for the explanation in milliseconds, including fractions of one. An explanation compiles and runs the search the way a search does, so it costs what a search costs.

Properties of a score step (detail and each entry in children):

PropertyTypeDescription
matchedBooleanWhether this step was satisfied. A non-matching step contributes nothing to the parent score.
scoreNumberScore contributed by this step to its parent step. Returns 0 if the step did not match.
descriptionStringHuman-readable explanation of the step.
clauseStringPath to the clause in the request body that produced this step (for example, query[0], filters[0], or query[0].filter[1]). Omitted when the step is not an individual clause.
clauseTypeStringClause type matching request syntax (field, text, knn, nested, and, or, not, or boost). Omitted when clause is omitted.
fieldStringIndex definition field name evaluated by the step. Omitted when the step reads no fields or multiple fields.
usageStringField usage mode evaluated by the step (such as matching, filter, autocomplete, matching_exact, hierarchy, or vector). Omitted when field is omitted.
localeStringBCP-47 locale tag of the field variant evaluated by the step. Omitted for fields that store a single variant across all languages.
childrenArrayChild score steps that compose this step. Empty for leaf steps.
  • Non-matching hits: Hits that do not match the query return matched: false and score: 0. Clause steps that failed return matched: false, while clauses that matched return matched: true.
  • Field names: Field names in the explanation tree correspond to schema names in the index definition rather than internal engine names.
  • Query relaxation: When zero results trigger query relaxation, relaxed is included and the explanation tree reflects the relaxed query that executed.
  • Interpreted filters: When quantities are read from the query text, the explanation tree reflects the search with the read filters in it.
  • Ranking signals: Ranking signals appear under a dedicated step with one child per ranking signal, specifying the field, function shape, weight, and value read from the document. A missing ranking signal value contributes a factor of 1.
  • Value hits: When hits.path targets a nested object field, each value is explained individually by specifying its zero-based position in index. With hits.when set, index is read only for documents that when matches; a document that returns as itself is explained as a document, whatever index says. See What a hit stands for.
  • Alternatives that did not match: Within an or clause that matched, only the alternatives that matched appear as steps. An or that matched nothing is reported as one non-matching step for the clause itself.

The duration of explanations is measured by the exofind.explain timer. See Metrics.

HTTP statusError codeDescription
400index:no_primary_keyThe index declares no primary key.
400Search error codesThe request body is not a valid search request. Returns the same error codes as POST .../search.
401Authentication errorsRequest lacks valid authentication.
403Authorization errorsMissing the search permission.
404search:explain:document_not_foundNo document exists with the specified key.
404search:explain:value_not_foundThe document contains no value at index along the hits.path.
409index:no_live_generationNo live index generation is available.
503index:closedThe index is closed.
503search:timeoutThe search behind the explanation ran longer than EXOFIND_SEARCH_TIMEOUT.

A freshness token identifies a specific index state, consisting of an index generation, a commit sequence number, and a search settings version. Each component is optional. The commit sequence counts Lucene commits containing changes, starting from 1. Tokens are opaque strings consisting of a format version byte and a Protocol Buffers message encoded as unpadded base64url, compatible with JSON strings, HTTP headers, and query parameters. Freshness tokens function in both local and object storage modes.

The following responses return a freshness token:

  • Document mutations on the documents API (POST /v1alpha1/indexes/{name}/documents, POST /v1alpha1/indexes/{name}/documents/actions/update, and POST /v1alpha1/indexes/{name}/documents/actions/delete): Returned in the freshness response property. A batch returns one token representing the commit containing the batch. A batch that wrote no documents returns the current state.
  • Single-document updates and deletions (PATCH and DELETE on /v1alpha1/indexes/{name}/documents/{key}): Returned in the X-Exofind-Freshness response header with HTTP 204 No Content.
  • Administrative actions (POST /v1alpha1/admin/indexes/{name}/actions/commit and POST /v1alpha1/admin/indexes/{name}/actions/promote): Returned in the freshness property of the index resource. Omitted on GET and PUT index responses.
  • Search settings modifications (PUT and PATCH on /v1alpha1/admin/indexes/{name}/settings): Returned in the freshness response property. Settings deletions (DELETE) return the token in the X-Exofind-Freshness response header. Omitted on GET settings responses.
  • Reindex jobs: Returned in the freshness property of the job record when the job reaches done, and set to null before completion.
  • Read operations (search, suggest, facet values, explain, and document scans): Returned in the freshness response property, naming the state that answered the request. NDJSON document scans return the token in the X-Exofind-Freshness response header.

To require a read operation to evaluate against a specific state or later, provide a freshness token in the request:

  • On search, suggest, facet values, and explain endpoints: Set freshness.atLeast in the JSON request body, or pass the X-Exofind-Freshness request header. If both are provided, freshness.atLeast takes precedence.
  • On document scans (GET /v1alpha1/indexes/{name}/documents): Pass the X-Exofind-Freshness request header.

Passing a freshness token requires no permissions beyond the read request itself. Requests that omit a freshness token evaluate against the current state held by the receiving node.

When a request includes a freshness token, the receiving node verifies each state component before answering:

  • Generation: If the node does not serve the generation named in the token, it reads the registry using one conditional request. A token naming a generation created before the live generation is satisfied immediately by the live generation. A request that explicitly targets a generation (such as products@2) answers from that generation regardless of the token’s generation.
  • Settings version: If the node holds a different settings version, it reads the settings object using one conditional request.
  • Commit sequence: If the node’s open reader is behind the sequence number, the node waits. The writer node is asked to commit pending changes at most once per second. Non-writer nodes poll the manifest using exponential backoff from 20 ms to 500 ms. Commits are only waited for when the token matches the generation answering the request.

The maximum wait time is bounded by EXOFIND_SEARCH_FRESHNESS_WAIT (default: 10s). Setting the wait to 0 checks once after one pull and fails if the state is not present. Searches are not forwarded to writer nodes; reads on non-writer nodes wait for the writer commit trigger (EXOFIND_INDEXES_COMMIT_MAX_INTERVAL, default: 5s) plus manifest polling.

The freshness token in a read response identifies the state that answered the request. Pass it on your next request to answer from the same state or a later one on any node. When you page with cursors, pass each freshness token into the next request so you never receive a page from an older state than the page before it.

HTTP statusError codeDescription
400search:freshness:invalidThe token is malformed or was not issued by the engine. Located at freshness.atLeast or X-Exofind-Freshness.
400search:freshness:version_unsupportedThe token was issued in a format version that this node does not support. The version argument specifies the unsupported format version.
400search:freshness:index_mismatchThe token belongs to an index other than the one specified in the request path. Arguments: index (token index) and expected (path index).
503search:freshness:unavailableThe node did not reach the requested state within the wait timeout. Returns a Retry-After header of 1 second. The wait argument specifies the wait duration in milliseconds.

A freshness token guarantees that the response reflects all changes up to the specified state, with one exception: HTTP 2xx responses on write requests confirm that the writer node accepted the write, not that the write is durable. If the writer node fails before committing changes and another node takes over the index, the successor continues the commit sequence from the last pulled commit, reaching the sequence number without including the uncommitted write. See What a write guarantees.

  • offset plus limit cannot exceed EXOFIND_SEARCH_MAX_PAGE_DEPTH, and a request whose page ends past the cap returns search:paging_too_deep even when its offset is below it.
  • next and previous cursors encode result positions rather than count offsets. Cursor navigation is uncapped by depth. Cursors are bound to the sort configuration of the original query; using a cursor with a different sort returns search:cursor:sort_mismatch.
  • A cursor a request cannot be resumed from returns one of three codes: search:cursor:invalid when the engine cannot read the token, search:cursor:sort_mismatch when it was taken under a different sort, and search:cursor:stale when it was taken under this sort but no longer names a position in it, which a sort field that changed type in the index definition leaves behind. All three are HTTP 400, and all three are answered by starting again from the first page.
  • A cursor says there may be results that way, not that there are. A page reached by after or before is a window around a position, and the engine does not count what lies outside it, so a full window always carries both cursors. Following one can answer an empty window with no hits - walking back to the first page and asking for previous again is the common way to reach one. A page reached by offset is counted, and there both cursors are exact.
  • Cursors inside pages encode count offsets and remain subject to EXOFIND_SEARCH_MAX_PAGE_DEPTH.
  • pages can be combined with offset or page cursors, but cannot be combined with after or before.
  • A search carrying a rescore block pages by counting inside the window and by cursor below it. See Paging a rescored search.
  • A cursor encodes a position and does not contain index state. To keep subsequent pages at or after the state of the first page across nodes, pass the freshness token from each response as freshness.atLeast on the next request. See Freshness.

A node caps what one request may ask it to do. Each cap is a configuration variable, so a deployment can set it higher or lower. For the settings and their defaults, see Search configuration.

SettingApplies toError code
EXOFIND_SEARCH_MAX_LIMITlimitsearch:limit_out_of_range
EXOFIND_SEARCH_MAX_PAGE_DEPTHoffset plus limitsearch:paging_too_deep
EXOFIND_SEARCH_MAX_RESCORE_WINDOWrescore.windowsearch:rescore:window_out_of_range
EXOFIND_SEARCH_MAX_KNN_Kk of a knn clausesearch:clause:k_out_of_range
EXOFIND_SEARCH_MAX_FUSE_DEPTHdepth of a fuse clausesearch:clause:depth_out_of_range
EXOFIND_SEARCH_MAX_CLAUSESClauses in query, filters, hits.when, rescore.boost, and the when of an interpret target, counted togethersearch:clauses_too_many
EXOFIND_SEARCH_MAX_CLAUSE_DEPTHNesting of clauses inside clauses, and of an interpret target inside a fallbacksearch:clauses_too_deep
EXOFIND_SEARCH_MAX_FACET_VALUESlimit of a facet, counted beside a search or asked for on its ownsearch:facet:limit_out_of_range
EXOFIND_SUGGEST_MAX_LIMITlimit of a suggest requestsearch:suggest:limit_out_of_range

Each of these returns 400, and the path of the error names where in the body the request went over. A request over EXOFIND_SEARCH_MAX_CLAUSES or EXOFIND_SEARCH_MAX_CLAUSE_DEPTH is answered with that error alone; the rest of the body is not read.

A search that passes these caps and then runs longer than EXOFIND_SEARCH_TIMEOUT returns 503 with search:timeout. The results it collected are dropped, so narrow the search instead of repeating it.

Exofind is built by Level Four AB and is available under the Apache License 2.0.