# Quick start

> Create an index, upload products from your server, choose what to search, and search from a web page.

You need a Findlane account ([start free](https://app.findlane.dev/dashboard)), and Node.js 18 or later, Bun, or Deno on your server.

## 1. Create an index

In the dashboard, choose **New index** and name it, for example `products`. Names use lowercase letters, digits, `_`, and `-`, and start with a letter.

A new index searches `title` and `brand.name` and counts values of `brand.name` as a facet, so a catalog with those fields searches well before you change anything.

## 2. Create an API key

Open **API keys**, choose **Create key**, and give it the `search` and `write` scopes. Put the key and the **Workspace ID** shown on that page in your server's environment:

```sh
FINDLANE_WORKSPACE_ID=your-workspace-id
FINDLANE_API_KEY=tsk_your_api_key
```

The key is shown once. Keep it on your server: it can change your catalog.

## 3. Upload products

```sh
npm install @findlane/server
```

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

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

type Product = {
  objectID: string
  title: string
  brand: { name: string }
  price: number
  inStock: boolean
}
const products = client.index<Product>("products")

await products.upsert([
  {
    objectID: "sku-1",
    title: "Trail running shoe",
    brand: { name: "Acme" },
    price: 129,
    inStock: true,
  },
  {
    objectID: "sku-2",
    title: "Waterproof rain jacket",
    brand: { name: "Northwind" },
    price: 189,
    inStock: false,
  },
])
```

`upsert` adds or replaces up to 1,000 records a call, matched by `objectID`, and they are searchable when it returns. To load or refresh a whole catalog, use [`replaceAllStaged`](/docs/records.md#replace-the-whole-catalog). You can also upload a JSON file on the index's **Records** page.

## 4. Choose what to search and filter

Settings refer to fields your records have, so save them after uploading. Fetch the current settings, change them, and save them whole:

```ts
const settings = await products.getSettings()

await products.replaceSettings({
  ...settings,
  searchableAttributes: [
    { field: "title", weight: 8 },
    { field: "brand.name", weight: 4 },
  ],
  facetFields: ["brand.name", "inStock"],
  sorts: [
    { id: "price-asc", label: "Price: low to high", field: "price", direction: "asc", thenBy: [] },
  ],
})

const results = await products.search({ q: "trail sho", facets: ["brand.name"] })
console.log(
  results.total,
  results.hits.map((hit) => hit.record.title)
)
```

The index's **Search settings** page does the same, and its **Playground** tries searches. [Search settings](/docs/settings.md) covers every option.

## 5. Search from the browser

On the index's **Public access** page, add the origins your storefront runs on under **Allowed origins**, such as `https://shop.example.com` and `http://localhost:5173`. Then copy the **Public ID**.

```sh
npm install @findlane/browser
```

```ts
import { SearchBrowserClient } from "@findlane/browser"

const client = new SearchBrowserClient({ publicId: "pub_your_public_id" })

const results = await client.search<Product>({
  q: "trail sho",
  filters: { inStock: true },
  facets: ["brand.name"],
  highlight: true,
  limit: 12,
})

for (const hit of results.hits) {
  // Highlights are HTML-escaped, with the matched words in <mark> tags.
  console.log(hit.highlights?.title ?? hit.record.title, hit.record.price)
}
console.log(results.facets["brand.name"]) // { Acme: 1 }
```

For search as you type, cancel the previous request when the query changes:

```ts
const input = document.querySelector<HTMLInputElement>("#search")!
let pending: AbortController | undefined

input.addEventListener("input", async () => {
  pending?.abort()
  pending = new AbortController()
  try {
    const { hits } = await client.search<Product>(
      { q: input.value, limit: 8, highlight: true },
      { signal: pending.signal }
    )
    renderHits(hits)
  } catch (error) {
    if ((error as Error).name !== "AbortError") throw error
  }
})
```

## Next steps

- Keep the index in step with your store: [Records](/docs/records.md#keep-records-in-sync).
- Build filters, facets, sorting, and paging: [Searching](/docs/search.md).
- Decide what browsers can read before you launch: [Security](/docs/security.md).
