Steven's Knowledge

Getting Started

Run Meilisearch in Docker, index sample documents, query from a JS app, build an instant-search UI

Getting Started

Meilisearch is the friendliest place to start — open-source, single-binary, Algolia-like API. This page boots it locally, indexes data, and wires up a real instant-search UI.

The same patterns transfer to Typesense and Algolia with minimal changes.

Run Meilisearch

# docker-compose.yml
services:
  meilisearch:
    image: getmeili/meilisearch:v1.10
    ports: ["7700:7700"]
    environment:
      MEILI_MASTER_KEY: "dev-master-key-change-me"
      MEILI_ENV: development
    volumes:
      - meili-data:/meili_data

volumes:
  meili-data:
docker compose up -d
curl http://localhost:7700/health
# {"status":"available"}

open http://localhost:7700      # dashboard UI (dev only)

Index Some Documents

# Create an index of "products"
curl -X POST 'http://localhost:7700/indexes' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  -H 'Content-Type: application/json' \
  --data '{"uid": "products", "primaryKey": "id"}'

# Add documents
curl -X POST 'http://localhost:7700/indexes/products/documents' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  -H 'Content-Type: application/json' \
  --data '[
    { "id": 1, "name": "Espresso Maker", "category": "kitchen", "price": 79, "in_stock": true },
    { "id": 2, "name": "French Press", "category": "kitchen", "price": 29, "in_stock": true },
    { "id": 3, "name": "Coffee Grinder", "category": "kitchen", "price": 49, "in_stock": false },
    { "id": 4, "name": "Reading Light", "category": "lighting", "price": 39, "in_stock": true }
  ]'

Query It

# Basic search
curl -X POST 'http://localhost:7700/indexes/products/search' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  -H 'Content-Type: application/json' \
  --data '{"q":"espreso"}'        # note the typo

The response includes the espresso maker — Meilisearch handles typos out of the box.

Filters & Facets

Configure which attributes are filterable:

curl -X PATCH 'http://localhost:7700/indexes/products/settings' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  -H 'Content-Type: application/json' \
  --data '{
    "filterableAttributes": ["category", "in_stock", "price"],
    "sortableAttributes": ["price"]
  }'

Then search with filters:

curl -X POST 'http://localhost:7700/indexes/products/search' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  --data '{
    "q": "coffee",
    "filter": "in_stock = true AND price < 50",
    "sort": ["price:asc"],
    "facets": ["category"]
  }'

The facets array returns counts per category — exactly what you'd render as filter chips in a UI.

Integrate From Node

npm install meilisearch
import { MeiliSearch } from 'meilisearch';

const client = new MeiliSearch({
  host: 'http://localhost:7700',
  apiKey: 'dev-master-key-change-me',
});

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

// Index a batch
await index.addDocuments([
  { id: 5, name: 'Kettle', category: 'kitchen', price: 25, in_stock: true },
]);

// Search
const result = await index.search('coffee', {
  filter: 'in_stock = true',
  sort: ['price:asc'],
  limit: 10,
});

console.log(result.hits);

Instant Search UI

The killer UX feature of dedicated search engines: instant results as you type, with filters, highlights, and infinite scroll.

npm install instantsearch.js @meilisearch/instant-meilisearch
import instantsearch from 'instantsearch.js';
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
import { searchBox, hits, refinementList, configure } from 'instantsearch.js/es/widgets';

const { searchClient } = instantMeiliSearch(
  'http://localhost:7700',
  // For client-side use a SEARCH key, not the master key (see below)
  'public-search-key',
);

const search = instantsearch({
  indexName: 'products',
  searchClient,
});

search.addWidgets([
  searchBox({ container: '#searchbox' }),
  configure({ hitsPerPage: 10 }),
  hits({
    container: '#hits',
    templates: {
      item: (item) => `
        <div>
          <h3>${item.name}</h3>
          <p>${item.category} · $${item.price}</p>
        </div>
      `,
    },
  }),
  refinementList({ container: '#category-filter', attribute: 'category' }),
]);

search.start();

Stand up a simple HTML page with <div id="searchbox">, <div id="hits">, <div id="category-filter">, and you have a working production-grade search UI with maybe 30 lines of code.

Meilisearch (and Algolia, Typesense) use scoped keys:

KeyWhat it can doWhere it goes
Master keyEverything (admin)Backend only, never in the browser
Admin keyIndex, configureBackend / CI
Search keySearch only, scoped to specific indexesSafe to ship to the browser

Generate a search-only key:

curl -X POST 'http://localhost:7700/keys' \
  -H 'Authorization: Bearer dev-master-key-change-me' \
  -H 'Content-Type: application/json' \
  --data '{
    "description": "Public search key",
    "actions": ["search"],
    "indexes": ["products"],
    "expiresAt": null
  }'

The response contains the key. Ship that to the browser, not the master key.

Tear Down

docker compose down -v

The Same Pattern, Other Engines

Typesense:

# Run Typesense
docker run -p 8108:8108 -v $(pwd)/typesense-data:/data \
  typesense/typesense:0.26.0 \
  --data-dir /data --api-key xyz123 --enable-cors

# Create a collection, add documents, search — same shape, different paths

Algolia: sign up, get an Application ID and Admin API Key, install algoliasearch SDK, push documents, search. Same conceptual API.

What's Next

You can index and query. Next:

On this page