# Meilisearch sync

Source: https://docs.externa.qiick.io/docs/meilisearch

Externa can optionally sync **published** collection items into **Meilisearch** for public site search. Admin SQL filters and AI embeddings stay unchanged.

> **Related**
> [Environment variables](/docs/environment-variables) · [Deploy with Docker](/docs/deploy-with-docker) · [Headless starter](/docs/headless-starter) · [Outbound webhooks](/docs/webhooks)

## Enable

```ini
MEILISEARCH_ENABLED=true
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_API_KEY=masterKey
MEILISEARCH_INDEX=externa
MEILISEARCH_INDEXABLE_STATUSES=published
```

When `MEILISEARCH_ENABLED` is false or `MEILISEARCH_HOST` is empty, sync is a no-op.

Run a **queue worker** (`composer run dev` locally, or `php artisan queue:work` / Horizon). Indexing is queued after published value writes.

> If you enabled Meilisearch on a pre-1.2.0 build, the index may exist without a primary key and stay empty. Upgrade to 1.2.0, delete the `externa` index, and republish or resync items; Externa now creates it with `primaryKey=id` ([#179](https://github.com/qiick-io/externa-core/issues/179)).

## What gets indexed

| Event                                        | Behavior                                                   |
| -------------------------------------------- | ---------------------------------------------------------- |
| Item create / update of **published** values | Upsert document                                            |
| Draft-only save (`draft_data`)               | **Not** indexed                                            |
| Item soft-delete / force-delete              | Delete document                                            |
| Item restore                                 | Re-upsert published values                                 |
| Bulk AI import                               | Suppressed (`withoutMeilisearch`); reindex later if needed |

If the item `data` bag has a `status` field, only values listed in `MEILISEARCH_INDEXABLE_STATUSES` stay indexed (default `published`). Missing `status` field → always eligible.

Document shape (single index, filter by `collection_slug`):

```json
{
  "id": "12",
  "collection_id": 3,
  "collection_slug": "posts",
  "updated_at": "2026-10-02T12:00:00+00:00",
  "searchable_text": "Hello world",
  "title": "Hello world",
  "status": "published"
}
```

## Docker Compose

```bash
docker compose --profile meilisearch up --build
```

Service listens on host **`:7700`**. Inside the Compose network set:

```ini
MEILISEARCH_HOST=http://meilisearch:7700
MEILISEARCH_API_KEY=masterKey
```

## Query from a headless site

Talk to Meilisearch **directly** from your Next.js (or other) server with a **search-only** API key — do not expose the master key in the browser.

```ts
// server-only example
const res = await fetch(
  `${process.env.MEILISEARCH_HOST}/indexes/externa/search`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MEILISEARCH_SEARCH_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      q: 'hello',
      filter: 'collection_slug = posts',
    }),
  },
)
```

See also [Headless starter](/docs/headless-starter) for the Public CMS API path; Meilisearch is an optional parallel index for full-text UX.

## Out of scope

- Algolia
- Laravel Scout on EAV items
- CRDT / collaborative index merge
- Replacing admin / Public API `filter[...]` SQL lists
