> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your first search experience

> Set up a React app, index sample data in Algolia, and build a search interface with React InstantSearch.

This quickstart builds a React app that searches a sample product catalog,
filters by product type, and paginates results.

<Frame>
  <iframe width="644" height="363" src="https://www.youtube.com/embed/gvkdmY6UWxw" title="Build your first search experience" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen className="video-embed mb-4" />
</Frame>

<Columns cols={2}>
  <Card title="View the demo" icon="monitor-play" href="https://product-search-react-typescript.netlify.app/">
    Explore the product search app you'll build in this quickstart.
  </Card>

  <Card title="Explore the source code" icon="github" href="https://github.com/algolia/quickstarts/tree/main/product-search-react-typescript">
    Download the complete quickstart app from GitHub.
  </Card>
</Columns>

## Before you begin

Make sure you have:

* **An Algolia account**. [Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **[Node.js](https://nodejs.org/en/download)** 20.19 or later.

## Set up your app and index sample data

Set up your project and index a sample product dataset.
In Algolia, the way you structure and configure your index affects both the search experience and how results are ranked.

<Steps>
  <Step title="Create a Vite app" id="create-app">
    Create a new Vite app with React and TypeScript.

    ```sh icon=square-terminal theme={"system"}
    npm create vite@latest algolia-quickstart-products -- --template react-ts --no-interactive
    cd algolia-quickstart-products
    ```
  </Step>

  <Step title="Install dependencies" id="install-dependencies">
    Install the packages for indexing and search:

    * `algoliasearch` sends data to Algolia.
    * `react-instantsearch` builds the search UI.
    * `instantsearch.css` provides base styles.
    * `tsx` runs the indexing script.
    * `ora` shows progress in the terminal while the script runs.

    ```sh icon=square-terminal theme={"system"}
    npm install algoliasearch instantsearch.css react-instantsearch ora tsx
    ```
  </Step>

  <Step title="Add your Algolia credentials" id="add-credentials">
    In the Algolia dashboard, open the [API Keys](https://dashboard.algolia.com/account/api-keys) page and copy these values:

    * **Application ID**. Identifies your Algolia application.
    * **Write API key**. Used by the indexing script to update records and index settings.
    * **Search API key**. Used by the React app to query the index from the browser.

    Create a `.env.local` file and add the values to it:

    ```dotenv .env.local icon=lock-keyhole theme={"system"}
    VITE_ALGOLIA_APPLICATION_ID=
    ALGOLIA_WRITE_API_KEY=
    VITE_ALGOLIA_SEARCH_API_KEY=
    ```

    <Note>
      Keep your Write API key secret.
      Don't commit `.env.local` to version control or expose the write key in your app.
    </Note>
  </Step>

  <Step title="Create the indexing script" id="add-indexing-script">
    Create a `scripts` directory and a `scripts/indexing.ts` file.

    ```sh icon=square-terminal theme={"system"}
    mkdir scripts && touch scripts/indexing.ts
    ```

    This script loads your credentials,
    fetches a sample product dataset,
    uploads those products as records to Algolia with `saveObjects`,
    and adds `product_type` as a facet with `setSettings` so your UI can filter results.

    Run this script once to create and configure the index before connecting it to your UI.
    Since indexing operations are asynchronous,
    `waitForTasks` and `waitForTask` make sure the records and settings are applied before the script exits.

    ```ts scripts/indexing.ts icon=code expandable theme={"system"}
    import { algoliasearch } from "algoliasearch";
    import ora from "ora";
    import { loadEnv } from "vite";

    const env = loadEnv(process.env.MODE ?? "dev", process.cwd(), "");

    const appId = env.VITE_ALGOLIA_APPLICATION_ID;
    const writeApiKey = env.ALGOLIA_WRITE_API_KEY;
    const indexName = "quickstart-products";

    if (!appId) {
      throw new Error("Missing VITE_ALGOLIA_APPLICATION_ID environment variable.");
    }

    if (!writeApiKey) {
      throw new Error("Missing ALGOLIA_WRITE_API_KEY environment variable.");
    }

    const client = algoliasearch(appId, writeApiKey);
    const spinner = ora();

    async function indexProducts() {
      spinner.start("Fetching the products dataset...");

      const response = await fetch(
        "https://dashboard.algolia.com/api/1/sample_datasets?type=apparel",
      );

      if (!response.ok) {
        throw new Error(
          `Error fetching products dataset: ${response.status} ${response.statusText}`,
        );
      }

      const products = await response.json();

      spinner.text = `Indexing ${products.length.toLocaleString()} products into ${indexName}...`;

      await client.saveObjects({
        indexName,
        objects: products,
        waitForTasks: true,
      });

      spinner.text = `Add facet to ${indexName}...`;

      const { taskID } = await client.setSettings({
        indexName,
        indexSettings: {
          attributesForFaceting: ["product_type"],
        },
      });

      await client.waitForTask({ indexName, taskID });
    }

    try {
      await indexProducts();
      spinner.succeed("Successfully indexed products.");
    } catch (error) {
      spinner.fail("Indexing failed.");
      console.error(error);
      process.exitCode = 1;
    }
    ```
  </Step>

  <Step title="Index the sample products" id="index-products">
    Add the indexing script to `package.json`:

    ```sh icon=square-terminal theme={"system"}
    npm pkg set scripts.index:products="tsx scripts/indexing.ts"
    ```

    Run the script to create the `quickstart-products` index and add the sample records.

    ```sh icon=square-terminal theme={"system"}
    npm run index:products
    ```

    <Check>
      You should see the message `Successfully indexed products.` in your terminal.
      At this point, your data is ready for search and you can explore the `quickstart-products` index
      in the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
      In the next section, you'll connect a React UI to this index.
    </Check>
  </Step>
</Steps>

## Build the search UI

Connect a React UI to your `quickstart-products` index.
React InstantSearch automatically keeps the search box, filters, and pagination in sync with the current results.

<Steps>
  <Step title="Connect your app to Algolia" id="build-search-page">
    Replace `src/App.tsx` with the following code to connect your app to Algolia and render a basic search interface.

    ```tsx src/App.tsx icon=code expandable theme={"system"}
    import { liteClient as algoliasearch } from "algoliasearch/lite";
    import type { Hit } from "instantsearch.js";
    import {
      Configure,
      Highlight,
      Hits,
      InstantSearch,
      Pagination,
      PoweredBy,
      RefinementList,
      SearchBox,
      Snippet,
    } from "react-instantsearch";
    import "instantsearch.css/themes/reset-min.css";
    import "./App.css";

    const appId = import.meta.env.VITE_ALGOLIA_APPLICATION_ID;
    const apiKey = import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY;

    const searchClient = algoliasearch(appId, apiKey);

    type ProductRecord = {
      title: string;
      description: string;
      product_type: string;
      price: number;
      showcase_image: string;
    };

    type ProductHit = Hit<ProductRecord>;

    type ProductCardProps = {
      hit: ProductHit;
    };

    function ProductCard({ hit }: ProductCardProps) {
      return (
        <article className="product-card">
          <div className="product-card-image">
            <img src={hit.showcase_image} alt={hit.title} />
          </div>
          <div className="product-card-body">
            <p className="product-card-type">{hit.product_type}</p>
            <h2 className="product-card-title">
              <Highlight attribute="title" hit={hit} />
            </h2>
            <p className="product-card-description">
              <Snippet attribute="description" hit={hit} />
            </p>
            <p className="product-card-price">${hit.price}</p>
          </div>
        </article>
      );
    }

    export default function App() {
      return (
        <InstantSearch indexName="quickstart-products" searchClient={searchClient}>
          <Configure hitsPerPage={12} />
          <div className="search-header">
            <SearchBox placeholder="Search products" />
            <PoweredBy />
          </div>

          <div className="search-body">
            <div className="filter-panel">
              <div className="filter-panel-section">
                <div className="filter-panel-section-title">Product type</div>
                <RefinementList attribute="product_type" sortBy={["count:desc"]} />
              </div>
            </div>

            <div className="search-results">
              <Hits hitComponent={ProductCard} />
              <Pagination />
            </div>
          </div>
        </InstantSearch>
      );
    }
    ```

    This app connects to your `quickstart-products` index and renders a search interface.

    <Note>
      * [`SearchBox`](/doc/api-reference/widgets/search-box/react) lets users enter search terms.
      * [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) adds filtering by product type.
      * [`Hits`](/doc/api-reference/widgets/hits/react) displays matching records from your index.
      * [`Pagination`](/doc/api-reference/widgets/pagination/react)lets users move through results.
      * [`Highlight`](/doc/api-reference/widgets/highlight/react) and [`Snippet`](https://www.algolia.com/doc/api-reference/widgets/snippet/react) emphasize matching parts of `title` and `description`.
    </Note>
  </Step>

  <Step title="Style the search interface" id="style-search">
    Replace `src/App.css` with the following code.
    These styles lay out the search interface and make the results and filters easier to scan.
    They change the presentation, not the search behavior.

    ```css src/App.css icon="paintbrush" expandable theme={"system"}
    *,
    *::before,
    *::after {
      box-sizing: border-box;
    }

    :root {
      --color-text: #111827;
      --color-background: #ffffff;
      --color-border: #e5e7eb;
      --color-surface: #ffffff;
      --color-surface-muted: #f3f4f6;
      --color-muted: #6b7280;
      --color-accent: #5468ff;
      --color-accent-strong: #5468ff;
      --color-accent-ring: rgb(84 104 255 / 0.15);
    }

    body {
      margin: 0;
      font-family: system-ui, sans-serif;
      color: var(--color-text);
      background-color: var(--color-background);
    }

    #root {
      inline-size: min(100%, 70rem);
      margin-inline: auto;
      padding-block: 2rem;
      padding-inline: 1.25rem;
      color: inherit;
    }

    .search-header {
      display: grid;
      gap: 0.5rem;
      margin-block-end: 1.5rem;
    }

    .search-body {
      display: grid;
      grid-template-columns: 12.5rem minmax(0, 1fr);
      gap: 2rem;
    }

    .ais-SearchBox-input,
    .ais-Pagination-link {
      border: 1px solid var(--color-border);
      border-radius: 0.5rem;
      background-color: var(--color-surface);
    }

    .ais-SearchBox-input {
      inline-size: 100%;
      padding-block: 0.75rem;
      padding-inline: 1rem;
      font: inherit;
      font-size: 1rem;
      color: inherit;
      outline: none;
    }

    .ais-SearchBox-input:focus {
      border-color: var(--color-accent);
      box-shadow: 0 0 0 0.1875rem var(--color-accent-ring);
    }

    .ais-SearchBox-submit,
    .ais-SearchBox-reset {
      display: none;
    }

    .ais-RefinementList-list,
    .ais-Hits-list {
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .ais-Hits-list {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
      gap: 1rem;
    }

    .ais-Hits-item {
      display: flex;
    }

    .ais-Pagination-list {
      display: flex;
      flex-wrap: wrap;
      gap: 0.5rem;
      justify-content: center;
      padding-block: 0.75rem;
    }

    .ais-RefinementList-label {
      display: flex;
      align-items: center;
      gap: 0.5rem;
      font-size: 0.875rem;
    }

    .ais-PoweredBy,
    .ais-RefinementList-count,
    .product-card-type {
      font-size: 0.75rem;
      color: var(--color-muted);
    }

    .ais-PoweredBy {
      display: flex;
      align-items: center;
      min-block-size: 1.5rem;
      line-height: 1;
      justify-self: end;
    }

    .ais-PoweredBy-link {
      display: flex;
      align-items: center;
    }

    .ais-PoweredBy-text {
      line-height: 1;
    }

    .ais-RefinementList-list {
      display: grid;
      gap: 0.5rem;
    }

    .ais-RefinementList-count {
      margin-inline-start: auto;
    }

    .ais-RefinementList-checkbox {
      inline-size: 0.875rem;
      block-size: 0.875rem;
      accent-color: var(--color-accent);
    }

    .ais-RefinementList-item--selected .ais-RefinementList-label {
      font-weight: 500;
    }

    .ais-Pagination-link {
      display: block;
      padding-block: 0.5rem;
      padding-inline: 0.75rem;
      font-size: 0.875rem;
      color: inherit;
      text-decoration: none;
    }

    .ais-Pagination-item--selected .ais-Pagination-link {
      color: #fff;
      background: var(--color-accent-strong);
      border-color: var(--color-accent-strong);
    }

    .ais-Pagination-item--disabled .ais-Pagination-link {
      opacity: 0.4;
      pointer-events: none;
    }

    .product-card {
      display: flex;
      flex: 1;
      flex-direction: column;
      inline-size: 100%;
      overflow: hidden;
      border: 1px solid var(--color-border);
      border-radius: 0.75rem;
      background-color: var(--color-surface);
    }

    .product-card-image {
      aspect-ratio: 1;
      overflow: hidden;
      background: var(--color-surface-muted);
    }

    .product-card-image img {
      inline-size: 100%;
      block-size: 100%;
      object-fit: cover;
    }

    .product-card-body {
      display: flex;
      flex: 1;
      flex-direction: column;
      gap: 0.25rem;
      padding: 0.75rem;
    }

    .product-card-type {
      margin: 0;
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }

    .product-card-title,
    .product-card-price,
    .filter-panel-section-title {
      margin: 0;
      font-size: 0.875rem;
      font-weight: 500;
      color: var(--color-text);
    }

    .product-card-description {
      flex: 1;
      margin: 0;
      overflow: hidden;
      font-size: 0.875rem;
      line-height: 1.5;
      color: var(--color-muted);
      display: -webkit-box;
      -webkit-line-clamp: 3;
      line-clamp: 3;
      -webkit-box-orient: vertical;
    }

    .filter-panel-section {
      padding-block-end: 0.75rem;
    }

    .filter-panel-section-title {
      margin-block-end: 0.5rem;
    }

    @media (max-width: 800px) {
      #root {
        padding-block: 1.5rem;
        padding-inline: 1rem;
      }

      .search-body {
        grid-template-columns: 1fr;
        gap: 1.5rem;
      }
    }
    ```
  </Step>

  <Step title="Start the app" id="start-app">
    Start the development server.

    ```sh icon=square-terminal theme={"system"}
    npm run dev
    ```

    Open [`http://localhost:5173`](http://localhost:5173) and start searching.

    <Check>
      You see a product grid with images, a product type filter,
      and a search box.
      As you type, the product grid updates.
      Matching text is highlighted in the results.
    </Check>

    You now have a working end-to-end search:

    * Records come from the `quickstart-products` index you created earlier.
    * Queries from `SearchBox` update the results and pagination together.
    * The `product_type` filter works because that attribute is configured for faceting.
    * `Highlight` and `Snippet` show which parts of each record match the query.
  </Step>
</Steps>

## Improve relevance and add features

<AccordionGroup>
  <Accordion title="Improve relevance">
    Your app already returns matching products.
    You can improve relevance by adjusting which matches appear first.

    Update the existing `indexSettings` object in `scripts/indexing.ts`,
    then rerun `npm run index:products`.

    ```ts scripts/indexing.ts icon=code expandable theme={"system"}
    const { taskID } = await client.setSettings({
      indexName,
      indexSettings: {
        attributesForFaceting: ["product_type"],
        searchableAttributes: [ // [!code ++:6]
          "unordered(title)",
          "unordered(product_type)",
          "unordered(description)",
        ],
        customRanking: ["desc(units_sold)", "desc(price)"],
      },
    });
    ```

    This makes search results more useful by [prioritizing matches](/doc/api-reference/api-parameters/searchableAttributes) in product titles and types before descriptions,
    then [ranking similar matches](/doc/api-reference/api-parameters/customRanking) by best-selling and higher-priced products.
  </Accordion>

  <Accordion title="Add more search features">
    Add features that help users understand and refine results.
    For example, you can add [search stats](/doc/api-reference/widgets/stats/react),
    [active filters](/doc/api-reference/widgets/current-refinements/react),
    or more ways to filter results, such as [filter lists](/doc/api-reference/widgets/refinement-list/react),
    [menus](/doc/api-reference/widgets/menu/react),
    and [numerical filters](/doc/api-reference/widgets/numeric-menu/react).

    <Note>
      If you add more filtering features,
      update `attributesForFaceting` in `scripts/indexing.ts` to match.
    </Note>
  </Accordion>

  <Accordion title="Collect events from your search interface">
    Collect events from your UI to measure engagement and
    support [Algolia features that use behavioral data](/doc/guides/sending-events/concepts/event-types).

    Update the `InstantSearch` component in `src/App.tsx`:

    ```tsx src/App.tsx icon=code theme={"system"}
    <InstantSearch
      indexName="quickstart-products"
      searchClient={searchClient}
      insights // [!code ++]
    >
    ```

    This turns on the default React InstantSearch events,
    including `view` events for hits and filter click events for `RefinementList`.

    To send click events for hits, see [Send events with React InstantSearch](/doc/guides/building-search-ui/events/react).
  </Accordion>
</AccordionGroup>

## What's next

* [Get started with React InstantSearch](/doc/guides/building-search-ui/getting-started/react) to continue building your search interface.
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data) to replace the sample catalog with your own product data.
* [Manage results](/doc/guides/managing-results/relevance-overview) to learn how to adjust relevance and ranking to your needs.
