# Algolia-compatible API

> Keep a site built on InstantSearch, Autocomplete or an Algolia search client, and point it at Findlane: searches, filters, facets, sorts, highlights, Query Suggestions, click and conversion events, searches from your server, and secured API keys, without rewriting your pages.

If your site's search runs on InstantSearch or Autocomplete, it can search Findlane without changing its pages. Findlane answers Algolia's search API, so you point the Algolia client your site already uses at Findlane, with Findlane's application ID and search key. Search boxes, refinement lists, range sliders, sort menus, highlighting, and paging keep working as they are.

Bring the index across with the [import](/docs/import-from-algolia.md) first: it keeps the Algolia index's name, its replicas' names as sorts, its facet order, and each rule's redirect and first banner, so the pages that ask for them get answers. Then switch the client.

The compatible API answers searches and the events behind your analytics. Records and settings aren't written through it: load records with the import, the [server SDK](/docs/server-sdk.md) or the [API](/docs/api.md), and change settings in the dashboard, from your server, or with an [agent](/docs/agents.md).

## Point your pages at Findlane

1. **Make the index public.** On the index's **Public access** page, turn on public search and add your site's origins.
2. **Create the search key.** On **API keys**, under **Sites built on InstantSearch**, create the key and choose the indexes it opens. The key is public, like a search-only key: each index it opens is held to its public search settings and the sites it allows. The application ID is your workspace ID.
3. **Check the names.** Your site asks for indexes by their Algolia names. The index answers to its own name and to the names the import kept: the Algolia index's, and each replica's as the sort it became. Under **Names…** you can add others, such as your Query Suggestions index, which then answers from the index's [query suggestions](/docs/search.md#query-suggestions).
4. **Point the client at Findlane.** Pass the application ID, the key, and Findlane's host:

```ts
// algoliasearch 5
import { liteClient as algoliasearch } from "algoliasearch/lite"

const searchClient = algoliasearch("YOUR_WORKSPACE_ID", "fsk_…", {
  hosts: [{ url: "api.findlane.dev", accept: "readWrite", protocol: "https" }],
})
```

```ts
// algoliasearch 4
import algoliasearch from "algoliasearch/lite"

const searchClient = algoliasearch("YOUR_WORKSPACE_ID", "fsk_…", {
  hosts: [{ url: "api.findlane.dev" }],
})
```

The Algolia extensions for Magento and WordPress don't let you change the host in their settings, so stores using them need a small change in their theme or a module to pass `hosts`.

## What it answers

- **Searches:** one query or several at once, and searches of a facet's values, such as a searchable brand list.
- **Filters:** `facetFilters`, `numericFilters`, `tagFilters`, and `filters` strings. Filters combine as Algolia's do: ANDs of OR groups. Without parentheses, OR binds first, so `brand:Nike OR brand:Asics AND inStock:true` keeps either brand in stock. One OR group may mix different fields, with up to four parts.
- **Results:** highlights and snippets with the tags you ask for, facet counts and stats, paging up to the first 1,000 results with up to 50 a page, sorts by replica name, and `aroundLatLng` and `insideBoundingBox` when the index allows location search.
- **Query Suggestions indexes:** answered from the index's suggestions.
- **Optional filters:** each `optionalFilters` entry, a `field:value` or a list of them, [boosts](/docs/search.md#boost-and-bury) the records matching it, so records matching more come first, as Algolia adds their scores. A negated entry, such as `brand:-Puma`, or one with a negative score, buries them instead. Scores count only by their sign.
- **Facet order:** `renderingContent.facetOrdering` lists the facets counted, in the order asked, so InstantSearch's `DynamicWidgets` show them. The import keeps the Algolia index's facet order.
- **Rules:** `ruleContexts` become [contexts](/docs/search.md#contexts), which rules with a context condition fire on. Rules' banners come in `userData`, as `{ banner, link, title, position }`, and a rule's redirect in `renderingContent.redirect.url`, where InstantSearch's widgets look. The importer carries a rule's redirect and its first banner over.
- **Grouping:** `distinct` works on indexes that [group variants](/docs/settings.md#group-variants): 0 or `false` shows every record, and a number that many of each group's.
- **Analytics:** a search sent with `analytics: false` is answered, and counts toward your plan, as any other, but stays out of search analytics and query suggestions, as on Algolia.

Parameters with no counterpart here are left out, and the response's `x-findlane-ignored` header names them. These include typo settings sent with a search, personalization, and optional filters on fields the index doesn't have.

## Click and conversion events

InstantSearch's insights (the `search-insights` library) sends clicks, views and conversions to Algolia's Insights API. Point it at Findlane too, and they become the index's [events](/docs/events.md), which feed analytics, [revenue from search](/docs/events.md#revenue-from-search) and Bought together:

```ts
import aa from "search-insights"

aa("init", { appId: "YOUR_WORKSPACE_ID", apiKey: "fsk_…", host: "https://api.findlane.dev" })
```

- **Clicks** keep their places, and the query they came from: with `clickAnalytics`, a result's `queryID` names its search.
- **Conversions** count when they're add-to-carts or purchases (`eventSubtype`), with each product's price and quantity from `objectData`. A purchase's order ID is made from the user token, time and products, so the same event sent twice is one order. Conversions without a subtype, and events on filters, are left out.
- **The user token** is the visitor ID when the index takes [visitor IDs](/docs/events.md#visitor-ids) and the token fits one, as Algolia's anonymous tokens do.
- **Each index** is held to the sites it allows, as its searches are.

A request counts as one search, however many of its queries ask for results, as Algolia counts it; the cache answering all of it makes it free. The queries InstantSearch adds only to count a refined facet's values, and searches of facet values, never count.

## Search from your server

If your server searches Algolia for your pages, such as a route that keeps the key out of the browser or a server-rendered page, point its Algolia client at Findlane the same way, with a secret API key:

1. **Create the key.** On **API keys**, create a key with the **search** scope. It searches every index in the workspace, or the one index you choose, with or without public search, and each index answers to its Algolia names as above.
2. **Point the client at Findlane,** with the workspace ID as the application ID and the key, on your server only:

```ts
import { algoliasearch } from "algoliasearch"

const client = algoliasearch("YOUR_WORKSPACE_ID", process.env.FINDLANE_SEARCH_KEY, {
  hosts: [{ url: "api.findlane.dev", accept: "readWrite", protocol: "https" }],
})
```

3. **Say who's searching.** Pass the shopper's address in `X-Forwarded-For`, as Algolia's clients do, and their browser's User-Agent in `findlane-visitor-agent`, so the index's [limits and bot rules](/docs/security.md) apply to each shopper rather than to your server, analytics counts shoppers, and searches from verified crawlers such as Googlebot are free:

```ts
const { results } = await client.search({ requests }, {
  headers: { "x-forwarded-for": shopperIp, "findlane-visitor-agent": shopperUserAgent },
})
```

A secret key's searches return any field the request names in `attributesToRetrieve`, and up to 100 results a page. Name only what your pages show, as your server's code decides what reaches the browser. Without `X-Forwarded-For`, searches count as your server's own, held to the key's limit. A request with an `Origin` header, as a browser sends, is refused, so a secret key put in a page's code fails at once. Use the search key (`fsk_…`) in browsers.

## Secured API keys

If your server makes secured API keys for logged-in visitors, such as trade customers who see their own prices, it can keep making them with `generateSecuredApiKey`, using a Findlane signing key as the parent key:

1. **Create a signing key** under **Search for logged-in visitors** on the **API keys** page, for every index or chosen ones. It's shown once: keep it in your server's environment, as `FINDLANE_SIGNING_KEY`, in place of the search-only key you signed with.
2. **Sign keys with it** as you do now, with a `validUntil`:

```ts
// algoliasearch 5; in 4, client.generateSecuredApiKey(parentApiKey, restrictions)
const securedKey = client.generateSecuredApiKey({
  parentApiKey: process.env.FINDLANE_SIGNING_KEY!,
  restrictions: {
    filters: `customer_group:${user.group}`,
    validUntil: Math.floor(Date.now() / 1000) + 3600,
    userToken: user.id,
  },
})
```

3. **Search with it** in the browser, with the workspace ID as the application ID and Findlane's host, as above.

The key's restrictions replace the index's public settings for that person's searches, so the index needs no public search or allowed origins:

- **`validUntil`** is required, at most 7 days ahead.
- **`restrictIndices`** names the indexes it searches, by their Algolia names or their own names, with `*` for any part. An index's own name opens its sorts too. Without it, the key searches every index its signing key signs for.
- **`filters`, `facetFilters`, `numericFilters` and `tagFilters`** are forced onto every search, facet counts and stats included, and the page's own filters apply as well. A forced filter on `objectID` refuses the search.
- **`attributesToRetrieve`** lists the only fields its results return. Leaving fields out with `-field` isn't supported: list the fields to return.
- **`userToken`** names the person, so rate limits and [heavy visitors](/docs/security.md#bots-and-rate-limits) count them rather than their address.
- **`restrictSources`** and **`referers`** limit the addresses and sites it works from.
- **Any other search parameter**, such as `hitsPerPage`, replaces the search's own.

A refused key answers `403`, with a message that says why, such as `This signed key has expired`; Algolia's clients don't fetch a new key, so your page should when it sees that. Click and conversion events sent with the key count for the indexes it searches. Query Suggestions indexes still need the search key (`fsk_…`). The [signed keys](/docs/security.md#logged-in-visitors) section covers the rest, such as revoking a signing key.

## When you're ready, move to the SDK

The compatible API is there so switching doesn't have to start with a rewrite, and it's a fine place to stay while you compare. Findlane's own [browser SDK](/docs/browser-sdk.md) and [React components](/docs/react.md) speak its API directly, so nothing is lost in translation, and they reach the parts Algolia's request format has no room for:

- [Related results](/docs/search.md#related-results) when a search's words find nothing, and did-you-mean suggestions.
- One request per search: facet counts for refined filters come with the results, not as extra queries.
- [Lists](/docs/lists.md), recommendations, and click and purchase [events](/docs/events.md) that feed Bought together and your analytics.

The React components replace InstantSearch's widgets one at a time, so you can move a page or a widget at a time.
