Tachyontachyon
Guides

Add search to a Next.js app

Index your data with the tachyon-sdk on the server, and query it from a route handler or server action.

This walks through wiring Tachyon into a Next.js App Router project: index documents server-side, and expose search through a route handler your client components can call.

1. Run Tachyon

docker run -p 8108:8108 -v tachyon-data:/data adikeshri/tachyon

See Configuration before deploying this anywhere reachable by more than your own machine.

2. Install the SDK

npm install tachyon-sdk

3. Create a collection and index your data

Do this once — from a script, a seed route, or wherever your data currently lives (a database, a CMS, static content).

// scripts/index-products.ts
import { Tachyon } from "tachyon-sdk";

const client = new Tachyon({
  url: process.env.TACHYON_URL!,
  apiKey: process.env.TACHYON_ADMIN_KEY!,
});

await client.collections.create({
  name: "products",
  fields: [
    { name: "title", type: "text" },
    { name: "description", type: "text" },
    { name: "brand", type: "keyword", facet: true },
    { name: "price", type: "int", filter: true, sort: true },
  ],
});

const products = await getProductsFromYourDatabase();

await client.collection("products").documents.index(
  products.map((p) => ({
    id: p.id,
    title: p.title,
    description: p.description,
    brand: p.brand,
    price: p.priceCents,
  })),
);

See Collections and Documents for the full schema and indexing shapes.

4. Expose search through a route handler

Query with the search key, not the admin key — the route handler is reachable from the client, so it should only be able to read, never write. See Configuration → Authentication.

// app/api/search/route.ts
import { NextRequest, NextResponse } from "next/server";
import { Tachyon } from "tachyon-sdk";

const client = new Tachyon({
  url: process.env.TACHYON_URL!,
  apiKey: process.env.TACHYON_SEARCH_KEY!,
});

export async function GET(req: NextRequest) {
  const q = req.nextUrl.searchParams.get("q") ?? "";
  if (!q) return NextResponse.json({ hits: [], found: 0 });

  const results = await client.collection("products").search({
    q,
    queryBy: ["title", "description"],
    facet: ["brand"],
    limit: 20,
  });

  return NextResponse.json(results);
}

5. Call it from a client component

"use client";

import { useState, useTransition } from "react";

export function ProductSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<{ document: Record<string, unknown> }[]>([]);
  const [isPending, startTransition] = useTransition();

  function onChange(value: string) {
    setQuery(value);
    startTransition(async () => {
      const res = await fetch(`/api/search?q=${encodeURIComponent(value)}`);
      const data = await res.json();
      setResults(data.hits);
    });
  }

  return (
    <div>
      <input value={query} onChange={(e) => onChange(e.target.value)} placeholder="Search products…" />
      {isPending ? <span>Searching…</span> : null}
      <ul>
        {results.map((hit) => (
          <li key={String(hit.document.id)}>{String(hit.document.title)}</li>
        ))}
      </ul>
    </div>
  );
}

Keeping the index in sync

Whenever a product is created, updated, or deleted in your database, call client.collection('products').documents.index(...) or .delete(id) from the same code path — a mutation route, a server action, a database trigger via a queue. Tachyon has no built-in connector to your database, so keeping search fresh is on your application, the same as with any external search engine.

Next steps

  • Filtering and Sorting for narrowing and ordering results beyond plain relevance
  • Faceting for a filter sidebar backed by real counts
  • Deployment for running Tachyon in production

On this page