← All posts
·10 min read

Claude Code with Meilisearch: Search Done Right

Claude CodeMeilisearchSearchBackend
Claude Code with Meilisearch: search done right

Why Meilisearch without CLAUDE.md returns irrelevant results

Meilisearch is a typo-tolerant, relevance-tuned search engine that you can run on a single binary. The relevance comes from how you configure the index, not from how you write the query. searchableAttributes defines which fields are searched and in what order. filterableAttributes enables faceted filtering. sortableAttributes enables explicit ordering. Ranking rules define how documents are scored against the query. Get these wrong and the engine returns plausible but unhelpful results.

Claude Code does not know which fields matter unless you tell it. Without a CLAUDE.md in place, Claude indexes every document field as searchable by default, which means the wrong fields rank highly. It skips the filterableAttributes step and tries to filter in app code after the fact. It leaves the default ranking rules in place even when the dataset needs a custom rule order. It does not differentiate between the master key and the search key, which is a security problem the moment a search key ends up in client code.

This guide gives you the CLAUDE.md configuration that anchors Claude Code to how Meilisearch actually works: searchable attributes ordered by importance, filterable and sortable attributes declared up front, ranking rules tuned to the data, and the right API keys handed to the right caller. If you are comparing search engines, Claude Code with PostgreSQL covers full-text search via tsvector and Claude Code with Elasticsearch covers the heavier alternative.

The Meilisearch CLAUDE.md template

The CLAUDE.md at your project root is read at the start of every Claude Code session. For a Meilisearch project it needs to declare: the index settings that must be set before any documents are indexed, the API key model, the document shape, the search query shape, and the verification approach.

# Meilisearch rules

## Stack
- Meilisearch 1.x (self-hosted or Cloud)
- meilisearch-js or the SDK matching the app language
- Master key in server env only, never client-side
- Search keys scoped per index, never the master key in the browser

## Index settings (set BEFORE indexing any documents)
- searchableAttributes , ordered by importance (most important first)
- filterableAttributes , every field used in WHERE-equivalent filters
- sortableAttributes , every field used in explicit ORDER BY
- displayedAttributes , default '*' but pin if you index private fields
- distinctAttribute , if duplicates by a field need deduplication

## Ranking rule order (default + custom)
Default ranking rules in order:
1. words
2. typo
3. proximity
4. attribute
5. sort
6. exactness
- Custom rules go AFTER 'sort' and BEFORE 'exactness' unless the dataset says otherwise
- Numeric rules use 'fieldName:asc' or 'fieldName:desc'

## Typo tolerance
- minWordSizeForTypos.oneTypo defaults to 5, raise to 7 for code-like content
- minWordSizeForTypos.twoTypos defaults to 9, raise to 12 for code-like content
- disableOnAttributes , add fields where typos should never apply (SKU, ID)
- disableOnWords , add stopwords that should be exact-match only

## API key model (STRICT)
- Master key , server bootstrap only, never in app code
- Admin key , server-side document indexing and settings management
- Search key , client-side search only, scoped to specific indexes
- ALWAYS scope search keys with index and expiry where possible

## Document shape rules
- ALWAYS include a unique id field
- Keep document size under 1MB (default limit) , chunk if larger
- Nested objects flatten with the dot path , 'author.name' is searchable
- ALWAYS update settings BEFORE bulk indexing , settings changes after force a reindex

## Search query rules
- ALWAYS include limit , default 20 is fine, never unbounded
- Use filter for boolean predicates , never post-filter in app code
- Use facets for sidebar counts , one request returns search + facets
- Use attributesToHighlight for snippet rendering
- Use attributesToCrop for snippet length control

## Hybrid search (vector + lexical)
- embedders block configured per index
- 'semanticRatio' on the search request blends lexical and vector scores
- Embedding model must match the dimensionality declared in the index

Drop this into CLAUDE.md, adjust the searchableAttributes order to match your data, and let Claude generate against it. The settings block is the load-bearing piece.

searchableAttributes order is the relevance dial

Meilisearch ranks documents based partly on which attribute the query matched against. Fields earlier in searchableAttributes rank higher than fields later in the list. This is the single biggest lever for relevance, and it is a lever Claude does not pull unless instructed.

For a typical product catalogue, the order is ["name", "brand", "description", "tags", "category"]. A query that matches the name ranks above a query that matches the description. For a knowledge base, the order might be ["title", "summary", "headings", "body", "tags"]. The principle is the same: tell Meilisearch which fields signal stronger relevance, and let the engine apply that ordering automatically.

Without a CLAUDE.md rule, Claude either omits searchableAttributes entirely (everything is searchable in document order) or pulls in every field including timestamps and internal IDs. Both shapes produce search results that look broken to users. The rule "ordered by importance (most important first)" is the line that fixes this on first generation.

filterableAttributes versus app-layer filtering

A search query with a filter clause is a single round-trip. A search query without filtering, followed by app-layer filtering, is two round-trips and a smaller result set than the user asked for. The second shape is what Claude defaults to when filterableAttributes is not declared, because filtering in JavaScript or Python feels familiar.

The CLAUDE.md rule "every field used in WHERE-equivalent filters" pulls Claude back to the engine. Once filterableAttributes is set, queries can use the full filter syntax: filter: "category = electronics AND price < 1000", filter: "tags IN [popular, featured]", and so on. The engine evaluates the filter before ranking, which means the returned page is fully filtered and the count of matching documents is accurate.

For faceted UIs, the same field appears in both filterableAttributes and the facets parameter on the search request. One request returns the search results plus the facet counts for the sidebar. Claude defaults to a separate request per facet without the rule. The rule consolidates it.

Get Claudify. Skills, hooks, and CLAUDE.md templates that make Claude Code generate Meilisearch integrations that are relevant on day one.

Ranking rules and the order that matters

The default ranking rules cover most cases. The order is: words, typo, proximity, attribute, sort, exactness. For most workloads this produces decent results. For specific workloads it does not.

The most common customisation is inserting a numeric ranking rule. Recency, popularity, and price are the usual ones. The rule "newer documents rank above older" maps to desc(created_at) placed in the rule order. The position matters: insert before proximity and the numeric rule dominates relevance. Insert after sort and the numeric rule is a tiebreaker. Most cases want the latter.

The CLAUDE.md guidance "Custom rules go AFTER 'sort' and BEFORE 'exactness' unless the dataset says otherwise" gives Claude a sensible default. The "unless the dataset says otherwise" clause is the place where you put project-specific overrides in the CLAUDE.md per-project. For a job board, recency dominates. For a recipe site, ratings dominate. For a product catalogue, conversion rate dominates. Each one wants a different rule order.

Typo tolerance and when to turn it off

Meilisearch's typo tolerance is on by default and is the right call for prose content. For code-like content, SKUs, identifiers, and version strings, the default thresholds produce false positives. The query "react 18" matching "react 19" because they differ by one character is correct behaviour for prose and incorrect for version search.

The CLAUDE.md template above raises the thresholds and exposes the disableOnAttributes and disableOnWords overrides. For a mixed dataset, the right shape is: leave typo tolerance on globally, but disable it on the version, SKU, and ID fields. Claude generates the override list when prompted to index a field that looks identifier-shaped, if the rule is in the template.

Content type minWordSizeForTypos.oneTypo minWordSizeForTypos.twoTypos disableOnAttributes
Prose (blog, docs) 5 (default) 9 (default) none
Product catalogue 6 10 sku, model_number
Code or technical strings 7 12 version, identifier
Person or place names 5 9 none, but consider synonyms

API key model and the search-key boundary

Meilisearch ships with a master key. The master key creates and revokes all other keys. It does not search, it does not index, it administers. Sending the master key to the client is the worst-case credential leak: anyone with it can wipe the entire instance.

The correct shape is: master key in the server env, used once to bootstrap admin and search keys. Admin keys go into the indexing pipeline. Search keys go into the client, scoped to specific indexes with an expiry. The CLAUDE.md rule "Master key , server bootstrap only, never in app code" is the line that prevents Claude from passing the master key through to a client SDK call. Without the rule, Claude reaches for the simplest credential available and the simplest is the master key.

For multi-tenant search, the scoped tenant token pattern is the right answer. Generate a token per tenant on the server, with the tenant filter baked in, and hand the token to the client. The client cannot bypass the tenant filter because the filter is baked into the signed token. Claude can generate this pattern when the CLAUDE.md flags multi-tenancy as a concern; without the flag, Claude is likely to apply the tenant filter in app code, which is the wrong layer.

Hybrid search and the embedders config

Meilisearch 1.x added hybrid search with embedders. Configure an embedder on the index, index documents normally (Meilisearch generates the embedding on insert), and search with semanticRatio between 0 and 1 to blend lexical and vector scoring.

The configuration looks like this:

{
  "embedders": {
    "default": {
      "source": "openAi",
      "model": "text-embedding-3-small",
      "documentTemplate": "{{doc.title}}: {{doc.description}}"
    }
  }
}

The documentTemplate is the load-bearing field. It tells Meilisearch which parts of the document to embed. Without it, Claude defaults to embedding the whole JSON document, which dilutes the signal with timestamps and internal IDs. The CLAUDE.md guidance "Embedding model must match the dimensionality declared in the index" prevents the other classic mistake: swapping the embedding model on an existing index without rebuilding it, which produces silent garbage.

Verifying Claude's Meilisearch setup before it ships

A CLAUDE.md is necessary but not sufficient. The verification loop runs against the index settings API. The check is:

  1. Apply settings with client.index('name').updateSettings({...})
  2. Wait for the task to complete via client.getTask(taskUid)
  3. Read settings back with client.index('name').getSettings()
  4. Diff against expected settings
  5. Run representative search queries and inspect processingTimeMs and the hits array

A hook that runs these steps after every settings change and surfaces the diff back to Claude prevents the common case where Claude updates one setting and silently overwrites unrelated settings, because the Meilisearch settings API does a full replace, not a merge. For broader hook patterns, Claude Code skills covers how to package the verification loop.

Reindexing strategy and the alias pattern

Meilisearch settings changes that affect the underlying index (most of them) trigger a reindex. For small indexes this is instant. For large indexes it is slow, and the index is partially available during the change. The standard pattern to handle this is index aliasing: build a new index with a versioned name, swap the alias atomically, drop the old index.

Meilisearch does not have a built-in alias system. The convention is to use the index name as the version, and to maintain the swap logic in app code. Claude needs the rule "ALWAYS update settings BEFORE bulk indexing , settings changes after force a reindex" in CLAUDE.md to default to the right order. The alias pattern is worth a separate section in CLAUDE.md if your index is large enough to need zero-downtime swaps.

Wrapping up

Meilisearch is configured, not queried, into relevance. The settings block is where the work goes. searchableAttributes order, filterableAttributes for predicates, sortableAttributes for ordering, ranking rules tuned to the data, typo tolerance scoped to the right fields, and embedders configured for hybrid search if you need it. The query shape is then thin: a string, a filter, a limit, optional facets and highlights.

Claude Code without a CLAUDE.md treats every document field as equal, filters in app code, leaves the default ranking in place, and reaches for the master key. With the template above, Claude Code generates index settings that respect the relevance dials, queries that use the engine's filtering and faceting features, and an API key model that does not leak credentials to the browser.

Get Claudify. Skills, hooks, and CLAUDE.md templates that make Claude Code generate Meilisearch integrations that are relevant on day one.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir Claudify - Featured on Startup Fame