Skip to main content

API guide

How to Add Full-Text Search with Meilisearch 2026

Add Meilisearch to your app in 2026: indexing, search UI, filters, facets, relevance tuning, deployment, and architecture tradeoffs.

·APIScout Team
Share:
Hero image for How to Add Full-Text Search with Meilisearch 2026

How to Add Full-Text Search with Meilisearch

Meilisearch is a strong default when you need fast, typo-tolerant search without building your own search engine. It is open source, easy to run with Docker, and has official SDKs that make indexing documents, configuring filters, and building a search UI straightforward.

The production decision is not “Meilisearch or no search.” It is where Meilisearch sits in your architecture, what data is safe to index, how you keep the index synchronized with your system of record, and when you should choose a managed search API instead of operating a search service yourself.

TL;DR

  • Use Meilisearch for documentation search, marketplace search, internal admin search, product catalogs, and API directories where typo tolerance, filters, facets, and simple ranking rules matter more than complex query languages.
  • Keep your primary database as the source of truth. Meilisearch should be a derived read model that can be rebuilt from canonical records.
  • Use separate keys for indexing and search. The admin key stays server-side; a search-only key can power client-side search when your indexed documents are safe to expose.
  • Configure filterable and sortable attributes deliberately. Meilisearch requires attributes to be configured before they can be used for filters or sorting, and broad filter surfaces can increase index work.
  • Pick a sync strategy before launch. Batch imports are fine for static content; product catalogs and user-generated content need webhook, queue, or change-data-capture based updates.

When Meilisearch Is the Right Fit

Meilisearch is a practical search layer for apps that need “good search now” more than a custom relevance science project.

Use caseFitNotes
Documentation searchExcellentTypos, synonyms, headings, and tags are usually enough.
Product or API directoryExcellentFacets, filters, and popularity ranking work well.
Internal admin searchExcellentFast setup and forgiving matching matter more than deep analytics.
Multi-tenant SaaS searchGoodWorks if tenant IDs are indexed and enforced with filters or tenant-scoped keys.
Log analytics / observabilityWeakUse systems built for high-volume append-only time-series search.
Hybrid semantic + keyword searchConditionalEvaluate vector/hybrid requirements before committing.

If you need SQL-like analytics, long retention over event streams, deep vector search, or complex field-level authorization, Meilisearch may still be useful, but it should not be the only search or data access layer.

Reference Architecture

A production Meilisearch integration usually has four seams:

  1. Source of truth: Postgres, MySQL, MongoDB, a CMS, or static content files.
  2. Indexer: a server-side job or queue worker that transforms canonical records into search documents.
  3. Meilisearch index: a derived search read model with searchable, displayed, filterable, sortable, and ranking settings.
  4. Search API or client UI: either a backend route that calls Meilisearch or a client-side search component using a restricted search key.

For public content search, a browser client can query Meilisearch directly with a search-only key. For private user data, put a backend route in front of Meilisearch so you can enforce tenant, user, and authorization filters before the search request reaches the engine.

1. Run Meilisearch Locally

For local development, Docker is the shortest path:

docker run -d --name meilisearch \
  -p 7700:7700 \
  -e MEILI_MASTER_KEY='replace-with-a-long-local-key' \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:latest

For production, pin the image version instead of using latest, store the master key in your secret manager, and persist /meili_data on durable storage. The official docs also cover Meilisearch Cloud if you want the managed path instead of operating the service yourself.

Install the JavaScript SDK:

bun add meilisearch

Or with npm:

npm install meilisearch

2. Create Server-Side and Search Clients

Keep write access on the server. A common Next.js setup is two clients: one admin client used only by indexing code, and one search client used by a public search UI only when the indexed data is public.

// lib/meilisearch.ts
import { MeiliSearch } from 'meilisearch';

export const meiliAdmin = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST ?? 'http://localhost:7700',
  apiKey: process.env.MEILISEARCH_ADMIN_KEY,
});

export const meiliSearch = new MeiliSearch({
  host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? 'http://localhost:7700',
  apiKey: process.env.NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY,
});

Do not expose a master key or admin key to the browser. Meilisearch’s own security documentation separates the master key from API keys; treat the master key as a bootstrap secret, then use restricted keys for actual application traffic.

3. Design the Search Document

Do not index your raw database row if the search UI only needs a small subset of fields. Build a search document that is safe to return and optimized for matching:

type ProductSearchDocument = {
  id: string;
  name: string;
  description: string;
  category: string;
  tags: string[];
  pricingModel: 'free' | 'paid' | 'open-source';
  popularityScore: number;
  updatedAt: string;
  url: string;
};

Good search documents are usually flatter and more public than database records. Avoid indexing secrets, private notes, raw billing fields, PII you do not need for search, and denormalized data you cannot rebuild.

4. Create an Index and Add Documents

Meilisearch infers a primary key when possible, but production import jobs should be explicit. Use a stable ID that maps back to your source record.

// scripts/index-products.ts
import { meiliAdmin } from '../lib/meilisearch';

const products: ProductSearchDocument[] = [
  {
    id: 'react-query',
    name: 'TanStack Query',
    description: 'Async state management and server-state caching for React applications.',
    category: 'Data Fetching',
    tags: ['react', 'cache', 'typescript'],
    pricingModel: 'open-source',
    popularityScore: 95,
    updatedAt: '2026-03-01T00:00:00.000Z',
    url: '/products/tanstack-query',
  },
];

const index = meiliAdmin.index('products');

const task = await index.addDocuments(products, { primaryKey: 'id' });
await meiliAdmin.waitForTask(task.taskUid);

Indexing, settings changes, and document updates are asynchronous tasks in Meilisearch. Waiting for the task is important in scripts, tests, and deploy smoke checks; otherwise your app may query before the new documents or settings are searchable.

5. Configure Index Settings

Settings are where most production search quality comes from. Configure them before relying on filters or sorts.

const index = meiliAdmin.index('products');

const task = await index.updateSettings({
  searchableAttributes: ['name', 'description', 'tags'],
  displayedAttributes: [
    'id',
    'name',
    'description',
    'category',
    'tags',
    'pricingModel',
    'url',
  ],
  filterableAttributes: ['category', 'tags', 'pricingModel'],
  sortableAttributes: ['popularityScore', 'updatedAt', 'name'],
  rankingRules: [
    'words',
    'typo',
    'proximity',
    'attribute',
    'sort',
    'exactness',
    'popularityScore:desc',
  ],
  synonyms: {
    auth: ['authentication', 'login', 'sign in'],
    js: ['javascript'],
    ts: ['typescript'],
  },
});

await meiliAdmin.waitForTask(task.taskUid);

Two details matter:

  • Filterable attributes are an explicit contract. If the UI filters by category, pricingModel, or tags, those fields must be configured as filterable first.
  • Ranking rules encode product judgment. Adding popularityScore:desc helps directories surface trusted products, but it can bury newer or niche results. Tune ranking rules against real query examples, not intuition.

6. Query the Index

A basic query returns hits plus optional formatted highlights:

const index = meiliSearch.index('products');

const response = await index.search('react data', {
  limit: 10,
  attributesToHighlight: ['name', 'description'],
});

console.log(response.hits);

Filtering and sorting use the configured attributes:

const response = await index.search('cache', {
  filter: ['category = "Data Fetching"', 'pricingModel = "open-source"'],
  sort: ['popularityScore:desc'],
  limit: 20,
});

Facets power category counts and left-side navigation:

const response = await index.search('', {
  facets: ['category', 'pricingModel'],
  limit: 20,
});

console.log(response.facetDistribution);

7. Build a Search UI

For public indexes, a client component can query Meilisearch directly. For private indexes, replace the direct SDK call with a backend route that injects authorization filters.

'use client';

import { useEffect, useState } from 'react';
import { meiliSearch } from '@/lib/meilisearch';

type ProductHit = {
  id: string;
  name: string;
  description: string;
  category: string;
  url: string;
  _formatted?: {
    name?: string;
    description?: string;
  };
};

export function ProductSearch() {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState<string | null>(null);
  const [hits, setHits] = useState<ProductHit[]>([]);
  const [facets, setFacets] = useState<Record<string, Record<string, number>>>({});

  useEffect(() => {
    const controller = new AbortController();

    const timer = window.setTimeout(async () => {
      const filters = category ? [`category = "${category}"`] : [];
      const index = meiliSearch.index('products');

      const response = await index.search<ProductHit>(query, {
        filter: filters.length ? filters : undefined,
        facets: ['category'],
        attributesToHighlight: ['name', 'description'],
        limit: 20,
      });

      if (!controller.signal.aborted) {
        setHits(response.hits);
        setFacets(response.facetDistribution ?? {});
      }
    }, 150);

    return () => {
      controller.abort();
      window.clearTimeout(timer);
    };
  }, [query, category]);

  return (
    <section>
      <label htmlFor="product-search">Search products</label>
      <input
        id="product-search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Try auth, billing, search, analytics..."
      />

      <div>
        <button type="button" onClick={() => setCategory(null)}>
          All categories
        </button>
        {Object.entries(facets.category ?? {}).map(([name, count]) => (
          <button key={name} type="button" onClick={() => setCategory(name)}>
            {name} ({count})
          </button>
        ))}
      </div>

      <ol>
        {hits.map((hit) => (
          <li key={hit.id}>
            <a href={hit.url}>{hit.name}</a>
            <p>{hit.description}</p>
          </li>
        ))}
      </ol>
    </section>
  );
}

If you render highlighted HTML from _formatted, only do it for trusted fields you indexed yourself. If user-generated content can reach the index, sanitize highlighted HTML before using dangerouslySetInnerHTML.

8. Keep the Index in Sync

Search quality decays when the index drifts from your database. Pick the simplest sync model that matches your content velocity.

Sync modelBest forTradeoff
Full rebuild scriptStatic docs, small catalogsSimple, but slower for large datasets.
Nightly incremental jobLow-change directoriesFreshness lag is acceptable.
Queue on writeProducts, listings, user contentMore moving parts, better freshness.
CDC / event streamHigh-volume systemsStrongest consistency story, highest operational cost.

For most apps, start with a full rebuild script and a small queue worker for create/update/delete events:

export async function syncProduct(product: ProductSearchDocument) {
  const index = meiliAdmin.index('products');
  const task = await index.addDocuments([product], { primaryKey: 'id' });
  return meiliAdmin.waitForTask(task.taskUid);
}

export async function removeProduct(productId: string) {
  const index = meiliAdmin.index('products');
  const task = await index.deleteDocument(productId);
  return meiliAdmin.waitForTask(task.taskUid);
}

Keep the rebuild script even after you add incremental sync. It is your recovery path after schema changes, indexing bugs, or accidental deletion.

9. Production Deployment Checklist

AreaRecommendation
VersioningPin a specific Meilisearch version in Docker or your hosting platform.
SecretsStore master/admin keys server-side only; expose only restricted search keys when safe.
PersistenceMount durable storage for /meili_data; test restore before relying on backups.
BackupsUse snapshots or dumps according to your hosting model and recovery needs.
SecurityPut the service behind TLS and avoid exposing an admin surface to the public internet.
MonitoringTrack task failures, disk growth, search latency, empty-result queries, and popular filters.
RebuildsMaintain an idempotent rebuild script from your source of truth.

Do not treat Meilisearch as an authorization layer. If a user should not see a record, either do not index it into a public index or enforce access control through a backend search route.

Meilisearch vs Algolia vs Typesense

DecisionMeilisearchAlgoliaTypesense
Best defaultSelf-hostable search with simple relevance tuningFully managed search API with mature analytics and operationsOpen-source search with strong developer ergonomics and hybrid-search evaluation paths
OperationsYou can self-host or use cloudManaged serviceYou can self-host or use cloud
Relevance tuningStraightforward ranking rules and synonymsMature dashboard, rules, analytics, and merchandisingSchema-first configuration and query-time controls
Cost modelDepends on self-hosting or cloud planManaged usage pricingDepends on self-hosting or cloud plan
When to chooseYou want control, open source, and fast setupYou want the most mature hosted search productYou want another open-source option and should benchmark both

Avoid choosing search engines from generic latency claims. Build a small representative index, test your real queries, and measure empty results, typo recovery, filter behavior, operational effort, and total cost for your expected document and query volume.

Common Mistakes

MistakeWhy it hurtsFix
Indexing private fieldsSearch responses can leak sensitive dataCreate a public search document shape.
Exposing admin keysAnyone with the key can change or delete indexesUse restricted search keys in clients.
Forgetting filterable attributesFilters fail or require later reindexing workConfigure filterable fields before launch.
Treating search as canonical storageDeletes and sync bugs become data loss incidentsRebuild from the source database.
Over-tuning ranking on day oneYou optimize for imagined queriesStart simple, then tune from query logs.
No empty-result monitoringYou miss vocabulary gaps and synonym needsTrack zero-hit searches and add targeted synonyms.

Verdict

Choose Meilisearch when you want a fast, open-source search layer that your team can understand and rebuild. It is especially good for API directories, documentation, product catalogs, and internal tools where typo tolerance, facets, filters, synonyms, and simple ranking rules cover most search needs.

Choose Algolia when you want the most mature managed search product and are comfortable paying for hosted operations, analytics, and merchandising tooling. Evaluate Typesense when you want another self-hostable search engine or when hybrid search is a near-term requirement.

The safest production architecture is boring: database as source of truth, Meilisearch as a derived index, server-only write keys, restricted read keys, explicit filter settings, and a rebuild script you can run any time.

Sources Checked


Related: Best Search APIs 2026, Algolia vs Meilisearch: Search-as-a-Service Compared, How to Add Algolia Search to Your Website

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.