Claude Code for Algolia: Instant Search and Index Management — Claude Skills 360 Blog
Blog / Backend / Claude Code for Algolia: Instant Search and Index Management
Backend

Claude Code for Algolia: Instant Search and Index Management

Published: April 12, 2027
Read time: 7 min read
By: Claude Skills 360

Algolia provides hosted full-text search with a JavaScript client and React InstantSearch library — algoliasearch(appId, apiKey) creates a client. index.saveObjects(records) indexes documents with an objectID. <InstantSearch searchClient={client} indexName="products"> wraps search UI. <SearchBox> renders the input. <Hits hitComponent={ProductHit}> renders results. useSearchBox() and useHits() are hook equivalents for headless UI. <RefinementList attribute="brand"> adds faceted filtering. <Configure hitsPerPage={20} filters="inStock:true"> sets search parameters. <Highlight attribute="name" hit={hit}> highlights matching text. index.setSettings({ searchableAttributes, ranking, attributesForFaceting }) configures how results are ranked. Claude Code generates Algolia indexing scripts, React InstantSearch UIs, custom hit components, faceted search, and webhook-based index synchronization patterns.

CLAUDE.md for Algolia

## Algolia Stack
- Version: algoliasearch >= 4.23, react-instantsearch >= 7.11
- Client: const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_API_KEY)
- Index: const index = client.initIndex("products"); index.saveObjects(records, { autoGenerateObjectIDIfNotExist: true })
- Search UI: <InstantSearch searchClient={sClient} indexName="products"><SearchBox /><Hits hitComponent={Hit} /></InstantSearch>
- Hooks: const { query, refine } = useSearchBox(); const { hits, results } = useHits<ProductHit>()
- Facets: <RefinementList attribute="brand" sortBy={["isRefined", "count"]} />
- Config: <Configure hitsPerPage={20} filters="inStock:true AND price<=100" />
- Highlight: <Highlight attribute="name" hit={hit} /> — wraps matches in <mark>

Index Setup and Management

// lib/algolia/client.ts — Algolia client setup
import algoliasearch from "algoliasearch"

// Admin client (server-side only — has write permissions)
export const algoliaAdmin = algoliasearch(
  process.env.ALGOLIA_APP_ID!,
  process.env.ALGOLIA_ADMIN_KEY!,  // Never expose to client
)

// Search-only client (safe for browser)
export const algoliaSearch = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!,  // Read-only key
)

export const productsIndex = algoliaAdmin.initIndex("products")
export const articlesIndex = algoliaAdmin.initIndex("articles")
// lib/algolia/indexing.ts — document indexing and settings
import { productsIndex } from "./client"

// Configure index settings (run once or on config change)
export async function configureProductsIndex() {
  await productsIndex.setSettings({
    // Which fields to search in, in order of importance
    searchableAttributes: [
      "name",                           // Tier 1 — most important
      "brand,categories",               // Tier 2 — comma = same priority
      "description",                    // Tier 3
      "unordered(tags)",                // Position in field doesn't matter
    ],

    // Custom ranking — tiebreak after text matching
    customRanking: [
      "desc(popularity)",
      "desc(rating)",
      "asc(price)",
    ],

    // Faceting — which attributes to filter by
    attributesForFaceting: [
      "brand",
      "categories",
      "searchable(tags)",
      "filterOnly(inStock)",
      "filterOnly(price)",
    ],

    // Return only these fields to save bandwidth
    attributesToRetrieve: [
      "objectID",
      "name",
      "brand",
      "price",
      "imageUrl",
      "slug",
      "rating",
      "inStock",
    ],

    // Highlight these attributes in results
    attributesToHighlight: ["name", "description"],

    // Snippet length for description previews
    attributesToSnippet: ["description:20"],

    // Query rules
    queryLanguages: ["en"],
    removeStopWords: true,
    minWordSizefor1Typo: 4,
    minWordSizefor2Typos: 8,
  })

  console.log("Algolia products index configured")
}

// Index record type
type ProductRecord = {
  objectID: string
  name: string
  brand: string
  categories: string[]
  tags: string[]
  description: string
  price: number
  imageUrl: string
  slug: string
  rating: number
  reviewCount: number
  inStock: boolean
  popularity: number
}

// Index a single product
export async function indexProduct(product: ProductRecord) {
  return productsIndex.saveObject(product)
}

// Batch index products
export async function indexProducts(products: ProductRecord[]) {
  const { objectIDs } = await productsIndex.saveObjects(products)
  console.log(`Indexed ${objectIDs.length} products`)
  return objectIDs
}

// Delete a product from index
export async function removeProduct(objectID: string) {
  return productsIndex.deleteObject(objectID)
}

// Partial update — only update changed fields
export async function updateProductStock(objectID: string, inStock: boolean) {
  return productsIndex.partialUpdateObject({ objectID, inStock })
}

React InstantSearch UI

// components/search/ProductSearch.tsx — full search page
"use client"
import {
  InstantSearch,
  SearchBox,
  Hits,
  RefinementList,
  Pagination,
  Configure,
  Stats,
  SortBy,
  ClearRefinements,
  CurrentRefinements,
  ToggleRefinement,
} from "react-instantsearch"
import { algoliaSearch } from "@/lib/algolia/client"
import { ProductHit } from "./ProductHit"

export function ProductSearch() {
  return (
    <InstantSearch
      searchClient={algoliaSearch}
      indexName="products"
      future={{ preserveSharedStateOnUnmount: true }}
    >
      {/* Configure sets default search parameters */}
      <Configure hitsPerPage={24} />

      <div className="grid grid-cols-[240px_1fr] gap-8">
        {/* Sidebar filters */}
        <aside className="space-y-6">
          <div>
            <h3 className="font-semibold mb-2 text-sm">Brand</h3>
            <RefinementList
              attribute="brand"
              sortBy={["isRefined", "count"]}
              limit={10}
              showMore
              showMoreLimit={30}
              classNames={{
                item: "flex items-center gap-2 py-1",
                label: "flex items-center gap-2 text-sm cursor-pointer",
                count: "ml-auto text-xs text-muted-foreground",
                selectedItem: "font-medium",
              }}
            />
          </div>

          <div>
            <h3 className="font-semibold mb-2 text-sm">Category</h3>
            <RefinementList attribute="categories" sortBy={["isRefined", "count"]} limit={8} showMore />
          </div>

          <div>
            <ToggleRefinement
              attribute="inStock"
              label="In stock only"
              on={true}
              classNames={{
                root: "flex items-center gap-2",
                label: "text-sm cursor-pointer",
              }}
            />
          </div>

          <ClearRefinements
            classNames={{
              button: "text-sm text-primary underline hover:no-underline",
              disabledButton: "text-sm text-muted-foreground cursor-not-allowed",
            }}
          />
        </aside>

        {/* Results */}
        <main className="space-y-4">
          <div className="flex items-center gap-4">
            <SearchBox
              placeholder="Search products..."
              classNames={{
                root: "flex-1",
                form: "relative",
                input: "input w-full pr-8",
                submit: "absolute right-2 top-1/2 -translate-y-1/2",
                reset: "absolute right-8 top-1/2 -translate-y-1/2 text-muted-foreground",
              }}
            />

            <SortBy
              items={[
                { label: "Relevance", value: "products" },
                { label: "Price: Low to High", value: "products_price_asc" },
                { label: "Price: High to Low", value: "products_price_desc" },
                { label: "Top Rated", value: "products_rating_desc" },
              ]}
              classNames={{ select: "input text-sm" }}
            />
          </div>

          <Stats
            classNames={{ root: "text-sm text-muted-foreground" }}
            translations={{
              rootElementText({ nbHits, processingTimeMS }) {
                return `${nbHits.toLocaleString()} results (${processingTimeMS}ms)`
              },
            }}
          />

          <CurrentRefinements
            classNames={{
              root: "flex flex-wrap gap-2",
              item: "flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-1 rounded-full",
              delete: "ml-1 hover:text-primary/70",
            }}
          />

          <Hits
            hitComponent={ProductHit}
            classNames={{
              list: "grid grid-cols-3 gap-4",
            }}
          />

          <Pagination
            classNames={{
              root: "flex justify-center",
              list: "flex gap-1",
              item: "w-8 h-8 flex items-center justify-center rounded text-sm",
              selectedItem: "bg-primary text-primary-foreground",
            }}
          />
        </main>
      </div>
    </InstantSearch>
  )
}

Custom Hit Component

// components/search/ProductHit.tsx — typed hit with highlighting
import { Highlight, Snippet } from "react-instantsearch"
import type { Hit } from "instantsearch.js"
import Image from "next/image"

type ProductHitType = Hit<{
  name: string
  brand: string
  price: number
  imageUrl: string
  slug: string
  rating: number
  reviewCount: number
  description: string
  inStock: boolean
}>

export function ProductHit({ hit }: { hit: ProductHitType }) {
  return (
    <a
      href={`/products/${hit.slug}`}
      className="group block rounded-xl border bg-card overflow-hidden hover:shadow-md transition-shadow"
    >
      <div className="relative aspect-square bg-muted">
        <Image
          src={hit.imageUrl}
          alt={hit.name}
          fill
          className="object-cover group-hover:scale-105 transition-transform duration-300"
          sizes="(max-width: 768px) 50vw, 25vw"
        />
        {!hit.inStock && (
          <div className="absolute inset-0 bg-black/50 flex items-center justify-center">
            <span className="text-white text-sm font-medium">Out of stock</span>
          </div>
        )}
      </div>

      <div className="p-3 space-y-1">
        <p className="text-xs text-muted-foreground">{hit.brand}</p>

        {/* Highlight wraps matching text in <mark> */}
        <h3 className="text-sm font-medium line-clamp-2">
          <Highlight attribute="name" hit={hit} classNames={{ highlighted: "bg-yellow-100 dark:bg-yellow-900/30" }} />
        </h3>

        {/* Snippet shows truncated text with highlights */}
        <p className="text-xs text-muted-foreground line-clamp-2">
          <Snippet attribute="description" hit={hit} />
        </p>

        <div className="flex items-center justify-between pt-1">
          <span className="font-semibold">${hit.price.toFixed(2)}</span>
          {hit.rating > 0 && (
            <span className="text-xs text-muted-foreground">★ {hit.rating.toFixed(1)} ({hit.reviewCount})</span>
          )}
        </div>
      </div>
    </a>
  )
}

Webhook Re-Indexing

// app/api/webhooks/algolia-sync/route.ts — trigger re-index from database events
import { NextRequest, NextResponse } from "next/server"
import { indexProduct, removeProduct } from "@/lib/algolia/indexing"
import { db } from "@/lib/db"

export async function POST(request: NextRequest) {
  const { type, record } = await request.json()

  // Verify webhook secret
  const secret = request.headers.get("x-webhook-secret")
  if (secret !== process.env.WEBHOOK_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  switch (type) {
    case "product.created":
    case "product.updated": {
      const product = await db.product.findUniqueOrThrow({
        where: { id: record.id },
        include: { categories: true, tags: true },
      })

      await indexProduct({
        objectID: product.id,
        name: product.name,
        brand: product.brand,
        categories: product.categories.map(c => c.name),
        tags: product.tags.map(t => t.name),
        description: product.description,
        price: product.priceCents / 100,
        imageUrl: product.imageUrl,
        slug: product.slug,
        rating: product.avgRating,
        reviewCount: product.reviewCount,
        inStock: product.stockQuantity > 0,
        popularity: product.viewCount,
      })
      break
    }

    case "product.deleted":
      await removeProduct(record.id)
      break
  }

  return NextResponse.json({ ok: true })
}

For the Meilisearch alternative when a self-hosted, open-source search engine with a similar API that avoids Algolia’s pricing for high-volume search is preferred — Meilisearch can be deployed on a VPS or used via Meilisearch Cloud, with a JavaScript client and React InstantSearch compatibility layer, see the self-hosted search guide. For the Typesense alternative when another open-source, self-hosted search engine with a focus on typo tolerance, faceting, and vector search (for AI semantic search) alongside keyword search is needed — Typesense is Algolia-compatible with typesense-instantsearch-adapter, see the vector search guide. The Claude Skills 360 bundle includes Algolia skill sets covering indexing, InstantSearch components, and faceted filtering. Start with the free tier to try search UI generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free