Skip to content

Fix: Algolia Not Working — Search Returning No Results, Index Not Updating, or InstantSearch Errors

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix Algolia issues — indexing, search queries, InstantSearch setup, ranking configuration, facets, API key permissions, and Next.js integration.

The Problem

An Algolia search returns zero hits even though records exist in the index:

const results = await index.search('javascript');
// { hits: [], nbHits: 0, ... }

Or InstantSearch renders nothing on the page:

<InstantSearch indexName="articles" searchClient={searchClient}>
  <SearchBox />
  <Hits />
  {/* Hits component renders no items */}
</InstantSearch>

Or the index doesn’t update when you push new records:

POST https://YOUR_APP_ID-dsn.algolia.net/1/indexes/articles/batch
→ 403 Invalid API key

Why This Happens

Algolia separates Admin API keys (read/write to indexes) from Search-only API keys (read-only, safe for the browser). This split is the most important security boundary in the platform, and most production incidents trace back to the wrong key ending up in the wrong context.

  • Wrong API key for the operation — using a search-only key to index records, or exposing the admin key in the browser. Exposing the admin key is catastrophic: anyone who reads it can delete every index in your account.
  • Searchable attributes not configured — Algolia only searches fields you designate as searchable. A new index searches all attributes by default, but a misconfigured index might search zero attributes. If searchableAttributes is set to an empty array (not unset), the index returns zero hits for every query.
  • Records not indexed — records must be explicitly pushed to Algolia. There’s no automatic sync unless you build it. Unlike PostgreSQL full-text search (which queries the same table the data lives in), Algolia is a separate datastore that must be kept in sync via webhook or scheduled job.
  • InstantSearch routing mismatch — the indexName in <InstantSearch> must match the exact index name in your Algolia dashboard (case-sensitive). A mismatch returns valid empty results, not an error.

A second class of failure relates to the v4 to v5 SDK rewrite. The algoliasearch v5 client (released 2024) changed the method names significantly: client.initIndex('foo').search(query) became client.searchSingleIndex({ indexName: 'foo', searchParams: { query } }). v4 code copy-pasted into a v5 project throws “function not found.” Pin to v4 if you can’t rewrite, or migrate in one pass using Algolia’s published codemod.

Fix 1: API Keys and Client Setup

// npm install algoliasearch
import { algoliasearch } from 'algoliasearch';

// Admin client — server-side only (indexes, configuration)
const adminClient = algoliasearch(
  process.env.ALGOLIA_APP_ID!,
  process.env.ALGOLIA_ADMIN_KEY!  // Admin key — NEVER expose to browser
);

// Search client — safe for browser (read-only)
const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!  // Search-only key
);
# .env.local
ALGOLIA_APP_ID=YOURAPPID
ALGOLIA_ADMIN_KEY=your_admin_api_key          # Server only — no NEXT_PUBLIC_ prefix
NEXT_PUBLIC_ALGOLIA_APP_ID=YOURAPPID
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=your_search_only_key  # Safe for browser

Get these from Algolia Dashboard → your app → Settings → API Keys.

Warning: Never commit API keys to git. The Admin key allows deleting all your indexes. The Search-only key is safe to expose publicly.

Fix 2: Indexing Records

// Push records to Algolia — run server-side
import { algoliasearch } from 'algoliasearch';

const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!);

// Save a single object
await client.saveObject({
  indexName: 'articles',
  body: {
    objectID: 'article-123',  // Unique identifier — required
    title: 'Getting Started with TypeScript',
    content: 'TypeScript is a strongly typed superset...',
    tags: ['typescript', 'javascript'],
    date: new Date().toISOString(),
    url: '/blog/getting-started-with-typescript',
  },
});

// Save multiple objects at once
await client.saveObjects({
  indexName: 'articles',
  objects: articles.map((article) => ({
    objectID: article.id,
    title: article.title,
    excerpt: article.excerpt,
    tags: article.tags,
    publishedAt: article.publishedAt,
    url: `/blog/${article.slug}`,
  })),
});

// Partial update (only specified fields)
await client.partialUpdateObject({
  indexName: 'articles',
  objectID: 'article-123',
  attributesToUpdate: { title: 'Updated Title' },
});

// Delete a record
await client.deleteObject({ indexName: 'articles', objectID: 'article-123' });

// Delete all records
await client.clearObjects({ indexName: 'articles' });
// Batch indexing from a database — run as a script or webhook
import { db } from './db';
import { algoliasearch } from 'algoliasearch';

async function reindexAll() {
  const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!);

  const articles = await db.query.articles.findMany({
    where: eq(articles.status, 'published'),
    with: { author: true, tags: true },
  });

  const objects = articles.map((a) => ({
    objectID: a.id,
    title: a.title,
    content: a.content.slice(0, 1000), // Truncate large fields
    author: a.author.name,
    tags: a.tags.map((t) => t.name),
    publishedAt: a.publishedAt,
  }));

  // Replace all records atomically (avoids showing stale results)
  await client.replaceAllObjects({ indexName: 'articles', objects });
  console.log(`Indexed ${objects.length} articles`);
}

reindexAll().catch(console.error);

Fix 3: Index Configuration

// Configure searchable attributes and ranking — run once during setup
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY!);

await client.setSettings({
  indexName: 'articles',
  indexSettings: {
    // Which fields Algolia searches (order = priority)
    searchableAttributes: [
      'title',
      'unordered(tags)',
      'unordered(content)',
      'author',
    ],

    // Which attributes can be used for filtering
    attributesForFaceting: [
      'filterOnly(status)',
      'tags',
      'author',
      'searchable(category)',
    ],

    // Custom ranking signals (after tie-breaking by relevance)
    customRanking: ['desc(publishedAt)', 'desc(popularity)'],

    // Highlight settings
    attributesToHighlight: ['title', 'content'],
    attributesToSnippet: ['content:20'],  // Show 20 words around the match

    // Remove stop words for better matching
    removeStopWords: true,
    ignorePlurals: true,

    // Typo tolerance
    typoTolerance: true,
    minWordSizefor1Typo: 4,
    minWordSizefor2Typos: 8,
  },
});

If search is returning no results and records definitely exist, check that searchableAttributes includes the field you’re searching. An empty searchableAttributes array means Algolia searches all attributes (the default for new indexes), but if it’s been set to a list that excludes the relevant field, you’ll get zero hits.

Fix 4: Search Queries

// Basic search
const { hits, nbHits, page, nbPages } = await client.searchSingleIndex({
  indexName: 'articles',
  searchParams: {
    query: 'typescript generics',
    hitsPerPage: 10,
    page: 0,
  },
});

// Filtered search
const { hits } = await client.searchSingleIndex({
  indexName: 'articles',
  searchParams: {
    query: 'react',
    filters: 'status:published AND tags:react',
    // Filter syntax: 'attribute:value', 'attr > 100', 'attr:a OR attr:b'
  },
});

// Faceted search
const { hits, facets } = await client.searchSingleIndex({
  indexName: 'articles',
  searchParams: {
    query: '',
    facets: ['tags', 'author'],
    facetFilters: [['tags:react', 'tags:nextjs']], // OR within array, AND between arrays
  },
});
// facets = { tags: { react: 42, nextjs: 28, ... }, author: { ... } }

// Numeric filters
const { hits } = await client.searchSingleIndex({
  indexName: 'articles',
  searchParams: {
    query: '',
    numericFilters: ['publishedAt > 1700000000', 'views >= 100'],
  },
});

// Geo search
const { hits } = await client.searchSingleIndex({
  indexName: 'restaurants',
  searchParams: {
    query: 'pizza',
    aroundLatLng: '48.8566,2.3522',
    aroundRadius: 5000, // meters
  },
});

Fix 5: React InstantSearch

npm install react-instantsearch algoliasearch
// app/search/page.tsx — Next.js App Router
'use client';

import {
  InstantSearch,
  SearchBox,
  Hits,
  Highlight,
  Pagination,
  RefinementList,
  Stats,
  Configure,
} from 'react-instantsearch';
import { algoliasearch } from 'algoliasearch';
import type { Hit } from 'instantsearch.js';

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
);

interface ArticleHit extends Hit {
  title: string;
  excerpt: string;
  url: string;
  tags: string[];
}

function ArticleHit({ hit }: { hit: ArticleHit }) {
  return (
    <article className="p-4 border rounded-lg mb-4">
      <a href={hit.url}>
        <h2 className="text-lg font-semibold">
          <Highlight attribute="title" hit={hit} />
        </h2>
      </a>
      <p className="text-gray-600 mt-1">
        <Highlight attribute="excerpt" hit={hit} />
      </p>
      <div className="flex gap-2 mt-2">
        {hit.tags.map((tag) => (
          <span key={tag} className="text-xs bg-gray-100 px-2 py-1 rounded">
            {tag}
          </span>
        ))}
      </div>
    </article>
  );
}

export default function SearchPage() {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="articles"
      routing={true}  // Sync search state with URL
    >
      <Configure hitsPerPage={10} />

      <div className="flex gap-8">
        <aside className="w-64">
          <h3 className="font-semibold mb-2">Tags</h3>
          <RefinementList attribute="tags" limit={10} showMore />
        </aside>

        <main className="flex-1">
          <SearchBox
            placeholder="Search articles..."
            classNames={{
              input: 'w-full border rounded-lg px-4 py-2',
              submit: 'hidden',
              reset: 'hidden',
            }}
          />
          <Stats className="text-sm text-gray-500 my-2" />
          <Hits hitComponent={ArticleHit} />
          <Pagination className="mt-6" />
        </main>
      </div>
    </InstantSearch>
  );
}
// SSR with Next.js App Router
import { getServerState } from 'react-instantsearch';
import { renderToString } from 'react-dom/server';
import { InstantSearchSSRProvider } from 'react-instantsearch';

export default async function SearchPage({ searchParams }) {
  const serverState = await getServerState(
    <SearchPageContent />,
    { renderToString }
  );

  return (
    <InstantSearchSSRProvider {...serverState}>
      <SearchPageContent />
    </InstantSearchSSRProvider>
  );
}

Fix 6: Webhooks for Real-time Indexing

// app/api/algolia-sync/route.ts — Next.js webhook to keep index in sync
import { NextRequest, NextResponse } from 'next/server';
import { algoliasearch } from 'algoliasearch';

const client = algoliasearch(
  process.env.ALGOLIA_APP_ID!,
  process.env.ALGOLIA_ADMIN_KEY!
);

export async function POST(req: NextRequest) {
  const { event, record } = await req.json();

  // Verify the webhook secret
  const secret = req.headers.get('x-webhook-secret');
  if (secret !== process.env.WEBHOOK_SECRET) {
    return new NextResponse('Unauthorized', { status: 401 });
  }

  try {
    if (event === 'create' || event === 'update') {
      await client.saveObject({
        indexName: 'articles',
        body: {
          objectID: record.id,
          title: record.title,
          content: record.content,
          tags: record.tags,
          publishedAt: record.publishedAt,
        },
      });
    } else if (event === 'delete') {
      await client.deleteObject({ indexName: 'articles', objectID: record.id });
    }

    return NextResponse.json({ success: true });
  } catch (err) {
    console.error('Algolia sync error:', err);
    return new NextResponse('Sync failed', { status: 500 });
  }
}

Fix 7: Debugging Common Issues

// Test that the index has records
const { nbHits } = await client.searchSingleIndex({
  indexName: 'articles',
  searchParams: { query: '', hitsPerPage: 0 },
});
console.log('Total records in index:', nbHits);

// List all indexes
const { items } = await client.listIndices();
console.log('Indexes:', items.map((i) => i.name));

// Fetch a specific record by objectID
const record = await client.getObject({
  indexName: 'articles',
  objectID: 'article-123',
});
console.log('Record:', record);

// Check index settings
const settings = await client.getSettings({ indexName: 'articles' });
console.log('Searchable attributes:', settings.searchableAttributes);

Fix 8: Algolia vs Meilisearch vs Typesense vs Elasticsearch vs OpenSearch vs Pagefind

The search-engine choice depends on whether you want SaaS or self-host, full-text or vector search, and how big the corpus is.

Algolia is the SaaS reference. The API surface is tiny, latency is the fastest in this category (sub-50ms median globally), and InstantSearch covers the UI layer end-to-end. Pricing is per-record per-month plus per-query, which scales smoothly until you hit millions of records — at that point the bill becomes the conversation. Pick Algolia when search is a core product feature and the team would rather pay than operate.

Meilisearch is the open-source Algolia-clone. The API is intentionally similar, ranking is good out of the box, and you can self-host on a $5 VPS. The trade-off: no hosted multi-region replication unless you pay for Meilisearch Cloud, and the InstantSearch adapter exists but lags Algolia’s by a few features. Pick Meilisearch when you want Algolia’s developer experience without the per-record bill. See Fix: Meilisearch Not Working for self-host debugging.

Typesense sits between Meilisearch and Algolia. It’s open-source, can be self-hosted, and offers Typesense Cloud for managed hosting. The query syntax is more flexible than Meilisearch (supports complex Boolean queries, geospatial, vector search), and the indexing throughput is higher. Pick Typesense when you need vector search alongside lexical, or when Meilisearch’s ranking doesn’t fit your data.

Elasticsearch is the heavyweight. It’s a distributed document store and search engine in one, with a query DSL that can express anything. The cost is operational: a production Elasticsearch cluster needs three master nodes for quorum, careful index sizing, and a JVM operator. Elastic Cloud is the managed option. Pick Elasticsearch when you also need log aggregation, time-series analytics, and SIEM in the same stack — search-only deployments are overkill.

OpenSearch is Amazon’s fork of Elasticsearch 7.10 (created after Elastic changed its license in 2021). The APIs are nearly identical to old Elasticsearch versions but diverged since 2021 — OpenSearch added native k-NN vector search, security plugins, and AWS-specific integrations. Pick OpenSearch when you’re already in AWS and want a managed Elasticsearch-compatible engine via Amazon OpenSearch Service.

Pagefind is the static-site option. It generates a search index at build time, ships JS to the browser, and runs the search entirely client-side. No server, no API key, no per-query cost. The trade-off: the index grows linearly with content size, so it’s perfect for blogs and docs (up to ~10MB indexes) and impractical for e-commerce catalogs. Pick Pagefind when your site is fully static and you want zero infrastructure.

Rule of thumb: SaaS (Algolia) for product-critical search where speed and DX matter, Meilisearch/Typesense self-hosted for cost-conscious SaaS-feel apps, Elasticsearch/OpenSearch when search is part of a broader analytics stack, Pagefind for static sites.

Still Not Working?

Zero hits despite records existing — run a test with an empty query (query: '') and no filters. If you get hits, the issue is your query string or filter syntax. If still empty, verify the index name matches exactly (check capitalization and spacing).

403 Invalid API Key — the operation requires the Admin key, but you’re using the Search-only key. Admin operations (saveObject, setSettings, deleteIndex) must use the Admin key from a server-side context only.

InstantSearch renders nothing — check the browser console for errors. The most common cause is indexName not matching the Algolia dashboard. Also verify NEXT_PUBLIC_ALGOLIA_APP_ID and NEXT_PUBLIC_ALGOLIA_SEARCH_KEY are set and the page is compiled after adding them.

Stale results after indexing — Algolia indexes propagate in near-real-time (under 1 second typically), but the search CDN can cache results briefly. Try with enablePersonalization: false and getRankingInfo: true to confirm the index is updated, then check if caching is the issue.

Facets not appearing — you must add the attribute to attributesForFaceting in the index settings and request it via facets: ['attribute'] in the search params.

v5 SDK methods not found — the v5 client renamed every method. client.initIndex('foo').search(query) is now client.searchSingleIndex({ indexName: 'foo', searchParams: { query } }). If you upgraded algoliasearch and got “function not found,” that’s why. Migrate the call sites or pin back to v4.

InstantSearch hydration mismatch in Next.js App Router — InstantSearch reads URL params on both server and client. If the server-rendered HTML doesn’t match the client’s initial render, React throws a hydration error. Wrap the search UI in <InstantSearchSSRProvider {...serverState}> and pass the same serverState to both sides.

Records over 10KB rejected — Algolia limits records to 10KB by default (configurable up to 100KB on paid plans). If indexing fails with record is too big, truncate large text fields before pushing. The fix: store only the searchable preview (first 1000 chars) in Algolia and fetch the full record from your database after a hit is clicked.

For related search tools, see Fix: TanStack Query Not Working, Fix: Elasticsearch Index Not Found, and Fix: GraphQL Yoga Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles