> For a complete page index of the Captain API documentation, fetch https://docs.captain.dev/llms.txt?excludeSpec=true

# Index settings

> Which fields are searched and how much each counts, facets, sort, synonyms and rules. What you set, what Captain sets, and how Algolia settings carry over.

> **Note**
>
> Product Search is in beta with design partners. Endpoints roll out per tenant.

Each index has settings that shape its search. They decide which fields are searched and how much each one counts, which fields can be faceted and sorted, and which synonyms and rules apply to a query.

Field types and facets are set when an index is created. Settings, synonyms and rules change later with the same write key, with no help from Captain. Captain sets two things with you: per-record access control (`acl`, `tenant_field`) and what the audit log keeps (`audit`). A write key that changes one of those gets `403 FORBIDDEN`. An [Algolia import](/guides/product-search/records#import-from-algolia) can also bring settings in.

## What you set when you create an index

Create Index takes an optional `fields` object. Give each field a `type`, and set `facet` to `true` for the fields you want counted in facets. A number can also carry a `unit`, and an enum its allowed `values`.

#### curl

```bash
curl -X POST "https://acme.captain.dev/v1/indexes" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "products",
  "fields": {
    "title": {
      "type": "text"
    },
    "description": {
      "type": "text"
    },
    "brand": {
      "type": "keyword",
      "facet": true
    },
    "category": {
      "type": "keyword",
      "facet": true
    },
    "price": {
      "type": "number",
      "unit": "usd"
    },
    "rating": {
      "type": "number"
    },
    "image_url": {
      "type": "url"
    }
  }
}'
```

#### Python

```python
import json
import requests

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_write_key"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

response = requests.post(
    f"{BASE_URL}/v1/indexes",
    headers=headers,
    json={
        "name": "products",
        "fields": {
            "title": {"type": "text"},
            "description": {"type": "text"},
            "brand": {"type": "keyword", "facet": True},
            "category": {"type": "keyword", "facet": True},
            "price": {"type": "number", "unit": "usd"},
            "rating": {"type": "number"},
            "image_url": {"type": "url"},
        },
    },
    timeout=30.0,
)

print(json.dumps(response.json(), indent=2))
```

#### TypeScript

```typescript
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_write_key";

const response = await fetch(`${BASE_URL}/v1/indexes`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "products",
    fields: {
      title: { type: "text" },
      description: { type: "text" },
      brand: { type: "keyword", facet: true },
      category: { type: "keyword", facet: true },
      price: { type: "number", unit: "usd" },
      rating: { type: "number" },
      image_url: { type: "url" },
    },
  }),
});

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```

**`Example: (201 Created)`**

```json Example: (201 Created)
{
  "name": "products",
  "status": "empty",
  "staged_documents": 0,
  "staged_records": 0,
  "record_count": 0,
  "build_id": null,
  "last_job": null,
  "created_at": 1759093140,
  "staged_changes_since_build": false,
  "pending_changes": false,
  "object_search": true,
  "last_build_at": null,
  "serving": null,
  "id_field": "id"
}
```

Fields you leave out are typed from the first records you add. [Records](/guides/product-search/records#field-types) lists the types and what each one supports.

A field accepts only `type`, `facet`, `unit` and `values`. Anything else answers `400` `VALIDATION_ERROR`. A write key that sends `settings` or `schema` gets `403` `FORBIDDEN`. Only a write key for every index in the tenant can create indexes. A key limited to some indexes gets `403`.

## Change settings

Get Settings returns the settings you set, the field table search uses, and up to three example values per field. Update Settings changes only the keys you send, as a JSON merge patch: objects merge key by key, `null` removes a key, and anything else replaces it.

#### curl

```bash
curl -X PATCH "https://acme.captain.dev/v1/indexes/products/settings" \
  -H "Authorization: Bearer $CAPTAIN_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "fields": {
    "brand": {"type": "keyword", "facet": true, "weight": "low"},
    "material": {"type": "keyword", "facet": true}
  }
}'
```

#### Python

```python
import json
import requests
import uuid

BASE_URL = "https://acme.captain.dev"
API_KEY = "your_write_key"
INDEX = "products"

response = requests.patch(
    f"{BASE_URL}/v1/indexes/{INDEX}/settings",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "fields": {
            "brand": {"type": "keyword", "facet": True, "weight": "low"},
            "material": {"type": "keyword", "facet": True},
        }
    },
    timeout=30.0,
)

print(json.dumps(response.json(), indent=2))
```

#### TypeScript

```typescript
const BASE_URL = "https://acme.captain.dev";
const API_KEY = "your_write_key";
const index = "products";

const response = await fetch(`${BASE_URL}/v1/indexes/${index}/settings`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    fields: {
      brand: { type: "keyword", facet: true, weight: "low" },
      material: { type: "keyword", facet: true },
    },
  }),
});

const result = await response.json();
console.log(JSON.stringify(result, null, 2));
```

**`Example: (202 Accepted)`**

```json Example: (202 Accepted)
{
  "object_search": true,
  "settings": {"fields": {"brand": {"type": "keyword", "facet": true, "weight": "low"}, "material": {"type": "keyword", "facet": true}}},
  "warnings": [],
  "job": {"id": "job_20261002193012_9f8e7d", "type": "index.build", "index": "products", "status": "queued"},
  "applies_at": "when job job_20261002193012_9f8e7d succeeds"
}
```

A field takes `type`, `searchable`, `weight` (`primary`, `secondary` or `low`), `facet`, `filterable`, `sortable`, `unit` and `values`. The settings are checked against your records before anything is saved, and a change that doesn't fit answers `400 VALIDATION_ERROR` and changes nothing.

A change rebuilds the index, the same way adding records does. The reply's `job` is that rebuild, and Get Job follows it. Search keeps answering with the old settings until the job succeeds, then switches in one step. If a rebuild of the index is already running, `job` is that one, and your change goes into the rebuild that starts right after it. An index with no records yet answers `200` with no job, and the settings apply to its first build.

Replace Settings (`PUT`) sends the whole settings object instead. Leave out `acl`, `tenant_field` and `audit` as they are: a write key that changes them gets `403`.

Get Synonyms and Replace Synonyms, and Get Rules and Replace Rules, work the same way with the write key. Their changes apply to the next search, with no rebuild.

## Searchable fields and weights

Search matches the words in text, keyword, enum and identifier fields, and in numbers and booleans as written. Dates, locations and URLs aren't searched.

Each text field has a weight. Weights are three tiers, not numbers:

* **Primary** is the field that names the record, such as a title. A match here counts most.
* **Secondary** is longer text, such as a description. This is the default for long text.
* **Low** is everything else that should still match, such as tags or attributes.

Captain picks the tiers from your records. Change a field's tier with `weight`, or stop it from being searched with `"searchable": false`, in [Update Settings](#change-settings). Ranking uses one relevance model for every index, so there is no ranking formula to tune per index.

## Facets

Enum and bool fields are facets by default. So are keyword fields whose values repeat, such as a brand or a category. Facet fields whose values repeat. A facet on a description lists almost every record once. For short labels such as a color, use the keyword type.

A number facet counts values only while the field has at most 1,000 distinct values, such as years or quantities. A date facet, or a number facet with more values such as prices, gives its minimum and maximum through `facet_stats` instead of value counts.

A search asks for facets with `facet_keys`. [Search](/guides/product-search/search) shows the request, and [List Filters](/reference/product-search/indexes/list-filters) says which fields can be faceted.

## Sort

Number and date fields are sortable. A search sorts by one of them instead of relevance with `sort`, as in `{"field": "price", "order": "asc"}`. Each request picks its own order, so an index needs no sorted copies.

## Synonyms

A synonym makes a query match words it didn't contain. There are two kinds:

* **Two-way:** any word in the group matches the others. `sofa`, `couch` and `settee` all find each other.
* **One-way:** the input adds the synonyms, but not the reverse. `tv` also matches `television`, and `television` doesn't match `tv`.

```json
[
  {"objectID": "s1", "type": "synonym", "synonyms": ["sofa", "couch", "settee"]},
  {"objectID": "s2", "type": "oneWaySynonym", "input": "tv", "synonyms": ["television"]}
]
```

Synonyms match whole words after spelling is folded and plurals are reduced, and a phrase matches only as written in order. They widen keyword matching. Meaning-based matching already relates words like these on its own.

Replace Synonyms (`PUT /v1/indexes/{index}/synonyms`) sets the whole list. Synonyms apply as soon as they are saved. There is no build to wait for.

## Rules

A rule changes the results of the queries that match its condition. The condition is a `pattern` and an `anchoring`: `is`, `contains`, `startsWith` or `endsWith`. An empty pattern matches every query.

A rule can do four things:

* **Promote** pins a record at a position, counted from `0`.
* **Hide** removes a record from the results.
* **Filter** adds a filter to the query, in Captain's syntax or as an Algolia `filters` string.
* **Boost** lifts results that match a filter. It reorders results the query already found, and never adds new ones.

```json
{
  "objectID": "r1",
  "condition": {"pattern": "rain jacket", "anchoring": "contains"},
  "consequence": {
    "promote": [{"objectID": "sku-20311", "position": 0}],
    "hide": [{"objectID": "sku-20402"}]
  }
}
```

Replace Rules (`PUT /v1/indexes/{index}/rules`) sets the whole list. Rules apply as soon as they are saved, like synonyms.

## Repeatable results

With `deterministic` on, the same query and filters return the same records in the same order, with the same `total`, for as long as the index receives no writes. That holds across restarts and under load. It isn't promised across rebuilds, or while records are added, changed or deleted.

An index built with fewer than 100,000 records has `deterministic` on by default. An index with 100,000 records or more has it off. A setting of `true` or `false` always wins over the default. Set it yourself with Update Settings. Captain can also move the 100,000-record limit for your tenant.

## When a change reaches search

A change to field types, weights, facets or sort applies when the index is rebuilt. Update Settings starts that rebuild and returns it as `job`. Search serves with the previous settings until the rebuild is live. Get Update Status reports where it stands in `build`.

Synonyms and rules don't need a build. They apply to the next search.

## Settings from Algolia

An [Algolia import](/guides/product-search/records#import-from-algolia) carries the index's settings, synonyms and rules, and runs as soon as you send it. Send `"object_search": true` so the settings become the field settings on this page. An export's reply lists each setting, synonym and rule as mapped, approximated or not mapped, with the reason. An import that reads from Algolia reports the counts in its job.

| Algolia setting                                                                                                                              | In Captain                                                                                                                                                                                                                |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `objectID`                                                                                                                                   | The record id.                                                                                                                                                                                                            |
| `searchableAttributes`                                                                                                                       | Tiers of text fields. The first entry is primary, the second secondary, the rest low. Fields not listed aren't searched. `ordered()` and `unordered()` aren't reproduced.                                                 |
| `attributesForFaceting`                                                                                                                      | Facets. `filterOnly(x)` becomes filterable but not faceted. `searchable(x)` and `afterDistinct(x)` become plain facets. Fields not listed aren't faceted.                                                                 |
| `customRanking`                                                                                                                              | Number and date fields become sortable, and each search picks its own `sort`. Algolia's tie-break inside equal relevance isn't reproduced. Other field types can't be sorted.                                             |
| `ranking`                                                                                                                                    | Not supported. Captain's relevance model decides the order.                                                                                                                                                               |
| `replicas`                                                                                                                                   | Not needed. Sort each request with `sort`.                                                                                                                                                                                |
| `distinct`, `attributeForDistinct`                                                                                                           | `group_by` on the field, up to 10 records per group, taken from the 100 most relevant records, or further down the 1,000 when a page needs more groups. The field needs one keyword, identifier or enum value per record. |
| `typoTolerance`                                                                                                                              | `true` works as in Algolia: one typo from 4 letters and two from 8. `min` and `strict` are approximated. `false` isn't supported.                                                                                         |
| `minWordSizefor1Typo`, `minWordSizefor2Typos`                                                                                                | Fixed at 4 and 8.                                                                                                                                                                                                         |
| `disableTypoToleranceOnAttributes`                                                                                                           | Identifier fields are never corrected. Other fields follow the usual typo rules.                                                                                                                                          |
| `allowTyposOnNumericTokens`                                                                                                                  | Numbers and codes are never corrected.                                                                                                                                                                                    |
| `disableTypoToleranceOnWords`                                                                                                                | Not supported.                                                                                                                                                                                                            |
| `ignorePlurals`                                                                                                                              | Plurals always match their singular.                                                                                                                                                                                      |
| `removeStopWords`                                                                                                                            | Captain's own English stop words.                                                                                                                                                                                         |
| `queryLanguages`, `indexLanguages`                                                                                                           | English only.                                                                                                                                                                                                             |
| `optionalWords`, `removeWordsIfNoResults`                                                                                                    | Approximated. A search never requires every word to match.                                                                                                                                                                |
| `decompoundQuery`                                                                                                                            | Approximated. Hyphens, and words joined or split, are handled.                                                                                                                                                            |
| `attributesToHighlight`, `attributesToSnippet`                                                                                               | Set per request with `highlight: true`.                                                                                                                                                                                   |
| `attributesToRetrieve`, `hitsPerPage`, `paginationLimitedTo`, `maxValuesPerFacet`, `highlightPreTag`, `highlightPostTag`, `responseFields`   | Supported.                                                                                                                                                                                                                |
| `unretrievableAttributes`                                                                                                                    | Not supported. A field is either returned or dropped from the index.                                                                                                                                                      |
| `advancedSyntax`, `relevancyStrictness`, `separatorsToIndex`, `camelCaseAttributes`, `enablePersonalization`, `renderingContent`, `userData` | Not supported.                                                                                                                                                                                                            |

Synonyms and rules map this way:

| Algolia                                                                                                                         | In Captain                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `synonym`, `oneWaySynonym`                                                                                                      | The same synonyms.                                                                                           |
| `altCorrection1`, `altCorrection2`                                                                                              | One-way synonyms. A correction isn't ranked below exact matches.                                             |
| `placeholder`                                                                                                                   | Not supported.                                                                                               |
| Rule `pattern` and `anchoring`                                                                                                  | The same condition.                                                                                          |
| Rule `promote`, `hide` and `params.filters`                                                                                     | The same consequence.                                                                                        |
| Rule `optionalFilters`                                                                                                          | A boost.                                                                                                     |
| Rule `validity`                                                                                                                 | Ignored: the rule always applies. A rule whose windows have all ended is skipped, and so is a disabled rule. |
| Rule contexts, filter conditions, more than one condition, query edits, `automaticFacetFilters`, `userData`, `renderingContent` | Not supported. A rule that has nothing else left isn't imported.                                             |

## Next steps

* [Records](/guides/product-search/records): field types, and how changes reach search.
* [Search](/guides/product-search/search): filters, facets, sort and grouping in a request.
* [Algolia compatibility](/guides/product-search/algolia-compatibility): keep your Algolia client and query Captain.