Admin API
The Admin API manages indexes under the /v1alpha1/admin path. Request and response bodies use JSON.
To index documents, use the Documents API. To manage API keys, see Authentication.
Every request requires credentials. For required permissions and information on why unauthorized indexes return 404 Not Found instead of 403 Forbidden, see Authentication.
Endpoints
Section titled “Endpoints”The Admin API provides the following endpoints:
GET /v1alpha1/admin/indexes # indexes the deployment holdsGET /v1alpha1/admin/indexes/{name} # definition and statusPUT /v1alpha1/admin/indexes/{name} # create or replace the definitionDELETE /v1alpha1/admin/indexes/{name} # remove an index or a generationGET /v1alpha1/admin/indexes/{name}/settings # search settings as storedPUT /v1alpha1/admin/indexes/{name}/settings # replace the search settingsPATCH /v1alpha1/admin/indexes/{name}/settings # change part of the search settingsDELETE /v1alpha1/admin/indexes/{name}/settings # remove the search settingsPOST /v1alpha1/admin/indexes/{name}/actions/promote # answer for this generationPOST /v1alpha1/admin/indexes/{name}/actions/commit # push pending changesPOST /v1alpha1/admin/indexes/{name}/actions/pull # fetch the latest state nowPOST /v1alpha1/admin/indexes/{name}/actions/reindex # start a job
GET /v1alpha1/admin/indexers # which node writes which indexGET /v1alpha1/admin/reindexes # every job across the deploymentGET /v1alpha1/admin/reindexes/{name} # job status of one indexPOST /v1alpha1/admin/reindexes/{name}/actions/cancel # stop a job
GET /v1alpha1/admin/registry/audit # compare the registry with the storagePOST /v1alpha1/admin/registry/actions/repair # register what the storage holdsEach endpoint also has a generated page stating every field it accepts and returns, grouped as Indexes, Search settings, Reindexes, Indexers, and Registry.
Requests that modify an index (all endpoints except read requests and pull) run on the node that holds that index. The holder node can differ for each index. If another node receives the request, it forwards the request with the original credentials to the holder node and returns the holder’s response.
When no node holds an index, the first candidate node that receives a write claims the index. If no candidate node is available to forward to, or if no candidate node sets EXOFIND_NODE_ADDRESS, the server returns 409 Conflict. If a holder node does not respond, the server returns 502 Bad Gateway.
Names and generations
Section titled “Names and generations”An index holds generations. Documents and definitions belong to a generation rather than directly to the index. One generation is live, which means the index serves queries from it. An index name without a generation specifier refers to the live generation.
The {name} parameter accepts two formats:
| Name format | Description |
|---|---|
products | The index, serving from the live generation. |
products@2 | Generation 2 of products, whether live or not. |
The @ character is reserved and cannot appear in an index name. A generation is only reachable through its parent index. The registry state shared across the deployment tracks which generations exist and which generation is live.
Updating an index definition in place does not reindex existing documents. If a definition change affects indexing—such as adding matching, changing an analysis chain, or editing a synonym set—create and populate a new generation, then promote it. For step-by-step instructions, see Roll out a definition change.
A DELETE request on an index deletes the index and all of its generations. A DELETE request on a generation deletes only that generation. Deleting the live generation fails with index:generation:is_live until you promote another generation. Deleting an index or generation removes it from the shared registry across the deployment; other nodes remove their local copies during their next registry read. What remote storage holds, the generations and for an index its search settings, is marked as deleted and removed by a background sweep once the mark is older than EXOFIND_INDEXES_REMOVAL_GRACE. An index or generation created again under the same name starts empty and does not pick old settings up. Within the grace period, a registry repair with restore brings it back. The request is served by the node writing the index and forwarded there when another node receives it. Both operations return 204 No Content.
The promote action configures the index to serve from the specified generation. The change takes effect immediately on the receiving node and within EXOFIND_INDEXES_REFRESH_INTERVAL on all other nodes. To roll back a deployment, promote the previous generation.
Listings
Section titled “Listings”The index, key and reindex listings answer every entry the key can see, ordered by name. Each takes the same three query parameters to read part of a listing:
| Parameter | Effect |
|---|---|
prefix | Keeps only the entries whose name starts with this text. For keys the name is the ID; for reindex jobs it is the index name. |
limit | Caps how many entries the response holds, from 1 to 1000. A listing cut short carries the last name it holds in next. Without a limit the whole listing is answered and next is absent. |
after | The name to continue after, as next gave it. The named entry is not included. |
To read a long listing, send the first request with a limit, and repeat it with after set to next until a response carries no next. An entry the key has no permission on is left out before the page is cut, so it counts against neither the limit nor the place to continue from. A limit outside its range returns 400 Bad Request with request:limit_out_of_range.
The reindex listing also takes index, which keeps the job of one index, and phase, which keeps the jobs in the named phases. See Job status and fleet-wide listing.
Index resource
Section titled “Index resource”A GET request on an index endpoint and every successful PUT request return an index resource:
{ "name": "products", "generation": "2", "live": true, "version": "9f2c1a0b3d4e5f60", "definition": { "...": "as stored" }, "status": { "state": "usable", "readOnly": false, "indexer": { "node": "node-a-7f21", "address": "http://node-a:8080" }, "luceneCompatibility": "current", "luceneCreatedMajor": 10 }, "generations": [ { "name": "1", "live": false, "createdAt": "2026-08-01T09:14:22Z" }, { "name": "2", "live": true, "createdAt": "2026-08-16T11:02:07Z" } ]}The resource contains the following fields:
name: The name of the index.generation: The generation described in the response. When the request specifies only the index name, this is the live generation.live: A boolean indicating whether this generation is the live generation.version: An identifier for the definition, also returned in theETagheader. Pass this value in theIf-Matchheader onPUTrequests to prevent overwriting concurrent updates, or in theIf-None-Matchheader on a laterGETto receive304 Not Modifiedwhile the definition is unchanged.definition: The active index definition. See Field types. Presets are stored expanded; the response returns the expanded chain rather than the preset name.status: The observed state reported by the answering node. The API does not accept this object as input.generations: A list of all generations for the index, including name, live status, and creation timestamp (createdAt).freshness: A token that identifies the state of the index. Present in responses tocommitandpromoteactions, and omitted onGETandPUTresponses. See Freshness.
A PUT request that creates a resource returns 201 Created. A PUT request that updates an existing resource returns 200 OK. The request body must contain the full definition; any omitted settings are removed.
The target of a PUT request depends on the name format:
PUT target | Create behavior | Update behavior |
|---|---|---|
products | Creates the index with an initial generation named 1. | Updates the definition of the live generation. |
products@2 | Creates generation 2 under an existing index. | Updates the definition of generation 2. |
A newly created generation contains no documents and is not live; the index continues serving from the previous live generation. A PUT products@2 request on a non-existent index returns 404 Not Found.
When the target generation already holds documents, a PUT request is refused if the new definition changes how documents are indexed. Written but uncommitted documents count; deleted documents do not. Incompatible updates return 409 Conflict with the error code index:definition:incompatible. The response includes one detail item per difference. Each detail carries a path naming the field that caused it, dotted for a field inside an object such as variants.sku, and one of the following error codes:
index:definition:usage_added: A usage was enabled on an existing field.index:definition:analysis_changed: An analyzer chain, decompounding setting, stopword list, or synonym set changed for a text usage.index:definition:setting_changed: A field setting that determines how data was indexed changed.index:definition:locale_fallback_changed: The index locale fallback changed.index:definition:source_added: The index started to keep document sources.
On a generation that holds documents, refused changes include:
- Enabling
filter,sort,facet,matching,autocomplete,hierarchy,highlight,exact, orlocaleson an existing field. - Changing field
type,primaryKey, ormultiple. - Changing the analyzer of a text usage (inline or via
analyzerRef), changingdecompound, or editing a stopword list or synonym set referenced by a field. - Changing
keyword.caseFolding,sort.collationon a string field, orhierarchy.separator. - Changing a vector field’s
dimensions,similarity,hnsw, orquantization. - Changing the
modeof an object field between nested and flattened. - Changing
locales.defaultLocale, adding a locale to a field’slocaleslist, or adding one to thesupportedlist the index declares. - Enabling or changing
localeFallback.
Accepted changes on a generation that holds documents include:
- Adding or removing a field.
- Disabling a usage.
- Changing
stored. - Changing the index
sourcemode. - Changing
metadata. - Changing
ranking(tie breakers and ranking signals). - Changing search-time settings:
weight,typoTolerance,lengthNormalization, and sortmissingplacement. - Changing document validation rules:
required,min, andmax. - Setting an explicit default that matches the engine’s default.
To force the update without reindexing, set the allowStaleDocuments query parameter to true (boolean, default false). Existing documents continue to serve queries as indexed until they are reindexed. The parameter has no effect on empty generations.
To apply an incompatible definition change to existing documents, reindex them into a new generation. See Reindex into a new generation.
If an index definition contains settings from a newer API version that the current node does not recognize, reading the index returns 409 Conflict. Updating such an index is rejected with 409 Conflict and the error code index:definition:unrepresentable. To resolve these errors, send the request to a node running a version that supports the definition.
Search settings
Section titled “Search settings”Search settings hold per-index configuration that affects how searches are answered, including ranking rules, synonym sets, words that are matched as they are spelled, and fields whose values are read out of the search text.
Search settings belong to the index name rather than to a generation. Promoting a generation preserves existing search settings. Search settings are stored as a separate object, so modifying them does not create a generation, does not change the index definition, and does not update the definition version. Requests that modify search settings run on the node that holds the index, like other modifying requests; GET requests are served by whichever node receives them.
A GET request returns the stored settings. If the index has no search settings and searches with its definition alone, the server returns 404 Not Found with the error code settings:not_found:
{ "ranking": { "tieBreakers": [ { "field": "sales", "direction": "descending" } ], "signals": [ { "field": "sales", "saturation": { "pivot": 50 } } ] }, "version": "9f2c1a0b3d4e5f60"}The response contains the following fields:
ranking: The ranking searches run with instead of the definition’s ranking, in the same shape as the definition’sranking. While present, it replaces the definition’s ranking completely; an empty object turns ranking off. Supplyingsignalsin a search request still replaces both. See Relevance.synonyms: Synonym sets applied to the text of a search, keyed by set name. See Synonyms.typoExclusions: Words matched as they are spelled, keyed by list name. See Typo exclusions.fields: Settings that apply to one field, keyed by field name. See Field settings.version: An identifier for the settings, also returned in theETagheader. Pass this value in theIf-Matchheader onPUTandPATCHrequests to prevent overwriting concurrent updates. A mismatch returns412 Precondition Failed. Pass it in theIf-None-Matchheader on a laterGETto receive304 Not Modifiedwhile the settings are unchanged.unsupportedFeatures: Present only when the answering node sets the settings aside because they use capabilities its version does not have. The node searches with the definition alone. Upgrade the node to put the settings in force.freshness: A token that identifies the state of the search settings. Present in responses toPUTandPATCHrequests, and omitted onGETrequests. Pass this value infreshness.atLeaston a search request to ensure the query runs with these settings in force on any node. See Freshness.
A PUT request replaces the settings completely and returns them as stored. An index that had no settings answers 201 Created, and one that had some answers 200 OK. The server validates the ranking against the generation the index name answers from, using the same index:ranking:* error codes used to validate a definition’s ranking. The server validates the fields named by synonyms, typoExclusions, and fields against the same generation.
A PUT or PATCH request carrying an If-Match header on an index that has no settings returns 404 Not Found with the error code settings:not_found, including If-Match: *. See Conditional requests for how the header is read.
A DELETE request removes the settings, returning the index to its definition’s ranking, and returns 204 No Content. Deleting settings that do not exist changes nothing and returns 204 No Content. The X-Exofind-Freshness response header carries a token that identifies the removal.
A change takes effect for searches on the node that holds the index immediately and on every other node within EXOFIND_SETTINGS_REFRESH_INTERVAL (default 10 seconds). Until then, two nodes can rank the same query differently. A search request that includes the freshness token runs with the updated settings on any node.
Search settings outlive generations. A generation promoted after the settings were written can lack a field used for ranking; searches then skip that entry rather than fail, so a promotion never depends on rewriting settings first.
Synonyms
Section titled “Synonyms”Search settings can configure query-time synonym sets under the synonyms field. Query-time synonym sets widen what a search asks for at query time, unlike index-time synonym sets defined in resources.synonyms of an index definition which widen document values during indexing.
Query-time synonym sets differ from index-time synonym sets in the following ways:
- Target: Query-time sets widen the search query. Index-time sets widen the indexed document values.
- Scope: Query-time sets apply to every document already in the index. Index-time sets apply only to documents indexed after the definition change.
- Rollout: Storing search settings takes effect immediately on the answering node and within
EXOFIND_SETTINGS_REFRESH_INTERVAL(default 10 seconds) on other nodes, without requiring a new generation or reindexing. - Rules: Do not put the same rule in both search settings and an index definition. A rule applied during indexing and again during search counts twice.
The synonyms field is an object keyed by set name:
{ "synonyms": { "merch": { "rules": [ { "equivalent": ["trainers", "sneakers"] }, { "mapping": { "from": ["ny"], "to": ["new york"] } } ], "fields": ["name", "description"], "boost": 0.8 } }}A synonym set contains the following fields:
rules: The rules of the set, using the same shape asresources.synonymsin an index definition:equivalent(a list of interchangeable terms) ormapping(one-wayfromandtoterms).fields: An optional list of field names the set applies to, named as a search names them. If omitted, the set applies to every field searched as text (any field with amatchingor anautocompleteusage).boost: A positive number specifying what a term added by the rules counts against the typed term. Default0.8. Values below1rank a document holding the typed term above one holding only a synonym. A value of1weighs synonyms and typed terms equally.
Rules apply after the field’s analysis chain processes the text, and rule terms are analyzed through that same chain. Rules match the analyzed form of terms (such as stems produced by a stemmer). If an analysis chain leaves nothing of a term (such as a stopword or punctuation), that term matches nothing and the rule is omitted from the set while remaining rules still apply. Sets are evaluated per field and locale.
A rule can stand for several words, such as ny and new york:
- On a field with
matchingusage, the engine searches for the words in sequence, matching documents where the words appear next to each other. - On a field with
autocompleteusage, the engine requires all words in the reading to be present. - In phrase searches, a phrase written with one side of a rule matches a document containing the other side.
The engine scores query-time synonyms as follows:
- Words added by a rule are counted together with the typed word as a single term, so a document matched through a rare synonym is not scored based on that synonym’s rarity.
boostweighs added terms against the typed term. Boost is not applied inside phrase searches.- A whole-value match (
exactsetting of a text usage) is never widened. - On an
autocompleteusage, a rule takes effect only after the word is typed in full. Partial prefixes do not trigger whole-word synonym rules.
The server validates synonym sets against the generation the index name answers from at write time. Invalid settings return 400 Bad Request with one of the following error codes:
settings:synonyms:field_unknown: The set is applied to a field that does not exist in the index.settings:synonyms:field_unsupported: The set is applied to a field that is not searched as text.settings:synonyms:boost_out_of_range: Theboostvalue is not a positive number.settings:synonyms:rule_invalid: A rule is not exactly one kind (equivalentormapping).
Typo exclusions
Section titled “Typo exclusions”Search settings can list words that are matched as they are spelled under the typoExclusions field. A word on a list is looked up as it was typed, however much typo tolerance the field it is searched in declares. Use a list for brand names and model codes that sit inside text you want typo tolerant otherwise.
Where typo tolerance is allowed remains part of the index definition, which declares it per field usage. See Field types. A word list narrows that per word, and changing one takes effect without reindexing.
The typoExclusions field is an object keyed by list name:
{ "typoExclusions": { "brands": { "words": ["canon", "leica"], "fields": ["name"] } }}A word list contains the following fields:
words: The words, as somebody would type them.fields: An optional list of field names the words are excluded in, named as a search names them. If omitted, the list covers every field searched as text (any field with amatchingor anautocompleteusage).
The engine applies a word list as follows:
- Words are read through the analysis chain of each field they are excluded in, so a word matches the term that field wrote. A word the chain leaves nothing of, such as a stopword, excludes nothing, and a word the chain leaves several terms of excludes each of them.
- The list is read against the words a search was typed with. A word that is not listed keeps the tolerance of its field, including when a near reading of it lands on a listed word.
- A clause that turns typo tolerance off with
"typos": "off"matches every word as it is typed, so a list changes nothing there. - A generation promoted later can lack a field a list names. Searches then forgive mistakes in that field as the definition says, rather than fail.
A node whose version does not support the typo_exclusions capability sets the whole settings object aside and searches with the definition alone. The unsupportedFeatures field of the settings response lists the name.
The server validates word lists against the generation the index name answers from at write time. Invalid settings return 400 Bad Request with one of the following error codes:
settings:typo_exclusions:field_unknown: The list is applied to a field that does not exist in the index.settings:typo_exclusions:field_unsupported: The list is applied to a field that is not searched as text.
Field settings
Section titled “Field settings”Search settings can configure how searches read individual fields under the fields field. Each entry configures how a search processes one field. Every capability in an entry remains disabled unless its configuration object is present. An empty object enables the capability with engine defaults.
The fields field is an object keyed by field name. For a field inside an object field, specify its dotted path, such as variants.colour. A PATCH request names such an entry with the dot escaped, as fields.variants\.colour, because a change path splits on the dot. See Change paths:
{ "fields": { "colour": { "interpret": {} }, "brand": { "interpret": {}, "suggest": {} }, "size": { "values": [ { "value": "S", "order": 1, "labels": { "en": "Small", "sv": "Liten" } }, { "value": "M", "order": 2, "labels": { "en": "Medium", "sv": "Mellan" } }, { "value": "L", "order": 3, "labels": { "en": "Large", "sv": "Stor" } } ] } }}A field settings entry contains the following fields:
interpret: Reads the values the field holds out of the query text of a search inusermode, as a filter on the field. Carries no configuration options. See Reading the values of a field.values: Declares values of the field with an order and labels per locale. See Declared values.suggest: Suggests values of the field as query text is typed. Carries no configuration options. See Suggesting what to search for.
The engine applies interpret according to the following rules:
- The field must be a
stringfield withfilterandfacetand withouthierarchy. Searches match terms against the facet values of the active generation, so a value is recognized as soon as you index a document containing it. - If a later promoted generation lacks the field or defines it without
filterorfacet, searches interpret the terms as plain query text rather than fail.
A node whose version does not support the interpret_values capability sets the entire settings object aside and searches with the index definition alone. The unsupportedFeatures field in the settings response lists the capability name.
The engine applies suggest according to the following rules:
- The field must be a
stringfield withfacetand withouthierarchy.filteris not required. Values are read from the facet values of the answering generation, so a value is suggested as soon as a document holding it is indexed. - If a later promoted generation lacks the field or defines it without
facet, suggest requests skip that field and answer from the others. The node logs the skipped fields once per settings version.
A node whose version does not support the suggest_values capability sets the entire settings object aside and searches with the index definition alone. The unsupportedFeatures field in the settings response lists the capability name.
The server validates field settings against the generation the index name answers from at write time. Invalid settings return 400 Bad Request with one of the following error codes:
settings:fields:field_unknown: The entry names a field that does not exist in the index.settings:fields:interpret_unsupported: Theinterpretsetting is applied to a field that is not astringfield withfilterandfacet, or that hashierarchy.settings:fields:suggest_unsupported: Thesuggestsetting is applied to a field that is not astringfield withfacet, or that hashierarchy.settings:fields:values_unsupported: Thevalueslist is given for a field that is not astringfield withfacet, or that hashierarchy.settings:fields:values_invalid: An entry invalueslacks avalue, duplicates avalue, keys a label with a tag that is not a canonical BCP-47 tag, contains a blank label, or exceeds 10,000 entries. The error message specifies the reason.
Declared values
Section titled “Declared values”The values list of a field settings entry declares values for a field, in the order specified. Each entry contains the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
value | String | Yes | The value as the field stores it. This is the value a facet counts and a filter matches. Each value must be unique within the list. |
order | Integer | No | The position of the value in a facet ordered by declared, lower first. Values sharing an order sort by count. A value without an order sorts by count after every value with an order. |
labels | Object | No | The text displayed instead of the value, keyed by BCP-47 tag in canonical form (sv, en-GB). |
A declaration affects search results in three ways:
- A facet with
"order": "declared"returns declared values first, sorted byorder, and all other values afterward, sorted by count. See Facets. - A facet returns each value with the
labelmatching the search locale. The engine resolves locale tags the same way as locale-specific field variants, sosv-SEresolves to a label keyed bysv. If the search locale has no matching label, the engine uses the label from the field’s default locale. For non-locale fields, this defaults to the engine default locale,en. Values without a label in either locale are returned without one. - Prefix searches on the facet match labels as well as stored values, and searches in
usermode withinterpreton the field read typed labels as their stored values. See Searching the values of a facet and Reading the values of a field.
Declaring a value that no document contains has no effect. Facets return only values present in matching documents, and searches do not interpret labels for values that do not exist in the index.
The target field must be a string field with facet and without hierarchy. If a later promoted generation lacks the field or defines it without facet, facets return values without the declaration rather than fail, and the node logs the skipped fields once per settings version.
A node whose version does not support the declared_values capability sets the entire settings object aside and searches with the index definition alone.
Changing part of the search settings
Section titled “Changing part of the search settings”PATCH /v1alpha1/admin/indexes/{name}/settingsA PATCH request changes named parts of the search settings instead of replacing all of them.
The request body is a single change object where each key is a path naming a location in the settings:
{ "ranking.signals[field=sales].weight": 2.0, "ranking.tieBreakers": null}The change object applies the following update rules:
- A path with a value replaces the target value.
- A path set to
nullclears the target value. - An omitted path leaves the existing value unchanged.
These are the rules a change object follows in Changing some of the fields of a document, so a change is described the same way for both.
The following paths name places in the search settings. The <name> of a field settings entry, a synonym set, or a word list is a name you chose, so escape any . or [ it holds. For the path syntax, the selector forms, the escaping rules, and what a path creates, see Change paths:
| Path | Description |
|---|---|
ranking | The whole ranking object. |
ranking.signals | The whole list of ranking signals. |
ranking.signals[] | A new ranking signal added to the list. |
ranking.signals[field=sales] | List entries whose field value equals sales. |
ranking.signals[sales] | The same entries, named by the key of the list. Two signals may read one field, so this can name several. |
ranking.signals[field=sales].weight | The weight field inside those matching ranking signal entries. |
ranking.tieBreakers | The whole list of tie breakers. |
ranking.tieBreakers[sales] | The tie breaker on the field sales, named by the key of the list. A field breaks ties once, so this names at most one. |
synonyms | The whole synonyms object. |
synonyms.<name> | A synonym set by name. |
synonyms.<name>.boost | The boost value of a synonym set. |
synonyms.<name>.fields | The list of target fields for a synonym set. |
synonyms.<name>.rules | The list of rules for a synonym set. |
synonyms.<name>.rules[] | A new rule added to a synonym set. |
typoExclusions | The whole typo exclusions object. |
typoExclusions.<name> | A word list by name. |
typoExclusions.<name>.fields | The list of target fields for a word list. |
typoExclusions.<name>.words | The words of a word list. |
typoExclusions.<name>.words[] | A new word added to a word list. |
fields | The whole field settings object. |
fields.<name> | The settings of one field by name. |
fields.<name>.interpret | The interpret configuration for a field. Set to {} to enable reading field values from query text, or null to disable. |
fields.<name>.suggest | The suggest configuration for a field. Set to {} to enable suggesting field values as query text is typed, or null to disable. |
fields.<name>.values | The whole list of declared values for a field. |
fields.<name>.values[] | A new declared value added to the list. |
fields.<name>.values[value=S] | List entries whose value equals S. |
fields.<name>.values[S] | The same entry, named by the key of the list. A value is unique, so this names at most one. |
fields.<name>.values[value=S].order | The order field inside those matching value entries. |
fields.<name>.values[value=S].labels.sv | The sv label inside those matching value entries. |
fields.variants\.colour.interpret | The interpret configuration of the field named variants.colour. Without the backslash, the path names the field colour inside variants. |
A successful request returns the search settings as stored and their new version in the ETag header. An index that had no settings answers 201 Created, because the change stores its first ones; one that had some answers 200 OK.
The endpoint enforces the following rules:
- The server validates the merged ranking against the generation the index name answers from, using the same
index:ranking:*error codes as aPUTrequest. - An index with no stored settings is modified as if it had empty settings.
- Without an
If-Matchheader, a change that conflicts with a concurrent update rebuilds on the newer version up to three times before returning409 Conflictwithstorage:conflict. - With an
If-Matchheader naming versions, a version mismatch returns412 Precondition Failedwithout retrying. - With an
If-Matchheader of any form, an index that has no settings returns404 Not Foundwithsettings:not_found. - If the stored settings contain capabilities that the answering node cannot describe, the request returns
409 Conflictwithsettings:unrepresentable.
The endpoint returns 400 Bad Request with one of the following error codes if a path cannot be applied. A path here is written the same way as a path into a document, and the same mistake in a path into a document reports the document:patch:* code of the same name. See Change paths.
| Code | Condition |
|---|---|
settings:patch:path_invalid | The key is not a valid path. |
settings:patch:field_unknown | The path names a field that search settings do not have. |
settings:patch:selector_unsupported | The path specifies a selector on a field that is not a list. |
settings:patch:selector_required | The path targets a list without specifying an entry selector. |
settings:patch:not_an_object | The path reaches inside a value that is not an object. |
settings:patch:match_not_an_object | The path matches on a field inside the entries of a list whose entries are not objects. |
settings:patch:key_unsupported | The path names a list entry by a single word on a list that declares no key, such as synonyms.<name>.rules[x]. Name a field inside the entry instead. |
settings:patch:add_unsupported | The path adds a value to a field that holds a single value, such as ranking[]. Name the field on its own to replace it. |
settings:patch:add_reaches_inside | The path sets a field on an entry being added (for example, ranking.signals[].weight). |
settings:patch:value_invalid | The path specifies a value that the target field cannot hold. |
settings:patch:no_match | The selector matches no stored entry in the list. |
Index states
Section titled “Index states”The status.state field indicates the remote synchronization state as observed by the answering node:
| State | Description |
|---|---|
needs_pull | A newer remote state exists and has not been pulled yet. |
pulling | The node is fetching remote state. The state becomes usable when complete. |
usable | The index is serving searches. On a read-only node, data is as current as the last pull. |
modified | The index has local changes that are not yet pushed. Only writer nodes reach this state. |
pushing | The node is pushing local changes. The state becomes usable when complete, or modified if the push does not reach the remote. |
unsupported | The definition requires engine features not present on this node version. Upgrade the node to resolve. |
incompatible | The Lucene files are too old for this build to open. Reindexing into a new generation is required. |
closed | The index is closed on this node. A new request opens a fresh instance. |
The exofind.index.state metric tags with the uppercase form of these names, such as NEEDS_PULL.
The status.readOnly field indicates whether the answering node can modify the index. Only the node holding the index can modify it.
The status.indexer object identifies the holder node and the address where writes are forwarded. This field is omitted if no node holds the index, if the holder could not be read, if the holder provided no address, or on nodes using local storage. Data in this field can lag behind a node handover by a few seconds. For more information, see Indexers.
The status.settingsUnsupportedFeatures field lists the capabilities the index’s search settings use that the answering node does not have. It is present only when the node has set the settings aside and searches with the definition alone.
Lucene compatibility
Section titled “Lucene compatibility”The status.luceneCompatibility field indicates Lucene version compatibility. Lucene supports indexes created by the current major version and the preceding major version:
| Value | Description |
|---|---|
current | Created by the current major version. Compatible with the current and next Lucene major versions. |
ending | Readable by the current version, but unsupported by the next Lucene major version. Reindex before upgrading across major versions. |
unreadable | Too old to open. The index reports the incompatible state and requires reindexing. |
unknown | No version was recorded and no commit exists to determine the version (for example, on an empty index). |
The status.luceneCreatedMajor field contains the recorded Lucene major version. This field is omitted when compatibility is unknown.
Actions
Section titled “Actions”Action endpoints trigger operations on an index or generation.
Every action returns the index resource of the generation it acted on, with the definition version in the ETag header. Nodes automatically discover indexes, generations, and changes at the interval configured by EXOFIND_INDEXES_REFRESH_INTERVAL.
Commit
Section titled “Commit”POST /v1alpha1/admin/indexes/{name}/actions/commitPushes pending changes (documents and definition) to storage and returns the updated index resource. The freshness field of the resource identifies the commit. A search request that includes this token reads from that commit on any node.
The {name} parameter specifies the target generation (for example, products@2). If you omit the generation specifier, the action targets the live generation.
POST /v1alpha1/admin/indexes/{name}/actions/pullFetches the latest remote state immediately instead of waiting for the refresh interval.
The {name} parameter specifies the target generation (for example, products@2). If you omit the generation specifier, the action targets the live generation.
Promote
Section titled “Promote”POST /v1alpha1/admin/indexes/{name}/actions/promoteConfigures the index to serve queries from the specified generation. The freshness field of the resource identifies the promoted generation. A search request that includes this token reads from that generation on any node.
The {name} parameter must specify a generation name (for example, products@2). Calling promote without a generation returns 400 Bad Request with the error code index:generation:name_required.
Promoting a generation interacts with reindex jobs in the following ways:
- Promoting the target generation of a reindex job in the
readyphase completes the job and transitions it todone. - Promoting a generation before its reindex job reaches the
readyphase is rejected with409 Conflictand the error codereindex:target_busy. - If another generation was promoted while the reindex job ran, promotion is rejected with
409 Conflictand the error codeindex:generation:live_moved.
Reindex
Section titled “Reindex”A reindex job populates a new generation by copying documents from an existing generation of the same index inside the engine, without resending documents from the client. For step-by-step instructions, see Reindex into a new generation.
Starting a job
Section titled “Starting a job”To start a reindex job, send a POST request to /v1alpha1/admin/indexes/{name}/actions/reindex, where {name} is the target generation the job fills. This request requires the indexes.reindex permission on the target, and indexes.promote as well unless the body specifies "promote": "manual", because a job left on automatic promotion promotes the generation it fills. A request missing indexes.promote returns 403 Forbidden.
The target must meet the following requirements:
- It must specify a generation by name (for example,
products@2). - The generation must already exist and must be empty.
- The generation must not be the live generation.
- The source generation must have a primary key and keep document sources (
sourcemode notnone). - The primary key of the source and target generations must have the same field name and type.
If the target does not meet these requirements, the server returns 400 Bad Request.
The request body accepts the following optional JSON fields:
from: The generation to read documents from. Defaults to the live generation. Must belong to the same index as the target.promote: The promotion mode.auto(default) automatically promotes the target generation once it catches up with changes.manualpauses the job in thereadyphase and keeps the target caught up until you manually promote it.
A successful request returns 202 Accepted with the job record in the pending phase, and a Location header naming the status endpoint of the job, /v1alpha1/admin/reindexes/{index}. A job waits for a concurrency slot on the node before it starts copying. The job runs in the background on the node holding the index.
An index can run at most one reindex job at a time. Starting a second job on an index returns 409 Conflict with the error code reindex:in_progress. A finished job’s record remains readable until a new job for that index replaces it.
Creation parameter
Section titled “Creation parameter”You can create a generation and start a reindex job in one request by adding the ?reindex=auto or ?reindex=manual query parameter to a PUT request:
PUT /v1alpha1/admin/indexes/products@2?reindex=autoThis creates the target generation with the definition in the request body and starts a reindex job reading from the live generation.
The reindex query parameter is one-shot and is not stored in the index definition. The server returns 400 Bad Request if the request does not create a generation, such as on an initial index creation or on a PUT request that updates an existing generation’s definition.
?reindex=auto requires the indexes.promote permission in addition to the indexes.write permission the PUT request requires, because the job it starts promotes the generation it fills. A request missing indexes.promote returns 403 Forbidden and creates no generation. ?reindex=manual requires only indexes.write.
Job record and phases
Section titled “Job record and phases”Reindex endpoints return a job record:
{ "id": "6f1c2a9d8b3e4c05", "index": "products", "target": "products@2", "source": "products@1", "phase": "copying", "promote": "auto", "documentsCopied": 125000, "sourceDocuments": 2400000, "backlog": 4100, "error": null, "startedBy": "3f9a1c7e2b8d4650", "node": "node-a-7f21", "startedAt": "2026-08-28T10:15:30Z", "updatedAt": "2026-08-28T10:16:02Z", "finishedAt": null, "freshness": null}The job record contains the following fields:
id: The id of the job, minted when the job was accepted. A new job on the same index gets a new id, so a client that polls the index can tell the job it started from one that replaced it.index: The name of the index.target: The generation being populated.source: The generation providing the source documents.phase: The current phase of the job.promote: The configured promote mode (autoormanual).documentsCopied: The number of confirmed documents copied to the target.sourceDocuments: The document count of the source generation when the copy started.backlog: The number of changed documents waiting to be replayed when the record was last written.error: The error message if the job failed, ornull.startedBy: The id of the principal whose request started the job: the id of a key, or the name of a configured principal such asroot.node: The name of the node running the job, as the indexer listing names it. A job resumed on another node after a failover names that node from its next checkpoint on. A finished job names the node that ended it.startedAt: The timestamp when the job started.updatedAt: The timestamp when the job record was last updated.finishedAt: The timestamp when the job reacheddone,failedorcancelled, ornullwhile it runs.freshness: A token that identifies the promotion of the target generation. Present once the job reachesdone, andnullotherwise. See Freshness.
id, startedBy and node are null on a record written by a version of the engine that did not keep them.
A job progresses through the following phases:
| Phase | Description |
|---|---|
pending | Accepted and waiting for a concurrency slot on the node. |
copying | Streaming documents from the source to the target in primary key order. |
replaying | Copying documents that changed in the source while the copy ran. |
ready | Used only with promote: manual. Caught up and waiting for manual promotion, while continuing to catch up periodically. |
promoting | Holding writes for the final drain and promotion. |
done | Completed and promoted successfully. |
failed | Stopped before promotion due to an error. The error field indicates the cause. |
cancelled | Stopped before completion in response to a cancellation request. |
Job status and fleet-wide listing
Section titled “Job status and fleet-wide listing”To check the status of a job on an index, send a GET request to /v1alpha1/admin/reindexes/{name}, where {name} is the index or one generation of it. The job belongs to the index in either case. If no job exists for the index, the server returns 404 Not Found with the error code reindex:not_found.
To list every reindex job across the deployment, send a GET request to /v1alpha1/admin/reindexes. The listing takes the prefix, limit and after parameters every listing takes, and two of its own:
indexkeeps the job of one index. An index without a job answers an empty listing rather than404, so a poll for a job needs no error handling.phasekeeps the jobs in the named phases. Repeat the parameter or separate the phases with commas, asphase=copying,replaying. A value that names no phase returns400 Bad Requestwithreindex:phase_invalid.
A finished job stays in the listing until the next job on its index replaces it, so a deployment that has reindexed many indexes answers a long listing. Filter on phase to see what is running, or page with limit and after.
Both endpoints require the indexes.read permission. Status and fleet-wide listings are served from a durable job record, so any node can serve the request and returns the same response.
Cancelling a job
Section titled “Cancelling a job”To stop an in-progress job, send a POST request to /v1alpha1/admin/reindexes/{name}/actions/cancel. This requires the indexes.reindex permission.
Cancelling a job stops the background process and leaves the partially populated target generation in place. You can remove the target generation with DELETE /v1alpha1/admin/indexes/{name}. Cancelling a finished job changes nothing.
Cancelling removes no record. The job record stays readable in the cancelled phase until a new job for that index replaces it, which is why cancelling is an action on the job rather than a DELETE of it.
Target constraints and promotion
Section titled “Target constraints and promotion”Direct document writes to a generation being filled by a reindex job return 409 Conflict with the error code reindex:target_busy. Document writes to the live generation continue unaffected.
When a job configured with promote: manual reaches the ready phase, calling POST /v1alpha1/admin/indexes/{name}/actions/promote on the target generation finishes the job by draining remaining changes, promoting the generation, and transitioning the job to done. Promoting a target generation before the job reaches the ready phase is refused with 409 Conflict and the error code reindex:target_busy.
A reindex job promotes its target only while the index still serves from the generation the job read. If another generation was promoted while the job ran, promotion is refused with 409 Conflict and the error code index:generation:live_moved, and the job transitions to failed. This applies to both auto and manual jobs.
Indexers
Section titled “Indexers”GET /v1alpha1/admin/indexers returns the candidate nodes competing to write indexes and the active writer claim for each index:
{ "candidates": [ { "node": "node-a-7f21", "address": "http://node-a:8080", "expiresAt": "2026-08-21T10:15:30Z" }, { "node": "node-b-90c4", "address": "http://node-b:8080", "expiresAt": "2026-08-21T10:15:32Z" } ], "claims": [ { "index": "events", "node": "node-b-90c4", "address": "http://node-b:8080", "expiresAt": "2026-08-21T10:15:32Z" }, { "index": "products", "node": "node-a-7f21", "address": "http://node-a:8080", "expiresAt": "2026-08-21T10:15:30Z" } ]}Any node can serve this endpoint from its local view of shared deployment state, including search-only nodes. The response can lag actual state by a few seconds.
This endpoint requires the indexes.read permission. If a credential lacks permissions for an index, that index is omitted from the claims list.
Indexes without an active claim are omitted from claims until a write operation assigns a writer. In candidate and claim entries:
address: The target address for write forwarding. Omitted if the node did not setEXOFIND_NODE_ADDRESS.expiresAt: The timestamp when the entry expires unless renewed by the node.
On nodes using local storage, candidates and claims are empty. If a node cannot read shared state from storage, it returns 503 Service Unavailable.
Registry
Section titled “Registry”The registry tracks which indexes and generations exist across the deployment. Registry endpoints compare the shared registry with remote storage and repair discrepancies.
Both endpoints are served by whichever node receives them and are never forwarded to the indexer. Both endpoints answer only in object storage mode. In local storage mode (EXOFIND_STORAGE_MODE=local), requests return 409 Conflict with the error code index:registry:audit_unavailable.
GET /v1alpha1/admin/registry/audit reads the registry and storage, comparing the two without changing either. This endpoint requires the registry.audit permission (deployment-scoped).
{ "registry": "present", "indexes": [ { "name": "products", "registered": true, "live": "2", "generations": [ { "name": "1", "registered": true, "stored": "synced" }, { "name": "2", "registered": true, "stored": "synced" } ] }, { "name": "analytics", "registered": false, "proposedLive": "1", "generations": [ { "name": "1", "registered": false, "stored": "synced" } ] } ], "unusable": []}The response contains the following fields:
registry: The state of the registry object:present,absent(no registry object), orcorrupt(contents cannot be parsed).indexes: Every index named by the registry or found in storage, ordered by name. Each entry contains:name: The name of the index.registered: A boolean indicating whether the registry has an entry for the index.live: The generation the index answers for. Omitted when unregistered or when no generation is live.proposedLive: The generation that a repair withpromoteNewestwould make live. Omitted when none would be promoted.removedAt: When the index was deleted, as an ISO 8601 timestamp. Present while the storage of the deleted index waits for the sweep that removes it; omitted otherwise.generations: A list of generations found for the index. Each entry containsname,registered(boolean),stored, andremovedAt.stored: What storage holds under the generation:synced: Storage holds a manifest; nodes can pull and serve this generation.incomplete: Storage holds a prefix without a manifest (such as an unfinished push or what an interrupted removal left of a deleted generation).missing: The generation is registered, but nothing exists in storage.
removedAt: When the generation was deleted on its own, as an ISO 8601 timestamp. Present while its storage waits for the sweep that removes it. A generation of a deleted index carries the index’sremovedAtinstead. Omitted otherwise.
unusable: A list of storage prefixes whose names no index or generation may carry (asindexorindex/generation). A repair never registers these prefixes.
Repair
Section titled “Repair”POST /v1alpha1/admin/registry/actions/repair registers what storage holds. This endpoint requires the registry.repair permission (deployment-scoped).
The repair operation only adds entries. It registers every synced generation that the registry does not name, and keeps existing entries as stored. Marked storage is skipped unless restored. It never deletes an index, a generation, or storage data. If the registry is absent, the repair writes it fresh. If the registry is corrupt, the repair replaces it with one rebuilt from storage.
The write is conditional and rebuilds on top of concurrent registry changes. The node that served the repair applies the repaired registry immediately. Other nodes pick up the changes within their registry refresh interval (EXOFIND_INDEXES_REFRESH_INTERVAL).
The request body accepts an optional JSON object:
{ "promoteNewest": true, "restore": [ "books" ]}The request body contains the following fields:
promoteNewest: A boolean. Whentrue, each index created by the repair answers for its highest-numbered generation. Hand-named generations are not selected. Indexes that are already registered keep what they answer for.restore: A list of names of deleted indexes (books) or generations (books@2) whose storage the sweep has not removed yet. The repair registers what each named entry holds like any other unregistered storage, then removes the removal mark from it. A name whose storage holds nosyncedgeneration keeps its mark, so the sweep still removes it. A name without a mark changes nothing.
A successful repair returns a summary of the changes:
{ "createdIndexes": [ "books" ], "addedGenerations": [ "books@1", "books@2" ], "promoted": [ "books@2" ], "restored": [ "books" ]}The response contains the following fields:
createdIndexes: Index entries added to the registry.addedGenerations: Generations added to the registry, formatted asindex@generation.promoted: Generations made live by the repair, formatted asindex@generation.restored: Deleted indexes and generations whose removal mark the repair removed, in the order the request named them.
Status codes
Section titled “Status codes”The Admin API returns the following status codes:
| Status code | Condition |
|---|---|
304 Not Modified | A GET of an index or its search settings carried an If-None-Match header naming the stored version. The response carries the ETag header and no body. |
400 Bad Request | The request body failed validation, or a PATCH of search settings named a place it cannot change (settings:patch:*). The response body details each validation error. See Errors. |
401 Unauthorized | The request lacks valid credentials. See Authentication. |
403 Forbidden | The credential does not have permission for the requested action on this index. |
404 Not Found | The specified index or generation does not exist, a PUT or PATCH request with If-Match targeted a resource that does not exist, no reindex job exists for the index (reindex:not_found), the index has no search settings (settings:not_found), or the index falls outside the credential’s allowed patterns. |
409 Conflict | The index cannot be modified because no forwarding node is available, the index is synchronizing, a reindex job is already in progress (reindex:in_progress), the target generation is busy being reindexed (reindex:target_busy), a definition change is incompatible with documents in the target generation (index:definition:incompatible), the definition contains unrepresentable settings, a PATCH targeted search settings the node cannot describe (settings:unrepresentable), the index requires unsupported engine features, storage holds a generation under the new name that nothing deleted (index:generation:storage_held), another generation was promoted while a reindex job ran (index:generation:live_moved), writing to the registry failed, writing search settings failed (storage:conflict, storage:io_error, storage:unavailable), or a registry endpoint was called in local storage mode (index:registry:audit_unavailable). |
412 Precondition Failed | The If-Match version does not match the current definition or search settings version. |
502 Bad Gateway | The request was forwarded to the holder node, but the node did not respond. |
503 Service Unavailable | The request conflicted with an index being closed to free resources (retrying reopens the index), or a node querying /v1alpha1/admin/indexers could not read the shared storage state. |
Exofind is built by Level Four AB and is available under the Apache License 2.0.