# Products

> Records and settings for a store's catalog: one record per product, variants as lists, prices as numbers, and the facets and sorts shoppers expect.

Shoppers type product names with typos, narrow by brand, size, and price, and sort by price. This recipe sets up a catalog for that, as in the [playground's store](/playground). On Shopify, [the app](/docs/shopify.md) does it for you.

## Records

One record per product:

```json
{
  "objectID": "stridewell-cadence-road-knit",
  "title": "Cadence Road Knit Running Shoe",
  "brand": "Stridewell",
  "category": "Running shoes",
  "description": "An everyday neutral trainer with a one-piece knit upper…",
  "price": 109,
  "compareAtPrice": 129,
  "inStock": true,
  "colors": ["Glacier blue", "Black"],
  "sizes": ["8", "9", "10", "11"],
  "popularity": 874,
  "createdAt": 1788220800,
  "image": "https://cdn.example.com/cadence-road-knit.webp",
  "url": "/products/cadence-road-knit"
}
```

- **Variants are lists.** Put the colors and sizes for sale in lists, so the product shows once and still matches a filter on any of them. A list matches when any of its values does, so a filter for black in size 11 also finds a shoe whose black pair stops at size 10. When that matters, make one record per color, with its own image and sizes.
- **`price` is the lowest variant price,** as a number. Add a `priceMax` to show a range, and a `compareAtPrice` to show a sale.
- **`inStock`** lets shoppers hide what they can't buy.
- **`popularity`,** such as units sold in the last 30 days, puts the best sellers first among equally good matches.
- **One currency per index,** or a price field per currency, such as `price_eur` and `price_usd`, each a filter and sort field.

## Settings

```ts
await products.replaceSettings({
  ...(await products.getSettings()),
  searchableAttributes: [
    { field: "title", weight: 8 },
    { field: "brand", weight: 4 },
    { field: "category", weight: 3 },
    { field: "description", weight: 1 },
  ],
  facetFields: ["brand", "category", "colors", "sizes"],
  filterFields: ["price", "inStock"],
  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: "price-desc",
      label: "Price: high to low",
      field: "price",
      direction: "desc",
      thenBy: [{ field: "title", direction: "asc" }],
    },
    { id: "newest", label: "Newest", field: "createdAt", direction: "desc", thenBy: [] },
  ],
  defaultSort: "relevance",
  customRanking: [{ field: "popularity", direction: "desc" }],
  synonyms: [
    ["sneakers", "trainers", "running shoes"],
    ["rucksack", "backpack"],
  ],
  didYouMean: true,
  languages: ["en"],
})
```

- **Weights:** a match in the title beats one in the brand, which beats one in the description.
- **Facets** are what shoppers tick: brand, category, color, and size. Price and stock are filters: a price slider takes its ends from [`facetStats`](/docs/search.md#number-ranges).
- **Custom ranking** orders products that match about equally well, so the popular one comes first among them, but never above a clearly better match.
- **Synonyms** are your shoppers' words for what you sell. Find them under **Searches with no results** on the **Analytics** page.
- **Did you mean** shows "Showing results for protein" for `protien`, and suggests a fix when a search finds nothing.

## Search

```ts
const results = await client.search({
  q: "trail shoes",
  filters: { inStock: true, price: { lte: 150 }, sizes: { in: ["10"] } },
  facets: ["brand", "category", "colors", "sizes"],
  disjunctiveFacets: ["brand", "colors", "sizes"],
  facetStats: ["price"],
})
```

`disjunctiveFacets` keeps the other brands, colors, and sizes listed after a shopper ticks one. For a results page with checkboxes, a price slider, and sorting ready-made, see [React](/docs/react.md#a-results-page).

## Keep it in sync

- **Prices and stock change often.** Upsert a product when it changes, from a webhook or your admin's save handler. Each write is searchable when it returns.
- **Replace the catalog nightly** with [`replaceAllStaged`](/docs/records.md#replace-the-whole-catalog), so products you deleted leave search too.
