Skip to main content

How to Add Full-Text Search with Meilisearch 2026

·APIScout Team
Share:

How to Add Full-Text Search with Meilisearch

Meilisearch is the open-source search engine that's as fast as Algolia but free to self-host. Sub-50ms search with typo tolerance, filtering, and facets out of the box. This guide covers everything from installation to a production search UI.

Search is often an afterthought until it isn't. Users type "javasript" and get zero results. They search for "login" when your docs say "authentication". They filter by category and then can't sort by relevance. Getting search right means thinking beyond the basic query → results loop. Meilisearch's defaults handle the most common failure modes (typo tolerance, multi-word matching, attribute weighting), but production search requires understanding how the engine works, not just how to call the API.

What You'll Build

  • Full-text search with typo tolerance
  • Filtering and faceted navigation
  • Search-as-you-type UI
  • Document indexing and updates
  • Self-hosted deployment

Prerequisites: Node.js 18+, Docker (for self-hosting).

1. Setup

Option A: Self-Hosted (Docker)

docker run -d --name meilisearch \
  -p 7700:7700 \
  -e MEILI_MASTER_KEY='your-master-key-here' \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:latest

Option B: Meilisearch Cloud

Sign up at cloud.meilisearch.com — free tier available.

Install SDK

npm install meilisearch

Initialize Client

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

// Admin client (server-side only — has write access)
export const meiliAdmin = new MeiliSearch({
  host: process.env.MEILISEARCH_HOST || 'http://localhost:7700',
  apiKey: process.env.MEILISEARCH_ADMIN_KEY,
});

// Search client (can be used client-side — read-only)
export const meiliSearch = new MeiliSearch({
  host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST || 'http://localhost:7700',
  apiKey: process.env.NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY,
});

Generate API Keys

// Create a search-only key (safe for client-side)
const keys = await meiliAdmin.getKeys();
// Use the "Default Search API Key" for client-side
// Use the "Default Admin API Key" for server-side indexing

2. Index Data

Create Index and Add Documents

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

const products = [
  {
    id: 1,
    name: 'React Query',
    description: 'Powerful asynchronous state management for React',
    category: 'Data Fetching',
    tags: ['react', 'data', 'cache', 'typescript'],
    stars: 38000,
    downloads: 5200000,
  },
  {
    id: 2,
    name: 'Zustand',
    description: 'Small, fast, scalable state management',
    category: 'State Management',
    tags: ['react', 'state', 'lightweight'],
    stars: 42000,
    downloads: 8100000,
  },
  // ... more products
];

// Add documents to index
const index = meiliAdmin.index('products');
const task = await index.addDocuments(products);
console.log('Indexing task:', task.taskUid);

// Wait for indexing to complete
await meiliAdmin.waitForTask(task.taskUid);
console.log('Indexing complete!');

Configure Index Settings

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

await index.updateSettings({
  // Fields to search in (order = priority)
  searchableAttributes: ['name', 'description', 'tags'],

  // Fields to return in results
  displayedAttributes: ['id', 'name', 'description', 'category', 'stars', 'downloads'],

  // Fields available for filtering
  filterableAttributes: ['category', 'tags', 'stars'],

  // Fields available for sorting
  sortableAttributes: ['stars', 'downloads', 'name'],

  // Custom ranking rules
  rankingRules: [
    'words',
    'typo',
    'proximity',
    'attribute',
    'sort',
    'exactness',
    'stars:desc', // Boost high-star results
  ],

  // Synonyms
  synonyms: {
    react: ['reactjs', 'react.js'],
    vue: ['vuejs', 'vue.js'],
    state: ['store', 'state management'],
  },
});
const index = meiliSearch.index('products');

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

// results.hits = [
//   {
//     id: 2,
//     name: 'Zustand',
//     description: 'Small, fast, scalable state management',
//     _formatted: {
//       name: 'Zustand',
//       description: 'Small, fast, scalable <em>state</em> management',
//     },
//   },
//   ...
// ]

Search with Filters

const results = await index.search('data', {
  filter: ['category = "Data Fetching"', 'stars > 10000'],
  sort: ['stars:desc'],
  limit: 20,
});
const results = await index.search('', {
  facets: ['category', 'tags'],
  limit: 20,
});

// results.facetDistribution = {
//   category: { 'Data Fetching': 5, 'State Management': 8, 'UI': 12 },
//   tags: { react: 15, typescript: 10, vue: 8 },
// }

4. Search UI Component

// components/Search.tsx
'use client';
import { useState, useEffect } from 'react';
import { meiliSearch } from '@/lib/meilisearch';

interface Product {
  id: number;
  name: string;
  description: string;
  category: string;
  stars: number;
  _formatted?: {
    name: string;
    description: string;
  };
}

export function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<Product[]>([]);
  const [facets, setFacets] = useState<Record<string, Record<string, number>>>({});
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);

  useEffect(() => {
    const search = async () => {
      const index = meiliSearch.index('products');

      const filters: string[] = [];
      if (selectedCategory) {
        filters.push(`category = "${selectedCategory}"`);
      }

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

      setResults(response.hits as Product[]);
      setFacets(response.facetDistribution ?? {});
    };

    const debounce = setTimeout(search, 150);
    return () => clearTimeout(debounce);
  }, [query, selectedCategory]);

  return (
    <div className="search-layout">
      <aside className="filters">
        <h3>Category</h3>
        <button
          onClick={() => setSelectedCategory(null)}
          className={!selectedCategory ? 'active' : ''}
        >
          All
        </button>
        {Object.entries(facets.category ?? {}).map(([cat, count]) => (
          <button
            key={cat}
            onClick={() => setSelectedCategory(cat)}
            className={selectedCategory === cat ? 'active' : ''}
          >
            {cat} ({count})
          </button>
        ))}
      </aside>

      <main>
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search..."
          autoFocus
        />

        <div className="results">
          {results.map((hit) => (
            <article key={hit.id}>
              <h3
                dangerouslySetInnerHTML={{
                  __html: hit._formatted?.name ?? hit.name,
                }}
              />
              <p
                dangerouslySetInnerHTML={{
                  __html: hit._formatted?.description ?? hit.description,
                }}
              />
              <span>{hit.category}</span>
              <span>⭐ {hit.stars.toLocaleString()}</span>
            </article>
          ))}
          {results.length === 0 && query && (
            <p>No results for "{query}"</p>
          )}
        </div>
      </main>
    </div>
  );
}

5. Keep Data in Sync

Real-Time Updates

// When a product is created or updated
export async function syncProduct(product: Product) {
  const index = meiliAdmin.index('products');
  await index.addDocuments([product]); // Upserts by id
}

// When a product is deleted
export async function removeProduct(productId: number) {
  const index = meiliAdmin.index('products');
  await index.deleteDocument(productId);
}

// Batch update
export async function syncProducts(products: Product[]) {
  const index = meiliAdmin.index('products');
  await index.addDocuments(products); // Upserts all
}

6. Meilisearch vs Algolia

FeatureMeilisearchAlgolia
Open source
Self-hosted✅ Free
Cloud hostingFrom $0From $0
Search speed<50ms<50ms
Typo tolerance
Facets/filters
Geo search
Free tier (cloud)10K docs, 10K searches10K records, 10K searches
Pricing at scaleMuch cheaper (or free self-hosted)Expensive at volume

Production Deployment

Docker Compose

version: '3.8'
services:
  meilisearch:
    image: getmeili/meilisearch:latest
    ports:
      - "7700:7700"
    environment:
      - MEILI_MASTER_KEY=your-production-master-key
      - MEILI_ENV=production
    volumes:
      - meili_data:/meili_data

volumes:
  meili_data:

Production Checklist

ItemNotes
Set MEILI_ENV=productionDisables dashboard, requires API keys
Set strong master keyAt least 16 characters
Use search-only key for clientNever expose admin key
Set up backupsSnapshot Meilisearch data directory
Put behind reverse proxynginx/Caddy for SSL + rate limiting
Monitor disk spaceIndex size grows with data

Common Mistakes

MistakeImpactFix
Exposing admin key to clientData can be deletedGenerate and use search-only key
Not setting searchableAttributesSearches all fields (slow)Explicitly list searchable fields
Indexing too much data per documentLarge index, slow updatesOnly index fields needed for search
Not waiting for indexing tasksSearching before data is indexedUse waitForTask() or check task status
No typo tolerance testingUsers can't find "javasript"Meilisearch handles this by default

Handling Large Datasets and Pagination

Meilisearch's default limit is 20 results, and the maximum is 1,000. For datasets over 100K documents, you need to think carefully about both indexing throughput and search pagination.

Indexing performance: addDocuments() accepts up to 1,000 documents per call. For large initial imports, batch your documents and track the task UIDs. Meilisearch processes indexing tasks asynchronously — a task with status enqueued means the documents are queued but not yet searchable. Use waitForTask() or poll getTask(taskUid) to confirm completion before running your smoke tests. Expect roughly 500-2,000 documents/second indexing throughput depending on document size and server specs.

Search pagination: Meilisearch supports two pagination strategies. The limit/offset approach works for most cases: index.search(q, { limit: 20, offset: 40 }) returns page 3 of 20 results per page. For page-number UI (1, 2, 3...), use the page/hitsPerPage parameters instead — they return totalPages and totalHits metadata. Note that totalHits is an estimate for large result sets (Meilisearch caps expensive exact counts); configure pagination.maxTotalHits in index settings if you need accurate counts.

Index size and disk: Meilisearch's index is typically 2-4x the raw document size due to inverted index structures. A dataset of 1M product records averaging 1KB each will produce a 2-4GB index. Monitor disk space, especially when you're updating documents frequently — Meilisearch writes new index segments on each update batch and compacts them in the background. An SSD is strongly recommended for production deployments.

Tuning Search Relevance

Meilisearch works well out of the box, but production search quality usually requires some tuning once you see real user queries.

Ranking rules are the most impactful lever. The default order (words, typo, proximity, attribute, sort, exactness) prioritizes documents that match more query words, have fewer typos, and match in adjacent fields. Adding a custom rule like stars:desc as shown in the code above biases results toward popular items — useful for package directories or app stores, but wrong if you want recent results to surface first.

Typo tolerance configuration is worth understanding. By default, Meilisearch allows 1 typo for words of 5+ characters and 2 typos for words of 9+ characters. For proper nouns, technical terms, or domain-specific jargon (API names, package names), you may want to tighten this. A search for "redux" shouldn't match "react" via typo tolerance. Use disableOnWords in typo tolerance settings to protect specific terms.

Stop words remove noise from queries. Words like "the", "a", "an", "with", and "how" don't carry semantic meaning in most search contexts. Adding them as stop words means a search for "how to add authentication" finds the same results as "add authentication" — the noise words don't affect ranking. Meilisearch's built-in stop words list is a good starting point; add domain-specific stop words (like "api", "library", or "npm" for a package search) as you analyze real query logs.

Synonym expansion handles vocabulary mismatches between what users type and what your content says. If your docs call it "authentication" but users search for "login" or "sign in", synonyms bridge that gap. Start with a small list of known mismatches from your query analytics, then expand as you discover more. Synonyms are one-way by default in Meilisearch — if you want "javascript" and "js" to be equivalent in both directions, define them as multi-way synonyms.

Meilisearch vs Typesense: The Other Open-Source Option

Algolia is the reference point for comparison, but Typesense is a direct Meilisearch competitor worth evaluating when you're choosing an open-source search engine.

Both are open-source, written in C++/Rust respectively (Typesense in C++, Meilisearch in Rust), and target the sub-50ms search experience. The practical differences:

Meilisearch strengths: Better typo tolerance out of the box, simpler initial setup, more intuitive ranking rules syntax. The dashboard (available in development mode) is genuinely useful for debugging index state. Document updates are immediate — no re-indexing required. The cloud offering (Meilisearch Cloud) integrates tightly with the self-hosted workflow.

Typesense strengths: Better multi-tenant support via scoped API keys, more mature vector search integration (for hybrid semantic + keyword search), and stronger query-time field weighting. Typesense Cloud's pricing model is slightly more predictable at high query volume. For applications that need both traditional keyword search and vector similarity search (e.g., semantic search over documentation), Typesense's hybrid search is more production-ready as of 2026.

For most use cases — e-commerce product search, documentation search, internal tools — Meilisearch is the easier path to a working implementation and has a larger community of self-hosting tutorials and Docker configurations. If you need semantic search capabilities or complex multi-tenant architectures, benchmark both before committing. The migration cost between them is low (both use similar index/search patterns), so starting with Meilisearch and switching to Typesense later is a viable path if requirements change.

Methodology

This guide targets Meilisearch v1.x (current as of 2026) with the meilisearch npm SDK v0.40+. The search UI component uses React with hooks and a 150ms debounce — appropriate for most applications; reduce to 0ms for instant search on fast connections or increase to 300ms for slower networks. The Docker Compose configuration uses the latest tag for simplicity; pin to a specific version tag (e.g., v1.7.0) in production to avoid unexpected breaking changes on container restarts. Benchmark data comparing Meilisearch to Algolia latency assumes similar hardware and index sizes — results vary significantly by document schema complexity and filter cardinality. For the Typesense comparison, we compared feature documentation and community benchmarks from the respective GitHub repositories rather than running independent benchmarks — test both engines against your specific dataset and query patterns before committing to either in production. The dangerouslySetInnerHTML pattern in the React component is safe when the content comes from Meilisearch's _formatted field, which wraps matched text in <em> tags only — but sanitize with DOMPurify if there is any possibility of user-controlled content making it into your search index.


Adding search? Compare Meilisearch vs Algolia vs Typesense on APIScout — open-source vs hosted, pricing, and feature comparison.

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

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.