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

# Algolia compatibility

> Point an existing Algolia search or indexing client at a Captain tenant.

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

Each tenant also answers Algolia's search and indexing routes, so an existing Algolia client or InstantSearch front end can search Captain, and your Algolia indexing code can keep Captain's copy up to date:

| Route                                                      | Algolia equivalent                                              |
| ---------------------------------------------------------- | --------------------------------------------------------------- |
| `POST /1/indexes/*/queries`                                | Multi-query, used by InstantSearch and `search()`               |
| `POST /1/indexes/{index}/query`                            | Single-index query                                              |
| `POST /1/events`                                           | Insights events                                                 |
| `POST /1/indexes/{index}/batch`, `POST /1/indexes/*/batch` | `batch`, `saveObjects`, `partialUpdateObjects`, `deleteObjects` |
| `POST /1/indexes/{index}`                                  | `addObject`                                                     |
| `PUT /1/indexes/{index}/{objectID}`                        | `saveObject`                                                    |
| `POST /1/indexes/{index}/{objectID}/partial`               | `partialUpdateObject`                                           |
| `DELETE /1/indexes/{index}/{objectID}`                     | `deleteObject`                                                  |
| `GET /1/indexes/{index}/{objectID}`                        | `getObject`                                                     |
| `POST /1/indexes/{index}/clear`                            | `clearObjects`                                                  |
| `DELETE /1/indexes/{index}`                                | `deleteIndex`                                                   |
| `GET /1/indexes/{index}/task/{taskID}`                     | `waitTask`                                                      |

These routes read and write Captain's copy of the index. They never forward requests to Algolia.

## Setup

1. Import the Algolia index into Captain under the same index name. See [Import from Algolia](/guides/product-search/records#import-from-algolia).
2. Point the Algolia client at the tenant host, `acme.captain.dev`.
3. Pass a Captain search key where the client expects the Algolia API key, or a write key for indexing. The write routes also take the key as `Authorization: Bearer`.

The application id can be any value. The hostname selects the tenant.

## Point the Algolia client at Captain

The JavaScript client takes the tenant host in `hosts`. The same client works with InstantSearch.

```typescript
import { liteClient as algoliasearch } from "algoliasearch/lite";

const searchClient = algoliasearch("acme", "your_search_key", {
  hosts: [{ url: "acme.captain.dev", accept: "readWrite", protocol: "https" }],
});

const { results } = await searchClient.search({
  requests: [{ indexName: "products", query: "hiking boots", hitsPerPage: 2 }],
});
```

## Multi-query

Each request names an index and its Algolia search parameters. The reply holds one result per request, in Algolia's format.

#### curl

```bash
curl -X POST "https://acme.captain.dev/1/indexes/*/queries" \
  -H "x-algolia-api-key: $CAPTAIN_SEARCH_KEY" \
  -H "x-algolia-application-id: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "requests": [
    {
      "indexName": "products",
      "params": "query=hiking%20boots&hitsPerPage=2"
    }
  ]
}'
```

#### Python

```python
import json
import requests

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

headers = {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
}

response = requests.post(
    f"{BASE_URL}/1/indexes/*/queries",
    headers=headers,
    json={
        "requests": [
            {"indexName": "products", "params": "query=hiking%20boots&hitsPerPage=2"},
        ],
    },
    timeout=30.0,
)

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

#### TypeScript

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

const response = await fetch(`${BASE_URL}/1/indexes/*/queries`, {
  method: "POST",
  headers: {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    requests: [{ indexName: "products", params: "query=hiking%20boots&hitsPerPage=2" }],
  }),
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "results": [
    {
      "hits": [
        {
          "title": "Moab 3 Mid Waterproof Hiking Boot",
          "brand": "Merrell",
          "category": "Hiking Boots",
          "price": 139.0,
          "objectID": "sku-10482",
          "_highlightResult": {
            "title": {
              "value": "Moab 3 Mid Waterproof <em>Hiking</em> <em>Boot</em>",
              "matchLevel": "full",
              "matchedWords": [
                "hiking",
                "boots"
              ],
              "fullyHighlighted": false
            }
          }
        },
        {
          "title": "X Ultra 4 Mid GTX Hiking Boot",
          "brand": "Salomon",
          "category": "Hiking Boots",
          "price": 165.0,
          "objectID": "sku-10517",
          "_highlightResult": {
            "title": {
              "value": "X Ultra 4 Mid GTX <em>Hiking</em> <em>Boot</em>",
              "matchLevel": "full",
              "matchedWords": [
                "hiking",
                "boots"
              ],
              "fullyHighlighted": false
            }
          }
        }
      ],
      "nbHits": 42,
      "page": 0,
      "hitsPerPage": 2,
      "nbPages": 21,
      "exhaustiveNbHits": true,
      "exhaustiveFacetsCount": true,
      "exhaustive": {
        "nbHits": true,
        "facetsCount": true
      },
      "facets": {},
      "query": "hiking boots",
      "params": "query=hiking%20boots&hitsPerPage=2",
      "index": "products",
      "processingTimeMS": 14,
      "queryID": "c97e0b2a4f6d41e3a58b1c7d2e9f0a36",
      "renderingContent": {}
    }
  ]
}
```

## Single-index query

#### curl

```bash
curl -X POST "https://acme.captain.dev/1/indexes/products/query" \
  -H "x-algolia-api-key: $CAPTAIN_SEARCH_KEY" \
  -H "x-algolia-application-id: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "params": "query=rain%20jacket&hitsPerPage=1"
}'
```

#### Python

```python
import json
import requests

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

headers = {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
}

response = requests.post(
    f"{BASE_URL}/1/indexes/{INDEX}/query",
    headers=headers,
    json={
        "params": "query=rain%20jacket&hitsPerPage=1",
    },
    timeout=30.0,
)

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

#### TypeScript

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

const response = await fetch(`${BASE_URL}/1/indexes/${index}/query`, {
  method: "POST",
  headers: {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    params: "query=rain%20jacket&hitsPerPage=1",
  }),
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "hits": [
    {
      "title": "Torrentshell 3L Rain Jacket",
      "brand": "Patagonia",
      "category": "Rain Jackets",
      "price": 179.0,
      "objectID": "sku-20311",
      "_highlightResult": {
        "title": {
          "value": "Torrentshell 3L <em>Rain</em> <em>Jacket</em>",
          "matchLevel": "full",
          "matchedWords": [
            "rain",
            "jacket"
          ],
          "fullyHighlighted": false
        }
      }
    }
  ],
  "nbHits": 18,
  "page": 0,
  "hitsPerPage": 1,
  "nbPages": 18,
  "exhaustiveNbHits": true,
  "exhaustiveFacetsCount": true,
  "exhaustive": {
    "nbHits": true,
    "facetsCount": true
  },
  "facets": {},
  "query": "rain jacket",
  "params": "query=rain%20jacket&hitsPerPage=1",
  "index": "products",
  "processingTimeMS": 11,
  "queryID": "4b8d2f0e6a1c43b79e5d3a2c8f7b6e10",
  "renderingContent": {}
}
```

## Insights events

Send clicks and conversions with the `queryID` from the search result. They feed the same [analytics](/guides/product-search/analytics) as the native events route.

#### curl

```bash
curl -X POST "https://acme.captain.dev/1/events" \
  -H "x-algolia-api-key: $CAPTAIN_SEARCH_KEY" \
  -H "x-algolia-application-id: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "events": [
    {
      "eventType": "click",
      "eventName": "Result clicked",
      "index": "products",
      "userToken": "user-481",
      "queryID": "c97e0b2a4f6d41e3a58b1c7d2e9f0a36",
      "objectIDs": [
        "sku-10482"
      ],
      "positions": [
        1
      ]
    }
  ]
}'
```

#### Python

```python
import json
import requests

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

headers = {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
}

response = requests.post(
    f"{BASE_URL}/1/events",
    headers=headers,
    json={
        "events": [
            {
                "eventType": "click",
                "eventName": "Result clicked",
                "index": "products",
                "userToken": "user-481",
                "queryID": "c97e0b2a4f6d41e3a58b1c7d2e9f0a36",
                "objectIDs": ["sku-10482"],
                "positions": [1],
            },
        ],
    },
    timeout=30.0,
)

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

#### TypeScript

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

const response = await fetch(`${BASE_URL}/1/events`, {
  method: "POST",
  headers: {
    "x-algolia-api-key": API_KEY,
    "x-algolia-application-id": "acme",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    events: [
      {
        eventType: "click",
        eventName: "Result clicked",
        index: "products",
        userToken: "user-481",
        queryID: "c97e0b2a4f6d41e3a58b1c7d2e9f0a36",
        objectIDs: ["sku-10482"],
        positions: [1],
      },
    ],
  }),
});

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

**`Example: (200 OK)`**

```json Example: (200 OK)
{
  "status": 200,
  "message": "OK"
}
```

## Indexing with an Algolia client

Writes take a Captain write key where the client expects the Algolia API key. Keep it on your server, as you would Algolia's admin key.

```typescript
import { algoliasearch } from "algoliasearch";

const client = algoliasearch("acme", "your_write_key", {
  hosts: [{ url: "acme.captain.dev", accept: "readWrite", protocol: "https" }],
});

const { taskID } = await client.saveObject({
  indexName: "products",
  body: { objectID: "sku-10482", title: "Moab 3 Mid Waterproof Hiking Boot", price: 129.0 },
});
await client.waitForTask({ indexName: "products", taskID });
```

Each write runs as the Captain write it maps to, so it takes effect as fast and fails the same way:

* `addObject`, `updateObject` and `saveObject` add or replace whole records, as Add Records does.
* `partialUpdateObject` changes the attributes you send, as Update Records does. With `createIfNotExists` (the default), an object the index doesn't have is added. Built-in operations such as `Increment` or `Add` answer `400`. Send the new value instead.
* `deleteObject` deletes, and an objectID the index doesn't have is not an error. A `deleteObject` with no `objectID` answers `400`.
* `clearObjects` empties the index and keeps its settings.
* `deleteIndex` deletes the index, with the same key rule as Delete Index.

A write to an index that doesn't exist creates it, as Algolia does, keyed by `objectID`. That needs a write key for every index. `objectID` is always the record's id. On an index created with `"id_field": "objectID"`, every other attribute is stored as you send it, an `id` attribute included. On an index keyed by another field (`id` by default), Captain stores the `objectID` in that field, and an object whose own value there is different answers `400 ID_CONFLICT` and nothing from the request is saved. This keeps objects that share a value apart. Shopify records, for example, carry the variant in `objectID` and the product in `id`: send them to an index created with `"id_field": "objectID"` so each variant stays its own record.

`taskID` is the write's `seq`. `waitTask` reports `published` once search has applied the write, which Get Update Status reports as `applied_seq`.

Settings, synonyms and rules through the `/1` routes (`setSettings`, `saveSynonyms`, `saveRules`), and `deleteBy`, aren't mapped. Those answer `501`. Change settings with [Update Settings](/guides/product-search/settings#change-settings), and delete by filter with [Delete Records By Filter](/guides/product-search/records#delete-records-by-filter).

## Limits

* Supported client versions, headers and parameters are validated per integration before a migration goes live.
* Indexes with per-record access control are not served on these routes. Use the native search routes with scoped keys.
* Facet counts are exact while a query matches up to 250 records. Above that they cover the relevance-cut result set, and the reply says `exhaustiveFacetsCount: false`, as Algolia does on large result sets.