<!-- APIScout AI-readable guide source -->
<!-- Canonical: https://apiscout.dev/guides/how-to-add-full-text-search-meilisearch-2026 -->
<!-- Raw Markdown: https://apiscout.dev/guides/how-to-add-full-text-search-meilisearch-2026/raw.md -->
<!-- Source path: content/guides/how-to-add-full-text-search-meilisearch-2026.mdx -->

---
og_image: "/images/guides/how-to-add-full-text-search-meilisearch-2026.webp"
title: "How to Add Full-Text Search with Meilisearch 2026"
description: "Add Meilisearch to your app in 2026: indexing, search UI, filters, facets, relevance tuning, deployment, and architecture tradeoffs."
date: "2026-03-08"
author: "APIScout Team"
tags: ["meilisearch", "search", "full-text-search", "tutorial", "api-integration"]
tier: 1
---

# 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 case | Fit | Notes |
|---|---|---|
| Documentation search | Excellent | Typos, synonyms, headings, and tags are usually enough. |
| Product or API directory | Excellent | Facets, filters, and popularity ranking work well. |
| Internal admin search | Excellent | Fast setup and forgiving matching matter more than deep analytics. |
| Multi-tenant SaaS search | Good | Works if tenant IDs are indexed and enforced with filters or tenant-scoped keys. |
| Log analytics / observability | Weak | Use systems built for high-volume append-only time-series search. |
| Hybrid semantic + keyword search | Conditional | Evaluate 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:

```bash
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:

```bash
bun add meilisearch
```

Or with npm:

```bash
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.

```typescript
// 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:

```typescript
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.

```typescript
// 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.

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
'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 model | Best for | Tradeoff |
|---|---|---|
| Full rebuild script | Static docs, small catalogs | Simple, but slower for large datasets. |
| Nightly incremental job | Low-change directories | Freshness lag is acceptable. |
| Queue on write | Products, listings, user content | More moving parts, better freshness. |
| CDC / event stream | High-volume systems | Strongest consistency story, highest operational cost. |

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

```typescript
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

| Area | Recommendation |
|---|---|
| Versioning | Pin a specific Meilisearch version in Docker or your hosting platform. |
| Secrets | Store master/admin keys server-side only; expose only restricted search keys when safe. |
| Persistence | Mount durable storage for `/meili_data`; test restore before relying on backups. |
| Backups | Use snapshots or dumps according to your hosting model and recovery needs. |
| Security | Put the service behind TLS and avoid exposing an admin surface to the public internet. |
| Monitoring | Track task failures, disk growth, search latency, empty-result queries, and popular filters. |
| Rebuilds | Maintain 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

| Decision | Meilisearch | Algolia | Typesense |
|---|---|---|---|
| Best default | Self-hostable search with simple relevance tuning | Fully managed search API with mature analytics and operations | Open-source search with strong developer ergonomics and hybrid-search evaluation paths |
| Operations | You can self-host or use cloud | Managed service | You can self-host or use cloud |
| Relevance tuning | Straightforward ranking rules and synonyms | Mature dashboard, rules, analytics, and merchandising | Schema-first configuration and query-time controls |
| Cost model | Depends on self-hosting or cloud plan | Managed usage pricing | Depends on self-hosting or cloud plan |
| When to choose | You want control, open source, and fast setup | You want the most mature hosted search product | You 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

| Mistake | Why it hurts | Fix |
|---|---|---|
| Indexing private fields | Search responses can leak sensitive data | Create a public search document shape. |
| Exposing admin keys | Anyone with the key can change or delete indexes | Use restricted search keys in clients. |
| Forgetting filterable attributes | Filters fail or require later reindexing work | Configure filterable fields before launch. |
| Treating search as canonical storage | Deletes and sync bugs become data loss incidents | Rebuild from the source database. |
| Over-tuning ranking on day one | You optimize for imagined queries | Start simple, then tune from query logs. |
| No empty-result monitoring | You miss vocabulary gaps and synonym needs | Track 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

- [Meilisearch quick start](https://www.meilisearch.com/docs/learn/getting_started/quick_start)
- [Meilisearch master key and API keys](https://www.meilisearch.com/docs/learn/security/master_api_keys)
- [Meilisearch filter search results](https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_search_results)
- [Meilisearch built-in ranking rules](https://www.meilisearch.com/docs/learn/relevancy/ranking_rules)
- [Meilisearch tasks API](https://www.meilisearch.com/docs/reference/api/tasks)
- [Meilisearch settings API](https://www.meilisearch.com/docs/reference/api/settings)

---

*Related: [Best Search APIs 2026](/guides/best-search-apis-2026), [Algolia vs Meilisearch: Search-as-a-Service Compared](/guides/algolia-vs-meilisearch-api-2026), [How to Add Algolia Search to Your Website](/guides/how-to-add-algolia-search-website-2026)*
