Guides
Search settings
Choose the fields to search and their weights, the fields to filter, facet, and sort by, and how results rank.
Each index has its own settings. Change them on the index's Search settings page, or from your server:
const settings = await products.getSettings()
const saved = await products.replaceSettings({
...settings,
facetFields: ["brand.name", "categories"],
})
console.log(saved.applied) // "fields"replaceSettings saves the full settings, so start from getSettings() and change what you need. Facet, filter, and sort fields must appear in at least one record first, because their types come from your data.
A new index searches title (weight 8) and brand.name (weight 4), counts brand.name as a facet, and sorts by relevance.
Suggested settings
After the first upload, until anyone changes its settings, the index's Search settings page suggests settings for its records: the fields to search and their weights, facets, sorts, and how to order equally relevant results. They follow the kind of data the index holds:
| Kind | Searched first | Facets | Sorts | Equally relevant results |
|---|---|---|---|---|
| Products | title, brand, categories, description | brand, categories | popular, price both ways, newest | in stock, then popular |
| Docs | headings, then text | none (sections filter) | relevance | reading order |
| Articles | title, authors, categories, abstract | categories | newest, most popular or cited | most popular |
| Listings | title, address or area, type, description | area, type | price both ways, newest | newest |
| Other | title, brand, categories, description | unchanged | unchanged | unchanged |
- The kind: choose it when you create the index. It stays on the index's settings page, where you can change it at any time, and agents set it with
set_index_kind. The dashboard's words follow it too, such as visitors and listings for listings (analytics). An index made through the API without one has its kind guessed from its records for these suggestions, and plain words in the dashboard. - What each field is for comes from its name, such as
title,brand.name, orprice. For fields whose names don't say, such aspriceFromorsoldOut, an AI model reads the field's name and a few of its values and guesses. Its guesses only shape suggestions, and each field is looked at once. - Nothing changes until you save. Use these settings puts the suggestions in the form, where you can change them first. Synonyms, languages, typo tolerance, did you mean, and compound words stay as they are.
- Settings you chose stay yours. Once the searched fields, facets, filters, sorts, ranking, or location differ from a new index's, whether you saved them, an agent did, or they were imported, nothing is suggested.
Agents get the same suggestions from get_index (AI agents), and servers from GET /suggestions.
Settings
{
searchableAttributes: [
{ field: "title", weight: 8 },
{ field: "brand.name", weight: 4 },
{ field: "sku", weight: 4, typos: false },
{ field: "description", weight: 1 },
],
facetFields: ["brand.name", "categories", "inStock"],
filterFields: ["price"],
sorts: [
{ id: "relevance", label: "Relevance", field: "relevance", direction: "desc", thenBy: [] },
{ id: "price-asc", label: "Price: low to high", field: "price", direction: "asc",
thenBy: [{ field: "title", direction: "asc" }] },
{ id: "newest", label: "Newest", field: "createdAt", direction: "desc", thenBy: [] },
],
defaultSort: "relevance",
rankFirst: [{ field: "inStock", direction: "desc" }],
customRanking: [{ field: "popularity", direction: "desc" }],
relevanceTolerance: 10,
synonyms: [["sneakers", "trainers", "running shoes"]],
oneWaySynonyms: [{ input: "shoes", synonyms: ["sneakers", "boots"] }],
rules: [{ id: "boots", if: { phrases: ["boots"] }, then: { boost: { categories: "Boots" } } }],
typoTolerance: true,
shortPrefixes: true,
typoWordLengths: { oneTypo: 4, twoTypos: 7 },
exactWords: ["puma", "nike"],
didYouMean: true,
compoundWords: true,
querySuggestions: { enabled: true, popularSearches: true, popularCount: 6, minSearches: 3, minDays: 2,
fields: null, added: [], pinned: ["black friday"], hidden: [] },
languages: ["en"],
normalizationProfile: "default-v2",
locationField: "_geoloc",
distinct: { field: "productId", limit: 1 },
}Searchable fields
searchableAttributes lists one to five text fields that queries match, each with a weight from 0.1 to 100 (1 by default). A match in a heavier field ranks higher, so give short, precise fields like titles, brands, and SKUs more weight than descriptions.
An index has two short search fields, which hold up to 1 KB of text each, and three long ones, which hold up to 16 KB each. The first two fields of a new index use the short ones; fields added later use long ones while any are free. When you add more fields at once than there are free long ones, the lightest get the long ones, since descriptions usually weigh least. A record with too much text in a searchable field is rejected with a message naming the field.
To find records by ID, add objectID as a searchable field. Its words match exactly, without typos, prefixes, or synonyms.
Facets and filters
facetFields are fields to count values for, such as brands and categories, which also makes them filterable. filterFields are fields to filter by without counting values, such as prices. Facets and filters work on text, numbers, and booleans, and on lists of them.
Field types
Facet, filter, sort, and ranking fields store one type of value, which comes from the records. To choose settings before any record has a field, give its type in fieldTypes:
{
facetFields: ["brand", "tags"],
sorts: [{ id: "price-asc", label: "Price: low to high", field: "price", direction: "asc", thenBy: [] }],
locationField: "_geoloc",
fieldTypes: { brand: "text", tags: "text[]", price: "number", _geoloc: "location" },
}- A type is
text,number, orboolean, with[]for a list, orlocation. A list field also accepts a single value. fieldTypesonly applies when settings are saved; reading settings doesn't return it.- Once records have a field, their type wins.
- A write whose value doesn't fit the type, such as
"cheap"for a number field, is rejected.
Location
locationField names the field that holds each record's location, such as _geoloc: an object with lat and lng numbers. Searches can then find records near a point or inside a box, and draw them on a map.
- An index has one location field, and each record one location.
nullremoves it. - The field can't also be a facet, filter, or sort field.
- Records without it are left out of location searches.
- Once it's set, a write with a location that isn't
{ lat, lng }(lat from -90 to 90, lng from -180 to 180) is rejected.
Sorting
sorts are the orders a search can ask for by id, besides relevance. Each sorts by one field that holds a single value (not a list), asc or desc. thenBy lists up to two more such fields, each with its own direction, that order records tied on the fields before them: the price-asc sort above puts products of the same price in title order. Records still tied after the last field are ordered by object ID. defaultSort is the sort used when a search doesn't choose one. Sort IDs use lowercase letters, digits, _, and -, and an index has up to eight. distance is taken: it's the built-in sort by distance from near.
The relevance sort can't have thenBy fields; customRanking orders its equally relevant results.
Custom ranking
customRanking orders results that are about equally relevant, within 10% of the best match's score, by up to three fields, such as popularity or stock. Relevance still comes first: a much better match is never pushed down by it.
relevanceTolerance sets how close counts as equally relevant: a whole percent from 1 to 50, 10 by default. Raise it to let popularity reorder more of the results, or lower it to keep the closest matches on top. It also sets which of the best matches distance orders in searches with near.
Rank first
rankFirst orders every result by up to two fields before relevance, for what should come first whatever the query, such as products in stock:
{ rankFirst: [{ field: "inStock", direction: "desc" }] }- Relevance orders each group. In-stock products come first, ordered by relevance and custom ranking, followed by the others in the same order.
- Sorts follow it too. The
price-ascsort above lists in-stock products by price, then the others. Thedistancesort doesn't. - Its fields hold one value, not a list, like sort fields. Records without the field come last.
Learn from clicks
learnedRanking: true puts the products shoppers pick most for a search first, learned from their clicks and purchases. Once a day, each search with 10 or more clicks in the last 30 days gets up to 5 products, best first: those with at least 3 of its clicks and a tenth of its picks. Recent clicks count more, a click from 10 days ago half as much, and with visitor IDs an add-to-cart credited to the search counts as 3 clicks and a purchase as 5.
- Rank first still comes first. Learned products go first within their rank first group, so in-stock products stay ahead of a sold-out favorite.
- Rules come before it. Products a rule pins or boosts for the search stay ahead of learned ones.
- Only those searches change. Other searches, sorts other than relevance, filters, and totals don't, and a learned product still has to match the search.
- It learns the search as it's typed, lowercased:
Batmanandbatmanshare a list,batman costumehas its own.
It's measured as it runs. Each day, 1 in 10 shoppers searching from the browser is held out and gets the ranking without learning, so the dashboard compares the two groups: clicks per 100 searches, the share of clicks on the first 3 results, and add-to-carts and purchases per 1,000 searches, over the last 14 days. A shopper's group is picked from their address and browser for the day; nothing about them is stored but the group.
learnedExclusions leaves searches or products out, at once: { queries: ["gift"], products: ["sku-123"] }. A product left out isn't learned for any search. The lists come within the hour of turning it on, then update daily; they're off by default, every plan has them, and changes apply without a rebuild. In the dashboard, it's under Search settings → Learn from clicks, which lists each search's learned products with their clicks and sales, and buttons to leave them out. Explain marks a learned hit with learned: { place }.
Group variants
distinct shows each product once when its colors or sizes are records of their own. Records with the same value in field, such as the product ID they share, show as one result: the record that best matches the search.
{ distinct: { field: "productId", limit: 1 } }- The best match shows. Searching "blue trail shoe" shows each shoe as its blue record when the color is a searchable field, even when every color shares the title. With no query, the sort or custom ranking picks.
- Groups are ranked by their best record, in the search's usual order: rank first, relevance, custom ranking, a sort, or distance.
limitshows up to 1 to 4 of a group's records, one after another; 1 by default.- Counts count groups.
total,offset,limitandhasMorecount products, andtotalRecordsthe matching records. Facet counts still count records. - The field holds one text or number value, not a list. A record without it is a group of its own. It's stored like a filter field, so choosing it updates the index in place.
- A search can choose:
distinct: falseshows every record, and a number 1 to 4 shows that many of each group's.
null turns grouping off.
Variants inside each record
variants is for products whose variants are listed inside them, such as a supplement's flavors, so each product stays one record. Each hit then says which variant the search matched, so your page can show that variant's image, name and price, and link to it:
{ variants: { field: "variants" } }{
"objectID": "amino-energy",
"title": "Amino Energy",
"image": "https://cdn.example.com/amino-energy.png",
"variants": [
{ "flavor": "Orange", "sku": "1038658", "price": 4290, "inStock": true, "image": "https://cdn.example.com/amino-orange.png" },
{ "flavor": "Blue Raspberry", "sku": "1041296", "price": 4290, "inStock": true, "image": "https://cdn.example.com/amino-blue.png" }
]
}- Words pick it. Make the variants' own fields searchable, such as
variants.flavor. Searching "amino blue raspberry" then points to the Blue Raspberry variant. - So do filters on the variants' fields, such as a flavor facet on
variants.flavor. When the words match a variant the filters leave out, such as a sold-out flavor in a search filtered tovariants.inStock, the words win. - Nothing stands out, no pointer. A search whose words match only the product's own fields, such as its brand, points to no variant, so the page shows the product as it is.
- Facets count products, since each product is one record.
- A filter on two variant fields can be met by two different variants: blue in one, size 42 in another. When that matters, as with clothing, make a record per color and group them instead.
The response's variant holds the item's place in the list. null turns it off.
Synonyms
synonyms are groups of words or phrases that match each other: searching for any of them finds the others, ranked slightly below the word itself. A phrase counts when its words are typed together and in order, so with ["sneakers", "running shoes"], both sneakers and running shoes find either one. An index has up to 1,000 groups of 2 to 20 terms, each of up to four words.
oneWaySynonyms work in one direction: a search for the input also finds its synonyms, but not the other way round. With { input: "shoes", synonyms: ["sneakers", "boots"] }, shoes finds sneakers and boots, while boots finds only boots. An index has up to 1,000, each with 1 to 20 synonyms, and the input and each synonym are up to four words.
To put some records first for a query, filter it, or change its words, use rules.
Typo tolerance
With typoTolerance on (the default), query words also match:
- as prefixes, for words of three or more letters, so
jackfindsjacket; - with one typo in words of four to six letters, and two in longer words;
- with two neighboring letters swapped, so
hsoefindsshoe.
Exact matches rank above prefixes and typos, and numbers match exactly. Turn it off to match whole words only.
typoTolerance also takes two stricter modes, as Algolia's do. They count each result's typos over the query's words. Exact words, prefixes and synonyms have none.
"min"keeps only the results with the fewest typos. When anything matches without typos, results with typos are left out."strict"keeps those and the results with one typo more, and ranks the fewer typos first in every sort. A search sorted by date shows the exact matches, newest first, then the near misses.
In both, totals, facets and stats count only what's kept. A search whose words match only with typos still finds them.
shortPrefixes: true is for search boxes that search with each letter typed: the query's last word matches as the start of longer words from its first letter, and numbers do too.
skfindsSkerplugata, andiphone 1finds the iPhone 13.- Without it, one- and two-letter words and numbers match only whole words, so shoppers see nothing until their third letter.
- Only the last word is matched this way: words before it were typed in full.
- Whole words still rank first.
- It's off by default. Importing from Algolia turns it on, as Algolia matches the last word this way.
typoWordLengths sets how long words must be for typos: words of at least oneTypo letters match with one typo, and words of at least twoTypos letters with two. It's { oneTypo: 4, twoTypos: 7 } by default. oneTypo is 3 to 20, and twoTypos is from oneTypo to 50. If short words find too much, raise them: with { oneTypo: 5, twoTypos: 8 }, boat no longer finds boot.
Some words shouldn't match with typos at all, but still match as prefixes:
- A field without typos. Add
typos: falseto a searchable field, such as{ field: "sku", weight: 4, typos: false }, for SKUs, product codes, and other fields where a near miss is a different product. Its words match exactly or as prefixes, and the other fields still match with typos. - Exact words.
exactWordslists up to 1,000 single words, of up to 64 characters, that match without typos in every field, such as brand names. WithexactWords: ["puma"],pumano longer findspump. Case and accents don't matter.
Did you mean
With didYouMean on (it's off by default):
- a search whose results matched some words only through typo tolerance says what it searched for, so you can show "Showing results for protein" for
protien; - a search that finds nothing can ask for a correction that typo tolerance doesn't reach, such as
prtien→protein,icecream→ice cream, orback pack→backpack. A suggestion is offered only if it finds results with the same filters.
See Did you mean for how to use them. Suggestions come from the words in your records, which update in the background within an hour of changes, and within minutes of a full catalog replacement or of turning the setting on.
Compound words
Many languages join words into compounds, and catalogs often write the same one both together and apart. With compoundWords on (it's off by default), searches find both:
- a query word that's a compound in your records also matches its parts written apart, so
sjóræningjabúningur(pirate costume) finds "Sjóræningja búningur", anddrumkitfinds "drum kit"; - neighboring query words also match the word they make, so
barna afmæli(children's birthday) finds "Barnaafmæli", andface paintfinds "facepaint".
A compound is a word in your records, of six or more letters, that two other words of the same field make up, each of three or more letters. It works in every language, and helps most in Icelandic, German, Danish, Norwegian, Swedish, Dutch, and English. Exact matches still rank first: a compound match weighs less than a synonym.
The compounds come from the words in your records, like did you mean's suggestions. Searches use them about a minute after you turn the setting on, and new words within about an hour of changes. To see them work, search in the index's Playground and open Why this result?: a compound match says "compound word", and its highlights mark both parts. explain reports it as how: "compound".
Related results
When a search's words find nothing, related results show the products closest to it in meaning, under a heading of their own. They catch searches that typo tolerance and synonyms can't:
- run-together words and heavy typos, such as
peanutjellycatorfuncup; - other words for the same thing, such as
waterlubeorfingravibrator; - searches in English for products described in Icelandic, such as
pale waterproof face paint.
Searches that find something don't change. Related results come back in the response's related, beside an empty hits, so analytics still count the search as one that found nothing.
Turn them on in the index's Search settings, under Suggestions, or with relatedResults: { enabled: true }. They're on paid plans, for indexes of up to 200,000 records, and off by default. Once they're on:
- Every record is embedded within a few minutes, from its searchable fields. New and changed records are embedded within minutes of their write. A change to a record's price or stock embeds nothing.
- The floor is calibrated from the index's own searches that found nothing in the last 30 days. It's the lowest similarity at which at most about 1 in 20 of them shows a poor first related result. The dashboard shows examples at that floor, and you can move it with
relatedResults.minScore, from 0.3 to 0.9. An index with fewer than 20 such searches starts at 0.55. No related results show until the floor is set.
What it costs is capped at 3% of your plan's price a month, which covers embedding a plan's records several times over. When that's used up, embedding waits until the next month. A query's own embedding is free.
On the MCP server, agents turn them on with update_settings, follow the embedding and the calibration with get_related_results, and see a search's related results with search.
Facet matches
Facet matches tell a search which facet values its query names, such as a category ("prót" names Prótein), a brand or a zip, so a search box can offer them beside the products. Searches ask for them with facetMatches: true. Searches that don't ask don't change, and matching takes microseconds.
Each field in facetMatches.fields is a facet. Its values match by their names:
- Names from records.
labelPathis where each value's name is, in the same list of objects as the value. A store whose records holdcategories: [{ name: "Prótein", slug: "protein" }]and filter by acategorySlugsfacet useslabelPath: "categories.name"andlabelKey: "categories.slug". So the matches filter on the facet the store's pages already use, and show its names. When the facet is itself in the list, such asbrand.slug,labelKeycan be left out. - Names written in the setting.
labelsnames values, up to 2,000: a name, or up to five, such as other spellings or forms of a town's name. Values that share a name match as one, so a town matches all its zips. A number also matches by itself, and with its name after it:104and101 reykjavík. - The value itself, when it has no name.
// Zips named by their towns.
{ field: "zip", labels: { "220": ["Hafnarfjörður", "Hafnarfirði"], "221": ["Hafnarfjörður", "Hafnarfirði"], "101": "Reykjavík" } }
// A shop's categories and brands, named from their records.
facetMatches: {
fields: [
{ field: "categorySlugs", labelPath: "categories.name", labelKey: "categories.slug",
link: "/shop?category={value}", hide: ["new", "discounts"] },
{ field: "brandSlugs", labelPath: "brand.name", labelKey: "brand.slug", link: "/shop?brand={value}" },
],
}| Setting | What it does |
|---|---|
only | "labelled": only values with a name match, for a field whose other values aren't worth offering. |
hide | Values that never match, such as campaign or internal categories. Up to 200. |
link | Where a match goes: a path on the site or an https address, with {value}. Several values are joined by commas. |
apply | When a search that asks filters by a match itself: "never" by default, "whole" when the match is the whole query, or "exact" whenever it names a value exactly, searching the query's other words. On the setting or a field; a field's goes over the setting's. |
perField | Matches per field, 1 to 10; 3 by default. A search gets at most 10. |
Up to 10 fields. A query's words match a value's as rules' phrases do:
- folded, without stop words, with plurals and Icelandic word forms;
- with the index's synonyms, so
creanames Kreatín through "creatine"; - two words joined, so
go fitnames GoFitness; - the last word as a prefix from two letters;
- words of five or more letters with one typo.
In the dashboard, set them up under Search settings → Filters and facets → Facet matches. Turn a facet on and the names found in your records are filled in, go through its values to hide some, choose when it filters the search, and try a search right there.
Before turning apply on for an index in use, run compare_settings or the page's compare. It asks in its searches, so it shows what searches that ask would find. A brand is usually a good first field: "whole" there means "diablo" shows Diablo's products. A category at "whole" can hide a lot: "protein" could go from 332 products to the 48 in the Prótein category.
A field's 2,000 values with the most records match. Changes apply at once, with no rebuild. On the MCP server, get_facet_matches lists a field's values with their names, counts, and whether they're hidden, for going through them, and update_settings hides and shows values one by one.
Query suggestions
Query suggestions offer whole searches as shoppers type, and popular searches when the search box is empty. They're on by default, and every index has them on every plan. Their settings, under querySuggestions, change what each index's list holds:
| Setting | Default | What it does |
|---|---|---|
enabled | true | false turns suggestions off: the index answers none. |
popularSearches | true | false leaves the empty box without popular searches. Typed suggestions still use them. |
popularCount | 6 | Popular searches an empty box shows, 1 to 10. |
minSearches | 3 | A shoppers' search becomes a suggestion once searched this many times… |
minDays | 2 | …on this many different days, which stand in for different shoppers. |
fields | null | The facet and searchable fields whose values are suggested, such as categories and brands. null chooses fields whose values read as names: not slugs, sizes, numbers, or long text. |
pinned | [] | Searches shown first, in the empty box too, in order. Up to 20. |
added | [] | Searches to suggest though shoppers haven't searched them yet, such as a campaign's name. Up to 1,000. |
hidden | [] | Searches never suggested, nor any suggestion with their words in a row. Up to 1,000. |
Each list holds searches of one to six words. When saving settings, querySuggestions changes only the settings it names. Changes apply within a few minutes, without a rebuild. In the dashboard, the index's Search settings page has them under Suggestions, with a box that shows what shoppers see as they type, and with the box empty.
Languages
languages lists the languages of your records and your shoppers' searches: any of da (Danish), de (German), en (English), is (Icelandic), no (Norwegian), and sv (Swedish). None are set by default. For each one, searches:
- leave out common words such as
the,for,og, andfür, soshoes for kidsfinds records without "for". A query of only such words keeps them; - match singular and plural forms, so
boxesfindsboxandSchuhfindsSchuhe, ranked slightly below the word itself. Icelandic matches its plurals and cases with Icelandic word forms; - match common spellings for letters typed without them:
ue,ae, andoeforü,ä, andöin German,aaandoeforåandøin Danish and Norwegian, andaa,ae, andoeforå,ä, andöin Swedish, somuellerfindsMüller.
Icelandic word forms
inflections: true, with is among the languages, matches every case and plural of Icelandic nouns and adjectives. A search word of four letters or more that no word in your records is, or begins with, also finds the other forms of its word: kjólar finds Kjóll, jokerinn finds Joker, and ömmu finds Amma, ranked like plurals are in the other languages. A search that still finds nothing is tried once more with every form of each word.
Searches that find results with their words as typed don't change: a word your records have matches as it's typed, and so does a word a shopper is still typing, such as ungbarna when your records have Ungbarnasett. It's off by default, applies within about a second without a rebuild, and goes when is leaves languages. In the dashboard, it's under Search settings → Languages, once Icelandic is chosen.
The word forms come from BÍN, the Database of Icelandic Morphology, © The Árni Magnússon Institute for Icelandic Studies, used under CC BY-SA 4.0. Findlane uses its nouns and adjectives, without names of people, companies, or places.
Text normalization
Searches ignore case and accents, so cafe finds Café. normalizationProfile changes how text is folded:
default-v2, the default for new indexes: case and accents, and letters such asø,ß,æ,ł, andþalso match their plain spellings, sokobenhavnfindsKøbenhavnandstrassefindsStraße.default-v1: case and accents only. Indexes created beforedefault-v2keep it until you change it.icelandic-ascii-v1: also matchesðasd,þasth, andæasae.default-v2does this too, for more letters.
A word keeps an apostrophe, period, or colon between its letters, and a period or comma between its digits, so don't, o'brien, j.r.r, 3.5mm, and 1,000 each match as one word. Either apostrophe works: levi's finds Levi’s, and levi’s finds Levi's. A word with an apostrophe also finds it written without one, ranked slightly below. Typed without its apostrophe, a word of four letters or more finds it through typo tolerance: dont finds Don't.
Rules
A rule changes the results of searches that meet its conditions: a query with one of its phrases, a category page's filters, a context your page sends, and the times it's active. It can boost or bury records, filter them, hide some, change the query's words, or return data for a banner. Rules apply when searching, so a change takes effect within a few seconds, without a rebuild. Edit them on the index's Search settings page, from your server with replaceSettings, or through an AI agent.
A party shop's rules:
{
rules: [
{
// Balloons first for "blöðrur" and "balloons", and not the delivery service.
id: "balloons",
if: { phrases: ["blöðrur", "balloons"] },
then: { boost: { category: "Balloons" }, hide: ["balloon-delivery"] },
},
{
// "kerti 5 ára" (candles, 5 years) searches "kerti" among the number candles for 5.
id: "age-5",
if: { phrases: ["5 ára"] },
then: { replace: "", filters: { birthdayNumbers: "5" } },
},
{
// "barnabúningar" (kids' costumes) searches every costume, kids' first.
id: "kids-costumes",
description: "Most costumes have no age group, so boost rather than filter",
if: { phrases: ["barnabúningar"] },
then: { replace: "búningar", boost: { ageGroup: "Kids" } },
},
{
// A banner for searches that start with "halloween".
id: "halloween",
if: { phrases: ["halloween"], match: "startsWith" },
then: { userData: { banner: "Everything for Halloween", url: "/halloween" } },
},
],
}iffires the rule when the query is one of itsphrases(match: "is"), starts with one ("startsWith"), or contains one ("contains", the default). A rule has up to 20 phrases of one to four words, and a phrase can take a number or a facet value from the query with a placeholder. A rule can also, or instead, fire on the search's filters, a context, or searches without words: see Category pages and campaigns.- Phrases are compared as synonyms are: case, accents, and the languages' common words don't matter, and plurals and other spellings match. With
en, a rule forballoonalso fires forBalloonsandthe balloons. thendoes one or more of the things below.enabled: falsekeeps a rule without applying it. Reading settings always returnsenabled; leave it out when saving to turn the rule on.
What rules do
boost: records matching these filters come before the others, such as balloons before balloon pumps. It applies to searches sorted by relevance, with a query or without one, such as a category page, after rank first's groups: relevance, or the browse order, orders the boosted records and the others. Named sorts like price and thedistancesort keep their order, since the shopper chose it.bury: records matching these filters come after the others, in the same searches. When several rules fire, each boost counts one and each bury minus one, and records with more come first.filters: added to the search's filters. A field the search already filters keeps the search's filter, so a shopper who chose another number still gets it.hide: up to 100 objectIDs left out of the results, such as a delivery service among the products.pin: up to 20 records at places from 1, such as[{ objectID: "nike-air", position: 1 }], on searches with words or without and with every sort. A pinned record shows when it passes the search's filters, even if the words didn't find it, so a pin on a category page can't put another category's product there. It leaves its own place, andtotalcounts it. When two rules pin to the same place, the later pin moves down one. With grouped variants, a pinned record shows its group. Hits a rule pinned carrypinned: true.banner: an image the results come with, inbanners, such as a sale's:{ image: "https://…/sale.jpg", link: "/sale", title: "Up to 50% off", position: 3 }. The image is an https address, the link an https address or a path on your site, andpositionits place among the results, from 1 (the top by default). React'sHitsand the Shopify app show it there.redirect: a page the search goes to instead of its results, such as/pages/returnsforreturns, an https address or a path on your site. React'sSearchBoxand the Shopify app go there when a shopper presses Enter; the results come with it inredirect.replace: words searched instead of the phrase the rule matched, up to 64 characters.""removes the phrase: with theage-5rule,kerti 5 árasearches forkerti. The response'srewrittenQuerysays what was searched.userData: any JSON up to 4 KB, returned with the results inappliedRules, for your site to show a banner or go to another page.
filters, boost, and bury take the same shapes as a search's filters, $or included, on 1 to 10 facet or filter fields. On the Search settings page, Pin products finds products to pin by searching the index.
Merchandising
An index's Merchandising page arranges category pages and searches without writing a rule by hand. It lists every arranged page and search with whether shoppers see it now: Live, Scheduled, Ended, or Off.
- Arrange a page opens a category (a facet field's value) or a search's exact words. The page shows its products in the index's own order, with their images.
- Your order: drag a product to a place, or use its menu (put first, move earlier or later, move to a place). The products you place come first, in your order, up to 20; below a line, the rest follow the index's ranking. Moving a product below the line brings the products above it into your order, so what you see is what's saved.
- Hide a product on the page, add a product found by name within the category, add a banner, and choose when: always, or only between two times for a campaign.
- Save keeps it as one rule for that page, which applies within seconds; opening the page again edits it, and keeps anything else the rule does, such as a boost. Unsaved changes are kept until you save or discard them.
The grid doesn't show other rules' boosts; Check it in the Playground shows the page as shoppers see it. A product that doesn't pass the page's filters is marked, since shoppers won't see it. Pins set through the API at places with gaps, such as 3 and 7, show at their places; the first change on this page makes places 1 to 7 your order.
Banner images and numbers
Upload an image in the Banner panel, or choose one uploaded before, and Findlane hosts it at https://images.findlane.dev. Banners can also use an image address of your own.
- Images: JPEG, PNG, WebP or AVIF, up to 2 MB, stored as uploaded. An index keeps up to 50 images and 100 MB. An image no rule shows is deleted after a week, and an index's images go when it's deleted. You need the rights to the images you upload; see the terms.
- Numbers: each banner shows how often shoppers' searches showed it, its clicks, and its click rate over the last 30 days, in the editor and the list. Searches answered from the cache count; crawlers and scripts don't. Clicks come from React's
Hitsand the Shopify app, or your own page withclient.events.bannerClick(rule, { query, position })(events).
Everything on this page can be done from your own app with the merchandising API or the server SDK (0.23.0), and by an AI agent with the MCP server's merchandising tools.
Placeholders
A placeholder in a phrase takes part of the query, and the rule's actions use what it took, so one rule covers every number or every value of a field:
{
rules: [
{
// "kerti 5 ára" (candles, 5 years) searches "kerti" among the candles for 5, for every number.
id: "kerti-ara",
if: { phrases: ["kerti {number} ára", "kertin {number} ára", "{number} ára kerti"] },
then: { filters: { birthdayNumbers: "{number}" }, replace: "kerti" },
},
{
// A query that names a theme, such as "star wars diskar", puts that theme first.
id: "themes",
if: { phrases: ["{Thema_name}"] },
then: { boost: { Thema_name: "{Thema_name}" }, replace: "" },
},
],
}{number}matches one word of the query that is a number, such as5or30. A phrase needs a word besides it.- A facet field's placeholder, such as
{Thema_name}, matches the longest run of up to four words of the query that is one of the field's values, compared as phrases are, sostar warsmatchesStar Wars. The field must be a facet: the values come from its facet counts. - A placeholder counts as one of a phrase's four words, and a phrase has up to two different ones. A placeholder that
thenuses must be in every phrase.
What the placeholder took goes into then:
- A filter, boost, or bury value that is only a placeholder takes the value as its field's type:
{ birthdayNumbers: "{number}" }filters to"5"when the field holds text and to5when it holds numbers, and{ price: { lte: "{number}" } }filters prices up to the number. When the value doesn't fit the field, such as a theme for a number field, the rule doesn't fire. - Placeholders in other text have the value written in, a facet value as the field stores it: in
replace, inuserData's strings, such as{ banner: "Kerti fyrir {number} ára" }, and in longer filter values. With thethemesrule,star wars diskarsearchesdiskar, with Star Wars first.
On the Search settings page, the rule editor lists {number} and the index's facet fields under the phrases; click one to put it in. A filter's count of the results it keeps uses an example value, such as the field's most common number, and says which.
Preview a placeholder rule before you save it. At a party shop, two placeholder rules gave the same results as its 28 candle rules, one for each number, and covered every other number too. But with {number}, a number that no record has, or whose records lack the field's value, finds nothing instead of loose text matches: a search for candles for 70 found two "70 ára" candles by their text before, and those records have no birthdayNumbers 70. Preview changes shows such searches among those that find fewer results, or now find nothing.
Category pages and campaigns
Rules aren't only for search words. A category page is a search without words that filters by its category, so a rule can merchandise it, and a campaign can run between two times:
{
rules: [
{
// The shoe category page shows the new trail shoe first, then Nike. Searches with words on the page don't fire it.
id: "shoes-page",
if: { filters: { category: "Skór" }, words: "none" },
then: { pin: [{ objectID: "trail-shoe-2026", position: 1 }], boost: { brand: "Nike" } },
},
{
// During the sale, sale items come first wherever the page sends the "black-friday" context.
id: "black-friday",
active: { from: "2026-11-27T00:00:00Z", until: "2026-12-01T00:00:00Z" },
if: { context: ["black-friday"] },
then: { boost: { onSale: true }, userData: { banner: "Black Friday: up to 50% off" } },
},
{
// Costumes first in February, for öskudagur, on every search that names them.
id: "oskudagur",
active: { from: "2027-02-01T00:00:00Z", until: "2027-02-20T00:00:00Z" },
if: { phrases: ["búningar", "búningur"] },
then: { boost: { category: "Búningar" } },
},
],
}filtersfires the rule when the search filters each of these fields (1 to 10 filter fields) to one of these values, alone or in aninlist:{ category: "Skór" }, or{ category: ["Skór", "Stígvél"] }for either page. It reads the search's own filters, before rules add theirs.contextfires the rule when the search sends one of these names as itscontext, up to 10. Your site chooses them, such ascategory-page,store:kringlan, orblack-friday.wordsis"none"for searches without words only, such as a category page, or"some"for searches with them. Leave it out for both. It's for rules without phrases, since phrases need words.- Every condition given must hold. A rule without any fires on every search, and with
active, on every search between its times. activefires the rule only fromfromand beforeuntil, ISO 8601 times with their zone. Either may be left out. Cached results change within about a minute of each time, with no settings change. The settings page shows each rule's times in your time zone, and whether it's scheduled, active, or ended.- On pages without words, a boost puts the boosted records first within rank first's groups, then the browse order, so a sold-out boosted product stays below what's in stock. Hide and the rule's filters apply to every sort.
Lists and recommendations don't use rules: their own pins and hidden products shape them.
Boost or filter
Boost, unless every record you want has the field. A filter leaves out every record without it: if one product in eight has an age group, filtering barnabúningar to kids' costumes hides most of them, while a boost puts the tagged ones first and keeps the rest. Filter when the query names something the records always have, like the number on a birthday candle.
Order and limits
- The list's order is the rules' priority. Up to 10 rules fire on a search: the first 10 in the list whose conditions it meets.
- Earlier rules win. When two rules filter the same field, or replace overlapping words, the earlier one's applies. A rule's
$orapplies when neither the search nor an earlier rule filters its fields or has an$or. - Up to 1,000 rules an index. IDs are 1 to 64 letters, digits,
_,.,:, or-, and descriptions up to 200 characters. - Saved only when valid. A rule that can't apply, such as one boosting a field that isn't a facet or filter field, is refused when you save, with a message that names it, like
Rule age-5: replace must be text of at most 64 characters.
Checking rules
- Rule hits. The Search settings page shows how many searches in the last 30 days had a query each rule fires on, with the most searched of those queries. They're counted from the index's most searched queries against the rules as they are now, disabled ones too, so you can see what a new rule would catch before turning it on. Agents read the same with
get_rule_hits, for the last day, week, 30, or 90 days. Rules with filter or context conditions, or without phrases, aren't counted, since a query's words don't tell when they fire. - The Playground. Searches in the index's Playground show the rules that fired, with their
userData, and the query searched after a rule replaced words. Turn Rules off to run the same search without them. Why this result? names the rules that boosted or buried each result. - From your server,
enableRules: falsesearches without the rules, andrulesAtchecks them as at another time, such as a campaign's start. See Rules in searches. - Before saving, Preview changes shows the searches a rule change would change, and the search tests it would break.
Preview changes and search tests
A change that looks harmless can break a search people rely on. Before you save, Preview changes on the Search settings page runs the same searches with the saved settings and with your changes, and shows what would be different. Nothing is saved until you save.
What a preview compares
- The index's search tests (below), each passing or failing with the saved settings and with your changes.
- Its 100 most searched queries of the last 30 days, from Analytics.
- Queries you name, from your server or an agent.
Each query is searched with both settings, for up to 20 seconds in all, so a preview of a busy index may stop early, and says so. It shows:
- A summary: how many queries' top 10 changes, how many find more or fewer results, and how many now find nothing or now find something, with their searches.
- Each search test before and after. Tests that now fail or now pass come first.
- Up to 50 changed queries, the tests' first and then the most searched: the number of results before and after, and the results that left, joined, or moved within the top 10.
Previews don't count as searches.
What can't be compared
A preview searches the index as it's stored, so it compares the settings that apply when searching: rules, synonyms, one-way synonyms, typo tolerance and its word lengths, exact words, rank first, custom ranking, relevance tolerance, languages, compound words, field weights, and did you mean. A change to what the index stores needs the index updated or rebuilt first, so you'll see its effect after saving: new searchable fields, new facet, filter, sort, ranking, or location fields, and text normalization. Save those on their own, then preview the rest.
Search tests
A search test is a search that must keep working, and what its results should hold. A party shop's tests:
await products.setSearchTests([
// Gold balloons first among the balloons for "gull" (gold).
{ id: "gold-balloons", q: "gull", filters: { category: "Balloons" }, expect: { objectID: "balloon-gold-30", inTop: 3 } },
// The delivery service stays out of the balloons.
{ id: "no-delivery", q: "blöðrur", expect: { objectID: "balloon-delivery", notInTop: 20 } },
// Halloween costumes keep finding costumes.
{ id: "halloween-costumes", q: "hrekkjavökubúningar", expect: { minResults: 100 }, note: "Two rules split this word" },
])inTop: the record is among the first N results, 1 to 20.notInTop: the record isn't among the first N.minResults: the search finds at least N records, 1 to 10,000.
q is the query (empty browses), and filters takes a search's filters on facet, filter, or sort fields. Tests search with the index's default sort and its rules. An index has up to 200 tests, saved apart from its settings, with IDs of 1 to 64 letters, digits, _, ., :, or -, and notes of up to 200 characters.
A rule that looked redundant
A party shop had two hand-made rules that split hrekkjavökubúningar (Halloween costumes) into its two words. They looked like leftovers, but without them that search finds 1 result instead of 1,247. With the halloween-costumes test above, a preview of removing them shows the test going from passing to Now fails, and the query among those that find fewer results, before anything is saved.
Save a test whenever you fix a search, with a rule, a synonym, or a ranking change, so later changes keep it working.
In the dashboard
- Preview changes is beside Save changes while there are unsaved changes. When a preview finds tests that would start failing, the bar says how many before you save.
- Search tests on the Search settings page lists the tests in words, such as "Balloon Delivery not in the top 20". Add, edit, and delete them there; they're saved at once, apart from the settings. Run tests runs them with the saved settings.
- Save as test in the Playground makes a test from a result: the search's query and filters, expecting the result in the top 3, 5, 10, or 20, whichever its place rounds up to. Change it before saving if you like.
From your server, compareSettings takes the settings to compare, or only those that change, and {} runs the tests with the current settings:
const comparison = await products.compareSettings({ rules: nextRules })
const failing = comparison.tests.filter((test) => test.before && !test.after)
console.log(comparison.summary.fewerResults, failing.map((test) => test.id))See the server SDK and the HTTP API. Agents use compare_settings, get_search_tests, and set_search_tests on the MCP server.
A/B tests
A preview says what a change does to results. An A/B test says whether shoppers do better with it. Instead of saving a change, test it: some shoppers search with the change, the rest with today's settings, and the Search settings page compares their clicks, add-to-carts, orders and revenue per search. Nothing changes for everyone until you apply it.
Start a test
Change settings on the Search settings page, then choose Test this change beside Save changes. Pick the share of shoppers who get the change (10 to 50%) and how many days the test runs (7 to 60; 14 by default). The dialog says how long the index's traffic needs to show a 10% change in clicks per search. One test runs on an index at a time.
The settings that apply when searching can be tested, as for a preview: rules and merchandising, synonyms and one-way synonyms, typo tolerance, exact words, rank first, custom ranking, relevance tolerance, compound words, did you mean, learning from clicks, and search field weights. A change to what the index stores can't be tested side by side: new search, facet, filter or sort fields, languages, and text normalization. Save those instead.
Run an A/A test gives both groups today's settings. Any difference it finds is chance, which shows how big a difference a real test needs before you trust it.
Who gets which
- Browser searches only. A shopper's group comes from their address and browser, and stays the same for the whole test, on every page and every day. Nothing about them is stored. Searches with an API key keep today's settings.
- Each group's results are cached apart, so while a test runs, some searches miss the cache that otherwise wouldn't. How many depends on how much the cache answers: the more of the index's searches it answers, the more a split costs, and smaller shares cost less. The start dialog works it out from the last 7 days and says about how many more searches a day. They count toward your plan as any search does.
- Events carry the shopper's group. The browser SDK (0.26.0) copies it from each search answer's
abTestonto the events it sends. Other events get it from the shopper's address: those a browser sends, and those your server relays from your own domain. Orders and other events your server sends itself count toward a group only with the shopper'sabTest: pass it to the server SDK'sevents.send(0.32.0).
Results
For today's settings and for the change, the test shows:
- searches;
- the share that found nothing;
- clicks per search, and the share of clicks on the first 3 results;
- add-to-carts and orders per 100 searches;
- revenue per search.
Each figure counts the first pages of searches with words, by relevance. Beside each is the change's difference, with the range it likely lies in (95%), worked out from how much the days differ.
Each order counts once. When your server sends orders with the shopper's group, the test counts those, and not the same orders sent from the shopper's browser; otherwise it counts the browser's.
A result is called once the test has run its days with 1,000 searches in each group: better, worse, or no clear difference, judged on clicks per search, then add-to-carts. Before that the numbers move a lot, and stopping a test as soon as they look good is the classic way to fool yourself, so nothing is called until the end. At most stores' traffic, clicks and add-to-carts settle within a couple of weeks. Orders and revenue settle only for large changes.
Ending a test
- End test keeps today's settings, or applies the change, at any time.
- At its end date, the test ends keeping today's settings, and owners and admins get an alert with its result. Apply the change under the ended test applies it afterwards.
- The guard ends a test early, keeping today's settings, when the change's group finds nothing on 5 points more of its searches than today's settings, two days running. Owners and admins hear why.
- While a test runs, saving settings changes both groups. The change keeps its own values for the settings it changes.
Agents use start_ab_test, get_ab_test, and end_ab_test on the MCP server. Servers read tests and their results with /ab-tests, or with an analytics key from /analytics/ab-test: the server SDK's abTests.get() and analytics.abTest() (0.32.0).
How changes apply
Searches keep working while settings change. applied in the response says how the change took effect:
applied | Changes | When it takes effect |
|---|---|---|
instant | Weights, synonyms, one-way synonyms, rules, languages and Icelandic word forms, typo tolerance and its modes, short prefixes, typo word lengths, exact words, fields without typos, relevance tolerance, did you mean, compound words, learning from clicks, sort labels and the default sort, removing or reordering searchable fields, and facet, filter, sort, rank first, custom ranking, and location changes the index already holds the data for: removing one, filtering on a field it keeps, or sorting by a number filter field | Within about a second |
fields | Facet, filter, sort, rank first, custom ranking, and location fields the index doesn't hold the data for yet | When the save returns: seconds for tens of thousands of records |
searchable | New searchable fields | In the background for large indexes |
rebuild | Text normalization | In the background for large indexes |
none | Nothing changed |
A searchable or rebuild change that takes longer than a few seconds continues in the background, with its progress in rebuild (also returned by getSettings). Searches use the previous settings until it finishes, and writes made meanwhile are included. Wait for it before depending on the new settings:
const saved = await products.replaceSettings(next)
if (!saved.rebuilt && saved.rebuild) await products.waitForSettings()