diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml
index e5d24d354..2f88e2df1 100644
--- a/.code-samples.meilisearch.yaml
+++ b/.code-samples.meilisearch.yaml
@@ -52,6 +52,16 @@ add_or_replace_documents_1: |-
"release_date": "2019-03-23"
}
]'
+add_or_replace_documents_csv_1: |-
+ curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents' \
+ -H 'Content-Type: text/csv' \
+ --data-binary @movies.csv
+add_or_replace_documents_ndjson_1: |-
+ curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents' \
+ -H 'Content-Type: application/x-ndjson' \
+ --data-binary @movies.ndjson
# put_indexes_indexUid_documents
add_or_update_documents_1: |-
curl \
@@ -431,6 +441,13 @@ update_faceting_settings_1: |-
"genres": "count"
}
}'
+update_faceting_settings_max_values_per_facet_1: |-
+ curl \
+ -X PATCH 'MEILISEARCH_URL/indexes/books/settings/faceting' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "maxValuesPerFacet": 20
+ }'
# delete_indexes_indexUid_settings_faceting
reset_faceting_settings_1: |-
curl \
@@ -578,6 +595,23 @@ update_embedders_1: |-
"documentTemplate": "A document titled '{{doc.title}}' whose description starts with {{doc.overview|truncatewords: 20}}"
}
}'
+update_embedders_distribution_1: |-
+ curl \
+ -X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings' \
+ -H 'Content-Type: application/json' \
+ -H 'Authorization: Bearer MEILISEARCH_KEY' \
+ --data-binary '{
+ "embedders": {
+ "default": {
+ "source": "openAi",
+ "model": "text-embedding-3-small",
+ "distribution": {
+ "mean": 0.7,
+ "sigma": 0.3
+ }
+ }
+ }
+ }'
# delete_indexes_indexUid_settings_embedders
reset_embedders_1: |-
curl \
@@ -665,6 +699,13 @@ get_documents_post_1: |-
"fields": ["title", "genres", "rating", "language"],
"limit": 3
}'
+get_documents_post_with_filter_1: |-
+ curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents/fetch' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "filter": "genres = Action AND rating > 8"
+ }'
# post_indexes_indexUid_documents_delete
delete_documents_by_filter_1: |-
curl \
@@ -1186,6 +1227,15 @@ facet_search_3: |-
"facetQuery": "c",
"facetName": "genres"
}'
+facet_search_exhaustive_1: |-
+ curl \
+ -X POST 'MEILISEARCH_URL/indexes/books/facet-search' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "facetName": "genres",
+ "facetQuery": "c",
+ "exhaustiveFacetCount": true
+ }'
analytics_event_click_1: |-
curl \
-X POST 'https://PROJECT_URL/events' \
@@ -1434,6 +1484,18 @@ chat_patch_settings_1: |-
-H "Authorization: Bearer MEILISEARCH_KEY" \
-H "Content-Type: application/json" \
--data-binary '{ "apiKey": "your-valid-api-key" }'
+chat_patch_settings_prompts_1: |-
+ curl \
+ -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
+ -H 'Authorization: Bearer MEILISEARCH_KEY' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "prompts": {
+ "searchDescription": "Search the outdoor gear catalog whenever the user asks about products, prices, availability, or specifications. Do not use it for generic small talk.",
+ "searchQParam": "A short natural-language query capturing the user intent. Rewrite long questions into 3 to 8 keywords. Preserve product names and brand names verbatim.",
+ "searchIndexUidParam": "Choose between: \"products\" for physical gear, accessories, and apparel; \"guides\" for how-to articles and buying guides; \"reviews\" for customer reviews and ratings. Use \"products\" by default."
+ }
+ }'
# delete_chats_workspaceUid_settings
reset_chat_workspace_settings_1: |-
curl \
diff --git a/capabilities/conversational_search/getting_started/chat.mdx b/capabilities/conversational_search/getting_started/chat.mdx
index 57c153ab0..0a8153215 100644
--- a/capabilities/conversational_search/getting_started/chat.mdx
+++ b/capabilities/conversational_search/getting_started/chat.mdx
@@ -19,6 +19,18 @@ In code examples, replace `WORKSPACE_NAME` with the name of your workspace. On M
All requests to the chat completions endpoint must include `"stream": true`. Non-streaming (`stream: false`) is not yet supported and returns a `501 Not Implemented` error.
+## Message roles
+
+Every entry in the `messages` array carries a `role` that tells the LLM who authored it. The chat completions endpoint uses three roles, each with a distinct origin:
+
+| Role | Origin | Typical content |
+|------|--------|-----------------|
+| `system` | Meilisearch | Workspace-level instructions (the `prompts.system` string) plus internal tool descriptions injected by Meilisearch. You do not need to send this role yourself; Meilisearch prepends it. |
+| `assistant` | LLM provider | Responses generated by the configured model, including tool calls. Push these back into `messages` to preserve context on follow-up turns. |
+| `user` | User input | Questions and follow-ups coming from the end user of your application. This is what you add to `messages` before each request. |
+
+Understanding the origin matters when debugging: unexpected `system` content usually means the workspace system prompt needs tuning, wrong answers are an `assistant` problem, and malformed input is a `user` problem.
+
## Send a streaming request
Send a `POST` request to `/chats/{workspace}/chat/completions` with `stream: true`:
diff --git a/capabilities/conversational_search/getting_started/setup.mdx b/capabilities/conversational_search/getting_started/setup.mdx
index 12ef4073c..a1e0f32bc 100644
--- a/capabilities/conversational_search/getting_started/setup.mdx
+++ b/capabilities/conversational_search/getting_started/setup.mdx
@@ -6,6 +6,7 @@ description: Enable the chat completions feature, configure your indexes, and cr
import CodeSamplesAuthorizationHeader1 from '/snippets/generated-code-samples/code_samples_authorization_header_1.mdx';
import CodeSamplesChatCreateKey1 from '/snippets/generated-code-samples/code_samples_chat_create_key_1.mdx';
+import CodeSamplesUpdateExperimentalFeaturesChat1 from '/snippets/generated-code-samples/code_samples_update_experimental_features_chat_1.mdx';
Before building a chat interface or generating summarized answers, you need to enable the feature, configure your indexes, and create a workspace. This setup is shared across all conversational search use cases.
@@ -17,17 +18,32 @@ Enable chat completions from your Meilisearch Cloud project in one of two ways:
- Or open the **Chat** tab in your project and activate the feature directly from there
-For self-hosted instances, enable the feature through the [experimental features API](/reference/api/experimental-features/configure-experimental-features).
+For self-hosted instances, enable the feature through the [experimental features API](/reference/api/experimental-features/configure-experimental-features) by sending a `PATCH` request with `chatCompletions` set to `true`:
+
+
## Find your chat API key
-Meilisearch automatically generates a "Default Chat API Key" with `chatCompletions` and `search` permissions on all indexes. Check if you have the key using:
+Meilisearch automatically generates a "Default Chat API Key" that combines `chatCompletions` and `search` permissions on all indexes. Conversational search requires both actions: `chatCompletions` authorises the LLM call, and `search` authorises the retrieval step that feeds documents to the model. Any key you use with the `/chats` routes must carry both actions, so prefer the default chat API key unless you have a specific reason to create a custom one.
+
+Check if you have the key using:
Look for the key with the description "Default Chat API Key".
+### Restrict chat access to specific indexes
+
+Chat queries only search the indexes that the API key can access. The default chat API key is scoped to all indexes. To limit which indexes a chat client can reach, you have two options:
+
+- Create a new API key with both `chatCompletions` and `search` actions, scoped to the exact indexes you want exposed. See [manage API keys](/capabilities/security/how_to/manage_api_keys) for the full workflow.
+- Generate a [tenant token](/capabilities/security/how_to/generate_token_from_scratch) from the default chat API key. Tenant tokens inherit both the `chatCompletions` and `search` actions from their parent key and let you narrow index access or attach search rules per user.
+
+
+A tenant token cannot grant access to an index its parent API key does not already cover. Make sure the parent key is scoped to every index the token should be allowed to reach.
+
+
### Troubleshooting: Missing default chat API key
If your instance does not have a Default Chat API Key, create one manually:
diff --git a/capabilities/conversational_search/how_to/configure_chat_workspace.mdx b/capabilities/conversational_search/how_to/configure_chat_workspace.mdx
index 66f4113e5..ddc00f5e5 100644
--- a/capabilities/conversational_search/how_to/configure_chat_workspace.mdx
+++ b/capabilities/conversational_search/how_to/configure_chat_workspace.mdx
@@ -6,7 +6,7 @@ description: Set up a chat workspace with a system prompt, tools, and connected
import CodeSamplesChatPatchSettings1 from '/snippets/generated-code-samples/code_samples_chat_patch_settings_1.mdx';
import CodeSamplesChatGetSettings1 from '/snippets/generated-code-samples/code_samples_chat_get_settings_1.mdx';
-A chat workspace defines the configuration for a conversational search session, including the LLM provider, system prompt, and search behavior. You can create multiple workspaces targeting different use cases, such as a public-facing knowledge base and an internal support tool.
+A chat workspace groups the chat settings tailored to a specific use case: the LLM provider credentials, the system prompt, and the search behavior. Workspaces are the entry point for conversational search. You must configure at least one workspace before the chat completions endpoint can serve requests. You can also create multiple workspaces targeting different use cases, such as a public-facing knowledge base and an internal support tool.
On Meilisearch Cloud, the default workspace name is `cloud`. Replace `WORKSPACE_NAME` with `cloud` in all API calls. If you need additional workspaces, contact us.
@@ -42,12 +42,20 @@ The `source` field determines which LLM provider Meilisearch uses. Each provider
| Provider | `source` value | Required fields | Optional fields |
|----------|---------------|-----------------|-----------------|
-| OpenAI | `openAi` | `apiKey` | `baseUrl`, `orgId`, `projectId` |
-| Azure OpenAI | `azureOpenAi` | `apiKey`, `baseUrl` | `deploymentId`, `apiVersion` |
+| OpenAI | `openAi` | `apiKey` | `baseUrl` |
+| Azure OpenAI | `azureOpenAi` | `apiKey`, `baseUrl`, `orgId`, `projectId`, `apiVersion` | `deploymentId` |
| Mistral | `mistral` | `apiKey`, `baseUrl` | |
-| vLLM | `vLlm` | `baseUrl` | |
+| vLLM | `vLlm` | `baseUrl` | `apiKey` |
-`baseUrl` is required for all providers except OpenAI. For OpenAI, it is optional and only needed when using a custom endpoint.
+A few provider-specific rules to keep in mind:
+
+- `orgId`, `projectId`, and `apiVersion` are required for Azure OpenAI and are incompatible with every other `source`. Sending them with `openAi`, `mistral`, or `vLlm` is rejected.
+- `baseUrl` is required for Azure OpenAI and vLLM. For Mistral it points to the Mistral API endpoint. For OpenAI it is optional and only needed when routing through a custom endpoint.
+- `apiKey` is optional for vLLM (self-hosted deployments often run without authentication) and mandatory for every other provider.
+
+
+The `apiKey` field is write-only. Meilisearch stores it for outbound LLM calls but redacts it in every response from the workspace settings endpoint. Retrieving the settings will show an obfuscated placeholder rather than the real secret, so keep your own copy in a secure location. To rotate the key, `PATCH` the workspace with the new value.
+
### Azure OpenAI example
diff --git a/capabilities/conversational_search/how_to/configure_index_chat_settings.mdx b/capabilities/conversational_search/how_to/configure_index_chat_settings.mdx
index c2be77304..7bb6d68c9 100644
--- a/capabilities/conversational_search/how_to/configure_index_chat_settings.mdx
+++ b/capabilities/conversational_search/how_to/configure_index_chat_settings.mdx
@@ -5,6 +5,10 @@ description: Control how each index is described to the LLM and how it is search
Each index you want to make available to conversational search must have its chat settings configured. These settings tell the LLM what the index contains, how to format document data, and what search parameters to use.
+
+`chat` is an **index-level** setting, distinct from the workspace-level configuration that connects Meilisearch to an LLM provider. Workspace settings define the model, API key, and global prompts; index chat settings describe each individual index to the LLM and control how it is queried. Both must be configured: first set up a workspace, then configure chat settings on every index you want the agent to access.
+
+
## Update chat settings
Use the `/indexes/{index_uid}/settings/chat` endpoint to configure chat settings for an index:
diff --git a/capabilities/conversational_search/how_to/optimize_chat_prompts.mdx b/capabilities/conversational_search/how_to/optimize_chat_prompts.mdx
index f4cb5890c..0f6dad055 100644
--- a/capabilities/conversational_search/how_to/optimize_chat_prompts.mdx
+++ b/capabilities/conversational_search/how_to/optimize_chat_prompts.mdx
@@ -3,6 +3,8 @@ title: Optimize chat prompts
description: Improve conversational search response quality by tuning system prompts, document templates, and index chat settings.
---
+import CodeSamplesChatPatchSettingsPrompts1 from '/snippets/generated-code-samples/code_samples_chat_patch_settings_prompts_1.mdx';
+
The quality of conversational search responses depends on three layers of configuration: the system prompt, the document template, and the index-level chat settings. Each layer shapes what the LLM receives and how it responds. This guide covers how to tune each one for better results.
## System prompt strategies
@@ -68,6 +70,22 @@ a detailed explanation.
+## Tune the tool prompts
+
+Beyond `prompts.system`, the workspace `prompts` object exposes three tool-facing prompts that Meilisearch injects into the function-calling schema the agent sees. They do not talk to the end user: they talk to the LLM about how to drive the Meilisearch search tool. Small edits here shift which index the agent picks, how it rewrites the query, and whether it decides to search at all.
+
+| Field | What it configures |
+|-------|--------------------|
+| `prompts.searchDescription` | Internal description of the Meilisearch chat tools. Use it to instruct the agent on how and when to use the configured tools, for example encouraging it to search for any factual question or to skip the search for greetings. |
+| `prompts.searchQParam` | Describes the expected user input and the desired query shape. Use it to tell the agent how to reformulate user messages into effective search queries and what output to expect back. |
+| `prompts.searchIndexUidParam` | Describes each index the agent has access to and explains how to pick between them. This is the main lever when you have multiple indexes and want the agent to route queries correctly. |
+
+
+
+
+Change one prompt at a time and re-run your evaluation queries. Tool prompts compound: a change in `searchIndexUidParam` can make earlier `searchQParam` wording look wrong even though it hasn't changed.
+
+
## Configure index chat settings
Each index has chat-specific settings that control how documents are prepared for the LLM. Configure these through the [index chat settings](/capabilities/conversational_search/how_to/configure_index_chat_settings) endpoint:
diff --git a/capabilities/conversational_search/how_to/stream_chat_responses.mdx b/capabilities/conversational_search/how_to/stream_chat_responses.mdx
index 4998be8f4..dc9e57c89 100644
--- a/capabilities/conversational_search/how_to/stream_chat_responses.mdx
+++ b/capabilities/conversational_search/how_to/stream_chat_responses.mdx
@@ -199,7 +199,15 @@ The `-N` flag in the cURL example disables output buffering, so you see each chu
## Understand the SSE response format
-Meilisearch streams responses as Server-Sent Events. Each event is a line prefixed with `data: ` followed by a JSON object. The stream ends with a `data: [DONE]` message.
+Meilisearch streams responses as [Server-Sent Events (SSE)](https://developer.mozilla.org/docs/Web/API/Server-sent_events) over a persistent HTTP connection. The wire format is deliberately OpenAI-compatible so you can point the official OpenAI SDKs, the Vercel AI SDK, or any other SSE-aware client at the endpoint without custom parsing.
+
+Concretely, each event on the wire follows three rules:
+
+- Every event is a line that begins with the literal prefix `data: `.
+- The payload after `data: ` is a single JSON object shaped like an OpenAI `chat.completion.chunk` (same `id`, `object`, `choices[].delta` structure as `/v1/chat/completions`).
+- The stream terminates with the sentinel line `data: [DONE]`. The `[DONE]` marker is a literal string, not JSON, so parsers must check for it before calling `JSON.parse`.
+
+Events are separated by blank lines. After consuming `[DONE]`, close the reader and treat the connection as complete.
### Content chunks
diff --git a/capabilities/conversational_search/overview.mdx b/capabilities/conversational_search/overview.mdx
index ad439ad6a..fa17c1ab6 100644
--- a/capabilities/conversational_search/overview.mdx
+++ b/capabilities/conversational_search/overview.mdx
@@ -4,16 +4,16 @@ sidebarTitle: Overview
description: Conversational search allows people to make search queries using natural languages and receive AI-generated answers grounded in your data.
---
+
+**Conversational search is still in early development and conversational agents can hallucinate.** LLMs may occasionally produce inaccurate or misleading answers even when the retrieved source documents are correct. Monitor responses closely in production, follow the [hallucination reduction guide](/capabilities/conversational_search/advanced/reduce_hallucination), and configure [guardrails](/capabilities/conversational_search/how_to/configure_guardrails) to minimise this risk.
+
+
Conversational search is an AI-powered feature built on top of Meilisearch's search engine. It works as a built-in Retrieval Augmented Generation (RAG) system: when a user asks a question, Meilisearch retrieves relevant documents from its indexes, then uses an LLM to generate a response grounded in those results.
With proper configuration, such as [system prompt engineering](/capabilities/conversational_search/advanced/reduce_hallucination#system-prompt-engineering) and [guardrails](/capabilities/conversational_search/how_to/configure_guardrails), you can ensure that responses are based on your indexed data rather than the LLM's general knowledge.
This is similar to how [Perplexity](https://www.perplexity.ai/) works: every answer comes with source documents so users can verify the information. Meilisearch brings the same pattern to your own data.
-
-Conversational search relies on large language models (LLMs) to generate responses. LLMs may occasionally hallucinate inaccurate or misleading information, even when provided with correct source documents. Follow the [hallucination reduction guide](/capabilities/conversational_search/advanced/reduce_hallucination) and configure [guardrails](/capabilities/conversational_search/how_to/configure_guardrails) to minimize this risk in production environments.
-
-
## Use cases
Conversational search supports three main use cases, all powered by the same `/chats` API route:
diff --git a/capabilities/filtering_sorting_faceting/getting_started.mdx b/capabilities/filtering_sorting_faceting/getting_started.mdx
index d599a0888..dbfedaecc 100644
--- a/capabilities/filtering_sorting_faceting/getting_started.mdx
+++ b/capabilities/filtering_sorting_faceting/getting_started.mdx
@@ -51,6 +51,10 @@ If you want to filter results based on an attribute, you must first add it to th
By default, `filterableAttributes` is empty. Filters do not work without first explicitly adding attributes to the `filterableAttributes` list.
+
+Calculating `comparison` filters (such as `<`, `>`, or `TO`) is a resource-intensive operation. Disabling them may lead to better search and indexing performance. `equality` filters use fewer resources and have limited impact on performance. Use [granular filterable attributes](/capabilities/filtering_sorting_faceting/how_to/configure_granular_filters) to enable only the filter operations you actually need for each attribute.
+
+
## Use `filter` when searching
After updating the [`filterableAttributes` index setting](/reference/api/settings/get-filterableattributes), you can use `filter` to fine-tune your search results.
diff --git a/capabilities/filtering_sorting_faceting/how_to/build_faceted_navigation.mdx b/capabilities/filtering_sorting_faceting/how_to/build_faceted_navigation.mdx
index ac710e881..17ea99738 100644
--- a/capabilities/filtering_sorting_faceting/how_to/build_faceted_navigation.mdx
+++ b/capabilities/filtering_sorting_faceting/how_to/build_faceted_navigation.mdx
@@ -6,6 +6,7 @@ description: Build an ecommerce-style faceted sidebar that shows available optio
import CodeSamplesFacetedSearchUpdateSettings1 from '/snippets/generated-code-samples/code_samples_faceted_search_update_settings_1.mdx';
import CodeSamplesFacetedSearch1 from '/snippets/generated-code-samples/code_samples_faceted_search_1.mdx';
+import CodeSamplesUpdateFacetingSettingsMaxValuesPerFacet1 from '/snippets/generated-code-samples/code_samples_update_faceting_settings_max_values_per_facet_1.mdx';
Faceted navigation displays filter options alongside the number of matching documents, letting users progressively refine their search. This is the pattern behind product sidebars on ecommerce sites, where users can click "Electronics (42)" or "Books (18)" to narrow results.
@@ -37,6 +38,10 @@ Add the attributes you want as facets to `filterableAttributes`:
Wait for the settings task to complete before searching.
+
+If an attribute passed to `facets` has not been added to `filterableAttributes`, Meilisearch silently ignores it. No error is raised; the attribute simply will not appear in the `facetDistribution` response. If a facet is missing from your UI, double-check that it is declared as a filterable attribute.
+
+
## Step 2: request facet distributions
Use the `facets` search parameter to tell Meilisearch which attributes should include distribution counts in the response:
@@ -87,6 +92,10 @@ The response includes a `facetDistribution` object showing every value for each
The `facetDistribution` tells you exactly which values exist and how many documents match each one. The `facetStats` object provides minimum and maximum values for numeric facets, useful for building range sliders.
+
+`facetStats` only considers values stored as JSON numbers. String values are ignored, even when the string contains a numeric value such as `"42"`. If you expect `min`/`max` for a given facet and the object is missing, confirm that the underlying field is indexed as a number rather than a string.
+
+
## Step 3: apply a filter when the user clicks a facet
When a user clicks a facet value, send a new search request with a `filter` parameter:
@@ -247,6 +256,16 @@ This pattern:
3. Renders facet values as checkboxes with document counts
4. Updates both facets and results when the user toggles a checkbox
+## Tune `maxValuesPerFacet`
+
+By default, Meilisearch returns up to 100 distinct values per facet in the `facetDistribution` object. If your UI only displays the top 10 or 20 options, lower `maxValuesPerFacet` to match what you actually render:
+
+
+
+
+Setting `maxValuesPerFacet` to a high value might negatively impact performance. Raising it only makes sense when you genuinely need to surface a large number of facet values at once.
+
+
## Key points
- Always include the `facets` parameter in every search request so the sidebar stays updated
diff --git a/capabilities/filtering_sorting_faceting/how_to/facet_search.mdx b/capabilities/filtering_sorting_faceting/how_to/facet_search.mdx
new file mode 100644
index 000000000..ca3ebbabe
--- /dev/null
+++ b/capabilities/filtering_sorting_faceting/how_to/facet_search.mdx
@@ -0,0 +1,89 @@
+---
+title: Search within facet values
+sidebarTitle: Facet search
+description: Use the facet search endpoint to type-ahead through facet values, build auto-complete experiences, and narrow large filter lists. This page also covers the two key limitations of facet search.
+---
+
+import CodeSamplesFacetSearch3 from '/snippets/generated-code-samples/code_samples_facet_search_3.mdx';
+import CodeSamplesFacetSearchExhaustive1 from '/snippets/generated-code-samples/code_samples_facet_search_exhaustive_1.mdx';
+
+Facet search is a dedicated endpoint for searching through the values of a single facet. It is typically used to power auto-complete and type-ahead interfaces on top of filter menus, especially when a facet has too many distinct values to display at once (for example, thousands of brands or authors).
+
+If you are new to facets, start with [Search with facets](/capabilities/filtering_sorting_faceting/how_to/filter_with_facets) and [Build faceted navigation](/capabilities/filtering_sorting_faceting/how_to/build_faceted_navigation). This page focuses on the `/facet-search` endpoint itself and the edge cases you need to know about before relying on it.
+
+## Prerequisites
+
+Before you can search a facet's values, the attribute must be declared in [`filterableAttributes`](/capabilities/filtering_sorting_faceting/getting_started). Facet search is enabled by default on every index; you can disable it per index with the `facetSearch` setting if you do not need it and want to speed up indexing.
+
+## Search a facet
+
+Send a `POST` request to `/indexes/{index_uid}/facet-search` with the name of the facet you want to search and the partial string typed by the user:
+
+
+
+The response contains a `facetHits` array with the matching values and the number of documents that carry each one:
+
+
+
+```json
+{
+ "facetHits": [
+ { "value": "Children's Literature", "count": 1 },
+ { "value": "Classics", "count": 6 },
+ { "value": "Comedy", "count": 2 },
+ { "value": "Coming-of-Age", "count": 1 }
+ ],
+ "facetQuery": "c"
+}
+```
+
+
+
+You can further scope the results by combining `facetQuery` with `q`, `filter`, and `matchingStrategy`. See the [facet search API reference](/reference/api/facet-search/search-in-facets) for the full list of parameters.
+
+## Facet search only works on string fields
+
+
+Meilisearch does not support facet search on numeric fields. If you need to type-ahead through a numeric facet, convert the values to strings before indexing them (for example, store `"rating": "4.5"` rather than `"rating": 4.5`).
+
+
+Internally, Meilisearch stores numbers as `float64`. Floating-point values lack exact decimal precision and can be represented in more than one way, which makes prefix matching on their textual form unreliable. Only string facet values are indexed for facet search.
+
+## Facet search only matches the first term of `facetQuery`
+
+
+Meilisearch's facet search is single-term: only the first word in `facetQuery` is used to match facet values. Subsequent words are ignored.
+
+
+For example, given a `author` facet that contains `"Jane Austen"`:
+
+- `facetQuery: "Jane"` returns `Jane Austen`
+- `facetQuery: "Austen"` does **not** return `Jane Austen`, because the word `Austen` is not the start of the stored value
+- `facetQuery: "Jane Aus"` effectively behaves like `facetQuery: "Jane"` (the second word is dropped)
+
+This is a deliberate trade-off that keeps the facet-search index compact and fast. If you need multi-word matching across facet values, perform a regular search with the `q` parameter instead and inspect the `facetDistribution` of the response.
+
+## Get exact facet counts
+
+By default, the counts returned by facet search are estimates, which is faster on large indexes. To force exact counts, set `exhaustiveFacetCount` to `true`:
+
+
+
+Exact counts are slower to compute. Prefer them when accuracy matters (for example, a storefront's category counts) and the defaults when latency matters most.
+
+## Next steps
+
+
+
+ Configure facets and filter search results
+
+
+ Patterns for category and filter menus
+
+
+ Strategies for facets with thousands of values
+
+
+ Full list of parameters accepted by `/facet-search`
+
+
diff --git a/capabilities/filtering_sorting_faceting/how_to/sort_results.mdx b/capabilities/filtering_sorting_faceting/how_to/sort_results.mdx
index 880f8b764..dba56b1aa 100644
--- a/capabilities/filtering_sorting_faceting/how_to/sort_results.mdx
+++ b/capabilities/filtering_sorting_faceting/how_to/sort_results.mdx
@@ -45,6 +45,10 @@ If a field has values of different types across documents, Meilisearch will give
This can lead to unexpected behavior when sorting. For optimal user experience, only sort based on fields containing the same type of value.
+
+Adding an attribute to `sortableAttributes` that does not exist in any document is accepted silently: if the field does not exist, no error will be thrown. Double-check the attribute name when configuring sorting, as a typo will not surface until you notice results are not sorted as expected.
+
+
#### Example
Suppose you have collection of books containing the following fields:
diff --git a/capabilities/full_text_search/advanced/performance_tuning.mdx b/capabilities/full_text_search/advanced/performance_tuning.mdx
index 32694dfc5..09508fb00 100644
--- a/capabilities/full_text_search/advanced/performance_tuning.mdx
+++ b/capabilities/full_text_search/advanced/performance_tuning.mdx
@@ -94,6 +94,8 @@ curl \
With `byAttribute`, Meilisearch only checks whether query terms appear in the same attribute, not their exact distance within it. This makes indexing significantly faster and reduces the work done during each search.
+Calculating the distance between words is a resource-intensive operation. Lowering the precision of this operation may significantly improve performance and will have little impact on result relevancy in most use cases.
+
The trade-off is that multi-word queries like "dark knight" will rank documents the same whether the words are adjacent or far apart within the same field. For most use cases (ecommerce, documentation, catalogs), this difference is negligible. Word-level precision matters most for long-form content where word proximity is a strong relevancy signal.
## Lower max values per facet
diff --git a/capabilities/full_text_search/how_to/configure_dictionary.mdx b/capabilities/full_text_search/how_to/configure_dictionary.mdx
new file mode 100644
index 000000000..5bd8af956
--- /dev/null
+++ b/capabilities/full_text_search/how_to/configure_dictionary.mdx
@@ -0,0 +1,54 @@
+---
+title: Configure a custom dictionary
+sidebarTitle: Configure dictionary
+description: Teach Meilisearch to treat groups of strings as a single term by supplying a supplementary dictionary of user-defined words.
+---
+
+import CodeSamplesGetDictionary1 from '/snippets/generated-code-samples/code_samples_get_dictionary_1.mdx';
+import CodeSamplesUpdateDictionary1 from '/snippets/generated-code-samples/code_samples_update_dictionary_1.mdx';
+import CodeSamplesResetDictionary1 from '/snippets/generated-code-samples/code_samples_reset_dictionary_1.mdx';
+
+The `dictionary` setting allows you to instruct Meilisearch to consider groups of strings as a single term by adding a supplementary dictionary of user-defined terms. Entries in the dictionary override the default tokenizer, so Meilisearch will recognize them as indivisible tokens during both indexing and search.
+
+## When to use a custom dictionary
+
+A custom dictionary is particularly useful in two situations:
+
+- **Datasets with many domain-specific words and in languages where words are not separated by whitespace**, such as Japanese. Adding domain terms or uninterrupted character sequences to the dictionary ensures Meilisearch treats them as single units instead of fragmenting them during tokenization.
+- **Space-separated languages that contain names or abbreviations with interleaved dots and spaces**, such as `"J. R. R. Tolkien"` and `"W. E. B. Du Bois"`. Without a custom dictionary, the default tokenizer splits these names into separate letters and periods, which makes them hard to match as a cohesive term.
+
+## Check current dictionary
+
+Retrieve the current `dictionary` setting for an index:
+
+
+
+By default, the response is an empty array `[]`, meaning no custom dictionary entries are configured.
+
+## Update the dictionary
+
+Add custom terms to the dictionary:
+
+
+
+After this request completes, Meilisearch treats `"J. R. R."` and `"W. E. B."` as single tokens. Queries for `"J. R. R. Tolkien"` will match documents where the name appears exactly as spelled, instead of being broken into separate characters.
+
+
+Updating `dictionary` triggers a re-indexing of all documents in the index. This is an [asynchronous](/capabilities/indexing/tasks_and_batches/async_operations) operation. Use the [task API](/reference/api/tasks/get-all-tasks) to monitor progress.
+
+
+## Reset the dictionary
+
+Clear the custom dictionary and return to the default tokenizer behavior:
+
+
+
+## Dictionary vs. synonyms vs. stop words
+
+- Use [`synonyms`](/capabilities/full_text_search/relevancy/synonyms) to map different words to the same concept (for example, `NYC` to `New York City`).
+- Use [`stopWords`](/capabilities/full_text_search/how_to/configure_stop_words) to ignore common terms that add no signal to search.
+- Use `dictionary` to preserve multi-character or whitespace-containing sequences that the default tokenizer would otherwise split.
+
+
+For the full API reference, see [get dictionary](/reference/api/settings/get-dictionary).
+
diff --git a/capabilities/full_text_search/how_to/configure_distinct_attribute.mdx b/capabilities/full_text_search/how_to/configure_distinct_attribute.mdx
index c87deed0c..f51a4755f 100644
--- a/capabilities/full_text_search/how_to/configure_distinct_attribute.mdx
+++ b/capabilities/full_text_search/how_to/configure_distinct_attribute.mdx
@@ -20,6 +20,10 @@ You may set a distinct attribute in two ways: using the `distinctAttribute` inde
There can be only one `distinctAttribute` per index. Trying to set multiple fields as a `distinctAttribute` will return an error.
+
+Updating `distinctAttribute` will re-index all documents in the index, which can take some time. When setting up a new index, we recommend updating your index settings first and then adding documents. This reduces RAM consumption compared to adding documents and then changing settings, since Meilisearch would have to re-index the whole dataset.
+
+
The value of a field configured as a distinct attribute will always be unique among returned documents. This means **there will never be more than one occurrence of the same value** in the distinct attribute field among the returned documents.
When multiple documents have the same value for the distinct attribute, Meilisearch returns only the highest-ranked result after applying [ranking rules](/capabilities/full_text_search/relevancy/ranking_rules). If two or more documents are equivalent in terms of ranking, Meilisearch returns the first result according to its `internal_id`.
diff --git a/capabilities/full_text_search/how_to/configure_search_cutoff.mdx b/capabilities/full_text_search/how_to/configure_search_cutoff.mdx
index 9e147870a..0b0899c1e 100644
--- a/capabilities/full_text_search/how_to/configure_search_cutoff.mdx
+++ b/capabilities/full_text_search/how_to/configure_search_cutoff.mdx
@@ -15,7 +15,7 @@ When a search query is processed, Meilisearch iterates through documents and [ra
The search cutoff sets an upper bound on this processing time. If Meilisearch reaches the cutoff before finishing, it returns the results collected up to that point. These results are still ranked correctly according to the ranking rules, but the result set may not include every possible match.
-By default, the search cutoff is `null`, meaning there is no time limit.
+By default, `searchCutoffMs` is `null`. When no explicit value is configured, Meilisearch interrupts searches after **1500 milliseconds**. Setting `searchCutoffMs` to an integer overrides this internal default with your chosen limit.
## Check current search cutoff
diff --git a/capabilities/full_text_search/how_to/configure_searchable_attributes.mdx b/capabilities/full_text_search/how_to/configure_searchable_attributes.mdx
index e57f601c1..fc4f2cdc1 100644
--- a/capabilities/full_text_search/how_to/configure_searchable_attributes.mdx
+++ b/capabilities/full_text_search/how_to/configure_searchable_attributes.mdx
@@ -42,6 +42,10 @@ Updating `searchableAttributes` triggers a re-indexing of all documents in the i
After manually updating `searchableAttributes`, new attributes found in subsequently indexed documents will not be automatically added to the list. You must either include them manually or [reset the setting](#reset-searchable-attributes).
+
+**Known issue**: due to an implementation bug, manually updating `searchableAttributes` will change the displayed order of document fields in the JSON response. This behavior is inconsistent and will be fixed in a future release. If your application depends on a specific field order in responses, rely on explicit key access rather than the order returned by Meilisearch.
+
+
## Reset searchable attributes
Reset `searchableAttributes` to its default value (`["*"]`), making all fields searchable again:
@@ -70,6 +74,10 @@ curl \
This searches only the `title` field for this request, regardless of the index-level `searchableAttributes` setting. The attributes specified must be a subset of the configured `searchableAttributes`.
+
+`attributesToSearchOn` narrows the search to the fields you list, so documents that only match in other searchable fields are silently excluded from the response. For example, if your index has `searchableAttributes: ["title", "overview", "genre"]` and you set `attributesToSearchOn: ["overview"]`, a document whose only match is in `title` or `genre` will not appear in the results, even though those fields are otherwise searchable. No error is raised; the results are simply narrower than you may expect.
+
+
For more details on how searchable and displayed attributes work together, see [displayed and searchable attributes](/capabilities/full_text_search/how_to/configure_displayed_attributes). For the full API reference, see [get searchable attributes](/reference/api/settings/get-searchableattributes).
diff --git a/capabilities/full_text_search/how_to/highlight_search_results.mdx b/capabilities/full_text_search/how_to/highlight_search_results.mdx
index 26094d284..fb69d2fd5 100644
--- a/capabilities/full_text_search/how_to/highlight_search_results.mdx
+++ b/capabilities/full_text_search/how_to/highlight_search_results.mdx
@@ -42,6 +42,10 @@ Matched terms appear in the `_formatted` object wrapped in `` tags:
Set `attributesToHighlight` to `["*"]` to highlight matched terms across all [displayed attributes](/capabilities/full_text_search/how_to/configure_displayed_attributes):
+
+`attributesToHighlight` highlights matches within any attribute you list, even attributes that are not part of `searchableAttributes`. Meilisearch does not produce new matches in non-searchable fields, but if the query term happens to appear verbatim in one of those fields, it will still be wrapped in highlight tags.
+
+
```bash
@@ -131,6 +135,14 @@ Result:
| `cropLength` | Integer | `10` | Maximum number of words in the cropped result. |
| `cropMarker` | String | `"..."` | String inserted at the beginning or end of cropped text. |
+
+`cropLength` counts every word in the returned snippet, including query terms and stop words. For example, if `cropLength` is `2` and you search for `shifu`, the cropped value might look like `"…Shifu informs…"`, containing two words in total (the query term plus one surrounding word).
+
+
+
+Crop markers are only inserted where content has been removed. If the cropped snippet begins at the first word of the field, no marker is added to the start; likewise, no marker is added to the end when the snippet reaches the end of the field.
+
+
### Per-attribute crop length
Override the global `cropLength` for specific attributes by appending `:length` to the attribute name:
diff --git a/capabilities/full_text_search/how_to/paginate_search_results.mdx b/capabilities/full_text_search/how_to/paginate_search_results.mdx
index 2c516c2b5..42a6b74c5 100644
--- a/capabilities/full_text_search/how_to/paginate_search_results.mdx
+++ b/capabilities/full_text_search/how_to/paginate_search_results.mdx
@@ -186,6 +186,14 @@ By default, Meilisearch queries only return `estimatedTotalHits`. This value is
When your query contains either [`hitsPerPage`](/reference/api/search/search-with-post#response-one-of-0-hits-per-page), [`page`](/reference/api/search/search-with-post#response-one-of-0-page), or both these search parameters, Meilisearch returns `totalHits` and `totalPages` instead of `estimatedTotalHits`. `totalHits` contains the exhaustive number of results for that query, and `totalPages` contains the exhaustive number of pages of search results for the same query:
+
+Queries containing `hitsPerPage` are exhaustive, which changes the response shape. `estimatedTotalHits` is replaced by `totalHits` and `totalPages`. If your frontend relies on the presence of `estimatedTotalHits`, switching pagination strategies may break it.
+
+
+
+`hitsPerPage` and `page` are resource-intensive options and might negatively impact search performance. This is particularly likely if `maxTotalHits` is set to a value higher than its default.
+
+
```json
diff --git a/capabilities/full_text_search/overview.mdx b/capabilities/full_text_search/overview.mdx
index 892152fa0..71bb10970 100644
--- a/capabilities/full_text_search/overview.mdx
+++ b/capabilities/full_text_search/overview.mdx
@@ -17,6 +17,14 @@ Full-text search is the core capability of Meilisearch. When a user types a quer
Full-text search works best when users search with keywords or short phrases and expect results ranked by textual relevance. If your users search with natural language questions or need results based on meaning rather than exact terms, consider [hybrid search](/capabilities/hybrid_search/overview) or [conversational search](/capabilities/conversational_search/overview).
+## Choosing a search endpoint
+
+Meilisearch exposes two search routes: `POST /indexes/{index_uid}/search` and `GET /indexes/{index_uid}/search`. **Prefer `POST` in nearly all cases.** `POST` accepts the full range of search parameters, including structured (array) `filter` expressions, and is the endpoint every official SDK uses.
+
+
+Use of the `GET /search` route is discouraged unless you have a specific reason to prefer it (for example, to leverage HTTP caching at a proxy layer). The `GET` route only accepts string filter expressions, so array-shaped filters must be serialized to strings before being passed as query parameters.
+
+
## Next steps
diff --git a/capabilities/full_text_search/relevancy/custom_ranking_rules.mdx b/capabilities/full_text_search/relevancy/custom_ranking_rules.mdx
index 23315f06d..76dcc8d1f 100644
--- a/capabilities/full_text_search/relevancy/custom_ranking_rules.mdx
+++ b/capabilities/full_text_search/relevancy/custom_ranking_rules.mdx
@@ -18,6 +18,10 @@ To add a custom ranking rule, you have to communicate the attribute name followe
**The attribute must have either a numeric or a string value** in all of the documents contained in that index.
+
+If some documents do not contain the attribute defined in a custom ranking rule, the application of the ranking rule is undefined and the search results might not be sorted as you expected. Make sure that any attribute used in a custom ranking rule is present in all of your documents.
+
+
You can add this rule to the existing list of ranking rules using the [update settings endpoint](/reference/api/settings/update-all-settings) or [update ranking rules endpoint](/reference/api/settings/update-rankingrules).
## How to use custom ranking rules
diff --git a/capabilities/full_text_search/relevancy/ranking_score.mdx b/capabilities/full_text_search/relevancy/ranking_score.mdx
index fd07413c9..c1a06609f 100644
--- a/capabilities/full_text_search/relevancy/ranking_score.mdx
+++ b/capabilities/full_text_search/relevancy/ranking_score.mdx
@@ -50,7 +50,7 @@ Each document in the response will include a `_rankingScore` field:
## Requesting a detailed breakdown
-For a deeper understanding of why a document received a particular score, set `showRankingScoreDetails` to `true`. This returns the contribution of each ranking rule:
+For a deeper understanding of why a document received a particular score, set `showRankingScoreDetails` to `true`. This adds a detailed global ranking score field, `_rankingScoreDetails`, to each document in the response. `_rankingScoreDetails` is an object containing one nested object per active ranking rule, showing how each rule contributed to the document's overall score:
diff --git a/capabilities/hybrid_search/advanced/composite_embedders.mdx b/capabilities/hybrid_search/advanced/composite_embedders.mdx
index da4b30ec5..aace060a2 100644
--- a/capabilities/hybrid_search/advanced/composite_embedders.mdx
+++ b/capabilities/hybrid_search/advanced/composite_embedders.mdx
@@ -120,6 +120,18 @@ Meilisearch automatically uses the search embedder for the query and the indexin
**Experimental status**: this feature requires the `compositeEmbedders` experimental flag. The API surface may change in future versions. Monitor the [changelog](/changelog) for updates.
+## Sub-embedder constraints
+
+Composite embedders impose additional rules on their `indexingEmbedder` and `searchEmbedder` sub-objects. Breaking any of these rules makes the embedder settings invalid.
+
+- `indexingEmbedder` and `searchEmbedder` must use the same model for generating embeddings.
+- `indexingEmbedder` and `searchEmbedder` must have identical `dimensions` and `pooling` methods.
+- `source` is mandatory for both `indexingEmbedder` and `searchEmbedder`.
+- Neither sub-embedder can set `source` to `composite` or `userProvided`.
+- `binaryQuantized` and `distribution` are not valid sub-embedder fields. They must always be declared on the main (composite) embedder.
+- `documentTemplate` and `documentTemplateMaxBytes` are invalid fields for `searchEmbedder`.
+- `documentTemplate` and `documentTemplateMaxBytes` are mandatory for `indexingEmbedder` when its source supports them (that is, every source except `userProvided`).
+
## Next steps
diff --git a/capabilities/hybrid_search/advanced/document_template_best_practices.mdx b/capabilities/hybrid_search/advanced/document_template_best_practices.mdx
index 49022a00b..bc3991b67 100644
--- a/capabilities/hybrid_search/advanced/document_template_best_practices.mdx
+++ b/capabilities/hybrid_search/advanced/document_template_best_practices.mdx
@@ -34,7 +34,11 @@ Take a look at this document from a database of movies:
Use a custom `documentTemplate` value in your embedder configuration.
-The default `documentTemplate` includes all searchable fields with non-`null` values. In most cases, this adds noise and more information than the embedder needs to provide relevant search results.
+
+If you do not manually set `documentTemplate`, Meilisearch falls back to a default template that includes **all searchable and non-null document fields**. This may lead to suboptimal performance and relevancy: the resulting prompt is usually longer than necessary, wastes tokens on fields that have little semantic value, and dilutes the parts of the document that actually matter.
+
+For best results, build short templates that only contain highly relevant data. If you are working with a long field, consider truncating it.
+
## Only include highly relevant information
diff --git a/capabilities/hybrid_search/advanced/tune_distribution.mdx b/capabilities/hybrid_search/advanced/tune_distribution.mdx
new file mode 100644
index 000000000..1337addb4
--- /dev/null
+++ b/capabilities/hybrid_search/advanced/tune_distribution.mdx
@@ -0,0 +1,61 @@
+---
+title: Tune the `distribution` of semantic scores
+description: Learn how to use the `distribution` embedder setting to correct `_rankingScore` values for semantic hits and surface more relevant results.
+---
+
+import CodeSamplesUpdateEmbeddersDistribution1 from '/snippets/generated-code-samples/code_samples_update_embedders_distribution_1.mdx';
+
+Use the `distribution` embedder setting to correct the returned `_rankingScore`s of semantic hits with an affine transformation. Tuning `distribution` is useful when your chosen embedder consistently rates unrelated documents as "somewhat relevant", making it hard for downstream code (or users) to tell truly good matches apart from noise.
+
+
+Changing `distribution` does not trigger a reindexing operation. This makes it safe to iterate on, unlike most other embedder settings.
+
+
+## When to tune `distribution`
+
+Different embedding models produce `_rankingScore` values on different effective ranges. Some models report high scores for nearly every document, while others spread their scores more evenly. Tuning `distribution` rescales these raw scores so that:
+
+- Very relevant hits land near `1`.
+- Somewhat relevant hits land near `0.5`.
+- Irrelevant hits land near `0`.
+
+This gives you a consistent scale across indexes and models, which makes it easier to set a score threshold or compare results across embedders.
+
+## How `distribution` works
+
+`distribution` is an optional field compatible with all embedder sources. It is an object with two fields, both numbers between `0` and `1`:
+
+| Field | Meaning |
+|---|---|
+| `mean` | The semantic score of "somewhat relevant" hits before applying the `distribution` setting. |
+| `sigma` | The average absolute difference in `_rankingScore`s between "very relevant" hits and "somewhat relevant" hits, and between "somewhat relevant" hits and "irrelevant" hits. |
+
+Meilisearch applies these values as an affine correction on top of the raw semantic scores.
+
+## Tuning workflow
+
+Configuring `distribution` requires a certain amount of trial and error. In practice:
+
+1. Run representative semantic searches against your index with `showRankingScore: true`.
+2. Note the `_rankingScore`s of hits you consider "very relevant", "somewhat relevant", and "irrelevant".
+3. Record the observed `mean` (the score of your "somewhat relevant" hits) and `sigma` (the average distance between your relevance tiers).
+4. Update your embedder with the new `distribution` values.
+5. Re-run the searches and check that top hits now score near `1`, borderline hits near `0.5`, and poor hits near `0`.
+6. Repeat until the scores match your expectations.
+
+## Example configuration
+
+
+
+In this example, documents the embedder currently rates around `0.7` are treated as "somewhat relevant", and scores are spread out so that truly good matches move toward `1` while weak matches drift toward `0`.
+
+## Next steps
+
+
+
+ Compare embedding providers and pick the right one for your use case.
+
+
+ Tune `semanticRatio` to balance keyword and semantic results.
+
+
diff --git a/capabilities/hybrid_search/how_to/configure_huggingface_embedder.mdx b/capabilities/hybrid_search/how_to/configure_huggingface_embedder.mdx
index 86c62c2b6..f805cb18d 100644
--- a/capabilities/hybrid_search/how_to/configure_huggingface_embedder.mdx
+++ b/capabilities/hybrid_search/how_to/configure_huggingface_embedder.mdx
@@ -49,6 +49,42 @@ In this configuration:
Unlike cloud-based embedders, the HuggingFace source does not require an API key.
+### Pin a model revision with `revision`
+
+Use the optional `revision` field to pin a specific revision of a HuggingFace model. The value is a commit hash, branch name, or tag from the model repository.
+
+
+
+```json
+{
+ "my-hf": {
+ "source": "huggingFace",
+ "model": "BAAI/bge-base-en-v1.5",
+ "revision": "a5beb1e3e68b9ab74eb54cfb186926f2123bc4b3"
+ }
+}
+```
+
+
+
+`revision` is optional and only valid for the `huggingFace` embedder. Pinning a revision makes your indexing output reproducible even if the upstream model is updated.
+
+### Choose a pooling method with `pooling`
+
+HuggingFace models combine per-token output vectors into a single document vector using a pooling strategy. The `pooling` field controls this behaviour:
+
+| Value | Behaviour |
+|---|---|
+| `"useModel"` | Meilisearch fetches the pooling method from the model configuration. **Default value for new embedders.** |
+| `"forceMean"` | Always use mean pooling. |
+| `"forceCls"` | Always use CLS pooling. |
+
+If in doubt, use `"useModel"`. `"forceMean"` and `"forceCls"` are compatibility options that might be necessary for certain embedders and models.
+
+
+`pooling` is optional for embedders with the `huggingFace` source. It is invalid for all other embedder sources.
+
+
## Update your index settings
Send the embedder configuration to Meilisearch:
diff --git a/capabilities/hybrid_search/how_to/image_search_with_multimodal.mdx b/capabilities/hybrid_search/how_to/image_search_with_multimodal.mdx
index f46d1a770..c2b5274d4 100644
--- a/capabilities/hybrid_search/how_to/image_search_with_multimodal.mdx
+++ b/capabilities/hybrid_search/how_to/image_search_with_multimodal.mdx
@@ -105,7 +105,16 @@ In this example, two modes of search are configured:
Search fragments have access to data present in the query parameters `media` and `q`.
-Each semantic search query for this embedder should match exactly one search fragment of this embedder, so the fragments should each have at least one disambiguating field
+Each semantic search query for this embedder should match exactly one search fragment of this embedder, so the fragments should each have at least one disambiguating field.
+
+
+`media` must match a single search fragment. If `media` matches more than one fragment, or matches no search fragments at all, Meilisearch returns an error.
+
+When you use `media`:
+
+- it is mandatory to specify an embedder (via `hybrid.embedder`).
+- `media` is incompatible with `vector`. Pass one or the other, never both.
+
### Complete embedder configuration
diff --git a/capabilities/hybrid_search/how_to/search_with_user_provided_embeddings.mdx b/capabilities/hybrid_search/how_to/search_with_user_provided_embeddings.mdx
index 42f02cf52..f9eabf323 100644
--- a/capabilities/hybrid_search/how_to/search_with_user_provided_embeddings.mdx
+++ b/capabilities/hybrid_search/how_to/search_with_user_provided_embeddings.mdx
@@ -25,6 +25,75 @@ Next, use [the `/documents` endpoint](/reference/api/documents/list-documents-wi
+## The `_vectors` field in detail
+
+Meilisearch stores pre-computed embeddings under the reserved `_vectors` field of a document. The field is an object whose keys match the names of the embedders configured in your index settings.
+
+### Full object form: `embeddings` + `regenerate`
+
+Each embedder entry accepts two fields, `embeddings` and `regenerate`:
+
+
+
+```json
+{
+ "id": 0,
+ "title": "Kung Fu Panda",
+ "_vectors": {
+ "default": {
+ "embeddings": [0.003, 0.1, 0.75],
+ "regenerate": false
+ }
+ }
+}
+```
+
+
+
+- `embeddings` is optional. It must be an array of numbers representing a single embedding for that document. It may also be an array of arrays of numbers, representing multiple embeddings for the same document. `embeddings` defaults to `null`.
+- `regenerate` is mandatory and must be a boolean. If `regenerate` is `true`, Meilisearch automatically generates embeddings for that document immediately and every time the document is updated. If `regenerate` is `false`, Meilisearch keeps the last value of `embeddings` on document updates and never overwrites it.
+
+
+Use `embeddings` as an array of arrays when a single document should be represented by multiple vectors (for example, one embedding per paragraph, or one per language).
+
+
+### Array shorthand
+
+You may also use an array shorthand to add embeddings to a document:
+
+
+
+```json
+{
+ "_vectors": {
+ "default": [0.003, 0.1, 0.75]
+ }
+}
+```
+
+
+
+Vector embeddings added with the shorthand are not replaced when Meilisearch generates new embeddings. The example above is equivalent to:
+
+
+
+```json
+{
+ "_vectors": {
+ "default": {
+ "embeddings": [0.003, 0.1, 0.75],
+ "regenerate": false
+ }
+ }
+}
+```
+
+
+
+### Null or empty embedder entries
+
+If the key for an embedder inside `_vectors` is empty or `null`, Meilisearch treats the document as not having any embeddings for that embedder. This document is then returned last during AI-powered searches.
+
## Vector search with user-provided embeddings
When using a custom embedder, you must vectorize both your documents and user queries.
@@ -35,6 +104,18 @@ Once you have the query's vector, pass it to the `vector` search parameter to pe
`vector` must be an array of numbers indicating the search vector. You must generate these yourself when using vector search with user-provided embeddings.
+
+`vector` is mandatory when performing searches with `userProvided` embedders. You may also use `vector` with automatic embedders to override an embedder's automatic vector generation, for example to experiment with a custom-generated query vector without changing your embedder configuration.
+
+
`vector` can be used together with [other search parameters](/reference/api/search/search-with-post), including [`filter`](/reference/api/search/search-with-post#body-filter) and [`sort`](/reference/api/search/search-with-post#body-sort):
+
+## Return stored vectors with `retrieveVectors`
+
+Set `retrieveVectors: true` on a search request to return document and query embeddings alongside the search results. When enabled, Meilisearch displays vector data in each document's `_vectors` field.
+
+
+`_vectors` must be included in the index's `displayedAttributes` list for it to be returned in the response. If `displayedAttributes` does not contain `_vectors` (or `*`), `retrieveVectors` has no visible effect on the response payload.
+
diff --git a/capabilities/hybrid_search/overview.mdx b/capabilities/hybrid_search/overview.mdx
index 000a53adb..a4b87816c 100644
--- a/capabilities/hybrid_search/overview.mdx
+++ b/capabilities/hybrid_search/overview.mdx
@@ -39,6 +39,12 @@ Meilisearch handles the entire embedding pipeline for you:
- **Rate limit handling**: Meilisearch automatically retries when providers return rate limit errors, with no configuration needed
- **Document templates**: you control exactly which fields are embedded using [Liquid templates](/capabilities/hybrid_search/advanced/document_template_best_practices), so the embedding captures the most relevant parts of each document
+
+Updating embedder settings may trigger a full reindex. When you partially update an index's embedder settings (for example, changing `model`, `source`, `documentTemplate`, `dimensions`, or `pooling`), Meilisearch may reindex all documents and regenerate their embeddings. For large indexes this can take a long time and, with paid providers, incur significant API costs.
+
+[`distribution`](/capabilities/hybrid_search/advanced/tune_distribution) is a notable exception: changing it does not trigger a reindex.
+
+
### Smart result ranking
When you perform a hybrid search, Meilisearch does not simply concatenate keyword and semantic results. It uses a scoring system that automatically determines, for each query, whether full-text or semantic results are more relevant:
@@ -88,6 +94,24 @@ Meilisearch supports a wide range of embedding providers. Some have native integ
If you pre-compute embeddings externally (for example, for images or audio content), you can supply them directly. See [search with user-provided embeddings](/capabilities/hybrid_search/how_to/search_with_user_provided_embeddings).
+## Embedder field compatibility
+
+Different embedder sources accept different configuration fields. Setting an invalid field for a given source returns an error on settings update.
+
+| Field | `openAi` | `huggingFace` | `ollama` | `rest` | `userProvided` |
+|---|---|---|---|---|---|
+| `url` / `apiKey` | optional | invalid | optional | required (`url`) | invalid |
+| `model` | optional | optional | optional | invalid | invalid |
+| `documentTemplate` / `documentTemplateMaxBytes` | optional | optional | optional | optional | invalid |
+| `dimensions` | optional | optional | optional | optional | **mandatory** |
+| `pooling` | invalid | optional (default `useModel`) | invalid | invalid | invalid |
+| `distribution` | optional | optional | optional | optional | optional |
+| `binaryQuantized` | optional | optional | optional | optional | optional |
+
+
+For `composite` embedders, these rules apply to each sub-embedder with additional constraints. See [composite embedders](/capabilities/hybrid_search/advanced/composite_embedders#sub-embedder-constraints).
+
+
## Next steps
diff --git a/capabilities/indexing/how_to/add_and_update_documents.mdx b/capabilities/indexing/how_to/add_and_update_documents.mdx
index 188f19940..8cf84f99f 100644
--- a/capabilities/indexing/how_to/add_and_update_documents.mdx
+++ b/capabilities/indexing/how_to/add_and_update_documents.mdx
@@ -6,6 +6,9 @@ description: Add new documents, replace existing ones, or partially update speci
import CodeSamplesAddOrReplaceDocuments1 from '/snippets/generated-code-samples/code_samples_add_or_replace_documents_1.mdx';
import CodeSamplesAddOrUpdateDocuments1 from '/snippets/generated-code-samples/code_samples_add_or_update_documents_1.mdx';
import CodeSamplesDeleteOneDocument1 from '/snippets/generated-code-samples/code_samples_delete_one_document_1.mdx';
+import CodeSamplesGetDocumentsPostWithFilter1 from '/snippets/generated-code-samples/code_samples_get_documents_post_with_filter_1.mdx';
+import CodeSamplesAddOrReplaceDocumentsCsv1 from '/snippets/generated-code-samples/code_samples_add_or_replace_documents_csv_1.mdx';
+import CodeSamplesAddOrReplaceDocumentsNdjson1 from '/snippets/generated-code-samples/code_samples_add_or_replace_documents_ndjson_1.mdx';
Meilisearch provides three document operations: add or replace, add or update, and delete. This guide explains the difference between each operation and when to use them.
@@ -70,7 +73,7 @@ The `genres` field is gone because it was not included in the replacement.
## Add or update documents
-Use `PUT /indexes/{index_uid}/documents` to add new documents or partially update existing ones. If a document with the same primary key already exists, Meilisearch **merges the new fields** into the existing document. Fields not included in the update remain unchanged.
+Use `PUT /indexes/{index_uid}/documents` to add new documents or partially update existing ones. If a document with the same primary key already exists, Meilisearch **merges the new fields** into the existing document. Fields not included in the update remain unchanged. Partial updates apply only to top-level fields: updating an object field replaces the entire object, removing any omitted subfields.
@@ -205,6 +208,34 @@ curl \
Meilisearch returns the matching documents in the `results` array. Note that documents are not returned in the order you queried them, and non-existent IDs are silently ignored.
+
+Prefer `POST /indexes/{index_uid}/documents/fetch` over `GET /indexes/{index_uid}/documents`. The GET variant is discouraged unless you have a specific reason to use it (for example, to take advantage of HTTP caching at the proxy or CDN level). The GET route accepts fewer parameters and only supports string filter expressions, while the POST route accepts the richer JSON body used throughout this guide.
+
+
+### Filter the documents you fetch
+
+You can pass a `filter` expression to `POST /indexes/{index_uid}/documents/fetch` to retrieve only documents that match a condition:
+
+
+
+
+Any attribute you reference in a document `filter` must first be declared in the index's [`filterableAttributes`](/capabilities/filtering_sorting_faceting/getting_started) setting. This rule is the same as for search filters and is specific to the documents endpoint when filtering the documents you retrieve or delete.
+
+
+## Supported content types
+
+By default, Meilisearch expects a JSON array in the request body and the `Content-Type: application/json` header. The documents endpoint also accepts NDJSON (`application/x-ndjson`) and CSV (`text/csv`) payloads when you set the matching header.
+
+
+
+
+
+When uploading CSV data, you can override the default comma separator with the `csvDelimiter` query parameter (for example, `?csvDelimiter=;`).
+
+
+`csvDelimiter` is only valid when the request uses `Content-Type: text/csv`. Passing it alongside a JSON or NDJSON payload returns an error.
+
+
## Next steps
diff --git a/capabilities/indexing/how_to/compact_an_index.mdx b/capabilities/indexing/how_to/compact_an_index.mdx
index 4e4037e57..8216dd272 100644
--- a/capabilities/indexing/how_to/compact_an_index.mdx
+++ b/capabilities/indexing/how_to/compact_an_index.mdx
@@ -5,6 +5,10 @@ description: Reclaim disk space by compacting an index's internal data structure
When you add, update, or delete documents, Meilisearch's internal data structures may retain unused space from previous versions of the data. Compaction reclaims this space by reorganizing the index on disk.
+
+Meilisearch Cloud monitors database fragmentation and compacts indexes automatically as needed. Self-hosted users do not benefit from automatic compaction and may need to build a pipeline that periodically compacts indexes to avoid performance degradation.
+
+
## When to compact
- **After bulk deletions**: Removing a large number of documents leaves gaps in the internal storage.
@@ -13,6 +17,17 @@ When you add, update, or delete documents, Meilisearch's internal data structure
You do not need to compact after every operation. It is most useful after large batch changes.
+### Estimating fragmentation
+
+Fragmentation is directly related to the number of indexing operations Meilisearch performs. Common indexing operations include adding and updating documents, as well as changes to index settings.
+
+To estimate your index's fragmentation, query the [`/stats` route](/reference/api/stats) and compare `databaseSize` to `usedDatabaseSize`:
+
+- If the ratio between `databaseSize` and `usedDatabaseSize` is bigger than 30%, compacting your indexes may improve performance.
+- If you update documents in your indexes a few times per day, checking fragmentation and compacting your database once per week is a reasonable baseline.
+
+Tune this cadence based on your own indexing patterns: higher write volumes warrant more frequent compaction, while mostly read-only indexes rarely need it.
+
## Compact an index
Send a `POST` request to `/indexes/{index_uid}/compact`:
diff --git a/capabilities/indexing/how_to/handle_multilingual_data.mdx b/capabilities/indexing/how_to/handle_multilingual_data.mdx
index 52aedb47b..0cb2b1c36 100644
--- a/capabilities/indexing/how_to/handle_multilingual_data.mdx
+++ b/capabilities/indexing/how_to/handle_multilingual_data.mdx
@@ -118,6 +118,10 @@ client.index('INDEX_NAME').search('schiff', { locales: ['deu'] })
This ensures queries are interpreted with the correct tokenizer and normalization rules, avoiding false mismatches.
+
+If the `locales` search parameter and the `localizedAttributes` index setting disagree, `locales` wins. The value passed on the query takes precedence over the index-level configuration for that request. This is useful when a user explicitly picks a language in your UI (for example, a locale switcher), but it also means a mis-specified `locales` value can override your carefully tuned index settings for that one search.
+
+
## Monitoring search quality across languages
When serving multiple languages, search quality can vary between them. [Meilisearch Cloud analytics](/capabilities/analytics/overview) can help you identify issues such as high no-result rates or low click-through rates for specific languages, so you can fine-tune settings per language or adjust your indexing strategy.
diff --git a/capabilities/indexing/overview.mdx b/capabilities/indexing/overview.mdx
index 3d78adc5e..b3c4f74f9 100644
--- a/capabilities/indexing/overview.mdx
+++ b/capabilities/indexing/overview.mdx
@@ -50,6 +50,16 @@ Meilisearch includes several endpoints for managing indexes and migrating data:
- **Export**: Transfer data directly from one instance to another over the network, without creating intermediate files. See [Export data to another instance](/capabilities/indexing/how_to/export_data).
- **Compact**: Reclaim disk space by reorganizing an index's internal data structures after bulk updates or deletions. See [Compact an index](/capabilities/indexing/how_to/compact_an_index).
+
+## Settings updates are atomic
+
+When you update multiple settings in a single request, Meilisearch processes them as an atomic operation.
+
+
+If Meilisearch encounters an error when updating any of the settings in a request, it immediately stops processing the request and returns an error message. In this case, the database settings remain unchanged. The returned error message will only address the first error encountered.
+
+
+This means a partial update can never leave your index in an inconsistent state, but it also means you cannot rely on later settings in the request being applied if an earlier one fails. Inspect the task's error details to identify the failing setting, fix it, then resubmit the full settings payload.
## Next steps
diff --git a/capabilities/indexing/tasks_and_batches/async_operations.mdx b/capabilities/indexing/tasks_and_batches/async_operations.mdx
index 185dd262f..dd8ec60f1 100644
--- a/capabilities/indexing/tasks_and_batches/async_operations.mdx
+++ b/capabilities/indexing/tasks_and_batches/async_operations.mdx
@@ -63,7 +63,15 @@ For a comprehensive description of each task object field, consult the [task API
#### Summarized task objects
-When you make an API request for an asynchronous operation, Meilisearch returns a [summarized version](/reference/api/tasks/get-task) of the full `task` object.
+Every `POST` and `PUT` endpoint that triggers an asynchronous operation returns a summarized task object rather than the full task. The summary contains only the fields required to track the new task:
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `taskUid` | integer | Unique identifier of the newly created task. |
+| `indexUid` | string | Index the task targets. `null` for [global tasks](#global-tasks). |
+| `status` | string | Always `enqueued` at this point. |
+| `type` | string | Task type, for example `indexCreation` or `documentAdditionOrUpdate`. |
+| `enqueuedAt` | string (RFC 3339) | Date and time the task entered the queue. |
@@ -79,7 +87,7 @@ When you make an API request for an asynchronous operation, Meilisearch returns
-Use the summarized task's `taskUid` to [track the progress of a task](/reference/api/tasks/get-task).
+Use the summarized task's `taskUid` to [track the progress of a task](/reference/api/tasks/get-task) and retrieve the full task object.
#### Task `status`
@@ -144,13 +152,17 @@ When you make a [request for an asynchronous operation](#which-operations-are-as
3. Meilisearch finishes the task. Status set to `succeeded` if task was successfully processed, or `failed` if there was an error
-**Terminating a Meilisearch instance in the middle of an asynchronous operation is completely safe** and will never adversely affect the database.
+**Terminating a Meilisearch instance in the middle of an asynchronous operation is completely safe** and will never adversely affect the database. Tasks are not canceled when the instance shuts down: any task that was `processing` is reset to `enqueued` on restart, and task handling proceeds as normal once the instance is relaunched.
### Task batches
Meilisearch processes tasks in batches, grouping tasks for the best possible performance. In most cases, batching should be transparent and have no impact on the overall task workflow. Use [the `/batches` route](/reference/api/batches/list-batches) to obtain more information on batches and how they are processing your tasks.
+
+A batch's `uid` is incremented globally across all indexes in an instance, not per-index. If you see a large `batchUid` on a task, that number reflects batches processed across every index combined.
+
+
#### How auto-batching works
Meilisearch automatically groups consecutive compatible tasks into a single batch. For tasks to be batched together, they must meet all of the following conditions:
@@ -165,18 +177,59 @@ Settings update tasks are also batched together when they target the same index.
### Canceling tasks
-You can cancel a task while it is `enqueued` or `processing` by using [the cancel tasks endpoint](/reference/api/tasks/cancel-tasks). Doing so changes a task's `status` to `canceled`.
+Use [the cancel tasks endpoint](/reference/api/tasks/cancel-tasks) to cancel any number of tasks based on their `uid`, `status`, `type`, `indexUid`, or the date at which they were enqueued (`enqueuedAt`) or processed (`startedAt`). Canceling a task changes its `status` to `canceled`.
-
-Tasks are not canceled when you terminate a Meilisearch instance. Meilisearch discards all progress made on `processing` tasks and resets them to `enqueued`. Task handling proceeds as normal once the instance is relaunched.
-
+
+Only `enqueued` and `processing` tasks can be canceled. Calling cancel on finished tasks (`succeeded`, `failed`, or `canceled`) has no effect on their state: the operation still succeeds, but the response reports `canceledTasks: 0`.
+
+
+Task cancellation is itself an asynchronous operation that creates a `taskCancelation` task. Because cancellation is an **atomic transaction**, either all matched tasks are successfully canceled, or none are.
+
+
+**`POST /tasks/cancel` requires at least one filter.** To prevent users from accidentally canceling every enqueued and processing task, Meilisearch rejects unfiltered cancel requests with the [`missing_task_filters`](/reference/errors/error_codes#missing_task_filters) error.
+
+
+You can also cancel `taskCancelation` tasks themselves as long as they are in the `enqueued` or `processing` state. This is possible because `taskCancelation` tasks are processed in **reverse order**: the last one you enqueue is processed first.
+
+#### The `canceledBy` field
+
+Every task object includes a `canceledBy` field:
+
+- If the task was canceled, `canceledBy` contains the `uid` of the `taskCancelation` task that canceled it.
+- If the task was not canceled, `canceledBy` is always `null`.
+
+You can filter tasks by `canceledBy` in [the list tasks endpoint](/reference/api/tasks/list-tasks) to retrieve every task canceled by a given `taskCancelation`.
### Deleting tasks
-[Finished tasks](#task-status) remain visible in [the task list](/reference/api/tasks/list-tasks). To delete them manually, use the [delete tasks route](/reference/api/tasks/delete-tasks).
+[Finished tasks](#task-status) remain visible in [the task list](/reference/api/tasks/list-tasks). To delete them manually, use the [delete tasks route](/reference/api/tasks/delete-tasks). Like cancellation, task deletion is an **atomic transaction**: either all matched tasks are successfully deleted, or none are.
+
+
+Only finished tasks can be deleted. If you call the delete endpoint on `enqueued` or `processing` tasks, the request succeeds but `deletedTasks` is `0`. Cancel those tasks first and then delete them once they reach a finished state.
+
Meilisearch stores up to 1M tasks in the task database. If enqueuing a new task would exceed this limit, Meilisearch automatically tries to delete the oldest 100K finished tasks. If there are no finished tasks in the database, Meilisearch does not delete anything and enqueues the new task as usual.
+### Task `details` by type
+
+Every task object contains a `details` field whose shape depends on the task's `type`. The table below lists the most common task types and the fields their `details` contains.
+
+| Task type | `details` fields |
+| --- | --- |
+| `documentAdditionOrUpdate` | `receivedDocuments`: number of documents in the payload. `indexedDocuments`: number successfully indexed. |
+| `documentDeletion` | `providedIds`: number of document ids received. `originalFilter`: the filter used, if any. `deletedDocuments`: number of documents actually removed. |
+| `indexCreation` / `indexUpdate` | `primaryKey`: the primary key set on the index, or `null`. |
+| `indexDeletion` | `deletedDocuments`: number of documents removed along with the index. |
+| `indexSwap` | `swaps`: array of swapped index pairs. |
+| `settingsUpdate` | A copy of the settings payload applied to the index. |
+| `dumpCreation` | `dumpUid`: identifier of the generated dump. |
+| `taskCancelation` / `taskDeletion` | `matchedTasks`: number of tasks matching the filter. `canceledTasks` or `deletedTasks`: number of tasks actually affected. `originalFilter`: the query string used. |
+| `snapshotCreation` | `details` is always `null` for `snapshotCreation` tasks. |
+
+
+Count fields such as `indexedDocuments`, `deletedDocuments`, `canceledTasks`, and `deletedTasks` are `null` while the task is `enqueued` or `processing`. They receive their final value only once the task reaches a finished state.
+
+
#### Examples
Suppose you add a new document to your instance using the [add documents endpoint](/reference/api/documents/add-or-replace-documents) and receive a `taskUid` in response.
diff --git a/capabilities/indexing/tasks_and_batches/filter_tasks.mdx b/capabilities/indexing/tasks_and_batches/filter_tasks.mdx
index 1aa5a774c..4ea1a579f 100644
--- a/capabilities/indexing/tasks_and_batches/filter_tasks.mdx
+++ b/capabilities/indexing/tasks_and_batches/filter_tasks.mdx
@@ -28,6 +28,46 @@ Use a comma to separate multiple values and fetch both `canceled` and `failed` t
You may filter tasks based on `uid`, `status`, `type`, `indexUid`, `canceledBy`, or date. Consult the [API reference](/reference/api/tasks/list-tasks) for a full list of task filtering parameters.
+## Available filter parameters
+
+All parameters below accept comma-separated values. Filters of different types are combined with a logical `AND`.
+
+| Parameter | Description |
+| --- | --- |
+| `uids` | Filter tasks by their `uid`. Separate multiple `uids` with a comma (`,`). |
+| `batchUids` | Filter tasks by their `batchUid`. Separate multiple `batchUids` with a comma (`,`). |
+| `statuses` | Filter tasks by their `status`: `enqueued`, `processing`, `succeeded`, `failed`, or `canceled`. |
+| `types` | Filter tasks by their `type`, for example `documentAdditionOrUpdate` or `indexDeletion`. |
+| `indexUids` | Filter tasks by their `indexUid`. Case-sensitive. |
+| `canceledBy` | Filter tasks by their `canceledBy` field. Separate multiple task `uids` with a comma (`,`). |
+
+### Date filters
+
+Use the following parameters to filter tasks by one of their timestamp fields. All values must be valid RFC 3339 dates.
+
+| Parameter | Filters tasks whose... |
+| --- | --- |
+| `beforeEnqueuedAt` | `enqueuedAt` is earlier than the provided date |
+| `afterEnqueuedAt` | `enqueuedAt` is later than the provided date |
+| `beforeStartedAt` | `startedAt` is earlier than the provided date |
+| `afterStartedAt` | `startedAt` is later than the provided date |
+| `beforeFinishedAt` | `finishedAt` is earlier than the provided date |
+| `afterFinishedAt` | `finishedAt` is later than the provided date |
+
+
+Date filters are equivalent to `<` or `>` operations. There is currently no way to perform `≤` or `≥` comparisons with a date filter.
+
+
+### Pagination parameters
+
+Combine the filters above with the following parameters to paginate results.
+
+| Parameter | Description |
+| --- | --- |
+| `limit` | Number of tasks to return. Defaults to `20`. |
+| `from` | `uid` of the first task returned. Defaults to the `uid` of the last created task. |
+| `reverse` | If `true`, returns results in reverse order, from oldest to most recent. Defaults to `false`. |
+
## Combining filters
Use the ampersand character (`&`) to combine filters, equivalent to a logical `AND`:
diff --git a/capabilities/indexing/tasks_and_batches/manage_task_database.mdx b/capabilities/indexing/tasks_and_batches/manage_task_database.mdx
index 3b9e43341..7a99ed8ed 100644
--- a/capabilities/indexing/tasks_and_batches/manage_task_database.mdx
+++ b/capabilities/indexing/tasks_and_batches/manage_task_database.mdx
@@ -69,6 +69,22 @@ When the value of `next` is `null`, you have reached the final set of results.
Use `from` and `limit` together with task filtering parameters to navigate filtered task lists.
+## Deleting tasks from the database
+
+Use the [delete tasks endpoint](/reference/api/tasks/delete-tasks) to remove tasks from the database based on `uid`, `status`, `type`, `indexUid`, `canceledBy`, or date.
+
+
+You can only delete **finished** tasks (`succeeded`, `failed`, or `canceled`). `enqueued` and `processing` tasks cannot be deleted: cancel them first using the [cancel tasks endpoint](/reference/api/tasks/cancel-tasks).
+
+
+Task deletion is an atomic transaction: either all matched tasks are successfully deleted, or none are.
+
+## Automatic task cleanup
+
+Meilisearch stores up to **1 million tasks** in the task database. If enqueuing a new task would exceed this limit, Meilisearch automatically attempts to delete the oldest **100,000 finished tasks** to make room.
+
+If there are no finished tasks in the database, Meilisearch does not delete anything and enqueues the new task as usual. This automatic cleanup keeps the task database bounded without interrupting ongoing work.
+
## Next steps
diff --git a/capabilities/indexing/tasks_and_batches/monitor_tasks.mdx b/capabilities/indexing/tasks_and_batches/monitor_tasks.mdx
index 34d16f42a..66f206519 100644
--- a/capabilities/indexing/tasks_and_batches/monitor_tasks.mdx
+++ b/capabilities/indexing/tasks_and_batches/monitor_tasks.mdx
@@ -95,6 +95,44 @@ When `status` changes to `succeeded`, Meilisearch has finished processing your r
If the task `status` changes to `failed`, Meilisearch was not able to fulfill your request. Check the task object's `error` field for more information.
+### Interpreting timestamps and `duration`
+
+A task object includes three timestamp fields, each formatted as an RFC 3339 date. Each field remains `null` until the task reaches the corresponding state.
+
+| Field | Description |
+| --- | --- |
+| `enqueuedAt` | Date and time when the task was first `enqueued`. Always present once the task exists. |
+| `startedAt` | Date and time when the task began `processing`. `null` while the task is still `enqueued`. |
+| `finishedAt` | Date and time when the task finished `processing`, whether it `succeeded`, `failed`, or was `canceled`. `null` until the task reaches a finished state. |
+
+The `duration` field is formatted according to the [ISO 8601 duration format](https://en.wikipedia.org/wiki/ISO_8601#Durations) (for example, `"PT1S"` for one second). It represents the **total elapsed time the task spent in the `processing` state**. Time spent waiting in the queue is not included. `duration` is `null` until the task finishes.
+
+### The `error` object
+
+When a task fails, its `error` field is populated with an object describing what went wrong. For succeeded, enqueued, processing, and canceled tasks, `error` is `null`.
+
+| Field | Description |
+| --- | --- |
+| `message` | Human-readable description of the error. |
+| `code` | The [error code](/reference/errors/error_codes). |
+| `type` | The error type, for example `invalid_request` or `internal`. |
+| `link` | A URL pointing to the relevant section of the documentation. |
+
+Example `error` object returned for a failed document addition:
+
+
+
+```json
+{
+ "message": "Document does not have a `:primaryKey` attribute: `:documentRepresentation`.",
+ "code": "missing_document_id",
+ "type": "invalid_request",
+ "link": "https://docs.meilisearch.com/errors#missing-document-id"
+}
+```
+
+
+
## Track tasks with custom metadata
You can attach a `customMetadata` query parameter to document operations. This metadata string appears in task responses and webhook payloads, making it easier to track which batch of data triggered a specific task.
diff --git a/capabilities/multi_search/overview.mdx b/capabilities/multi_search/overview.mdx
index 8cf1160fa..bac1e9fcb 100644
--- a/capabilities/multi_search/overview.mdx
+++ b/capabilities/multi_search/overview.mdx
@@ -19,6 +19,12 @@ Send an array of search queries to the `/multi-search` endpoint. Each query can
In federated mode, Meilisearch merges and re-ranks results from all indexes using configurable weights, giving you control over which index's results appear higher.
+## Error handling
+
+
+Multi-search requests fail fast. If Meilisearch encounters an error when handling any of the queries in a multi-search request, it immediately stops processing the request and returns an error message. The returned message only addresses the first error encountered, so earlier queries may succeed but no results are returned until every query is valid.
+
+
## Next steps
diff --git a/capabilities/security/how_to/manage_api_keys.mdx b/capabilities/security/how_to/manage_api_keys.mdx
index 3611a64f1..3cdb0382c 100644
--- a/capabilities/security/how_to/manage_api_keys.mdx
+++ b/capabilities/security/how_to/manage_api_keys.mdx
@@ -57,6 +57,10 @@ curl \
+
+For security reasons, we do not recommend creating keys that can perform all actions (`"actions": ["*"]`). Scope each key to the narrowest set of actions it needs so that a compromised key cannot be used to take over your instance.
+
+
### Available actions
Actions define what operations a key can perform:
@@ -135,6 +139,10 @@ curl \
Replace `API_KEY_UID` with the key's `uid` value (not the key itself).
+
+Custom API keys are deterministic: `key` is a SHA256 hash of the `uid` and the master key. To reuse the exact same custom API keys on a new instance, launch that instance with the same master key and recreate each key with the same `uid`. This is useful when restoring from a backup, cloning an environment, or rotating instances without changing client code.
+
+
## Delete an API key
Permanently revoke a key by deleting it. Any requests using this key will be rejected immediately.
diff --git a/config/navigation.json b/config/navigation.json
index c6b20b1d9..9beea3d4f 100644
--- a/config/navigation.json
+++ b/config/navigation.json
@@ -90,6 +90,7 @@
"capabilities/full_text_search/how_to/configure_searchable_attributes",
"capabilities/full_text_search/how_to/configure_stop_words",
"capabilities/full_text_search/how_to/configure_prefix_search",
+ "capabilities/full_text_search/how_to/configure_dictionary",
"capabilities/full_text_search/how_to/highlight_search_results",
"capabilities/full_text_search/how_to/use_matching_strategy",
"capabilities/full_text_search/how_to/configure_search_cutoff",
@@ -142,7 +143,8 @@
"capabilities/hybrid_search/advanced/custom_hybrid_ranking",
"capabilities/hybrid_search/advanced/composite_embedders",
"capabilities/hybrid_search/advanced/binary_quantization",
- "capabilities/hybrid_search/advanced/multiple_embedders"
+ "capabilities/hybrid_search/advanced/multiple_embedders",
+ "capabilities/hybrid_search/advanced/tune_distribution"
]
},
{
@@ -244,6 +246,7 @@
"group": "How to",
"pages": [
"capabilities/filtering_sorting_faceting/how_to/filter_with_facets",
+ "capabilities/filtering_sorting_faceting/how_to/facet_search",
"capabilities/filtering_sorting_faceting/how_to/sort_results",
"capabilities/filtering_sorting_faceting/how_to/filter_and_sort_by_date",
"capabilities/filtering_sorting_faceting/how_to/combine_filters_and_sort",
diff --git a/resources/help/experimental_features_overview.mdx b/resources/help/experimental_features_overview.mdx
index dedd99b99..66407908b 100644
--- a/resources/help/experimental_features_overview.mdx
+++ b/resources/help/experimental_features_overview.mdx
@@ -41,6 +41,10 @@ Some experimental features can be activated via an HTTP call using the [`/experi
Activating or deactivating experimental features this way does not require you to relaunch Meilisearch.
+
+The **logs** and **metrics** experimental features are not available on Meilisearch Cloud. Both require controlling the Meilisearch process at launch to enable the `--experimental-enable-logs-route` and `--experimental-enable-metrics` flags, which Cloud users cannot do. Use Cloud's built-in Analytics dashboard for observability instead.
+
+
## Current experimental features
| Name | Description | How to configure |
diff --git a/resources/internals/documents.mdx b/resources/internals/documents.mdx
index 39de74d80..6f48a7ff6 100644
--- a/resources/internals/documents.mdx
+++ b/resources/internals/documents.mdx
@@ -55,6 +55,10 @@ You can modify this behavior using the [update settings endpoint](/reference/api
In the latter case, the field will be completely ignored during search. However, it will still be [stored](/capabilities/full_text_search/how_to/configure_displayed_attributes#data-storing) in the document.
+
+The `fieldDistribution` object returned by the [`/stats` route](/reference/api/stats) is not affected by `searchableAttributes` or `displayedAttributes`. Even if a field is neither displayed nor searchable, it still appears in `fieldDistribution` with its document count. Rely on `displayedAttributes` and permissions, not on stats output, to keep a field name hidden from clients.
+
+
To learn more, refer to our [displayed and searchable attributes guide](/capabilities/full_text_search/how_to/configure_displayed_attributes).
## Primary field
diff --git a/resources/self_hosting/sharding/manage_network.mdx b/resources/self_hosting/sharding/manage_network.mdx
index f6d87fc3f..791a6b72f 100644
--- a/resources/self_hosting/sharding/manage_network.mdx
+++ b/resources/self_hosting/sharding/manage_network.mdx
@@ -135,6 +135,12 @@ Supported `_shard` filter operators:
| `_shard != "shard-a"` | Documents associated to all shards except `shard-a` |
| `_shard IN ["shard-a", "shard-b"]` | Documents associated to both `shard-a` and `shard-b` |
+## Attribute visibility via `/network`
+
+
+If an attribute is not on the `displayedAttributes` list but is present on `sortableAttributes`, its value can become publicly accessible through the `/network` endpoint. Do not enable the `network` feature if you rely on the value of attributes not present in `displayedAttributes` to remain hidden at all times. Either add such attributes to `displayedAttributes` so their exposure is explicit, or remove them from `sortableAttributes` before opting into network search.
+
+
## Private network security
By default, Meilisearch blocks requests to non-global IP addresses. If your instances communicate over a private network, configure the `--experimental-allowed-ip-networks` flag on each instance:
diff --git a/resources/self_hosting/webhooks.mdx b/resources/self_hosting/webhooks.mdx
index 6fb510c04..408681620 100644
--- a/resources/self_hosting/webhooks.mdx
+++ b/resources/self_hosting/webhooks.mdx
@@ -33,6 +33,20 @@ meilisearch --task-webhook-url http://localhost:8000
You may also define the webhook URL with environment variables or in the configuration file with `MEILI_TASK_WEBHOOK_URL`.
+## Limits and constraints
+
+
+You can create up to 20 webhooks per instance via the [`/webhooks` API route](/reference/api/webhooks). Having many webhooks active at the same time may negatively impact performance, so only register the webhooks you actively need.
+
+
+
+The value of `Authorization` headers is redacted in responses from `GET /webhooks` and `GET /webhooks/{uid}`. Do not use the redacted header values returned by Meilisearch when updating a webhook, or the webhook will start sending invalid credentials to your endpoint. Store the original secret on your side and resend it explicitly whenever you patch the webhook.
+
+
+
+Meilisearch Cloud may create internal webhooks to support features such as Analytics and monitoring. These Cloud-reserved webhooks are always returned with `isEditable: false` and cannot be updated or deleted through the API.
+
+
## Optional: configure an authorization header and allow requests on private networks
Depending on your setup, you may need to provide an authorization header and allow requests on private networks.
diff --git a/snippets/generated-code-samples/code_samples_add_or_replace_documents_csv_1.mdx b/snippets/generated-code-samples/code_samples_add_or_replace_documents_csv_1.mdx
new file mode 100644
index 000000000..3518849ab
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_add_or_replace_documents_csv_1.mdx
@@ -0,0 +1,9 @@
+
+
+```bash cURL
+curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents' \
+ -H 'Content-Type: text/csv' \
+ --data-binary @movies.csv
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_add_or_replace_documents_ndjson_1.mdx b/snippets/generated-code-samples/code_samples_add_or_replace_documents_ndjson_1.mdx
new file mode 100644
index 000000000..5da914a2b
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_add_or_replace_documents_ndjson_1.mdx
@@ -0,0 +1,9 @@
+
+
+```bash cURL
+curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents' \
+ -H 'Content-Type: application/x-ndjson' \
+ --data-binary @movies.ndjson
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_chat_patch_settings_prompts_1.mdx b/snippets/generated-code-samples/code_samples_chat_patch_settings_prompts_1.mdx
new file mode 100644
index 000000000..10059b227
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_chat_patch_settings_prompts_1.mdx
@@ -0,0 +1,16 @@
+
+
+```bash cURL
+curl \
+ -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
+ -H 'Authorization: Bearer MEILISEARCH_KEY' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "prompts": {
+ "searchDescription": "Search the outdoor gear catalog whenever the user asks about products, prices, availability, or specifications. Do not use it for generic small talk.",
+ "searchQParam": "A short natural-language query capturing the user intent. Rewrite long questions into 3 to 8 keywords. Preserve product names and brand names verbatim.",
+ "searchIndexUidParam": "Choose between: \"products\" for physical gear, accessories, and apparel; \"guides\" for how-to articles and buying guides; \"reviews\" for customer reviews and ratings. Use \"products\" by default."
+ }
+ }'
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_facet_search_exhaustive_1.mdx b/snippets/generated-code-samples/code_samples_facet_search_exhaustive_1.mdx
new file mode 100644
index 000000000..9be22d9c0
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_facet_search_exhaustive_1.mdx
@@ -0,0 +1,13 @@
+
+
+```bash cURL
+curl \
+ -X POST 'MEILISEARCH_URL/indexes/books/facet-search' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "facetName": "genres",
+ "facetQuery": "c",
+ "exhaustiveFacetCount": true
+ }'
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_get_documents_post_with_filter_1.mdx b/snippets/generated-code-samples/code_samples_get_documents_post_with_filter_1.mdx
new file mode 100644
index 000000000..741600411
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_get_documents_post_with_filter_1.mdx
@@ -0,0 +1,11 @@
+
+
+```bash cURL
+curl \
+ -X POST 'MEILISEARCH_URL/indexes/movies/documents/fetch' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "filter": "genres = Action AND rating > 8"
+ }'
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_update_embedders_distribution_1.mdx b/snippets/generated-code-samples/code_samples_update_embedders_distribution_1.mdx
new file mode 100644
index 000000000..a5d22ccf2
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_update_embedders_distribution_1.mdx
@@ -0,0 +1,21 @@
+
+
+```bash cURL
+curl \
+ -X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings' \
+ -H 'Content-Type: application/json' \
+ -H 'Authorization: Bearer MEILISEARCH_KEY' \
+ --data-binary '{
+ "embedders": {
+ "default": {
+ "source": "openAi",
+ "model": "text-embedding-3-small",
+ "distribution": {
+ "mean": 0.7,
+ "sigma": 0.3
+ }
+ }
+ }
+ }'
+```
+
\ No newline at end of file
diff --git a/snippets/generated-code-samples/code_samples_update_faceting_settings_max_values_per_facet_1.mdx b/snippets/generated-code-samples/code_samples_update_faceting_settings_max_values_per_facet_1.mdx
new file mode 100644
index 000000000..97c596a92
--- /dev/null
+++ b/snippets/generated-code-samples/code_samples_update_faceting_settings_max_values_per_facet_1.mdx
@@ -0,0 +1,11 @@
+
+
+```bash cURL
+curl \
+ -X PATCH 'MEILISEARCH_URL/indexes/books/settings/faceting' \
+ -H 'Content-Type: application/json' \
+ --data-binary '{
+ "maxValuesPerFacet": 20
+ }'
+```
+
\ No newline at end of file