Skip to navigation

Algolia compatibility

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:

RouteAlgolia equivalent
POST /1/indexes/*/queriesMulti-query, used by InstantSearch and search()
POST /1/indexes/{index}/querySingle-index query
POST /1/eventsInsights events
POST /1/indexes/{index}/batch, POST /1/indexes/*/batchbatch, saveObjects, partialUpdateObjects, deleteObjects
POST /1/indexes/{index}addObject
PUT /1/indexes/{index}/{objectID}saveObject
POST /1/indexes/{index}/{objectID}/partialpartialUpdateObject
DELETE /1/indexes/{index}/{objectID}deleteObject
GET /1/indexes/{index}/{objectID}getObject
POST /1/indexes/{index}/clearclearObjects
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.
  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.

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 -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"
}
]
}'
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 -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"
}'
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 as the native events route.

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
]
}
]
}'
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.

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, and delete by filter with 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.