# Set up search

> Start from what people will type: list the searches, set the index up for each kind, keep them working with search tests, and tune from analytics. With an AI agent, in the dashboard, or in code.

Good search starts from the searches, not the settings. Write down what people will type and what each search should find, set the index up for those searches, and save them as search tests, so a later change can't break them without you seeing. Once people search, analytics show what to fix next.

This page goes through the steps once, then shows them [with an AI agent](#with-an-ai-agent), [in the dashboard](#in-the-dashboard), and [in code](#in-code). It starts once you know what a record is ([Plan your records](/docs/data.md)). The [Shopify app](/docs/shopify.md), the [WooCommerce plugin](/docs/woocommerce.md), and [product feeds](/docs/feeds.md) make the records for you, so stores on them start here.

## List the searches

Before you change a setting, write down 20 to 30 searches, and what each should find first. Take them from:

- **Your current search's analytics:** its top searches, and those that found nothing. If you're moving from another search service, [import its settings](/docs/import-from-algolia.md) too.
- **Google Search Console:** the searches that bring people to your site say what they call your products.
- **Your menus and best sellers:** category names, top brands, and the products that sell most.
- **The people who answer customers:** what customers ask for by phone, by chat, or in the shop, in their words.

Then add a few hard ones: a typo, a plural, a word written apart, a product code, and something you don't sell. A running shop's list starts like this:

| Search                   | Should find first                                   |
| ------------------------ | --------------------------------------------------- |
| `cadence`                | Cadence Road Knit Running Shoe                      |
| `trail shoes waterproof` | Waterproof trail shoes                              |
| `40211`                  | The product with that code                          |
| `sneakers`               | Running shoes, though the records say "trainers"    |
| `cadense`                | Cadence Road Knit Running Shoe, despite the typo    |
| `returns`                | The returns page                                    |
| `kayak`                  | Nothing sold here, so the closest products, or none |

## Set up for each kind of search

Most searches are of a few kinds, and each kind has settings that make it work:

| Kind              | Searches such as                                        | What makes them work |
| ----------------- | ------------------------------------------------------- | -------------------- |
| Names             | `cadence`, `cadence road knit`                          | The title as the first [searchable field](/docs/settings.md#searchable-fields), with the most weight. If your search box searches with each letter typed, [`shortPrefixes`](/docs/settings.md#typo-tolerance), so the first letters find something. |
| Codes             | `40211`, `3.5mm`, a model number                        | A searchable field of their own, with `typos: false` ([fields without typos](/docs/settings.md#typo-tolerance)), since a near miss is a different product. |
| Brands            | `stridewell`, `puma`                                    | The brand as a searchable field after the title, and as a facet. [`exactWords`](/docs/settings.md#typo-tolerance) for brands a typo away from other words, so `puma` doesn't find `pump`. |
| Categories        | `running shoes`, `jackets`                              | The category as a searchable field and a facet. When accessories come before the thing, such as laces for `running shoes`, a [rule](/docs/settings.md#rules) that boosts the category. |
| Attributes        | `red jacket`, `waterproof`, `size 11`                   | Colors, materials, and tags in a searchable list, weighted below the title, and as facets. A [rule with a placeholder](/docs/settings.md#placeholders), such as `size {sizes}`, turns a value in the query into a filter. |
| Other words       | `sneakers` for trainers, `hoodie` for hooded sweatshirt | [Synonyms](/docs/settings.md#synonyms). A one-way synonym when a broad word should find narrower ones, but not the other way round: `shoes` finds boots, and `boots` finds only boots. |
| Word forms        | `jackets`, `Schuhe`, `kjólar`, `barna afmæli`           | [`languages`](/docs/settings.md#languages) for plurals and common words, [Icelandic word forms](/docs/settings.md#icelandic-word-forms), and [compound words](/docs/settings.md#compound-words) for words written together or apart. |
| Typos             | `cadense`, `protien`                                    | [Typo tolerance](/docs/settings.md#typo-tolerance), on by default. [Did you mean](/docs/settings.md#did-you-mean) corrects what it doesn't reach, and tells shoppers what was searched. |
| Prices and intent | `under 100`, `gifts`                                    | Rules: `under {number}` filters prices up to the number and searches the rest ([placeholders](/docs/settings.md#placeholders)), and `gifts` can boost products tagged as gifts. |
| Pages             | `returns`, `shipping`, `opening hours`                  | A rule with a [`redirect`](/docs/settings.md#what-rules-do) to the page. |
| Not sold here     | `kayak`                                                 | [Related results](/docs/settings.md#related-results) show the products closest in meaning, when they're close enough, on paid plans. Analytics lists these searches as demand ([why, and fixes](/docs/analytics.md#why-and-fixes)). |

An index searches up to five fields. When you have more, put fields that work alike in one list, such as colors, materials, and tags in `tags`, and leave text that's only shown, such as a care guide, unsearched.

Docs, articles, and listings get searches of the same kinds. Their recipes have settings to start from: [docs and help centers](/docs/help-centers.md), [articles and research](/docs/articles.md), and [listings and places](/docs/listings.md).

## Decide what comes first

Relevance orders most searches. Three settings change the order for all of them, so set them once:

- **[Rank first](/docs/settings.md#rank-first)** puts what should always lead first, such as products in stock.
- **[Custom ranking](/docs/settings.md#custom-ranking)** orders equally good matches, such as by units sold in the last 30 days.
- **[Learn from clicks](/docs/settings.md#learn-from-clicks)**, once a store has traffic, puts the products shoppers pick most for a search first.

For one search or one category page, use a [rule](/docs/settings.md#rules) or [merchandising](/docs/settings.md#merchandising), not a setting that changes every search.

## Try each search

Run each search on your list in the index's **Playground**. **Why this result?** shows what each result matched and how, such as through a typo or a synonym, and the rules that moved it. When a search finds the wrong thing, change one setting and search again.

## Save them as search tests

Each search that works becomes a [search test](/docs/settings.md#search-tests):

- **A result that must come first:** `inTop`, such as the Cadence shoe in the top 3 for `cadence`.
- **A search that must keep finding plenty:** `minResults`, such as 20 for `trail shoes`.
- **A result that must stay out:** `notInTop`, such as the gift card out of the top 20 for `shoes`.

In the Playground, **Save as test** makes one from a result. From then on, **Preview changes** on the **Search settings** page shows the tests a change would break, and the searches it changes, before you save it.

> **Note:** A preview compares the settings that apply when searching, such as weights, synonyms, and rules.
> New searchable, facet, filter, or sort fields need the index updated first: save those on their
> own, then preview the rest.

## After launch

Real searches show what your list missed. In the first weeks, look at the index's [analytics](/docs/analytics.md) each week:

- **Searches with no results:** add the fixes that make sense from [why, and fixes](/docs/analytics.md#why-and-fixes).
- **Top queries:** look at the first results of the 20 most searched.
- **Searches without clicks:** results that didn't look right to shoppers.
- **Clicked further down:** searches whose shoppers pick a result below the top three, which may belong higher ([where clicks land](/docs/analytics.md#where-clicks-land)).
- **Rising searches:** new demand, and things you may not sell yet.
- **Rule hits,** on the **Search settings** page: rules that fire on searches they shouldn't, or on none.

Save a test for each search you fix. The [weekly report](/docs/analytics.md#weekly-report) emails the main numbers on Mondays.

## With an AI agent

Connect the [MCP server](/docs/agents.md), and an agent can do every step above. It reads your data, but it doesn't know your shoppers, so tell it:

- **What you sell, and who searches:** professionals who type part numbers, or gift shoppers who type "for dad".
- **The languages** people search in.
- **Your list of searches,** and what each should find first.
- **What isn't public,** such as cost prices and supplier details, so it stays out of the records.
- **For a live index,** to preview each change and not save one that breaks a search test.

To set up a new index:

```text
Set up Findlane search for our products, from data/products.json.
We sell running and trail gear. Shoppers search in English and
Icelandic, often by model name, size, or code. These searches must
work, with what should come first:

- cadence: Cadence Road Knit Running Shoe
- trail shoes waterproof: waterproof trail shoes
- 40211: the product with that code
- sneakers: running shoes
- returns: go to /pages/returns

Save them as search tests, and tell me which you couldn't make work,
and why.
```

In Claude Code, `/mcp__findlane__add-search` starts the same way, and asks for your searches if you don't give them.

To tune a live index:

```text
Look at the last 30 days of searches in our products index: top
queries, searches that found nothing, and searches whose shoppers
click below the top three. Propose fixes, preview each with
compare_settings against our search tests, and wait for my yes
before saving. Save a search test for each fix.
```

`/mcp__findlane__fix-no-results` starts from the searches that found nothing.

## In the dashboard

Without an agent or code, in this order:

1. **Records:** upload a JSON file on the index's **Records** page, or connect the [Shopify app](/docs/shopify.md), the [WooCommerce plugin](/docs/woocommerce.md), or a [product feed](/docs/feeds.md).
2. **Search settings:** until anyone changes them, **Use these settings** puts the [suggested settings](/docs/settings.md#suggested-settings) for your kind of data in the form. Check them against your list, section by section:
   - **Matching:** the searchable attributes, in order and with weights, typo tolerance and exact words, compound words, and languages.
   - **Ranking and sorting:** custom ranking, rank first, learn from clicks, sorts, and variants.
   - **Filters and facets:** what shoppers narrow by.
   - **Synonyms and rules:** other words, and fixes for single searches.
   - **Suggestions:** query suggestions, did you mean, and related results.
3. **Playground:** run each search on your list. Open **Why this result?** for those that find the wrong thing, and **Save as test** for those that work.
4. **Merchandising:** if you want an order of your own, arrange your main category pages and most searched words.
5. **Public access:** allow your site's origins, so its pages can search.
6. **Analytics,** once people search.

## In code

The [server SDK](/docs/server-sdk.md) does the same. Start from the recipe for your data, such as [products](/docs/products.md), then check your list and save it as tests:

```ts
import { SearchServerClient } from "@findlane/server"

const client = new SearchServerClient({
  workspaceId: process.env.FINDLANE_WORKSPACE_ID!,
  apiKey: process.env.FINDLANE_API_KEY!,
})
const products = client.index("products")

// What each search on the list finds first.
const list = ["cadence", "trail shoes waterproof", "40211", "sneakers"]
for (const q of list) {
  const { hits } = await products.search({ q, limit: 3 })
  console.log(q, hits.map((hit) => hit.record.title))
}

// Searches that must keep working.
await products.setSearchTests([
  {
    id: "cadence",
    q: "cadence",
    expect: { objectID: "stridewell-cadence-road-knit", inTop: 3 },
  },
  { id: "trail-shoes", q: "trail shoes", expect: { minResults: 20 } },
  {
    id: "no-gift-card",
    q: "shoes",
    expect: { objectID: "gift-card", notInTop: 20 },
  },
])

// A change is saved only when it breaks no test.
const settings = await products.getSettings()
const synonyms = settings.synonyms ?? []
const next = {
  ...settings,
  synonyms: [...synonyms, ["sneakers", "trainers", "running shoes"]],
}
const comparison = await products.compareSettings(next)
if (comparison.tests.every((test) => test.after || !test.before)) {
  await products.replaceSettings(next)
}
```

For a search that finds the wrong thing, search with `explain: true` to see what each result matched ([explain results](/docs/search.md#explain-results)).
