# Documentation Source: https://www.meilisearch.com/docs/home Discover our guides, examples, and APIs to build fast and relevant search experiences with Meilisearch. ## Overview Get an overview of Meilisearch features and philosophy. See how Meilisearch compares to alternatives. Use Meilisearch with your favorite language and framework. ## Use case demos Take at look at example applications built with Meilisearch. Search through multiple Eloquent models with Laravel. Browse millions of products in our Nuxt 3 e-commerce demo app. Search across the TMDB movies databases using Next.js. # Which embedder should I choose? Source: https://www.meilisearch.com/docs/learn/ai_powered_search/choose_an_embedder General guidance on how to choose the embedder best suited for projects using AI-powered search. Meilisearch officially supports many different embedders, such as OpenAI, Hugging Face, and Ollama, as well as the majority of embedding generators with a RESTful API. This article contains general guidance on how to choose the embedder best suited for your project. ## When in doubt, choose OpenAI OpenAI returns relevant search results across different subjects and datasets. It is suited for the majority of applications and Meilisearch actively supports and improves OpenAI functionality with every new release. In the majority of cases, and especially if this is your first time working with LLMs and AI-powered search, choose OpenAI. ## If you are already using a specific AI service, choose the REST embedder If you are already using a specific model from a compatible embedder, choose Meilisearch's REST embedder. This ensures you continue building upon tooling and workflows already in place with minimal configuration necessary. ## If dealing with non-textual content, choose the user-provided embedder Meilisearch does not support searching images, audio, or any other content not presented as text. This limitation applies to both queries and documents. For example, Meilisearch's built-in embedder sources cannot search using an image instead of text. They also cannot use text to search for images without attached textual metadata. In these cases, you will have to supply your own embeddings. ## Only choose Hugging Face when self-hosting small static datasets Although it returns very relevant search results, the Hugging Face embedder must run directly in your server. This may lead to lower performance and extra costs when you are hosting Meilisearch in a service like DigitalOcean or AWS. That said, Hugging Face can be a good embedder for datasets under 10k documents that you don't plan to update often. Meilisearch Cloud does not support embedders with `{"source": "huggingFace"}`. To implement Hugging Face embedders in the Cloud, use [HuggingFace inference points with the REST embedder](/guides/embedders/huggingface). # Configure a REST embedder Source: https://www.meilisearch.com/docs/learn/ai_powered_search/configure_rest_embedder Create Meilisearch embedders using any provider with a REST API You can integrate any text embedding generator with Meilisearch if your chosen provider offers a public REST API. The process of integrating a REST embedder with Meilisearch varies depending on the provider and the way it structures its data. This guide shows you where to find the information you need, then walks you through configuring your Meilisearch embedder based on the information you found. ## Find your embedder provider's documentation Each provider requires queries to follow a specific structure. Before beginning to create your embedder, locate your provider's documentation for embedding creation. This should contain the information you need regarding API requests, request headers, and responses. For example, [Mistral's embeddings documentation](https://docs.mistral.ai/api/#tag/embeddings) is part of their API reference. In the case of [Cloudflare's Workers AI](https://developers.cloudflare.com/workers-ai/models/bge-base-en-v1.5/#Parameters), expected input and response are tied to your chosen model. ## Set up the REST source and URL Open your text editor and create an embedder object. Give it a name and set its source to `"rest"`: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest" } } ``` Next, configure the URL Meilisearch should use to contact the embedding provider: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL" } } ``` Setting an embedder name, a `source`, and a `url` is mandatory for all REST embedders. ## Configure the data Meilisearch sends to the provider Meilisearch's `request` field defines the structure of the input it will send to the provider. The way you must fill this field changes for each provider. For example, Mistral expects two mandatory parameters: `model` and `input`. It also accepts one optional parameter: `encoding_format`. Cloudflare instead only expects a single field, `text`. ### Choose a model In many cases, your provider requires you to explicitly set which model you want to use to create your embeddings. For example, in Mistral, `model` must be a string specifying a valid Mistral model. Update your embedder object adding this field and its value: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME" } } } ``` In Cloudflare's case, the model is part of the API route itself and doesn't need to be specified in your `request`. ### The embedding prompt The prompt corresponds to the data that the provider will use to generate your document embeddings. Its specific name changes depending on the provider you chose. In Mistral, this is the `input` field. In Cloudflare, it's called `text`. Most providers accept either a string or an array of strings. A single string will generate one request per document in your database: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": "{{text}}" } } } ``` `{{text}}` indicates Meilisearch should replace the contents of a field with your document data, as indicated in the embedder's [`documentTemplate`](/reference/api/settings#documenttemplate). An array of strings allows Meilisearch to send up to 10 documents in one request, reducing the number of API calls to the provider: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": [ "{{text}}", "{{..}}" ] } } } ``` When using array prompts, the first item must be `{{text}}`. If you want to send multiple documents in a single request, the second array item must be `{{..}}`. When using `"{{..}}"`, it must be present in both `request` and `response`. When using other embedding providers, `input` might be called something else, like `text` or `prompt`: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "text": "{{text}}" } } } ``` ### Provide other request fields You may add as many fields to the `request` object as you need. Meilisearch will include them when querying the embeddings provider. For example, Mistral allows you to optionally configure an `encoding_format`. Set it by declaring this field in your embedder's `request`: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": ["{{text}}", "{{..}}"], "encoding_format": "float" } } } ``` ## The embedding response You must indicate where Meilisearch can find the document embeddings in the provider's response. Consult your provider's API documentation, paying attention to where it places the embeddings. Cloudflare's embeddings are located in an array inside `response.result.data`. Describe the full path to the embedding array in your embedder's `response`. The first array item must be `"{{embedding}}"`: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "text": "{{text}}" }, "response": { "result": { "data": ["{{embedding}}"] } } } } ``` If the response contains multiple embeddings, use `"{{..}}"` as its second value: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": [ "{{text}}", "{{..}}" ] }, "response": { "data": [ { "embedding": "{{embedding}}" }, "{{..}}" ] } } } ``` When using `"{{..}}"`, it must be present in both `request` and `response`. It is possible the response contains a single embedding outside of an array. Use `"{{embedding}}"` as its value: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": "{{text}}" }, "response": { "data": { "text": "{{embedding}}" } } } } ``` It is also possible the response is a single item or array not nested in an object: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": [ "{{text}}", "{{..}}" ] }, "response": [ "{{embedding}}", "{{..}}" ] } } ``` The prompt data type does not necessarily match the response data type. For example, Cloudflare always returns an array of embeddings, even if the prompt in your request was a string. Meilisearch silently ignores `response` fields not pointing to an `"{{embedding}}"` value. ## The embedding header Your provider might also request you to add specific headers to your request. For example, Azure's AI services require an `api-key` header containing an API key. Add the `headers` field to your embedder object: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "text": "{{text}}" }, "response": { "result": { "data": ["{{embedding}}"] } }, "headers": { "FIELD_NAME": "FIELD_VALUE" } } } ``` By default, Meilisearch includes a `Content-Type` header. It may also include an authorization bearer token, if you have supplied an API key. ## Configure remainder of the embedder `source`, `request`, `response`, and `header` are the only fields specific to REST embedders. Like other remote embedders, you're likely required to supply an `apiKey`: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": ["{{text}}", "{{..}}"], "encoding_format": "float" }, "response": { "data": [ { "embedding": "{{embedding}}" }, "{{..}}" ] }, "apiKey": "PROVIDER_API_KEY", } } ``` You should also set a `documentTemplate`. Good templates are short and include only highly relevant document data: ```json theme={null} { "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": ["{{text}}", "{{..}}"], "encoding_format": "float" }, "response": { "data": [ { "embedding": "{{embedding}}" }, "{{..}}" ] }, "apiKey": "PROVIDER_API_KEY", "documentTemplate": "SHORT_AND_RELEVANT_DOCUMENT_TEMPLATE" } } ``` ## Update your index settings Now the embedder object is complete, update your index settings: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/embedders' \ -H 'Content-Type: application/json' \ --data-binary '{ "EMBEDDER_NAME": { "source": "rest", "url": "PROVIDER_URL", "request": { "model": "MODEL_NAME", "input": ["{{text}}", "{{..}}"], }, "response": { "data": [ { "embedding": "{{embedding}}" }, "{{..}}" ] }, "apiKey": "PROVIDER_API_KEY", "documentTemplate": "SHORT_AND_RELEVANT_DOCUMENT_TEMPLATE" } }' ``` ## Conclusion In this guide you have seen a few examples of how to configure a REST embedder in Meilisearch. Though it used Mistral and Cloudflare, the general steps remain the same for all providers: 1. Find the provider's REST API documentation 2. Identify the embedding creation request parameters 3. Include parameters in your embedder's `request` 4. Identify the embedding creation response 5. Reproduce the path to the returned embeddings in your embedder's `response` 6. Add any required HTTP headers to your embedder's `header` 7. Update your index settings with the new embedder # Differences between full-text and AI-powered search Source: https://www.meilisearch.com/docs/learn/ai_powered_search/difference_full_text_ai_search Meilisearch offers two types of search: full-text search and AI-powered search. This article explains their differences and intended use cases. Meilisearch offers two types of search: full-text search and AI-powered search. This article explains their differences and intended use cases. ## Full-text search This is Meilisearch's default search type. When performing a full-text search, Meilisearch checks the indexed documents for acceptable matches to a set of search terms. It is a fast and reliable search method. For example, when searching for `"pink sandals"`, full-text search will only return clothing items explicitly mentioning these two terms. Searching for `"pink summer shoes for girls"` is likely to return fewer and less relevant results. ## AI-powered search AI-powered search is Meilisearch's newest search method. It returns results based on a query's meaning and context. AI-powered search uses LLM providers such as OpenAI and Hugging Face to generate vector embeddings representing the meaning and context of both query terms and documents. It then compares these vectors to find semantically similar search results. When using AI-powered search, Meilisearch returns both full-text and semantic results by default. This is also called hybrid search. With AI-powered search, searching for `"pink sandals"` will be more efficient, but queries for `"cute pink summer shoes for girls"` will still return relevant results including light-colored open shoes. ## Use cases Full-text search is a reliable choice that works well in most scenarios. It is fast, less resource-intensive, and requires no extra configuration. It is best suited for situations where you need precise matches to a query and your users are familiar with the relevant keywords. AI-powered search combines the flexibility of semantic search with the performance of full-text search. Most searches, whether short and precise or long and vague, will return very relevant search results. In most cases, AI-powered search will offer your users the best search experience, but will require extra configuration. AI-powered search may also entail extra costs if you use a third-party service such as OpenAI to generate vector embeddings. # Document template best practices Source: https://www.meilisearch.com/docs/learn/ai_powered_search/document_template_best_practices This guide shows you what to do and what to avoid when writing a `documentTemplate`. When using AI-powered search, Meilisearch generates prompts by filling in your embedder's `documentTemplate` with each document's data. The better your prompt is, the more relevant your search results. This guide shows you what to do and what to avoid when writing a `documentTemplate`. ## Sample document Take a look at this document from a database of movies: ```json theme={null} { "id": 2, "title": "Ariel", "overview": "Taisto Kasurinen is a Finnish coal miner whose father has just committed suicide and who is framed for a crime he did not commit. In jail, he starts to dream about leaving the country and starting a new life. He escapes from prison but things don't go as planned...", "genres": [ "Drama", "Crime", "Comedy" ], "poster": "https://image.tmdb.org/t/p/w500/ojDg0PGvs6R9xYFodRct2kdI6wC.jpg", "release_date": 593395200 } ``` ## Do not use the default `documentTemplate` 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. ## Only include highly relevant information Take a look at your document and identify the most relevant fields. A good `documentTemplate` for the sample document could be: ``` "A movie called {{doc.title}} about {{doc.overview}}" ``` In the sample document, `poster` and `id` contain data that has little semantic importance and can be safely excluded. The data in `genres` and `release_date` is very useful for filters, but say little about this specific film. This leaves two relevant fields: `title` and `overview`. ## Keep prompts short For the best results, keep prompts somewhere between 15 and 45 words: ``` "A movie called {{doc.title}} about {{doc.overview | truncatewords: 20}}" ``` In the sample document, the `overview` alone is 49 words. Use Liquid's [`truncate`](https://shopify.github.io/liquid/filters/truncate/) or [`truncatewords`](https://shopify.github.io/liquid/filters/truncatewords/) to shorten it. Short prompts do not have enough information for the embedder to properly understand the query context. Long prompts instead provide too much information and make it hard for the embedder to identify what is truly relevant about a document. ## Add guards for missing fields Some documents might not contain all the fields you expect. If your template directly references a missing field, Meilisearch will throw an error when indexing documents. To prevent this, use Liquid’s `if` statements to add guards around fields: ``` {% if doc.title %} A movie called {{ doc.title }} {% endif %} ``` This ensures the template only tries to include data that already exists in a document. If a field is missing, the embedder still receives a valid and useful prompt without errors. ## Conclusion In this article you saw the main steps to generating prompts that lead to relevant AI-powered search results: * Do not use the default `documentTemplate` * Only include relevant data * Truncate long fields * Add guards for missing fields # Getting started with AI-powered search Source: https://www.meilisearch.com/docs/learn/ai_powered_search/getting_started_with_ai_search AI-powered search uses LLMs to retrieve search results. This tutorial shows you how to configure an OpenAI embedder and perform your first search. [AI-powered search](https://meilisearch.com/solutions/vector-search), sometimes also called vector search or hybrid search, uses [large language models (LLMs)](https://en.wikipedia.org/wiki/Large_language_model) to retrieve search results based on the meaning and context of a query. This tutorial will walk you through configuring AI-powered search in your Meilisearch project. You will see how to set up an embedder with OpenAI, generate document embeddings, and perform your first search. ## Requirements * A running Meilisearch project * An [OpenAI API key](https://platform.openai.com/api-keys) * A command-line console ## Create a new index First, create a new Meilisearch project. If this is your first time using Meilisearch, follow the [quick start](/learn/getting_started/cloud_quick_start) then come back to this tutorial. Next, create a `kitchenware` index and add [this kitchenware products dataset](/assets/datasets/kitchenware.json) to it. It will take Meilisearch a few moments to process your request, but you can continue to the next step while your data is indexing. ## Generate embeddings with OpenAI In this step, you will configure an OpenAI embedder. Meilisearch uses **embedders** to translate documents into **embeddings**, which are mathematical representations of a document's meaning and context. Open a blank file in your text editor. You will only use this file to build your embedder one step at a time, so there's no need to save it if you plan to finish the tutorial in one sitting. ### Choose an embedder name In your blank file, create your `embedder` object: ```json theme={null} { "products-openai": {} } ``` `products-openai` is the name of your embedder for this tutorial. You can name embedders any way you want, but try to keep it simple, short, and easy to remember. ### Choose an embedder source Meilisearch relies on third-party services to generate embeddings. These services are often referred to as the embedder source. Add a new `source` field to your embedder object: ```json theme={null} { "products-openai": { "source": "openAi" } } ``` Meilisearch supports several embedder sources. This tutorial uses OpenAI because it is a good option that fits most use cases. ### Choose an embedder model Models supply the information required for embedders to process your documents. Add a new `model` field to your embedder object: ```json theme={null} { "products-openai": { "source": "openAi", "model": "text-embedding-3-small" } } ``` Each embedder service supports different models targeting specific use cases. `text-embedding-3-small` is a cost-effective model for general usage. ### Create your API key Log into OpenAI, or create an account if this is your first time using it. Generate a new API key using [OpenAI's web interface](https://platform.openai.com/api-keys). Add the `apiKey` field to your embedder: ```json theme={null} { "products-openai": { "source": "openAi", "model": "text-embedding-3-small", "apiKey": "OPEN_AI_API_KEY", } } ``` Replace `OPEN_AI_API_KEY` with your own API key. You may use any key tier for this tutorial. Use at least [Tier 2 keys](https://platform.openai.com/docs/guides/rate-limits/usage-tiers?context=tier-two) in production environments. ### Design a prompt template Meilisearch embedders only accept textual input, but documents can be complex objects containing different types of data. This means you must convert your documents into a single text field. Meilisearch uses [Liquid](https://shopify.github.io/liquid/basics/introduction/), an open-source templating language to help you do that. A good template should be short and only include the most important information about a document. Add the following `documentTemplate` to your embedder: ```json theme={null} { "products-openai": { "source": "openAi", "model": "text-embedding-3-small", "apiKey": "OPEN_AI_API_KEY", "documentTemplate": "An object used in a kitchen named '{{doc.name}}'" } } ``` This template starts by giving the general context of the document: `An object used in a kitchen`. Then it adds the information that is specific to each document: `doc` represents your document, and you can access any of its attributes using dot notation. `name` is an attribute with values such as `wooden spoon` or `rolling pin`. Since it is present in all documents in this dataset and describes the product in few words, it is a good choice to include in the template. ### Create the embedder Your embedder object is ready. Send it to Meilisearch by updating your index settings: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/kitchenware/settings/embedders' \ -H 'Content-Type: application/json' \ --data-binary '{ "products-openai": { "source": "openAi", "apiKey": "OPEN_AI_API_KEY", "model": "text-embedding-3-small", "documentTemplate": "An object used in a kitchen named '{{doc.name}}'" } }' ``` Replace `MEILISEARCH_URL` with the address of your Meilisearch project, and `OPEN_AI_API_KEY` with your [OpenAI API key](https://platform.openai.com/api-keys). Meilisearch and OpenAI will start processing your documents and updating your index. This may take a few moments, but once it's done you are ready to perform an AI-powered search. ## Perform an AI-powered search AI-powered searches are very similar to basic text searches. You must query the `/search` endpoint with a request containing both the `q` and the `hybrid` parameters: ```sh theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/kitchenware/search' \ -H 'content-type: application/json' \ --data-binary '{ "q": "kitchen utensils made of wood", "hybrid": { "embedder": "products-openai" } }' ``` For this tutorial, `hybrid` is an object with a single `embedder` field. Meilisearch will then return an equal mix of semantic and full-text matches. ## Conclusion Congratulations! You have created an index, added a small dataset to it, and activated AI-powered search. You then used OpenAI to generate embeddings out of your documents, and performed your first AI-powered search. ## Next steps Now you have a basic overview of the basic steps required for setting up and performing AI-powered searches, you might want to try and implement this feature in your own application. For practical information on implementing AI-powered search with other services, consult our [guides section](/guides/embedders/openai). There you will find specific instructions for embedders such as [LangChain](/guides/langchain) and [Cloudflare](/guides/embedders/cloudflare). For more in-depth information, consult the API reference for [embedder settings](/reference/api/settings#embedders) and [the `hybrid` search parameter](/reference/api/search#hybrid-search). # Image search with multimodal embeddings Source: https://www.meilisearch.com/docs/learn/ai_powered_search/image_search_with_multimodal_embeddings This article shows you the main steps for performing multimodal text-to-image searches This guide shows the main steps to search through a database of images using Meilisearch's experimental multimodal embeddings. ## Requirements * A database of images * A Meilisearch project * Access to a multimodal embedding provider (for example, [VoyageAI multimodal embeddings](https://docs.voyageai.com/reference/multimodal-embeddings-api)) ## Enable multimodal embeddings First, enable the `multimodal` experimental feature: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/experimental-features/' \ -H 'Content-Type: application/json' \ --data-binary '{ "multimodal": true }' ``` You may also enable multimodal in your Meilisearch Cloud project's general settings, under "Experimental features". ## Configure a multimodal embedder Much like other embedders, multimodal embedders must set their `source` to `rest` and explicitly declare their `url`. Depending on your chosen provider, you may also have to specify `apiKey`. All multimodal embedders must contain an `indexingFragments` field and a `searchFragments` field. Fragments are sets of embeddings built out of specific parts of document data. Fragments must follow the structure defined by the REST API of your chosen provider. ### `indexingFragments` Use `indexingFragments` to tell Meilisearch how to send document data to the provider's API when generating document embeddings. For example, when using VoyageAI's multimodal model, an indexing fragment might look like this: ```json theme={null} "indexingFragments": { "TEXTUAL_FRAGMENT_NAME": { "value": { "content": [ { "type": "text", "text": "A document named {{doc.title}} described as {{doc.description}}" } ] } }, "IMAGE_FRAGMENT_NAME": { "value": { "content": [ { "type": "image_url", "image_url": "{{doc.poster_url}}" } ] } } } ``` The example above requests Meilisearch to create two sets of embeddings during indexing: one for the textual description of an image, and another for the actual image. Any JSON string value appearing in a fragment is handled as a Liquid template, where you interpolate document data present in `doc`. In `IMAGE_FRAGMENT_NAME`, that's `image_url` which outputs the plain URL string in the document field `poster_url`. In `TEXT_FRAGMENT_NAME`, `text` contains a longer string contextualizing two document fields, `title` and `description`. ### `searchFragments` Use `searchFragments` to tell Meilisearch how to send search query data to the chosen provider's REST API when converting them into embeddings: ```json theme={null} "searchFragments": { "USER_TEXT_FRAGMENT": { "value": { "content": [ { "type": "text", "text": "{{q}}" } ] } }, "USER_SUBMITTED_IMAGE_FRAGMENT": { "value": { "content": [ { "type": "image_base64", "image_base64": "data:{{media.image.mime}};base64,{{media.image.data}}" } ] } } } ``` In this example, two modes of search are configured: 1. A textual search based on the `q` parameter, which will be embedded as text 2. An image search based on [data url](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data) rebuilt from the `image.mime` and `image.data` field in the `media` field of the query 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 ### Complete embedder configuration Your embedder should look similar to this example with all fragments and embedding provider data: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings' \ -H 'Content-Type: application/json' \ --data-binary '{ "embedders": { "MULTIMODAL_EMBEDDER_NAME": { "source": "rest", "url": "https://api.voyageai.com/v1/multimodal-embeddings", "apiKey": "VOYAGE_API_KEY", "indexingFragments": { "TEXTUAL_FRAGMENT_NAME": { "value": { "content": [ { "type": "text", "text": "A document named {{doc.title}} described as {{doc.description}}" } ] } }, "IMAGE_FRAGMENT_NAME": { "value": { "content": [ { "type": "image_url", "image_url": "{{doc.poster_url}}" } ] } } }, "searchFragments": { "USER_TEXT_FRAGMENT": { "value": { "content": [ { "type": "text", "text": "{{q}}" } ] } }, "USER_SUBMITTED_IMAGE_FRAGMENT": { "value": { "content": [ { "type": "image_base64", "image_base64": "data:{{media.image.mime}};base64,{{media.image.data}}" } ] } } } } } }' ``` ## Add documents Once your embedder is configured, you can [add documents to your index](/learn/getting_started/cloud_quick_start) with the [`/documents` endpoint](/reference/api/documents). During indexing, Meilisearch will automatically generate multimodal embeddings for each document using the configured `indexingFragments`. ## Perform searches The final step is to perform searches using different types of content. ### Use text to search for images Use the following search query to retrieve a mix of documents with images matching the description, documents with and documents containing the specified keywords: ```sh theme={null} curl -X POST 'http://localhost:7700/indexes/INDEX_NAME/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "a mountain sunset with snow", "hybrid": { "embedder": "MULTIMODAL_EMBEDDER_NAME" } }' ``` ### Use an image to search for images You can also use an image to search for other, similar images: ```sh theme={null} curl -X POST 'http://localhost:7700/indexes/INDEX_NAME/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "media": { "image": { "mime": "image/jpeg", "data": "" } }, "hybrid": { "embedder": "MULTIMODAL_EMBEDDER_NAME" } }' ``` In most cases you will need a GUI interface that allows users to submit their images and converts these images to Base64 format. Creating this is outside the scope of this guide. ## Conclusion With multimodal embedders you can: 1. Configure Meilisearch to embed both images and queries 2. Add image documents — Meilisearch automatically generates embeddings 3. Accept text or image input from users 4. Run hybrid searches using a mix of textual and input from other types of media, or run pure semantic semantic searches using only non-textual input # Image search with user-provided embeddings Source: https://www.meilisearch.com/docs/learn/ai_powered_search/image_search_with_user_provided_embeddings This article shows you the main steps for performing multimodal text-to-image searches This article shows you the main steps for performing multimodal searches where you can use text to search through a database of images with no associated metadata. ## Requirements * A database of images * A Meilisearch project * An embedding generation provider you can install locally ## Configure your local embedding generation pipeline First, set up a system that sends your images to your chosen embedding generation provider, then integrates the returned embeddings into your dataset. The exact procedure depends heavily on your specific setup, but should include these main steps: 1. Choose a provider you can run locally 2. Choose a model that supports both image and text input 3. Send your images to the embedding generation provider 4. Add the returned embeddings to the `_vector` field for each image in your database In most cases your system should run these steps periodically or whenever you update your database. ## Configure a user-provided embedder Configure the `embedder` index setting, settings its source to `userProvided`: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/movies/settings' \ -H 'Content-Type: application/json' \ --data-binary '{ "embedders": { "EMBEDDER_NAME": { "source": "userProvided", "dimensions": MODEL_DIMENSIONS } } }' ``` Replace `EMBEDDER_NAME` with the name you wish to give your embedder. Replace `MODEL_DIMENSIONS` with the number of dimensions of your chosen model. ## Add documents to Meilisearch Next, use [the `/documents` endpoint](/reference/api/documents) to upload the vectorized images. In most cases, you should automate this step so Meilisearch is up to date with your primary database. ## Set up pipeline for vectorizing queries Since you are using a `userProvided` embedder, you must also generate the embeddings for the search query. This process should be similar to generating embeddings for your images: 1. Receive user query from your front-end 2. Send query to your local embedding generation provider 3. Perform search using the returned query embedding ## Vector search with user-provided embeddings Once you have the query's vector, pass it to the `vector` search parameter to perform a semantic AI-powered search: ```sh theme={null} curl -X POST -H 'content-type: application/json' \ 'localhost:7700/indexes/products/search' \ --data-binary '{ "vector": VECTORIZED_QUERY, "hybrid": { "embedder": "EMBEDDER_NAME", } }' ``` Replace `VECTORIZED_QUERY` with the embedding generated by your provider and `EMBEDDER_NAME` with your embedder. If your images have any associated metadata, you may perform a hybrid search by including the original `q`: ```sh theme={null} curl -X POST -H 'content-type: application/json' \ 'localhost:7700/indexes/products/search' \ --data-binary '{ "vector": VECTORIZED_QUERY, "hybrid": { "embedder": "EMBEDDER_NAME", } "q": "QUERY", }' ``` ## Conclusion You have seen the main steps for implementing image search with Meilisearch: 1. Prepare a pipeline that converts your images into vectors 2. Index the vectorized images with Meilisearch 3. Prepare a pipeline that converts your users' queries into vectors 4. Perform searches using the converted queries # Retrieve related search results Source: https://www.meilisearch.com/docs/learn/ai_powered_search/retrieve_related_search_results This guide shows you how to use the similar documents endpoint to create an AI-powered movie recommendation workflow. # Retrieve related search results This guide shows you how to use the [similar documents endpoint](/reference/api/similar) to create an AI-powered movie recommendation workflow. First, you will create an embedder and add documents to your index. You will then perform a search, and use the top result's primary key to retrieve similar movies in your database. ## Prerequisites * A running Meilisearch project * A [tier >=2](https://platform.openai.com/docs/guides/rate-limits#usage-tiers) OpenAI API key ## Create a new index Create an index called `movies` and add this `movies.json` dataset to it. If necessary, consult the [getting started](/learn/getting_started/cloud_quick_start) for more instructions on index creation. Each document in the dataset represents a single movie and has the following structure: * `id`: a unique identifier for each document in the database * `title`: the title of the movie * `overview`: a brief summary of the movie's plot * `genres`: an array of genres associated with the movie * `poster`: a URL to the movie's poster image * `release_date`: the release date of the movie, represented as a Unix timestamp ## Configure an embedder Next, use the Cloud UI to configure an OpenAI embedder: Animated image of the Meilisearch Cloud UI showing a user clicking on "add embedder". This opens up a modal window, where the user fills in the name of the embedder, chooses OpenAI as its source. They then select a model, input their API key, and type out a document template. You may also use the `/settings/embedders` API subroute to configure your embedder: Replace `MEILISEARCH_URL`, `MEILISEARCH_API_KEY`, and `OPENAI_API_KEY` with the corresponding values in your application. Meilisearch will start generating the embeddings for all movies in your dataset. Use the returned `taskUid` to [track the progress of this task](/learn/async/asynchronous_operations). Once it is finished, you are ready to start searching. ## Perform a hybrid search With your documents added and all embeddings generated, you can perform a search: This request returns a list of movies. Pick the top result and take note of its primary key in the `id` field. In this case, it's the movie "Batman" with `id` 192. ## Return similar documents Pass "Batman"'s `id` to your index's [`/similar` route](/reference/api/similar), specifying `movies-text` as your embedder: Meilisearch will return a list of the 20 documents most similar to the movie you chose. You may then choose to display some of these similar results to your users, pointing them to other movies that may also interest them. ## Conclusion Congratulations! You have successfully built an AI-powered movie search and recommendation system using Meilisearch by: * Setting up a Meilisearch project and configured it for AI-powered search * Implementing hybrid search combining keyword and semantic search capabilities * Integrating Meilisearch's similarity search for movie recommendations In a real-life application, you would now start integrating this workflow into a front end, like the one in this [official Meilisearch blog post](https://www.meilisearch.com/blog/add-ai-powered-search-to-react). # Use AI-powered search with user-provided embeddings Source: https://www.meilisearch.com/docs/learn/ai_powered_search/search_with_user_provided_embeddings This guide shows how to perform AI-powered searches with user-generated embeddings instead of relying on a third-party tool. This guide shows how to perform AI-powered searches with user-generated embeddings instead of relying on a third-party tool. ## Requirements * A Meilisearch project ## Configure a custom embedder Configure the `embedder` index setting, settings its source to `userProvided`: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/movies/settings' \ -H 'Content-Type: application/json' \ --data-binary '{ "embedders": { "image2text": { "source": "userProvided", "dimensions": 3 } } }' ``` Embedders with `source: userProvided` are incompatible with `documentTemplate` and `documentTemplateMaxBytes`. ## Add documents to Meilisearch Next, use [the `/documents` endpoint](/reference/api/documents?utm_campaign=vector-search\&utm_source=docs\&utm_medium=vector-search-guide) to upload vectorized documents. Place vector data in your documents' `_vectors` field: ```sh theme={null} curl -X POST -H 'content-type: application/json' \ 'localhost:7700/indexes/products/documents' \ --data-binary '[ { "id": 0, "_vectors": {"image2text": [0, 0.8, -0.2]}, "text": "frying pan" }, { "id": 1, "_vectors": {"image2text": [1, -0.2, 0]}, "text": "baking dish" } ]' ``` ## Vector search with user-provided embeddings When using a custom embedder, you must vectorize both your documents and user queries. Once you have the query's vector, pass it to the `vector` search parameter to perform an AI-powered search: ```sh theme={null} curl -X POST -H 'content-type: application/json' \ 'localhost:7700/indexes/products/search' \ --data-binary '{ "vector": [0, 1, 2] }' ``` `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` can be used together with [other search parameters](/reference/api/search?utm_campaign=vector-search\&utm_source=docs\&utm_medium=vector-search-guide), including [`filter`](/reference/api/search#filter) and [`sort`](/reference/api/search#sort): ```sh theme={null} curl -X POST -H 'content-type: application/json' \ 'localhost:7700/indexes/products/search' \ --data-binary '{ "vector": [0, 1, 2], "filter": "price < 10", "sort": ["price:asc"] }' ``` # Bind search analytics events to a user Source: https://www.meilisearch.com/docs/learn/analytics/bind_events_user This guide shows you how to manually differentiate users across search analytics using the X-MS-USER-ID HTTP header. By default, Meilisearch uses IP addresses to identify users and calculate the total user metrics. This guide shows you how to use the `X-MS-USER-ID` HTTP header to manually link analytics events to specific users. This is useful if you're searching from your back end, as all searches would otherwise appear to come from your server's IP address, making it difficult to accurately track the number of individual users. ## Requirements * A Meilisearch Cloud project with analytics and monitoring enabled * A working pipeline for submitting analytics events ## Add `X-MS-USER-ID` to your search query Include the `X-MS-USER-ID` header in your search requests: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/INDEX_NAME/search' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer DEFAULT_SEARCH_API_KEY' \ -H 'X-MS-USER-ID: MEILISEARCH_USER_ID' \ --data-binary '{}' ``` Replace `MEILISEARCH_USER_ID` with any value that uniquely identifies that user. This may be an authenticated user's ID when running searches from your own back end, or a hash of the user's IP address. ## Add `X-MS-USER-ID` to the analytics event Next, submit your analytics event to the analytics endpoint. Send the same header and value in your API call: ```bash cURL theme={null} curl \ -X POST 'https://edge.meilisearch.com/events' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer DEFAULT_SEARCH_API_KEY' \ -H 'X-MS-USER-ID: MEILISEARCH_USER_ID' \ --data-binary '{ "eventType": "click", "eventName": "Search Result Clicked", "indexUid": "products", "objectId": "0", "position": 0 }' ``` ## Conclusion In this guide you have seen how to bind analytics events to specific users by specifying the same HTTP header for both the search request and the analytics event. # Configure search analytics Source: https://www.meilisearch.com/docs/learn/analytics/configure_analytics Meilisearch Cloud offers in-depth search analytics to help you understand how users search in your application. Enable Meilisearch Cloud analytics to help you understand how users search in your application. This guide walks you through activating analytics, updating your project URL, and configuring all data points. ## Requirements You must have a [Meilisearch Cloud](https://meilisearch.com/cloud?utm_campaign=oss\&utm_source=docs\&utm_medium=analytics) account to access search analytics. ## Enable analytics in the project overview Log into your Meilisearch Cloud account and navigate to your project's overview. Find the "Analytics and monitoring" section and click on the "Enable analytics and monitoring" button: The analytics section of the project overview. It shows one button, 'Enable analytics', and a short explanation of the feature. Meilisearch Cloud will begin processing your request. The "Analytics and monitoring" section will update when the feature is enabled. Activating analytics will automatically activate [monitoring](/learn/analytics/configure_monitoring). ## Update URLs in your application When you enable analytics, Meilisearch Cloud changes your project's API URL. Meilisearch Cloud is only able to track metrics for queries sent to this URL. Update your application so all API requests point to the new URL: ```sh theme={null} curl \ -X POST 'https://edge.meilisearch.com/indexes/products/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "green socks" }' ``` **The previous API URL will remain functional**, but requests targeting it will not send any data to the analytics interface. If you created any custom API keys using the previous URL, you will need to replace them. ## Configure click-through rate and average click position To track metrics like click-through rate and average click position, Meilisearch Cloud needs to know when users click on search results. Every time a user clicks on a search result, your application must send a `click` event to the `POST` endpoint of Meilisearch Cloud analytics route: ```bash cURL theme={null} curl \ -X POST 'https://edge.meilisearch.com/events' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer DEFAULT_SEARCH_API_KEY' \ --data-binary '{ "eventType": "click", "eventName": "Search Result Clicked", "indexUid": "products", "objectId": "0", "position": 0 }' ``` By default, Meilisearch associates analytics events with the most recent search of the user who triggered them. For more information, consult the [analytics events endpoint reference](/learn/analytics/events_endpoint#the-conversion-event-object). ## Configure conversion rate To track conversion rate, first identify what should count as a conversion for your application. For example, in a web shop, a conversion might be a user finalizing the checkout process. Once you have established what counts as a conversion in your application, configure it to send a `conversion` event to the `POST` endpoint of Meilisearch Cloud analytics route: ```bash cURL theme={null} curl \ -X POST 'https://edge.meilisearch.com/events' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer DEFAULT_SEARCH_API_KEY' --data-binary '{ "eventType": "conversion", "eventName": "Product Added To Cart", "indexUid": "products", "objectId": "0", "position": 0 }' ``` By default, Meilisearch associates analytics events with the most recent search of the user who triggered them. It is not possible to associate multiple `conversion` events with the same search. For more information, consult the [analytics events endpoint reference](/learn/analytics/events_endpoint#the-conversion-event-object). # Configure application monitoring metrics Source: https://www.meilisearch.com/docs/learn/analytics/configure_monitoring Meilisearch Cloud offers in-depth metrics to help monitor your application performance. Enable Meilisearch Cloud monitoring to keep track of application performance and service status. ## Requirements You must have a [Meilisearch Cloud](https://meilisearch.com/cloud?utm_campaign=oss\&utm_source=docs\&utm_medium=monitoring) account to access monitoring metrics. ## Enable monitoring in the project overview Log into your Meilisearch Cloud account and navigate to your project's overview. Find the "Analytics and monitoring" section and click on the "Enable analytics and monitoring" button: The analytics and monitoring section of the project overview. It shows one button, 'Enable analytics and monitoring', and a short explanation of both features. Meilisearch Cloud will begin processing your request. The "Analytics and monitoring" section will update with new instruction text and buttons when the feature is enabled. Activating monitoring will automatically activate [analytics](/learn/analytics/configure_analytics). ## Update URLs in your application When you enable monitoring, Meilisearch Cloud changes your project's API URL. Meilisearch Cloud is only able to track metrics for queries sent to this URL. Update your application so all API requests point to the new URL: ```sh theme={null} curl \ -X POST 'http://edge.meilisearch.com/indexes/products/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "green socks" }' ``` **The previous API URL will remain functional**, but requests targeting it will not send any data to the monitoring interface. # Deactivate search analytics and monitoring Source: https://www.meilisearch.com/docs/learn/analytics/deactivate_analytics_monitoring Meilisearch Cloud offers in-depth search analytics to help you understand how users search in your application. This guide shows you how to deactivate Meilisearch Cloud's search analytics and monitoring. ## Disable analytics and monitoring in the project overview Log into your Meilisearch Cloud account and navigate to your project's overview. Find the "Analytics and monitoring" section and press the "Disable analytics and monitoring" button: The analytics section of the project overview. It shows one button, 'Disable analytics and monitoring', and a short explanation of both features. ## Update URLs in your application Disabling analytics and monitoring changes your API URL. Update your application so all API requests point to the correct URL: ```sh theme={null} curl \ -X POST 'https://PROJECT_URL/indexes/products/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "green socks" }' ``` **The previous API URL will remain functional**, but Meilisearch recommends not using it after disabling analytics in your project. If you created any custom API keys using the previous URL, you will need to replace them. ## Update `conversion` and `click` events If you were tracking `conversion` and `click` events, update your application to stop sending them to Meilisearch Cloud. # Analytics events endpoint Source: https://www.meilisearch.com/docs/learn/analytics/events_endpoint This reference describes /events, the endpoint you should use to submit analytics events to Meilisearch Cloud. It also describes the accepted event objects and the data you must include in them. This reference describes `/events`, the endpoint you should use to submit analytics events to Meilisearch Cloud. It also describes the accepted event objects and the data you must include in them. ## The `/events` endpoint The `/events` endpoint is only available for Meilisearch Cloud projects with analytics and monitoring activated. ### Send an event ```http theme={null} POST https://edge.meilisearch.com/events ``` Send an analytics event to Meilisearch Cloud. Accepts [`click`](#the-click-event-object) and [`conversion`](#the-conversion-event-object) events. By default, Meilisearch associates analytics events with the most recent search of the user who triggered them. Include the same `X-MS-USER-ID` header in your search and event requests to manually [bind analytics events to a user](/learn/analytics/bind_events_user). #### Example ##### Response: `201 Created` ### The `click` event object The `click` event must deliver an object with the following fields: ```json theme={null} { "eventType": "click", "eventName": "Search Result Clicked", "indexUid": "products", "objectId": "0", "position": 0 } ``` * `eventType`: a string indicating this is a `click` event * `eventName`: a string describing the event * `indexUid`: a string indicating the clicked document's index * `objectId`: a string indicating the clicked document's primary key * `position`: an integer indicating the clicked document's position in the list of search results ### The `conversion` event object The `conversion` event must deliver an object with the following fields: ```json theme={null} { "eventType": "conversion", "eventName": "Product Added To Cart", "indexUid": "products", "objectID": "0", "position": 0 } ``` * `eventType`: indicates this is a `conversion` event * `eventName`: a string describing the event * `indexUid`: the document's index * `objectID`: the document's primary key * `position`: the document's position in the list of search results # Tasks and asynchronous operations Source: https://www.meilisearch.com/docs/learn/async/asynchronous_operations Meilisearch uses a task queue to handle asynchronous operations. This in-depth guide explains tasks, their uses, and how to manage them using Meilisearch's API. Many operations in Meilisearch are processed **asynchronously**. These API requests are not handled immediately—instead, Meilisearch places them in a queue and processes them in the order they were received. ## Which operations are asynchronous? Every operation that might take a long time to be processed is handled asynchronously. Processing operations asynchronously allows Meilisearch to handle resource-intensive tasks without impacting search performance. Currently, these are Meilisearch's asynchronous operations: * Creating an index * Updating an index * Swapping indexes * Deleting an index * Updating index settings * Adding documents to an index * Updating documents in an index * Deleting documents from an index * Canceling a task * Deleting a task * Creating a dump * Creating snapshots ## Understanding tasks When an API request triggers an asynchronous process, Meilisearch creates a task and places it in a [task queue](#task-queue). ### Task objects Tasks are objects containing information that allow you to track their progress and troubleshoot problems when things go wrong. A [task object](/reference/api/tasks#task-object) includes data not present in the original request, such as when the request was enqueued, the type of request, and an error code when the task fails: ```json theme={null} { "uid": 1, "indexUid": "movies", "status": "enqueued", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 67493, "indexedDocuments": null }, "error": null, "duration": null, "enqueuedAt": "2021-08-10T14:29:17.000000Z", "startedAt": null, "finishedAt": null } ``` For a comprehensive description of each task object field, consult the [task API reference](/reference/api/tasks). #### Summarized task objects When you make an API request for an asynchronous operation, Meilisearch returns a [summarized version](/reference/api/tasks#summarized-task-object) of the full `task` object. ```json theme={null} { "taskUid": 0, "indexUid": "movies", "status": "enqueued", "type": "indexCreation", "enqueuedAt": "2021-08-11T09:25:53.000000Z" } ``` Use the summarized task's `taskUid` to [track the progress of a task](/reference/api/tasks#get-one-task). #### Task `status` Tasks always contain a field indicating the task's current `status`. This field has one of the following possible values: * **`enqueued`**: the task has been received and will be processed soon * **`processing`**: the task is being processed * **`succeeded`**: the task has been successfully processed * **`failed`**: a failure occurred when processing the task. No changes were made to the database * **`canceled`**: the task was canceled `succeeded`, `failed`, and `canceled` tasks are finished tasks. Meilisearch keeps them in the task database but has finished processing these tasks. It is possible to [configure a webhook](/reference/api/webhooks) to notify external services when a task is finished. `enqueued` and `processing` tasks are unfinished tasks. Meilisearch is either processing them or will do so in the future. #### Global tasks Some task types are not associated with a particular index but apply to the entire instance. These tasks are called global tasks. Global tasks always display `null` for the `indexUid` field. Meilisearch considers the following task types as global: * [`dumpCreation`](/reference/api/tasks#dumpcreation) * [`taskCancelation`](/reference/api/tasks#taskcancelation) * [`taskDeletion`](/reference/api/tasks#taskdeletion) * [`indexSwap`](/reference/api/tasks#indexswap) * [`snapshotCreation`](/reference/api/tasks#snapshotcreation) In a protected instance, your API key must have access to all indexes (`"indexes": [*]`) to view global tasks. ### Task queue After creating a task, Meilisearch places it in a queue. Enqueued tasks are processed one at a time, following the order in which they were requested. When the task queue reaches its limit (about 10GiB), it will throw a `no_space_left_on_device` error. Users will need to delete tasks using the [delete tasks endpoint](/reference/api/tasks#delete-tasks) to continue write operations. #### Task queue priority Meilisearch considers certain tasks high-priority and always places them at the front of the queue. The following types of tasks are always processed as soon as possible: 1. `taskCancelation` 2. `taskDeletion` 3. `snapshotCreation` 4. `dumpCreation` All other tasks are processed in the order they were enqueued. ## Task workflow When you make a [request for an asynchronous operation](#which-operations-are-asynchronous), Meilisearch processes all tasks following the same steps: 1. Meilisearch creates a task, puts it in the task queue, and returns a [summarized `task` object](/learn/async/asynchronous_operations#summarized-task-objects). Task `status` set to `enqueued` 2. When your task reaches the front of the queue, Meilisearch begins working on it. Task `status` set to `processing` 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. ### 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) to obtain more information on batches and how they are processing your tasks. ### 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`. 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. ### Deleting tasks [Finished tasks](#task-status) remain visible in [the task list](/reference/api/tasks#get-tasks). To delete them manually, use the [delete tasks route](/reference/api/tasks#delete-tasks). 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. #### 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. When you query the [get task endpoint](/reference/api/tasks#get-one-task) using this value, you see that it has been `enqueued`: ```json theme={null} { "uid": 1, "indexUid": "movies", "status": "enqueued", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 67493, "indexedDocuments": null }, "error": null, "duration": null, "enqueuedAt": "2021-08-10T14:29:17.000000Z", "startedAt": null, "finishedAt": null } ``` Later, you check the task's progress one more time. It was successfully processed and its `status` changed to `succeeded`: ```json theme={null} { "uid": 1, "indexUid": "movies", "status": "succeeded", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 67493, "indexedDocuments": 67493 }, "error": null, "duration": "PT1S", "enqueuedAt": "2021-08-10T14:29:17.000000Z", "startedAt": "2021-08-10T14:29:18.000000Z", "finishedAt": "2021-08-10T14:29:19.000000Z" } ``` Had the task failed, the response would have included a detailed `error` object: ```json theme={null} { "uid": 1, "indexUid": "movies", "status": "failed", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { "receivedDocuments": 67493, "indexedDocuments": 0 }, "error": { "message": "Document does not have a `:primaryKey` attribute: `:documentRepresentation`.", "code": "internal", "type": "missing_document_id", "link": "https://docs.meilisearch.com/errors#missing-document-id" }, "duration": "PT1S", "enqueuedAt": "2021-08-10T14:29:17.000000Z", "startedAt": "2021-08-10T14:29:18.000000Z", "finishedAt": "2021-08-10T14:29:19.000000Z" } ``` If the task had been [canceled](/reference/api/tasks#cancel-tasks) while it was `enqueued` or `processing`, it would have the `canceled` status and a non-`null` value for the `canceledBy` field. After a task has been [deleted](/reference/api/tasks#delete-tasks), trying to access it returns a [`task_not_found`](/reference/errors/error_codes#task_not_found) error. # Filtering tasks Source: https://www.meilisearch.com/docs/learn/async/filtering_tasks This guide shows you how to use query parameters to filter tasks and obtain a more readable list of asynchronous operations. Querying the [get tasks endpoint](/reference/api/tasks#get-tasks) returns all tasks that have not been deleted. This unfiltered list may be difficult to parse in large projects. This guide shows you how to use query parameters to filter tasks and obtain a more readable list of asynchronous operations. Filtering batches with [the `/batches` route](/reference/api/batches) follows the same rules as filtering tasks. Keep in mind that many `/batches` parameters such as `uids` target the tasks included in batches, instead of the batches themselves. ## Requirements * a command-line terminal * a running Meilisearch project ## Filtering tasks with a single parameter Use the get tasks endpoint to fetch all `canceled` tasks: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks?statuses=failed' ``` ```javascript JS theme={null} client.tasks.getTasks({ statuses: ['failed', 'canceled'] }) ``` ```python Python theme={null} client.get_tasks({'statuses': ['failed', 'canceled']}) ``` ```php PHP theme={null} $client->getTasks((new TasksQuery())->setStatuses(['failed', 'canceled'])); ``` ```java Java theme={null} TasksQuery query = new TasksQuery().setStatuses(new String[] {"failed", "canceled"}); client.getTasks(query); ``` ```ruby Ruby theme={null} client.get_tasks(statuses: ['failed', 'canceled']) ``` ```go Go theme={null} client.GetTasks(&meilisearch.TasksQuery{ Statuses: []meilisearch.TaskStatus{ meilisearch.TaskStatusFailed, meilisearch.TaskStatusCanceled, }, }) ``` ```csharp C# theme={null} await client.GetTasksAsync(new TasksQuery { Statuses = new List { TaskInfoStatus.Failed, TaskInfoStatus.Canceled } }); ``` ```rust Rust theme={null} let mut query = TasksQuery::new(&client); let tasks = query .with_statuses(["failed"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} client.getTasks(params: TasksQuery(statuses: [.failed, .canceled])) { result in switch result { case .success(let taskResult): print(taskResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTasks( params: TasksQuery( statuses: ['failed', 'canceled'], ), ); ``` Use a comma to separate multiple values and fetch both `canceled` and `failed` tasks: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks?statuses=failed,canceled' ``` ```rust Rust theme={null} let mut query = TasksQuery::new(&client); let tasks = query .with_statuses(["failed", "canceled"]) .execute() .await .unwrap(); ``` You may filter tasks based on `uid`, `status`, `type`, `indexUid`, `canceledBy`, or date. Consult the API reference for a full list of task filtering parameters. ## Combining filters Use the ampersand character (`&`) to combine filters, equivalent to a logical `AND`: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks?indexUids=movies&types=documentAdditionOrUpdate,documentDeletion&statuses=processing' ``` ```javascript JS theme={null} client.tasks.getTasks({ indexUids: ['movies'], types: ['documentAdditionOrUpdate','documentDeletion'], statuses: ['processing'] }) ``` ```python Python theme={null} client.get_tasks( { 'indexUids': 'movies', 'types': ['documentAdditionOrUpdate', 'documentDeletion'], 'statuses': ['processing'], } ) ``` ```php PHP theme={null} $client->getTasks( (new TasksQuery()) ->setStatuses(['processing']) ->setUids(['movies']) ->setTypes(['documentAdditionOrUpdate', 'documentDeletion']) ); ``` ```java Java theme={null} TasksQuery query = new TasksQuery() .setStatuses(new String[] {"processing"}) .setTypes(new String[] {"documentAdditionOrUpdate", "documentDeletion"}) .setIndexUids(new String[] {"movies"}); client.getTasks(query); ``` ```ruby Ruby theme={null} client.get_tasks(index_uids: ['movies'], types: ['documentAdditionOrUpdate', 'documentDeletion'], statuses: ['processing']) ``` ```go Go theme={null} client.GetTasks(&meilisearch.TasksQuery{ IndexUIDS: []string{"movie"}, Types: []meilisearch.TaskType{ meilisearch.TaskTypeDocumentAdditionOrUpdate, meilisearch.TaskTypeDocumentDeletion, }, Statuses: []meilisearch.TaskStatus{ meilisearch.TaskStatusProcessing, }, }) ``` ```csharp C# theme={null} var query = new TasksQuery { IndexUids = new List { "movies" }, Types = new List { TaskInfo.DocumentAdditionOrUpdate, TaskInfo.DocumentDeletion }, Statuses = new List { TaskInfoStatus.Processing } }; await client.GetTasksAsync(query); ``` ```rust Rust theme={null} let mut query = TasksQuery::new(&client); let tasks = query .with_index_uids(["movies"]) .with_types(["documentAdditionOrUpdate","documentDeletion"]) .with_statuses(["processing"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} client.getTasks(params: TasksQuery(indexUids: "movies", types: ["documentAdditionOrUpdate", "documentDeletion"], statuses: ["processing"])) { result in switch result { case .success(let taskResult): print(taskResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTasks( params: TasksQuery( indexUids: ['movies'], types: ['documentAdditionOrUpdate', 'documentDeletion'], statuses: ['processing'], ), ); ``` This code sample returns all tasks in the `movies` index that have the type `documentAdditionOrUpdate` or `documentDeletion` and have the `status` of `processing`. **`OR` operations between different filters are not supported.** For example, you cannot view tasks which have a type of `documentAddition` **or** a status of `failed`. # Managing the task database Source: https://www.meilisearch.com/docs/learn/async/paginating_tasks Meilisearch uses a task queue to handle asynchronous operations. This document describes how to navigate long task queues with filters and pagination. By default, Meilisearch returns a list of 20 tasks for each request when you query the [get tasks endpoint](/reference/api/tasks#get-tasks). This guide shows you how to navigate the task list using query parameters. Paginating batches with [the `/batches` route](/reference/api/batches) follows the same rules as paginating tasks. ## Configuring the number of returned tasks Use the `limit` parameter to change the number of returned tasks: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks?limit=2&from=10 ``` ```javascript JS theme={null} client.tasks.getTasks({ limit: 2, from: 10 }) ``` ```python Python theme={null} client.get_tasks({ 'limit': 2, 'from': 10 }) ``` ```php PHP theme={null} $taskQuery = (new TasksQuery())->setLimit(2)->setFrom(10)); $client->getTasks($taskQuery); ``` ```java Java theme={null} TasksQuery query = new TasksQuery() .setLimit(2) .setFrom(10); client.index("movies").getTasks(query); ``` ```ruby Ruby theme={null} client.tasks(limit: 2, from: 10) ``` ```go Go theme={null} client.GetTasks(&meilisearch.TasksQuery{ Limit: 2, From: 10, }); ``` ```csharp C# theme={null} ResourceResults taskResult = await client.GetTasksAsync(new TasksQuery { Limit = 2, From = 10 }); ``` ```rust Rust theme={null} let mut query = TasksSearchQuery::new(&client) .with_limit(2) .with_from(10) .execute() .await .unwrap(); ``` ```swift Swift theme={null} client.getTasks(params: TasksQuery(limit: 2, from: 10)) { result in switch result { case .success(let taskResult): print(taskResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTasks(params: TasksQuery(limit: 2, from: 10)); ``` Meilisearch will return a batch of tasks. Each batch of returned tasks is often called a "page" of tasks, and the size of that page is determined by `limit`: ```json theme={null} { "results": [ … ], "total": 50, "limit": 2, "from": 10, "next": 8 } ``` It is possible none of the returned tasks are the ones you are looking for. In that case, you will need to use the [get all tasks request response](/reference/api/tasks#response) to navigate the results. ## Navigating the task list with `from` and `next` Use the `next` value included in the response to your previous query together with `from` to fetch the next set of results: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks?limit=2&from=8 ``` ```javascript JS theme={null} client.tasks.getTasks({ limit: 2, from: 8 }) ``` ```python Python theme={null} client.get_tasks({ 'limit': 2, 'from': 8 }) ``` ```php PHP theme={null} $taskQuery = (new TasksQuery())->setLimit(2)->setFrom(8)); $client->getTasks($taskQuery); ``` ```java Java theme={null} TasksQuery query = new TasksQuery() .setLimit(2) .setFrom(8); client.index("movies").getTasks(query); ``` ```ruby Ruby theme={null} client.tasks(limit: 2, from: 8) ``` ```go Go theme={null} client.GetTasks(&meilisearch.TasksQuery{ Limit: 2, From: 8, }); ``` ```csharp C# theme={null} ResourceResults taskResult = await client.GetTasksAsync(new TasksQuery { Limit = 2, From = 8 }); ``` ```rust Rust theme={null} let mut query = TasksSearchQuery::new(&client) .with_limit(2) .from(8) .execute() .await .unwrap(); ``` ```swift Swift theme={null} client.getTasks(params: TasksQuery(limit: 2, from: 8)) { result in switch result { case .success(let taskResult): print(taskResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTasks(params: TasksQuery(limit: 2, from: 8)); ``` This will return a new batch of tasks: ```json theme={null} { "results": [ … ], "total": 50, "limit": 2, "from": 8, "next": 6 } ``` 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. # Using task webhooks Source: https://www.meilisearch.com/docs/learn/async/task_webhook Learn how to use webhooks to react to changes in your Meilisearch database. This guide teaches you how to configure a single webhook via instance options to notify a URL when Meilisearch completes a [task](/learn/async/asynchronous_operations). If you are using Meilisearch Cloud or need to configure multiple webhooks, use the [`/webhooks` API route](/reference/api/webhooks) instead. ## Requirements * a command-line console * a self-hosted Meilisearch instance * a server configured to receive `POST` requests with an ndjson payload ## Configure the webhook URL Restart your Meilisearch instance and provide the webhook URL to `--task-webhook-URL`: ```sh theme={null} 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`. ## Optional: configure an authorization header Depending on your setup, you may need to provide an authorization header. Provide it to `task-webhook-authorization-header`: ```sh theme={null} meilisearch --task-webhook-url http://localhost:8000 --task-webhook-authorization-header Bearer aSampleMasterKey ``` ## Test the webhook A common asynchronous operation is adding or updating documents to an index. The following example adds a test document to our `books` index: ```sh theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/documents' \ -H 'Content-Type: application/json' \ --data-binary '[ { "id": 1, "title": "Nuestra parte de noche", "author": "Mariana Enríquez" } ]' ``` When Meilisearch finishes indexing this document, it will send a `POST` request the URL you configured with `--task-webhook-url`. The request body will be one or more task objects in [ndjson](https://github.com/ndjson/ndjson-spec) format: ```ndjson theme={null} {"uid":4,"indexUid":"books","status":"succeeded","type":"documentAdditionOrUpdate","canceledBy":null,"details.receivedDocuments":1,"details.indexedDocuments":1,"duration":"PT0.001192S","enqueuedAt":"2022-08-04T12:28:15.159167Z","startedAt":"2022-08-04T12:28:15.161996Z","finishedAt":"2022-08-04T12:28:15.163188Z"} ``` If Meilisearch has batched multiple tasks, it will only trigger the webhook once all tasks in a batch are finished. In this case, the response payload will include all tasks, each separated by a new line: ```ndjson theme={null} {"uid":4,"indexUid":"books","status":"succeeded","type":"documentAdditionOrUpdate","canceledBy":null,"details.receivedDocuments":1,"details.indexedDocuments":1,"duration":"PT0.001192S","enqueuedAt":"2022-08-04T12:28:15.159167Z","startedAt":"2022-08-04T12:28:15.161996Z","finishedAt":"2022-08-04T12:28:15.163188Z"} {"uid":5,"indexUid":"books","status":"succeeded","type":"documentAdditionOrUpdate","canceledBy":null,"details.receivedDocuments":1,"details.indexedDocuments":1,"duration":"PT0.001192S","enqueuedAt":"2022-08-04T12:28:15.159167Z","startedAt":"2022-08-04T12:28:15.161996Z","finishedAt":"2022-08-04T12:28:15.163188Z"} {"uid":6,"indexUid":"books","status":"succeeded","type":"documentAdditionOrUpdate","canceledBy":null,"details.receivedDocuments":1,"details.indexedDocuments":1,"duration":"PT0.001192S","enqueuedAt":"2022-08-04T12:28:15.159167Z","startedAt":"2022-08-04T12:28:15.161996Z","finishedAt":"2022-08-04T12:28:15.163188Z"} ``` # Working with tasks Source: https://www.meilisearch.com/docs/learn/async/working_with_tasks In this tutorial, you'll use the Meilisearch API to add documents to an index, and then monitor its status. [Many Meilisearch operations are processed asynchronously](/learn/async/asynchronous_operations) in a task. Asynchronous tasks allow you to make resource-intensive changes to your Meilisearch project without any downtime for users. In this tutorial, you'll use the Meilisearch API to add documents to an index, and then monitor its status. ## Requirements * a running Meilisearch project * a command-line console ## Adding a task to the task queue Operations that require indexing, such as adding and updating documents or changing an index's settings, will always generate a task. Start by creating an index, then add a large number of documents to this index: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/movies/documents'\ -H 'Content-Type: application/json' \ --data-binary @movies.json ``` ```javascript JS theme={null} const movies = require('./movies.json') client.index('movies').addDocuments(movies).then((res) => console.log(res)) ``` ```python Python theme={null} import json json_file = open('movies.json', encoding='utf-8') movies = json.load(json_file) client.index('movies').add_documents(movies) ``` ```php PHP theme={null} $moviesJson = file_get_contents('movies.json'); $movies = json_decode($moviesJson); $client->index('movies')->addDocuments($movies); ``` ```java Java theme={null} import com.meilisearch.sdk; import org.json.JSONArray; import java.nio.file.Files; import java.nio.file.Path; Path fileName = Path.of("movies.json"); String moviesJson = Files.readString(fileName); Client client = new Client(new Config("http://localhost:7700", "masterKey")); Index index = client.index("movies"); index.addDocuments(moviesJson); ``` ```ruby Ruby theme={null} require 'json' movies_json = File.read('movies.json') movies = JSON.parse(movies_json) client.index('movies').add_documents(movies) ``` ```go Go theme={null} import ( "encoding/json" "os" ) file, _ := os.ReadFile("movies.json") var movies interface{} json.Unmarshal([]byte(file), &movies) client.Index("movies").AddDocuments(&movies, nil) ``` ```csharp C# theme={null} // Make sure to add this using to your code using System.IO; var jsonDocuments = await File.ReadAllTextAsync("movies.json"); await client.Index("movies").AddDocumentsJsonAsync(jsonDocuments); ``` ```rust Rust theme={null} use meilisearch_sdk::{ indexes::*, client::*, search::*, settings::* }; use serde::{Serialize, Deserialize}; use std::{io::prelude::*, fs::File}; use futures::executor::block_on; fn main() { block_on(async move { let client = Client::new("http://localhost:7700", Some("masterKey")); // reading and parsing the file let mut file = File::open("movies.json") .unwrap(); let mut content = String::new(); file .read_to_string(&mut content) .unwrap(); let movies_docs: Vec = serde_json::from_str(&content) .unwrap(); // adding documents client .index("movies") .add_documents(&movies_docs, None) .await .unwrap(); })} ``` ```swift Swift theme={null} let path = Bundle.main.url(forResource: "movies", withExtension: "json")! let documents: Data = try Data(contentsOf: path) client.index("movies").addDocuments(documents: documents) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} // import 'dart:io'; // import 'dart:convert'; final json = await File('movies.json').readAsString(); await client.index('movies').addDocumentsJson(json); ``` Instead of processing your request immediately, Meilisearch will add it to a queue and return a summarized task object: ```json theme={null} { "taskUid": 0, "indexUid": "movies", "status": "enqueued", "type": "documentAdditionOrUpdate", "enqueuedAt": "2021-08-11T09:25:53.000000Z" } ``` The summarized task object is confirmation your request has been accepted. It also gives you information you can use to monitor the status of your request, such as the `taskUid`. You can add documents to a new Meilisearch Cloud index using the Cloud interface. To get the `taskUid` of this task, visit the "Task" overview and look for a "Document addition or update" task associated with your newly created index. ## Monitoring task status Meilisearch processes tasks in the order they were added to the queue. You can check the status of a task using the Meilisearch Cloud interface or the Meilisearch API. ### Monitoring task status in the Meilisearch Cloud interface Log into your [Meilisearch Cloud](https://meilisearch.com/cloud) account and navigate to your project. Click the "Tasks" link in the project menu: Meilisearch Cloud menu with "Tasks" highlighted This will lead you to the task overview, which shows a list of all batches enqueued, processing, and completed in your project: A table listing multiple Meilisearch Cloud tasks All Meilisearch tasks are processed in batches. When the batch containing your task changes its `status` to `succeeded`, Meilisearch has finished processing your request. If the `status` changes to `failed`, Meilisearch was not able to fulfill your request. Check the object's `error` field for more information. ### Monitoring task status with the Meilisearch API Use the `taskUid` from your request's response to check the status of a task: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks/1' ``` ```javascript JS theme={null} client.tasks.getTask(1) ``` ```python Python theme={null} client.get_task(1) ``` ```php PHP theme={null} $client->getTask(1); ``` ```java Java theme={null} client.getTask(1); ``` ```ruby Ruby theme={null} client.task(1) ``` ```go Go theme={null} client.GetTask(1); ``` ```csharp C# theme={null} TaskInfo task = await client.GetTaskAsync(1); ``` ```rust Rust theme={null} let task: Task = client .get_task(1) .await .unwrap(); ``` ```swift Swift theme={null} client.getTask(taskUid: 1) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTask(1); ``` This will return the full task object: ```json theme={null} { "uid": 4, "indexUid" :"movie", "status": "succeeded", "type": "documentAdditionOrUpdate", "canceledBy": null, "details": { … }, "error": null, "duration": "PT0.001192S", "enqueuedAt": "2022-08-04T12:28:15.159167Z", "startedAt": "2022-08-04T12:28:15.161996Z", "finishedAt": "2022-08-04T12:28:15.163188Z" } ``` If the task is still `enqueued` or `processing`, wait a few moments and query the database once again. You may also [set up a webhook listener](/reference/api/webhooks). When `status` changes to `succeeded`, Meilisearch has finished processing your request. 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. ## Conclusion You have seen what happens when an API request adds a task to the task queue, and how to check the status of a that task. Consult the [task API reference](/reference/api/tasks) and the [asynchronous operations explanation](/learn/async/asynchronous_operations) for more information on how tasks work. # Chat tooling reference Source: https://www.meilisearch.com/docs/learn/chat/chat_tooling_reference An exhaustive reference of special chat tools supported by Meilisearch When creating your conversational search agent, you may be able to extend the model's capabilities with a number of tools. This page lists Meilisearch-specific tools that may improve user experience. This is an experimental feature. Use the Meilisearch Cloud UI or the experimental features endpoint to activate it: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/experimental-features/' \ -H 'Content-Type: application/json' \ --data-binary '{ "chatCompletions": true }' ``` ## Meilisearch chat tools For the best user experience, configure all following tools. 1. **Handle progress updates** by displaying search status to users during streaming 2. **Append conversation messages** as requested to maintain context for future requests 3. **Display source documents** to users for transparency and verification 4. **Use `call_id`** to associate progress updates with their corresponding source results These special tools are handled internally by Meilisearch and are not forwarded to the LLM provider. They serve as a communication mechanism between Meilisearch and your application to provide enhanced user experience features. ### `_meiliSearchProgress` This tool reports real-time progress of internal search operations. When declared, Meilisearch will call this function whenever search operations are performed in the background. **Purpose**: Provides transparency about search operations and reduces perceived latency by showing users what's happening behind the scenes. **Arguments**: * `call_id`: Unique identifier to track the search operation * `function_name`: Name of the internal function being executed (e.g., "\_meiliSearchInIndex") * `function_parameters`: JSON-encoded string containing search parameters like `q` (query) and `index_uid` **Example Response**: ```json theme={null} { "function": { "name": "_meiliSearchProgress", "arguments": "{\"call_id\":\"89939d1f-6857-477c-8ae2-838c7a504e6a\",\"function_name\":\"_meiliSearchInIndex\",\"function_parameters\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"search engine\\\"}\"}" } } ``` ### `_meiliAppendConversationMessage` Since the `/chats/{workspace}/chat/completions` endpoint is stateless, this tool helps maintain conversation context by requesting the client to append internal messages to the conversation history. **Purpose**: Maintains conversation context for better response quality in subsequent requests by preserving tool calls and results. **Arguments**: * `role`: Message author role ("user" or "assistant") * `content`: Message content (for tool results) * `tool_calls`: Array of tool calls made by the assistant * `tool_call_id`: ID of the tool call this message responds to **Example Response**: ```json theme={null} { "function": { "name": "_meiliAppendConversationMessage", "arguments": "{\"role\":\"assistant\",\"tool_calls\":[{\"id\":\"call_ijAdM42bixq9lAF4SiPwkq2b\",\"type\":\"function\",\"function\":{\"name\":\"_meiliSearchInIndex\",\"arguments\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"search engine\\\"}\"}}]}" } } ``` ### `_meiliSearchSources` This tool provides the source documents that were used by the LLM to generate responses, enabling transparency and allowing users to verify information sources. **Purpose**: Shows users which documents were used to generate responses, improving trust and enabling source verification. **Arguments**: * `call_id`: Matches the `call_id` from `_meiliSearchProgress` to associate queries with results * `documents`: JSON object containing the source documents with only displayed attributes **Example Response**: ```json theme={null} { "function": { "name": "_meiliSearchSources", "arguments": "{\"call_id\":\"abc123\",\"documents\":[{\"id\":197302,\"title\":\"The Sacred Science\",\"overview\":\"Diabetes. Prostate cancer...\",\"genres\":[\"Documentary\",\"Adventure\",\"Drama\"]}]}" } } ``` ### Sample OpenAI tool declaration Include these tools in your request's `tools` array to enable enhanced functionality: ```json theme={null} { … "tools": [ { "type": "function", "function": { "name": "_meiliSearchProgress", "description": "Provides information about the current Meilisearch search operation", "parameters": { "type": "object", "properties": { "call_id": { "type": "string", "description": "The call ID to track the sources of the search" }, "function_name": { "type": "string", "description": "The name of the function we are executing" }, "function_parameters": { "type": "string", "description": "The parameters of the function we are executing, encoded in JSON" } }, "required": ["call_id", "function_name", "function_parameters"], "additionalProperties": false }, "strict": true } }, { "type": "function", "function": { "name": "_meiliAppendConversationMessage", "description": "Append a new message to the conversation based on what happened internally", "parameters": { "type": "object", "properties": { "role": { "type": "string", "description": "The role of the messages author, either `role` or `assistant`" }, "content": { "type": "string", "description": "The contents of the `assistant` or `tool` message. Required unless `tool_calls` is specified." }, "tool_calls": { "type": ["array", "null"], "description": "The tool calls generated by the model, such as function calls", "items": { "type": "object", "properties": { "function": { "type": "object", "description": "The function that the model called", "properties": { "name": { "type": "string", "description": "The name of the function to call" }, "arguments": { "type": "string", "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function." } } }, "id": { "type": "string", "description": "The ID of the tool call" }, "type": { "type": "string", "description": "The type of the tool. Currently, only function is supported" } } } }, "tool_call_id": { "type": ["string", "null"], "description": "Tool call that this message is responding to" } }, "required": ["role", "content", "tool_calls", "tool_call_id"], "additionalProperties": false }, "strict": true } }, { "type": "function", "function": { "name": "_meiliSearchSources", "description": "Provides sources of the search", "parameters": { "type": "object", "properties": { "call_id": { "type": "string", "description": "The call ID to track the original search associated to those sources" }, "documents": { "type": "object", "description": "The documents associated with the search (call_id). Only the displayed attributes of the documents are returned" } }, "required": ["call_id", "documents"], "additionalProperties": false }, "strict": true } } ] } ``` # What is conversational search? Source: https://www.meilisearch.com/docs/learn/chat/conversational_search Conversational search allows people to make search queries using natural languages. Conversational search is an AI-powered search feature that allows users to ask questions in everyday language and receive answers based on the information in Meilisearch's indexes. ## When to use conversational vs traditional search Use conversational search when: * Users need easy-to-read answers to specific questions * You are handling informational-dense content, such as knowledge bases * Natural language interaction improves user experience Use traditional search when: * Users need to browse multiple options, such as an ecommerce website * Approximate answers are not acceptable * Your users need very quick responses Conversational search is still in early development. Conversational agents may occasionally hallucinate inaccurate and misleading information, so it is important to closely monitor it in production environments. ## Conversational search user workflow ### Traditional search workflow 1. User enters keywords 2. Meilisearch returns matching documents 3. User reviews results to find answers ### Conversational search workflow 1. User asks a question in natural language 2. Meilisearch retrieves relevant documents 3. AI generates a direct answer based on those documents ## Implementation strategies ### Retrieval Augmented Generation (RAG) In the majority of cases, you should use the [`/chats` route](/reference/api/chats) to build a Retrieval Augmented Generation (RAG) pipeline. RAGs excel when working with unstructured data and emphasise high-quality responses. Meilisearch's chat completions API consolidates RAG creation into a single process: 1. **Query understanding**: automatically transforms questions into search parameters 2. **Hybrid retrieval**: combines keyword and semantic search for better relevancy 3. **Answer generation**: uses your chosen LLM to generate responses 4. **Context management**: maintains conversation history by constantly pushing the full conversation to the dedicated tool Follow the [chat completions tutorial](/learn/chat/getting_started_with_chat) for information on how to implement a RAG with Meilisearch. ### Model Context Protocol (MCP) An alternative method is using a Model Context Protocol (MCP) server. MCPs are designed for broader uses that go beyond answering questions, but can be useful in contexts where having up-to-date data is more important than comprehensive answers. Follow the [dedicated MCP guide](/guides/ai/mcp) if you want to implement it in your application. # Getting started with conversational search Source: https://www.meilisearch.com/docs/learn/chat/getting_started_with_chat Learn how to implement AI-powered conversational search in your application This guide walks you through implementing Meilisearch's chat completions feature to create conversational search experiences in your application. ## Prerequisites Before starting, ensure you have: * A [secure](/learn/security/basic_security) Meilisearch >= v1.15.1 project * An API key from an LLM provider * At least one index with searchable content ## Enable the chat completions feature First, enable the chat completions experimental feature: ```bash curl \ -X PATCH 'http://localhost:7700/experimental-features/' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "chatCompletions": true }' ``` ## Find your chat API key When Meilisearch runs with a master key on an instance created after v1.15.1, it automatically generates a "Default Chat API Key" with `chatCompletions` and `search` permissions on all indexes. Check if you have the key using: ```bash curl http://localhost:7700/keys \ -H "Authorization: Bearer MEILISEARCH_KEY" ``` Look for the key with the description "Default Chat API Key" Use this key when querying the `/chats` endpoint. ### Troubleshooting: Missing default chat API key If your instance does not have a Default Chat API Key, create one manually: ```bash curl \ -X POST 'http://localhost:7700/keys' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "name": "Chat API Key", "description": "API key for chat completions", "actions": ["search", "chatCompletions"], "indexes": ["*"], "expiresAt": null }' ``` ## Configure your indexes for chat Each index that you want to be searchable through chat needs specific configuration: ```bash curl \ -X PATCH 'http://localhost:7700/indexes/movies/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "chat": { "description": "A comprehensive movie database containing titles, descriptions, genres, and release dates to help users find movies", "documentTemplate": "{% for field in fields %}{% if field.is_searchable and field.value != nil %}{{ field.name }}: {{ field.value }}\n{% endif %}{% endfor %}", "documentTemplateMaxBytes": 400, "searchParameters": {} } }' ``` The `description` field helps the LLM understand what data is in the index, improving search relevance. ## Configure a chat completions workspace Create a workspace with your LLM provider settings. Here are examples for different providers: ```bash openAi curl \ -X PATCH 'http://localhost:7700/chats/my-assistant/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "source": "openAi", "apiKey": "sk-abc...", "baseUrl": "https://api.openai.com/v1", "prompts": { "system": "You are a helpful assistant. Answer questions based only on the provided context." } }' ``` ```bash azureOpenAi curl \ -X PATCH 'http://localhost:7700/chats/my-assistant/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "source": "azureOpenAi", "apiKey": "your-azure-key", "baseUrl": "https://your-resource.openai.azure.com", "prompts": { "system": "You are a helpful assistant. Answer questions based only on the provided context." } }' ``` ```bash mistral curl \ -X PATCH 'http://localhost:7700/chats/my-assistant/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "source": "mistral", "apiKey": "your-mistral-key", "prompts": { "system": "You are a helpful assistant. Answer questions based only on the provided context." } }' ``` ```bash gemini curl \ -X PATCH 'http://localhost:7700/chats/my-assistant/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "source": "gemini", "apiKey": "your-gemini-key", "prompts": { "system": "You are a helpful assistant. Answer questions based only on the provided context." } }' ``` ```bash vLlm curl \ -X PATCH 'http://localhost:7700/chats/my-assistant/settings' \ -H 'Authorization: Bearer MEILISEARCH_KEY' \ -H 'Content-Type: application/json' \ --data-binary '{ "source": "vLlm", "baseUrl": "http://localhost:8000", "prompts": { "system": "You are a helpful assistant. Answer questions based only on the provided context." } }' ``` ## Send your first chat completions request Now you can start a conversation. Note the `-N` flag for handling streaming responses: ```bash curl -N \ -X POST 'http://localhost:7700/chats/my-assistant/chat/completions' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ --data-binary '{ "model": "gpt-3.5-turbo", "messages": [ { "role": "user", "content": "What movies do you have about space exploration?" } ], "stream": true, "tools": [ { "type": "function", "function": { "name": "_meiliSearchProgress", "description": "Reports real-time search progress to the user" } }, { "type": "function", "function": { "name": "_meiliSearchSources", "description": "Provides sources and references for the information" } } ] }' ``` Take particular note of the `tools` array. These settings are optional, but greatly improve user experience: * **`_meiliSearchProgress`**: shows users what searches are being performed * **`_meiliSearchSources`**: displays the actual documents used to generate responses ## Build a chat interface using the OpenAI SDK Since Meilisearch's chat endpoint is OpenAI-compatible, you can use the official OpenAI SDK: ```javascript JavaScript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:7700/chats/my-assistant', apiKey: 'YOUR_CHAT_API_KEY', }); const completion = await client.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'What is Meilisearch?' }], stream: true, }); for await (const chunk of completion) { console.log(chunk.choices[0]?.delta?.content || ''); } ``` ```python Python from openai import OpenAI client = OpenAI( base_url="http://localhost:7700/chats/my-assistant", api_key="YOUR_CHAT_API_KEY" ) stream = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "What is Meilisearch?"}], stream=True, ) for chunk in stream: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="") ``` ```typescript TypeScript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:7700/chats/my-assistant', apiKey: 'YOUR_CHAT_API_KEY', }); const stream = await client.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'What is Meilisearch?' }], stream: true, }); for await (const chunk of stream) { const content = chunk.choices[0]?.delta?.content || ''; process.stdout.write(content); } ``` ### Error handling When using the OpenAI SDK with Meilisearch's chat completions endpoint, errors from the streamed responses are natively handled by OpenAI. This means you can use the SDK's built-in error handling mechanisms without additional configuration: ```javascript JavaScript import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'http://localhost:7700/chats/my-assistant', apiKey: 'MEILISEARCH_KEY', }); try { const stream = await client.chat.completions.create({ model: 'gpt-3.5-turbo', messages: [{ role: 'user', content: 'What is Meilisearch?' }], stream: true, }); for await (const chunk of stream) { console.log(chunk.choices[0]?.delta?.content || ''); } } catch (error) { // OpenAI SDK automatically handles streaming errors console.error('Chat completion error:', error); } ``` ```python Python from openai import OpenAI client = OpenAI( base_url="http://localhost:7700/chats/my-assistant", api_key="MEILISEARCH_KEY" ) try: stream = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "What is Meilisearch?"}], stream=True, ) for chunk in stream: if chunk.choices[0].delta.content is not None: print(chunk.choices[0].delta.content, end="") except Exception as error: # OpenAI SDK automatically handles streaming errors print(f"Chat completion error: {error}") ``` ## Troubleshooting ### Common issues and solutions #### Empty reply from server (curl error 52) **Causes:** * Meilisearch not started with a master key * Experimental features not enabled * Missing authentication in requests **Solution:** 1. Restart Meilisearch with a master key: `meilisearch --master-key yourKey` 2. Enable experimental features (see setup instructions above) 3. Include Authorization header in all requests #### "Invalid API key" error **Cause:** Using the wrong type of API key **Solution:** * Use either the master key or the "Default Chat API Key" * Don't use search or admin API keys for chat endpoints * Find your chat key: `curl http://localhost:7700/keys -H "Authorization: Bearer MEILISEARCH_KEY"` #### "Socket connection closed unexpectedly" **Cause:** Usually means the OpenAI API key is missing or invalid in workspace settings **Solution:** 1. Check workspace configuration: ```bash curl http://localhost:7700/chats/my-assistant/settings \ -H "Authorization: Bearer MEILISEARCH_KEY" ``` 2. Update with valid API key: ```bash curl -X PATCH http://localhost:7700/chats/my-assistant/settings \ -H "Authorization: Bearer MEILISEARCH_KEY" \ -H "Content-Type: application/json" \ -d '{"apiKey": "your-valid-api-key"}' ``` #### Chat not searching the database **Cause:** Missing Meilisearch tools in the request **Solution:** * Include `_meiliSearchProgress` and `_meiliSearchSources` tools in your request * Ensure indexes have proper chat descriptions configured #### "stream: false is not supported" error **Cause:** Trying to use non-streaming responses **Solution:** * Always set `"stream": true` in your requests * Non-streaming responses are not yet supported ## Next steps * Explore [advanced chat API features](/reference/api/chats) * Learn about [conversational search concepts](/learn/chat/conversational_search) * Review [security best practices](/learn/security/basic_security) # Configuring index settings Source: https://www.meilisearch.com/docs/learn/configuration/configuring_index_settings This tutorial shows how to check and change an index setting using the Meilisearch Cloud interface. This tutorial will show you how to check and change an index setting using the [Meilisearch Cloud](https://cloud.meilisearch.com/projects/) interface. ## Requirements * an active [Meilisearch Cloud](https://cloud.meilisearch.com/projects/) account * a Meilisearch Cloud project with at least one index ## Accessing a project's index settings Log into your Meilisearch account and navigate to your project. Then, click on "Indexes": The main menu of the project view in the Meilisearch Cloud interface. Menu items include 'Indexes' among other options such as 'Settings' and 'Analytics'. Find the index you want to configure and click on its "Settings" button: A list of indexes in a Meilisearch Cloud project. It shows an index named 'books' along with a few icons and buttons. One of these buttons is 'Settings.' ## Checking a setting's current value Using the menu on the left-hand side, click on "Attributes": The index configuration overview together with a menu with links to pages dedicated to various index settings. The first setting is "Searchable attributes" and lists all attributes in your dataset's documents: The 'Searchable attributes' configuration section showing six attributes. One of them, 'id' is this index's primary key. Clicking on other settings will show you similar interfaces that allow visualizing and editing all Meilisearch index settings. ## Updating a setting All documents include a primary key attribute. In most cases, this attribute does not contain information relevant for searches, so you can improve your application's search by explicitly removing it from the searchable attributes list. Find your primary key, then click on the bin icon: The same 'Searchable attributes' list as before, with the bin-shaped 'delete' icon highlighted. Meilisearch will display a pop-up window asking you to confirm you want to remove the attribute from the searchable attributes list. Click on "Yes, remove attribute": A pop-up window over the index settings interface. It reads: 'Are you sure you want to remove the attribute id?' Below it are two buttons: 'Cancel' and 'Yes, remove attribute'. Most updates to an index's settings will cause Meilisearch to re-index all its data. Wait a few moments until this operation is complete. You are not allowed to update any index settings during this time. Once Meilisearch finishes indexing, the primary key will no longer appear in the searchable attributes list: The same 'Searchable attributes' list as before. It only contains five searchable attributes after removing the primary key. If you deleted the wrong attribute, click on "Add attributes" to add it back to the list. You may also click on "Reset to default", which will bring back the searchable list to its original state when you first added your first document to this index: The same 'Searchable attributes' list as before. Two buttons on its top-right corner are highlighted: 'Reset to default' and 'Add attributes'. ## Conclusion You have used the Meilisearch Cloud interface to check the value of an index setting. This revealed an opportunity to improve your project's performance, so you updated this index setting to make your application better and more responsive. This tutorial used the "Searchable attributes" setting, but the procedure is the same no matter which index setting you are editing. ## What's next If you prefer to access the settings API directly through your console, you can also [configure index settings using the Meilisearch Cloud API](/learn/configuration/configuring_index_settings_api). For a comprehensive reference of all index settings, consult the [settings API reference](/reference/api/settings). # Configuring index settings with the Meilisearch API Source: https://www.meilisearch.com/docs/learn/configuration/configuring_index_settings_api This tutorial shows how to check and change an index setting using the Meilisearch API. This tutorial shows how to check and change an index setting using one of the setting subroutes of the Meilisearch API. If you are Meilisearch Cloud user, you may also [configure index settings using the Meilisearch Cloud interface](/learn/configuration/configuring_index_settings). ## Requirements * a new [Meilisearch Cloud](https://cloud.meilisearch.com/projects/) project or a self-hosted Meilisearch instance with at least one index * a command-line terminal with `curl` installed ## Getting the value of a single index setting Start by checking the value of the searchable attributes index setting. Use the `GET` endpoint of the `/settings/searchable-attributes` subroute, replacing `INDEX_NAME` with your index: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' ``` ```rust Rust theme={null} let searchable_attributes: Vec = index .get_searchable_attributes() .await .unwrap(); ``` Depending on your setup, you might also need to replace `localhost:7700` with the appropriate address and port. You should receive a response immediately: ```json theme={null} [ "*" ] ``` If this is a new index, you should see the default value, \["\*"]. This indicates Meilisearch looks through all document attributes when searching. ## Updating an index setting All documents include a primary key attribute. In most cases, this attribute does not contain any relevant data, so you can improve your application search experience by explicitly removing it from your searchable attributes list. Use the `PUT` endpoint of the `/settings/searchable-attributes` subroute, replacing `INDEX_NAME` with your index and the sample attributes `"title"` and `"overview"` with attributes present in your dataset: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "title", "overview" ]' ``` ```rust Rust theme={null} let task = index .set_searchable_attributes(["title", "overview"]) .await .unwrap(); ``` This time, Meilisearch will not process your request immediately. Instead, you will receive a summarized task object while the search engine works on updating your index setting as soon as it has enough resources: ```json theme={null} { "taskUid": 1, "indexUid": "INDEX_NAME", "status": "enqueued", "type": "settingsUpdate", "enqueuedAt": "2021-08-11T09:25:53.000000Z" } ``` Processing the index setting change might take some time, depending on how many documents you have in your index. Wait a few seconds and use the task object's `taskUid` to monitor the status of your request: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks/TASK_UID' ``` ```rust Rust theme={null} let task_status = index.get_task(&task).await.unwrap(); ``` Meilisearch will respond with a task object: ```json theme={null} { "uid": 1, "indexUid": "INDEX_NAME", "status": "succeeded", "type": "settingsUpdate", … } ``` If `status` is `enqueued` or `processed`, wait a few more moments and check the task status again. If `status` is `failed`, make sure you have used a valid index and attributes, then try again. If task `status` is `succeeded`, you successfully updated your index's searchable attributes. Use the subroute to check the new setting's value: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' ``` ```rust Rust theme={null} let searchable_attributes: Vec = index .get_searchable_attributes() .await .unwrap(); ``` Meilisearch should return an array with the new values: ```json theme={null} [ "title", "overview" ] ``` ## Conclusion You have used the Meilisearch API to check the value of an index setting. This revealed an opportunity to improve your project's performance, so you updated this index setting to make your application better and more responsive. This tutorial used the searchable attributes setting, but the procedure is the same no matter which index setting you are editing. For a comprehensive reference of all index settings, consult the [settings API reference](/reference/api/settings). # Exporting and importing dumps Source: https://www.meilisearch.com/docs/learn/data_backup/dumps Dumps are data backups containing all data related to a Meilisearch instance. They are often useful when migrating to a new Meilisearch release. A [dump](/learn/data_backup/snapshots_vs_dumps#dumps) is a compressed file containing an export of your Meilisearch instance. Use dumps to migrate to new Meilisearch versions. This tutorial shows you how to create and import dumps. Creating a dump is also referred to as exporting it. Launching Meilisearch with a dump is referred to as importing it. ## Creating a dump ### Creating a dump in Meilisearch Cloud **You cannot manually export dumps in Meilisearch Cloud**. To [migrate your project to the most recent Meilisearch release](/learn/update_and_migration/updating), use the Cloud interface: The General settings interface displaying various data fields relating to a Meilisearch Cloud project. One of them reads 'Meilisearch version'. Its value is 'v1.6.2'. Next to the value is a button 'Update to v1.7.0' If you need to create a dump for reasons other than upgrading, contact the support team via the Meilisearch Cloud interface or the [official Meilisearch Discord server](https://discord.meilisearch.com). ### Creating a dump in a self-hosted instance To create a dump, use the [create a dump endpoint](/reference/api/dump#create-a-dump): ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/dumps' ``` ```javascript JS theme={null} client.createDump() ``` ```python Python theme={null} client.create_dump() ``` ```php PHP theme={null} $client->createDump(); ``` ```java Java theme={null} client.createDump(); ``` ```ruby Ruby theme={null} client.create_dump ``` ```go Go theme={null} resp, err := client.CreateDump() ``` ```csharp C# theme={null} await client.CreateDumpAsync(); ``` ```rust Rust theme={null} client .create_dump() .await .unwrap(); ``` ```swift Swift theme={null} client.createDump { result in switch result { case .success(let dumpStatus): print(dumpStatus) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.createDump(); ``` This will return a [summarized task object](/learn/async/asynchronous_operations#summarized-task-objects) that you can use to check the status of your dump. ```json theme={null} { "taskUid": 1, "indexUid": null, "status": "enqueued", "type": "dumpCreation", "enqueuedAt": "2022-06-21T16:10:29.217688Z" } ``` The dump creation process is an asynchronous task that takes time proportional to the size of your database. Replace `1` with the `taskUid` returned by the previous command: ```bash cURL theme={null} curl \ -X GET 'MEILISEARCH_URL/tasks/1' ``` ```javascript JS theme={null} client.tasks.getTask(1) ``` ```python Python theme={null} client.get_task(1) ``` ```php PHP theme={null} $client->getTask(1); ``` ```java Java theme={null} client.getTask(1); ``` ```ruby Ruby theme={null} client.task(1) ``` ```go Go theme={null} client.GetTask(1); ``` ```csharp C# theme={null} TaskInfo task = await client.GetTaskAsync(1); ``` ```rust Rust theme={null} let task: Task = client .get_task(1) .await .unwrap(); ``` ```swift Swift theme={null} client.getTask(taskUid: 1) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.getTask(1); ``` This should return an object with detailed information about the dump operation: ```json theme={null} { "uid": 1, "indexUid": null, "status": "succeeded", "type": "dumpCreation", "canceledBy": null, "details": { "dumpUid": "20220621-161029217" }, "error": null, "duration": "PT0.025872S", "enqueuedAt": "2022-06-21T16:10:29.217688Z", "startedAt": "2022-06-21T16:10:29.218297Z", "finishedAt": "2022-06-21T16:10:29.244169Z" } ``` All indexes of the current instance are exported along with their documents and settings and saved as a single `.dump` file. The dump also includes any tasks registered before Meilisearch starts processing the dump creation task. Once the task `status` changes to `succeeded`, find the dump file in [the dump directory](/learn/self_hosted/configure_meilisearch_at_launch#dump-directory). By default, this folder is named `dumps` and can be found in the same directory where you launched Meilisearch. If a dump file is visible in the file system, the dump process was successfully completed. **Meilisearch will never create a partial dump file**, even if you interrupt an instance while it is generating a dump. Since the `key` field depends on the master key, it is not propagated to dumps. If a malicious user ever gets access to your dumps, they will not have access to your instance's API keys. ## Importing a dump ### Importing a dump in Meilisearch Cloud You can import a dump into Meilisearch when creating a new project, below the plan selector: The project creation interface, with a few inputs fields: project name, region selection, and plan selection. Right below all of these, is a file upload button named 'Import .dump' ### Importing a dump in self-hosted instances Import a dump by launching a Meilisearch instance with the [`--import-dump` configuration option](/learn/self_hosted/configure_meilisearch_at_launch#import-dump): ```bash theme={null} ./meilisearch --import-dump /dumps/20200813-042312213.dump ``` Depending on the size of your dump file, importing it might take a significant amount of time. You will only be able to access Meilisearch and its API once this process is complete. Meilisearch imports all data in the dump file. If you have already added data to your instance, existing indexes with the same `uid` as an index in the dump file will be overwritten. Do not use dumps to migrate from a new Meilisearch version to an older release. Doing so might lead to unexpected behavior. # Exporting and using Snapshots Source: https://www.meilisearch.com/docs/learn/data_backup/snapshots Snapshots are exact copies of Meilisearch databases. They are often useful for periodical backups. A [snapshot](/learn/data_backup/snapshots_vs_dumps#snapshots) is an exact copy of the Meilisearch database. Snapshots are useful as quick backups, but cannot be used to migrate to a new Meilisearch release. This tutorial shows you how to schedule snapshot creation to ensure you always have a recent backup of your instance ready to use. You will also see how to start Meilisearch from this snapshot. Meilisearch Cloud does not support snapshots. ## Scheduling periodic snapshots It is good practice to create regular backups of your Meilisearch data. This ensures that you can recover from critical failures quickly in case your Meilisearch instance becomes compromised. Use the [`--schedule-snapshot` configuration option](/learn/self_hosted/configure_meilisearch_at_launch#schedule-snapshot-creation) to create snapshots at regular time intervals: ```bash theme={null} meilisearch --schedule-snapshot ``` The first snapshot is created on launch. You will find it in the [snapshot directory](/learn/self_hosted/configure_meilisearch_at_launch#snapshot-destination), `/snapshots`. Meilisearch will then create a new snapshot every 24 hours until you terminate your instance. Meilisearch **automatically overwrites** old snapshots during snapshot creation. Only the most recent snapshot will be present in the folder at any given time. In cases where your database is updated several times a day, it might be better to modify the interval between each new snapshot: ```bash theme={null} meilisearch --schedule-snapshot=3600 ``` This instructs Meilisearch to create a new snapshot once every hour. If you need to generate a single snapshot without relaunching your instance, use [the `/snapshots` route](/reference/api/snapshots). ## Starting from a snapshot To import snapshot data into your instance, launch Meilisearch using `--import-snapshot`: ```bash theme={null} meilisearch --import-snapshot mySnapShots/data.ms.snapshot ``` Because snapshots are exact copies of your database, starting a Meilisearch instance from a snapshot is much faster than adding documents manually or starting from a dump. For security reasons, Meilisearch will never overwrite an existing database. By default, Meilisearch will throw an error when importing a snapshot if there is any data in your instance. You can change this behavior by specifying [`--ignore-snapshot-if-db-exists=true`](/learn/self_hosted/configure_meilisearch_at_launch#ignore-dump-if-db-exists). This will cause Meilisearch to launch with the existing database and ignore the dump without throwing an error. # Snapshots and dumps Source: https://www.meilisearch.com/docs/learn/data_backup/snapshots_vs_dumps Meilisearch offers two types of backups: snapshots and dumps. Snapshots are mainly intended as a safeguard, while dumps are useful when migrating Meilisearch. This article explains Meilisearch's two backup methods: snapshots and dumps. ## Snapshots A snapshot is an exact copy of the Meilisearch database, located by default in `./data.ms`. [Use snapshots for quick and efficient backups of your instance](/learn/data_backup/snapshots). The documents in a snapshot are already indexed and ready to go, greatly increasing import speed. However, snapshots are not compatible between different versions of Meilisearch. Snapshots are also significantly bigger than dumps. In short, snapshots are a safeguard: if something goes wrong in an instance, you're able to recover and relaunch your database quickly. You can also schedule periodic snapshot creation. ## Dumps A dump isn't an exact copy of your database like a snapshot. Instead, it is closer to a blueprint which Meilisearch can later use to recreate a whole instance from scratch. Importing a dump requires Meilisearch to re-index all documents. This process uses a significant amount of time and memory proportional to the size of the database. Compared to the snapshots, importing a dump is a slow and inefficient operation. At the same time, dumps are not bound to a specific Meilisearch version. This means dumps are ideal for migrating your data when you upgrade Meilisearch. Use dumps to transfer data from an old Meilisearch version into a more recent release. Do not transfer data from a new release into a legacy Meilisearch version. For example, you can import a dump from Meilisearch v1.2 into v1.6 without any problems. Importing a dump generated in v1.7 into a v1.2 instance, however, can lead to unexpected behavior. ## Snapshots VS dumps Both snapshots and dumps are data backups, but they serve different purposes. Snapshots are highly efficient, but not portable between different versions of Meilisearch. **Use snapshots for periodic data backups.** Dumps are portable between different Meilisearch versions, but not very efficient. **Use dumps when updating to a new Meilisearch release.** # Concatenated and split queries Source: https://www.meilisearch.com/docs/learn/engine/concat When a query contains several terms, Meilisearch looks for both individual terms and their combinations. ## Concatenated queries When your search contains several words, Meilisearch applies a concatenation algorithm to it. When searching for multiple words, a search is also done on the concatenation of those words. When concatenation is done on a search query containing multiple words, it will concatenate the words following each other. Thus, the first and third words will not be concatenated without the second word. ### Example A search on `The news paper` will also search for the following concatenated queries: * `Thenews paper` * `the newspaper` * `Thenewspaper` This concatenation is done on a **maximum of 3 words**. ## Split queries When you do a search, it **applies the splitting algorithm to every word** (*string separated by a space*). This consists of finding the most interesting place to separate the words and to create a parallel search query with this proposition. This is achieved by finding the best frequency of the separate words in the dictionary of all words in the dataset. It will look out that both words have a minimum of interesting results, and not just one of them. Split words are not considered as multiple words in a search query because they must stay next to each other. ### Example On a search on `newspaper`, it will split into `news` and `paper` and not into `new` and `spaper`. A document containing `news` and `paper` separated by other words will not be relevant to the search. # Data types Source: https://www.meilisearch.com/docs/learn/engine/datatypes Learn about how Meilisearch handles different data types: strings, numerical values, booleans, arrays, and objects. This article explains how Meilisearch handles the different types of data in your dataset. **The behavior described here concerns only Meilisearch's internal processes** and can be helpful in understanding how the tokenizer works. Document fields remain unchanged for most practical purposes not related to Meilisearch's inner workings. ## String String is the primary type for indexing data in Meilisearch. It enables to create the content in which to search. Strings are processed as detailed below. String tokenization is the process of **splitting a string into a list of individual terms that are called tokens.** A string is passed to a tokenizer and is then broken into separate string tokens. A token is a **word**. ### Tokenization Tokenization relies on two main processes to identifying words and separating them into tokens: separators and dictionaries. #### Separators Separators are characters that indicate where one word ends and another word begins. In languages using the Latin alphabet, for example, words are usually delimited by white space. In Japanese, word boundaries are more commonly indicated in other ways, such as appending particles like `に` and `で` to the end of a word. There are two kinds of separators in Meilisearch: soft and hard. Hard separators signal a significant context switch such as a new sentence or paragraph. Soft separators only delimit one word from another but do not imply a major change of subject. The list below presents some of the most common separators in languages using the Latin alphabet: * **Soft spaces** (distance: 1): whitespaces, quotes, `'-' | '_' | '\'' | ':' | '/' | '\\' | '@' | '"' | '+' | '~' | '=' | '^' | '*' | '#'` * **Hard spaces** (distance: 8): `'.' | ';' | ',' | '!' | '?' | '(' | ')' | '[' | ']' | '{' | '}'| '|'` For more separators, including those used in other writing systems like Cyrillic and Thai, [consult this exhaustive list](https://docs.rs/charabia/0.8.3/src/charabia/separators.rs.html#16-62). #### Dictionaries For the tokenization process, dictionaries are lists of groups of characters which should be considered as single term. Dictionaries are particularly useful when identifying words in languages like Japanese, where words are not always marked by separator tokens. Meilisearch comes with a number of general-use dictionaries for its officially supported languages. When working with documents containing many domain-specific terms, such as a legal documents or academic papers, providing a [custom dictionary](/reference/api/settings#dictionary) may improve search result relevancy. ### Distance Distance plays an essential role in determining whether documents are relevant since [one of the ranking rules is the **proximity** rule](/learn/relevancy/relevancy). The proximity rule sorts the results by increasing distance between matched query terms. Then, two words separated by a soft space are closer and thus considered **more relevant** than two words separated by a hard space. After the tokenizing process, each word is indexed and stored in the global dictionary of the corresponding index. ### Examples To demonstrate how a string is split by space, let's say you have the following string as an input: ``` "Bruce Willis,Vin Diesel" ``` In the example above, the distance between `Bruce` and `Willis` is equal to **1**. The distance between `Vin` and `Diesel` is also **1**. However, the distance between `Willis` and `Vin` is equal to **8**. The same calculations apply to `Bruce` and `Diesel` (10), `Bruce` and `Vin` (9), and `Willis` and `Diesel` (9). Let's see another example. Given two documents: ```json theme={null} [ { "movie_id": "001", "description": "Bruce.Willis" }, { "movie_id": "002", "description": "Bruce super Willis" } ] ``` When making a query on `Bruce Willis`, `002` will be the first document returned, and `001` will be the second one. This will happen because the proximity distance between `Bruce` and `Willis` is equal to **2** in the document `002`, whereas the distance between `Bruce` and `Willis` is equal to **8** in the document `001` since the full-stop character `.` is a hard space. ## Numeric A numeric type (`integer`, `float`) is converted to a human-readable decimal number string representation. Numeric types can be searched as they are converted to strings. You can add [custom ranking rules](/learn/relevancy/custom_ranking_rules) to create an ascending or descending sorting rule on a given attribute that has a numeric value in the documents. You can also create [filters](/learn/filtering_and_sorting/filter_search_results). The `>`, `>=`, `<`, `<=`, and `TO` relational operators apply only to numerical values. ## Boolean A Boolean value, which is either `true` or `false`, is received and converted to a lowercase human-readable text (`true` and `false`). Booleans can be searched as they are converted to strings. ## `null` The `null` type can be pushed into Meilisearch but it **won't be taken into account for indexing**. ## Array An array is an ordered list of values. These values can be of any type: number, string, boolean, object, or even other arrays. Meilisearch flattens arrays and concatenates them into strings. Non-string values are converted as described in this article's previous sections. ### Example The following input: ```json theme={null} [ [ "Bruce Willis", "Vin Diesel" ], "Kung Fu Panda" ] ``` Will be processed as if all elements were arranged at the same level: ```json theme={null} "Bruce Willis. Vin Diesel. Kung Fu Panda." ``` Once the above array has been flattened, it will be parsed exactly as explained in the [string example](/learn/engine/datatypes#examples). ## Objects When a document field contains an object, Meilisearch flattens it and brings the object's keys and values to the root level of the document itself. Keep in mind that the flattened objects represented here are an intermediary snapshot of internal processes. When searching, the returned document will keep its original structure. In the example below, the `patient_name` key contains an object: ```json theme={null} { "id": 0, "patient_name": { "forename": "Imogen", "surname": "Temult" } } ``` During indexing, Meilisearch uses dot notation to eliminate nested fields: ```json theme={null} { "id": 0, "patient_name.forename": "Imogen", "patient_name.surname": "Temult" } ``` Using dot notation, no information is lost when flattening nested objects, regardless of nesting depth. Imagine that the example document above includes an additional object, `address`, containing home and work addresses, each of which are objects themselves. After flattening, the document would look like this: ```json theme={null} { "id": 0, "patient_name.forename": "Imogen", "patient_name.surname": "Temult", "address.home.street": "Largo Isarco, 2", "address.home.postcode": "20139", "address.home.city": "Milano", "address.work.street": "Ca' Corner Della Regina, 2215", "address.work.postcode": "30135", "address.work.city": "Venezia" } ``` Meilisearch's internal flattening process also eliminates nesting in arrays of objects. In this case, values are grouped by key. Consider the following document: ```json theme={null} { "id": 0, "patient_name": "Imogen Temult", "appointments": [ { "date": "2022-01-01", "doctor": "Jester Lavorre", "ward": "psychiatry" }, { "date": "2019-01-01", "doctor": "Dorian Storm" } ] } ``` After flattening, it would look like this: ```json theme={null} { "id": 0, "patient_name": "Imogen Temult", "appointments.date": [ "2022-01-01", "2019-01-01" ], "appointments.doctor": [ "Jester Lavorre", "Dorian Storm" ], "appointments.ward": [ "psychiatry" ] } ``` Once all objects inside a document have been flattened, Meilisearch will continue processing it as described in the previous sections. For example, arrays will be flattened, and numeric and boolean values will be turned into strings. ### Nested document querying and subdocuments Meilisearch has no concept of subdocuments and cannot perform nested document querying. In the previous example, the relationship between an appointment's date and doctor is lost when flattening the `appointments` array: ```json theme={null} … "appointments.date": [ "2022-01-01", "2019-01-01" ], "appointments.doctor": [ "Jester Lavorre", "Dorian Storm" ], … ``` This may lead to unexpected behavior during search. The following dataset shows two patients and their respective appointments: ```json theme={null} [ { "id": 0, "patient_name": "Imogen Temult", "appointments": [ { "date": "2022-01-01", "doctor": "Jester Lavorre" } ] }, { "id": 1, "patient_name": "Caleb Widowgast", "appointments": [ { "date": "2022-01-01", "doctor": "Dorian Storm" }, { "date": "2023-01-01", "doctor": "Jester Lavorre" } ] } ] ``` The following query returns patients `0` and `1`: ```sh theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/clinic_patients/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "", "filter": "(appointments.date = 2022-01-01 AND appointments.doctor = 'Jester Lavorre')" }' ``` Meilisearch is unable to only return patients who had an appointment with `Jester Lavorre` in `2022-01-01`. Instead, it returns patients who had an appointment with `Jester Lavorre`, and patients who had an appointment in `2022-01-01`. The best way to work around this limitation is reformatting your data. The above example could be fixed by merging appointment data in a new `appointmentsMerged` field so the relationship between appointment and doctor remains intact: ```json theme={null} [ { "id": 0, "patient_name": "Imogen Temult", "appointmentsMerged": [ "2022-01-01 Jester Lavorre" ] }, { "id": 1, "patient_name": "Caleb Widowgast", "appointmentsMerged": [ "2023-01-01 Jester Lavorre" "2022-01-01 Dorian Storm" ] } ] ``` ## Possible tokenization issues Even if it behaves exactly as expected, the tokenization process may lead to counterintuitive results in some cases, such as: ``` "S.O.S" "George R. R. Martin" 10,3 ``` For the two strings above, the full stops `.` will be considered as hard spaces. `10,3` will be broken into two strings—`10` and `3`—instead of being processed as a numeric type. # Prefix search Source: https://www.meilisearch.com/docs/learn/engine/prefix Prefix search is a core part of Meilisearch's design and allows users to receive results even when their query only contains a single letter. In Meilisearch, **you can perform a search with only a single letter as your query**. This is because we follow the philosophy of **prefix search**. Prefix search is when document sorting starts by comparing the search query against the beginning of each word in your dataset. All documents with words that match the query term are added to the [bucket sort](https://en.wikipedia.org/wiki/Bucket_sort), before the [ranking rules](/learn/relevancy/ranking_rules) are applied sequentially. In other words, prefix search means that it's not necessary to type a word in its entirety to find documents containing that word—you can just type the first one or two letters. Prefix search is only performed on the last word in a search query—prior words must be typed out fully to get accurate results. Searching by prefix (rather than using complete words) has a significant impact on search time. The shorter the query term, the more possible matches in the dataset. ### Example Given a set of words in a dataset: `film` `cinema` `movies` `show` `harry` `potter` `shine` `musical` query: `s`: response: * `show` * `shine` but not * `movies` * `musical` query: `sho`: response: * `show` Meilisearch also handles typos while performing the prefix search. You can [read more about the typo rules on the dedicated page](/learn/relevancy/typo_tolerance_settings). We also [apply splitting and concatenating on search queries](/learn/engine/concat). # Storage Source: https://www.meilisearch.com/docs/learn/engine/storage Learn about how Meilisearch stores and handles data in its LMDB storage engine. Meilisearch is in many ways a database: it stores indexed documents along with the data needed to return relevant search results. ## Database location Meilisearch creates the database the moment you first launch an instance. By default, you can find it inside a `data.ms` folder located in the same directory as the `meilisearch` binary. The database location can change depending on a number of factors, such as whether you have configured a different database path with the [`--db-path` instance option](/learn/self_hosted/configure_meilisearch_at_launch#database-path), or if you're using an OS virtualization tool like [Docker](https://docker.com). ## LMDB Creating a database from scratch and managing it is hard work. It would make no sense to try and reinvent the wheel, so Meilisearch uses a storage engine under the hood. This allows the Meilisearch team to focus on improving search relevancy and search performance while abstracting away the complicated task of creating, reading, and updating documents on disk and in memory. Our storage engine is called [Lightning Memory-Mapped Database](http://www.lmdb.tech/doc/) (LMDB for short). LMDB is a transactional key-value store written in C that was developed for OpenLDAP and has ACID properties. Though we considered other options, such as [Sled](https://github.com/spacejam/sled) and [RocksDB](https://rocksdb.org/), we chose LMDB because it provided us with the best combination of performance, stability, and features. ### Memory mapping LMDB stores its data in a [memory-mapped file](https://en.wikipedia.org/wiki/Memory-mapped_file). All data fetched from LMDB is returned straight from the memory map, which means there is no memory allocation or memory copy during data fetches. All documents stored on disk are automatically loaded in memory when Meilisearch asks for them. This ensures LMDB will always make the best use of the RAM available to retrieve the documents. For the best performance, it is recommended to provide the same amount of RAM as the size the database takes on disk, so all the data structures can fit in memory. ### Understanding LMDB The choice of LMDB comes with certain pros and cons, especially regarding database size and memory usage. We summarize the most important aspects of LMDB here, but check out this [blog post by LMDB's developers](https://www.symas.com/post/understanding-lmdb-database-file-sizes-and-memory-utilization) for more in-depth information. #### Database size When deleting documents from a Meilisearch index, you may notice disk space usage remains the same. This happens because LMDB internally marks that space as free, but does not make it available for the operating system at large. This design choice leads to better performance, as there is no need for periodic compaction operations. As a result, disk space occupied by LMDB (and thus by Meilisearch) tends to increase over time. It is not possible to calculate the precise maximum amount of space a Meilisearch instance can occupy. #### Memory usage Since LMDB is memory mapped, it is the operating system that manages the real memory allocated (or not) to Meilisearch. Thus, if you run Meilisearch as a standalone program on a server, LMDB will use the maximum RAM it can use. In general, you should have the same amount of RAM as the space taken on disk by Meilisearch for optimal performance. On the other hand, if you run Meilisearch along with other programs, the OS will manage memory based on everyone's needs. This makes Meilisearch's memory usage quite flexible when used in development. **Virtual Memory != Real Memory** Virtual memory is the disk space a program requests from the OS. It is not the memory that the program will actually use. Meilisearch will always demand a certain amount of space to use as a [memory map](#memory-mapping). This space will be used as virtual memory, but the amount of real memory (RAM) used will be much smaller. ## Measured disk usage The following measurements were taken using movies.json an 8.6 MB JSON dataset containing 19,553 documents. After indexing, the dataset size in LMDB is about 122MB. | Raw JSON | Meilisearch database size on disk | RAM usage | Virtual memory usage | | :------- | :-------------------------------- | :-------- | :------------------- | | 9.1 MB | 224 MB | ≃ 305 MB | 205 Gb (memory map) | This means the database is using **305 MB of RAM and 224 MB of disk space.** Note that [virtual memory](https://www.enterprisestorageforum.com/hardware/virtual-memory/) **refers only to disk space allocated by your computer for Meilisearch—it does not mean that it's actually in use by the database.** See [Memory Usage](#memory-usage) for more details. These metrics are highly dependent on the machine that is running Meilisearch. Running this test on significantly underpowered machines is likely to give different results. It is important to note that **there is no reliable way to predict the final size of a database**. This is true for just about any search engine on the market—we're just the only ones saying it out loud. Database size is affected by a large number of criteria, including settings, relevancy rules, use of facets, the number of different languages present, and more. # Filter expression reference Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_expression_reference The `filter` search parameter expects a filter expression. Filter expressions are made of attributes, values, and several operators. export const NoticeTag = ({label}) => {label} ; The `filter` search parameter expects a filter expression. Filter expressions are made of attributes, values, and several operators. `filter` expects a **filter expression** containing one or more **conditions**. A filter expression can be written as a string, array, or mix of both. ## Data types Filters accept numeric and string values. Empty fields or fields containing an empty array will be ignored. Filters do not work with [`NaN`](https://en.wikipedia.org/wiki/NaN) and infinite values such as `inf` and `-inf` as they are [not supported by JSON](https://en.wikipedia.org/wiki/JSON#Data_types). It is possible to filter infinite and `NaN` values if you parse them as strings, except when handling [`_geo` fields](/learn/filtering_and_sorting/geosearch#preparing-documents-for-location-based-search). For best results, enforce homogeneous typing across fields, especially when dealing with large numbers. Meilisearch does not enforce a specific schema when indexing data, but the filtering engine may coerce the type of `value`. This can lead to undefined behavior, such as when big floating-point numbers are coerced into integers. ## Conditions Conditions are a filter's basic building blocks. They are written in the `attribute OPERATOR value` format, where: * `attribute` is the attribute of the field you want to filter on * `OPERATOR` can be `=`, `!=`, `>`, `>=`, `<`, `<=`, `TO`, `EXISTS`, `IN`, `NOT`, `AND`, or `OR` * `value` is the value the `OPERATOR` should look for in the `attribute` ### Examples A basic condition requesting movies whose `genres` attribute is equal to `horror`: ``` genres = horror ``` String values containing whitespace must be enclosed in single or double quotes: ``` director = 'Jordan Peele' director = "Tim Burton" ``` ## Filter operators ### Equality (`=`) The equality operator (`=`) returns all documents containing a specific value for a given attribute: ``` genres = action ``` When operating on strings, `=` is case-insensitive. The equality operator does not return any results for `null` and empty arrays. ### Inequality (`!=`) The inequality operator (`!=`) returns all documents not selected by the equality operator. When operating on strings, `!=` is case-insensitive. The following expression returns all movies without the `action` genre: ``` genres != action ``` ### Comparison (`>`, `<`, `>=`, `<=`) The comparison operators (`>`, `<`, `>=`, `<=`) select documents satisfying a comparison. Comparison operators apply to both numerical and string values. The expression below returns all documents with a user rating above 85: ``` rating.users > 85 ``` String comparisons resolve in lexicographic order: symbols followed by numbers followed by letters in alphabetic order. The expression below returns all documents released after the first day of 2004: ``` release_date > 2004-01-01 ``` ### `TO` `TO` is equivalent to `>= AND <=`. The following expression returns all documents with a rating of 80 or above but below 90: ``` rating.users 80 TO 89 ``` ### `EXISTS` The `EXISTS` operator checks for the existence of a field. Fields with empty or `null` values count as existing. The following expression returns all documents containing the `release_date` field: ``` release_date EXISTS ``` The negated form of the above expression can be written in two equivalent ways: ``` release_date NOT EXISTS NOT release_date EXISTS ``` #### Vector filters When using AI-powered search, you may also use `EXISTS` to filter documents containing vector data: * `_vectors EXISTS`: matches all documents with an embedding * `_vectors.{embedder_name} EXISTS`: matches all documents with an embedding for the given embedder * `_vectors.{embedder_name}.userProvided EXISTS`: matches all documents with a user-provided embedding on the given embedder * `_vectors.{embedder_name}.documentTemplate EXISTS`: matches all documents with an embedding generated from a document template. Excludes user-provided embeddings * `_vectors.{embedder_name}.regenerate EXISTS`: matches all documents with an embedding scheduled for regeneration * `_vectors.{embedder_name}.fragments.{fragment_name} EXISTS`: matches all documents with an embedding generated from the given multimodal fragment. Excludes user-provided embeddings `_vectors` is only compatible with the `EXISTS` operator. ### `IS EMPTY` The `IS EMPTY` operator selects documents in which the specified attribute exists but contains empty values. The following expression only returns documents with an empty `overview` field: ``` overview IS EMPTY ``` `IS EMPTY` matches the following JSON values: * `""` * `[]` * `{}` Meilisearch does not treat `null` values as empty. To match `null` fields, use the [`IS NULL`](#is-null) operator. Use `NOT` to build the negated form of `IS EMPTY`: ``` overview IS NOT EMPTY NOT overview IS EMPTY ``` ### `IS NULL` The `IS NULL` operator selects documents in which the specified attribute exists but contains a `null` value. The following expression only returns documents with a `null` `overview` field: ``` overview IS NULL ``` Use `NOT` to build the negated form of `IS NULL`: ``` overview IS NOT NULL NOT overview IS NULL ``` ### `IN` `IN` combines equality operators by taking an array of comma-separated values delimited by square brackets. It selects all documents whose chosen field contains at least one of the specified values. The following expression returns all documents whose `genres` includes either `horror`, `comedy`, or both: ``` genres IN [horror, comedy] genres = horror OR genres = comedy ``` The negated form of the above expression can be written as: ``` genres NOT IN [horror, comedy] NOT genres IN [horror, comedy] ``` ### `CONTAINS` `CONTAINS` filters results containing partial matches to the specified string pattern, similar to a [SQL `LIKE`](https://dev.mysql.com/doc/refman/8.4/en/string-comparison-functions.html#operator_like). The following expression returns all dairy products whose names contain `"kef"`: ``` dairy_products.name CONTAINS kef ``` The negated form of the above expression can be written as: ``` dairy_products.name NOT CONTAINS kef NOT dairy_product.name CONTAINS kef ``` This is an experimental feature. Use the experimental features endpoint to activate it: ```sh theme={null} curl \ -X PATCH 'MEILISEARCH_URL/experimental-features/' \ -H 'Content-Type: application/json' \ --data-binary '{ "containsFilter": true }' ``` ### `STARTS WITH` `STARTS WITH` filters results whose values start with the specified string pattern. The following expression returns all dairy products whose name start with `"kef"`: ``` dairy_products.name STARTS WITH kef ``` The negated form of the above expression can be written as: ``` dairy_products.name NOT STARTS WITH kef NOT dairy_product.name STARTS WITH kef ``` ### `NOT` The negation operator (`NOT`) selects all documents that do not satisfy a condition. It has higher precedence than `AND` and `OR`. The following expression will return all documents whose `genres` does not contain `horror` and documents with a missing `genres` field: ``` NOT genres = horror ``` ## Filter expressions You can build filter expressions by grouping basic conditions using `AND` and `OR`. Filter expressions can be written as strings, arrays, or a mix of both. ### Filter expression grouping operators #### `AND` `AND` connects two conditions and only returns documents that satisfy both of them. `AND` has higher precedence than `OR`. The following expression returns all documents matching both conditions: ``` genres = horror AND director = 'Jordan Peele' ``` #### `OR` `OR` connects two conditions and returns results that satisfy at least one of them. The following expression returns documents matching either condition: ``` genres = horror OR genres = comedy ``` ### Creating filter expressions with string operators and parentheses Meilisearch reads string expressions from left to right. You can use parentheses to ensure expressions are correctly parsed. For instance, if you want your results to only include `comedy` and `horror` documents released after March 1995, the parentheses in the following query are mandatory: ``` (genres = horror OR genres = comedy) AND release_date > 795484800 ``` Failing to add these parentheses will cause the same query to be parsed as: ``` genres = horror OR (genres = comedy AND release_date > 795484800) ``` Translated into English, the above expression will only return comedies released after March 1995 or horror movies regardless of their `release_date`. When creating an expression with a field name or value identical to a filter operator such as `AND` or `NOT`, you must wrap it in quotation marks: `title = "NOT" OR title = "AND"`. ### Creating filter expressions with arrays Array expressions establish logical connectives by nesting arrays of strings. **Array filters can have a maximum depth of two.** Expressions with three or more levels of nesting will throw an error. Outer array elements are connected by an `AND` operator. The following expression returns `horror` movies directed by `Jordan Peele`: ``` ["genres = horror", "director = 'Jordan Peele'"] ``` Inner array elements are connected by an `OR` operator. The following expression returns either `horror` or `comedy` films: ``` [["genres = horror", "genres = comedy"]] ``` Inner and outer arrays can be freely combined. The following expression returns both `horror` and `comedy` movies directed by `Jordan Peele`: ``` [["genres = horror", "genres = comedy"], "director = 'Jordan Peele'"] ``` ### Combining arrays and string operators You can also create filter expressions that use both array and string syntax. The following filter is written as a string and only returns movies not directed by `Jordan Peele` that belong to the `comedy` or `horror` genres: ``` "(genres = comedy OR genres = horror) AND director != 'Jordan Peele'" ``` You can write the same filter mixing arrays and strings: ``` [["genres = comedy", "genres = horror"], "NOT director = 'Jordan Peele'"] ``` # Filter search results Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/filter_search_results In this guide you will see how to configure and use Meilisearch filters in a hypothetical movie database. In this guide you will see how to configure and use Meilisearch filters in a hypothetical movie database. ## Configure index settings Suppose you have a collection of movies called `movie_ratings` containing the following fields: ```json theme={null} [ { "id": 458723, "title": "Us", "director": "Jordan Peele", "release_date": 1552521600, "genres": [ "Thriller", "Horror", "Mystery" ], "rating": { "critics": 86, "users": 73 }, }, … ] ``` If you want to filter results based on an attribute, you must first add it to the `filterableAttributes` list: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/movie_ratings/settings/filterable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "genres", "director", "release_date", "ratings" ]' ``` ```javascript JS theme={null} client.index('movies') .updateFilterableAttributes([ 'director', 'genres' ]) ``` ```python Python theme={null} client.index('movies').update_filterable_attributes([ 'director', 'genres', ]) ``` ```php PHP theme={null} $client->index('movies')->updateFilterableAttributes(['director', 'genres']); ``` ```java Java theme={null} client.index("movies").updateFilterableAttributesSettings(new String[] { "genres", "director" }); ``` ```ruby Ruby theme={null} client.index('movies').update_filterable_attributes([ 'director', 'genres' ]) ``` ```go Go theme={null} resp, err := client.Index("movies").UpdateFilterableAttributes(&[]interface{}{ "director", "genres", }) ``` ```csharp C# theme={null} await client.Index("movies").UpdateFilterableAttributesAsync(new [] { "director", "genres" }); ``` ```rust Rust theme={null} let task: TaskInfo = client .index("movies") .set_filterable_attributes(["director", "genres"]) .await .unwrap(); ``` ```swift Swift theme={null} client.index("movies").updateFilterableAttributes(["genre", "director"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('movies').updateFilterableAttributes([ 'director', 'genres', ]); ``` **This step is mandatory and cannot be done at search time**. Updating `filterableAttributes` requires Meilisearch to re-index all your data, which will take an amount of time proportionate to your dataset size and complexity. By default, `filterableAttributes` is empty. Filters do not work without first explicitly adding attributes to the `filterableAttributes` list. ## Use `filter` when searching After updating the [`filterableAttributes` index setting](/reference/api/settings#filterable-attributes), you can use `filter` to fine-tune your search results. `filter` is a search parameter you may use at search time. `filter` accepts [filter expressions](/learn/filtering_and_sorting/filter_expression_reference) built using any attributes present in the `filterableAttributes` list. The following code sample returns `Avengers` movies released after 18 March 1995: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "Avengers", "filter": "release_date > 795484800" }' ``` ```javascript JS theme={null} client.index('movie_ratings').search('Avengers', { filter: 'release_date > 795484800' }) ``` ```python Python theme={null} client.index('movie_ratings').search('Avengers', { 'filter': 'release_date > 795484800' }) ``` ```php PHP theme={null} $client->index('movie_ratings')->search('Avengers', [ 'filter' => 'release_date > 795484800' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("Avengers").filter(new String[] {"release_date > \"795484800\""}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('movie_ratings').search('Avengers', { filter: 'release_date > 795484800' }) ``` ```go Go theme={null} resp, err := client.Index("movie_ratings").Search("Avengers", &meilisearch.SearchRequest{ Filter: "release_date > \"795484800\"", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = "release_date > \"795484800\"" }; var movies = await client.Index("movie_ratings").SearchAsync("Avengers", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("movie_ratings") .search() .with_query("Avengers") .with_filter("release_date > 795484800") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "Avengers", filter: "release_date > 795484800" ) client.index("movie_ratings").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('movie_ratings').search( 'Avengers', SearchQuery( filterExpression: Meili.gt( Meili.attr('release_date'), DateTime.utc(1995, 3, 18).toMeiliValue(), ), ), ); ``` Use dot notation to filter results based on a document's [nested fields](/learn/engine/datatypes). The following query only returns thrillers with good user reviews: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "thriller", "filter": "rating.users >= 90" }' ``` ```javascript JS theme={null} client.index('movie_ratings').search('thriller', { filter: 'rating.users >= 90' }) ``` ```python Python theme={null} client.index('movie_ratings').search('thriller', { 'filter': 'rating.users >= 90' }) ``` ```php PHP theme={null} $client->index('movie_ratings')->search('thriller', [ 'filter' => 'rating.users >= 90' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("thriller").filter(new String[] {"rating.users >= 90"}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('movies_ratings').search('thriller', { filter: 'rating.users >= 90' }) ``` ```go Go theme={null} resp, err := client.Index("movie_ratings").Search("thriller", &meilisearch.SearchRequest{ Filter: "rating.users >= 90", }) ``` ```csharp C# theme={null} var filters = new SearchQuery() { Filter = "rating.users >= 90" }; var movies = await client.Index("movie_ratings").SearchAsync("thriller", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("movie_rating") .search() .with_query("thriller") .with_filter("rating.users >= 90") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "thriller", filter: "rating.users >= 90" ) client.index("movie_ratings").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('movie_ratings').search( 'thriller', SearchQuery( filterExpression: Meili.gte( //or Meili.attr('rating.users') //or 'rating.users'.toMeiliAttribute() Meili.attrFromParts(['rating', 'users']), Meili.value(90), ), ), ); ``` You can also combine multiple conditions. For example, you can limit your search so it only includes `Batman` movies directed by either `Tim Burton` or `Christopher Nolan`: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "Batman", "filter": "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")" }' ``` ```javascript JS theme={null} client.index('movie_ratings').search('Batman', { filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```python Python theme={null} client.index('movie_ratings').search('Batman', { 'filter': 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```php PHP theme={null} $client->index('movie_ratings')->search('Batman', [ 'filter' => 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("Batman").filter(new String[] {"release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")"}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('movie_ratings').search('Batman', { filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```go Go theme={null} resp, err := client.Index("movie_ratings").Search("Batman", &meilisearch.SearchRequest{ Filter: "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")" }; var movies = await client.Index("movie_ratings").SearchAsync("Batman", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("movie_ratings") .search() .with_query("Batman") .with_filter(r#"release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")"#) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "Batman", filter: "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\"") client.index("movie_ratings").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('movie_ratings').search( 'Batman', SearchQuery( filterExpression: Meili.and([ Meili.attr('release_date') .gt(DateTime.utc(1995, 3, 18).toMeiliValue()), Meili.or([ 'director'.toMeiliAttribute().eq('Tim Burton'.toMeiliValue()), 'director' .toMeiliAttribute() .eq('Christopher Nolan'.toMeiliValue()), ]), ]), ), ); ``` Here, the parentheses are mandatory: without them, the filter would return movies directed by `Tim Burton` and released after 1995 or any film directed by `Christopher Nolan`, without constraints on its release date. This happens because `AND` takes precedence over `OR`. If you only want recent `Planet of the Apes` movies that weren't directed by `Tim Burton`, you can use this filter: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "Planet of the Apes", "filter": "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }' \ ``` ```javascript JS theme={null} client.index('movie_ratings').search('Planet of the Apes', { filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }) ``` ```python Python theme={null} client.index('movie_ratings').search('Planet of the Apes', { 'filter': 'release_date > 1577884550 AND (NOT director = "Tim Burton"))' }) ``` ```php PHP theme={null} $client->index('movie_ratings')->search('Planet of the Apes', [ 'filter' => 'release_date > 1577884550 AND (NOT director = "Tim Burton")' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("Planet of the Apes").filter(new String[] {"release_date > 1577884550 AND (NOT director = \"Tim Burton\")"}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('movie_ratings').search('Planet of the Apes', { filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }) ``` ```go Go theme={null} resp, err := client.Index("movie_ratings").Search("Planet of the Apes", &meilisearch.SearchRequest{ Filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }; var movies = await client.Index("movie_ratings").SearchAsync("Planet of the Apes", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("movie_ratings") .search() .with_query("Planet of the Apes") .with_filter(r#"release_date > 1577884550 AND (NOT director = "Tim Burton")"#) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "Planet of the Apes", filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")) client.index("movie_ratings").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('movie_ratings').search( 'Planet of the Apes', SearchQuery( filterExpression: Meili.and([ Meili.attr('release_date') .gt(DateTime.utc(2020, 1, 1, 13, 15, 50).toMeiliValue()), Meili.not( Meili.attr('director').eq("Tim Burton".toMeiliValue()), ), ]), ), ); ``` ``` release_date > 1577884550 AND (NOT director = "Tim Burton" AND director EXISTS) ``` [Synonyms](/learn/relevancy/synonyms) don't apply to filters. Meaning, if you have `SF` and `San Francisco` set as synonyms, filtering by `SF` and `San Francisco` will show you different results. # Geosearch Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/geosearch Filter and sort search results based on their geographic location. Meilisearch allows you to filter and sort results based on their geographic location. This can be useful when you only want results within a specific area or when sorting results based on their distance from a specific location. Due to Meilisearch allowing malformed `_geo` fields in the following versions (v0.27, v0.28 and v0.29), please ensure the `_geo` field follows the correct format. ## Preparing documents for location-based search To start filtering documents based on their geographic location, you must make sure they contain a valid `_geo` or `_geojson` field. If you also want to sort documents geogeraphically, they must have a valid `_geo` field. `_geo` and `_geojson` are reserved fields. If you include one of them in your documents, Meilisearch expects its value to conform to a specific format. When using JSON and NDJSON, `_geo` must contain an object with two keys: `lat` and `lng`. Both fields must contain either a floating point number or a string indicating, respectively, latitude and longitude: ```json theme={null} { … "_geo": { "lat": 0.0, "lng": "0.0" } } ``` `_geojson` must be an object whose contents follow the [GeoJSON specification](https://geojson.org/): ```json theme={null} { … "_geojson": { "type": "Feature", "geometry": { "type": "Point", "coordinates": [0.0, 0.0] } } } ``` Meilisearch does not support transmeridian shapes. If your document includes a transmeridian shape, split it into two separate shapes grouped as a `MultiPolygon` or `MultiLine`. Transmeridian shapes are polygons or lines that cross the 180th meridian. **Meilisearch does not support polygons with holes**. If your polygon consists of an external ring and an inner empty space, Meilisearch ignores the hole and treats the polygon as a solid shape. ### Using `_geo` and `_geojson` together If your application requires both sorting by distance to a point and filtering by shapes other than a circle or a rectangle, you will need to add both `_geo` and `_geojson` to your documents. When handling documents with both fields, Meilisearch: * Ignores `_geojson` values when sorting * Ignores `_geo` values when filtering with `_geoPolygon` * Matches both `_geo` and `_geojson` values when filtering with `_geoRadius` and `_geoBoundingBox` ### Examples Suppose we have a JSON array containing a few restaurants: ```json theme={null} [ { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9 }, { "id": 2, "name": "Bouillon Pigalle", "address": "22 Bd de Clichy, 75018 Paris, France", "type": "french", "rating": 8 }, { "id": 3, "name": "Artico Gelateria Tradizionale", "address": "Via Dogana, 1, 20123 Milan, Italy", "type": "ice cream", "rating": 10 } ] ``` Our restaurant dataset looks like this once we add `_geo` data: ```json theme={null} [ { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9, "_geo": { "lat": 45.4777599, "lng": 9.1967508 } }, { "id": 2, "name": "Bouillon Pigalle", "address": "22 Bd de Clichy, 75018 Paris, France", "type": "french", "rating": 8, "_geo": { "lat": 48.8826517, "lng": 2.3352748 } }, { "id": 3, "name": "Artico Gelateria Tradizionale", "address": "Via Dogana, 1, 20123 Milan, Italy", "type": "ice cream", "rating": 10, "_geo": { "lat": 45.4632046, "lng": 9.1719421 } } ] ``` Trying to index a dataset with one or more documents containing badly formatted `_geo` values will cause Meilisearch to throw an [`invalid_document_geo_field`](/reference/errors/error_codes#invalid_document_geo_field) error. In this case, the update will fail and no documents will be added or modified. ### Using `_geo` with CSV If your dataset is formatted as CSV, the file header must have a `_geo` column. Each row in the dataset must then contain a column with a comma-separated string indicating latitude and longitude: ```csv theme={null} "id:number","name:string","address:string","type:string","rating:number","_geo:string" "1","Nàpiz Milano","Viale Vittorio Veneto, 30, 20124, Milan, Italy","pizzeria",9,"45.4777599,9.1967508" "2","Bouillon Pigalle","22 Bd de Clichy, 75018 Paris, France","french",8,"48.8826517,2.3352748" "3","Artico Gelateria Tradizionale","Via Dogana, 1, 20123 Milan, Italy","ice cream",10,"48.8826517,2.3352748" ``` CSV files do not support the `_geojson` attribute. ## Filtering results with `_geoRadius`, `_geoBoundingBox`, and `_geoPolygon` You can use `_geo` and `_geojson` data to filter queries so you only receive results located within a given geographic area. ### Configuration To filter results based on their location, you must add `_geo` or `_geojson` to the `filterableAttributes` list: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/restaurants/settings/filterable-attributes' \ -H 'Content-type:application/json' \ --data-binary '["_geo"]' ``` ```javascript JS theme={null} client.index('restaurants') .updateFilterableAttributes([ '_geo' ]) ``` ```python Python theme={null} client.index('restaurants').update_filterable_attributes([ '_geo' ]) ``` ```php PHP theme={null} $client->index('restaurants')->updateFilterableAttributes([ '_geo' ]); ``` ```java Java theme={null} Settings settings = new Settings(); settings.setFilterableAttributes(new String[] {"_geo"}); client.index("restaurants").updateSettings(settings); ``` ```ruby Ruby theme={null} client.index('restaurants').update_filterable_attributes(['_geo']) ``` ```go Go theme={null} filterableAttributes := []interface{}{ "_geo", } client.Index("restaurants").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# theme={null} List attributes = new() { "_geo" }; TaskInfo result = await client.Index("movies").UpdateFilterableAttributesAsync(attributes); ``` ```rust Rust theme={null} let task: TaskInfo = client .index("restaurants") .set_filterable_attributes(&["_geo"]) .await .unwrap(); ``` ```swift Swift theme={null} client.index("restaurants").updateFilterableAttributes(["_geo"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').updateFilterableAttributes(['_geo']); ``` Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take a considerable amount of time. [You can read more about configuring `filterableAttributes` in our dedicated filtering guide.](/learn/filtering_and_sorting/filter_search_results) ### Usage Use the [`filter` search parameter](/reference/api/search#filter) along with `_geoRadius` and `_geoBoundingBox`. These are special filter rules that ensure Meilisearch only returns results located within a specific geographic area. If you are using GeoJSON for your documents, you may also filter results with `_geoPolygon`. ### `_geoRadius` ``` _geoRadius(lat, lng, distance_in_meters, resolution) ``` ### `_geoBoundingBox` ``` _geoBoundingBox([LAT, LNG], [LAT, LNG]) ``` ### `_geoPolygon` ``` _geoPolygon([LAT, LNG], [LAT, LNG], [LAT, LNG], …) ``` ### Examples Using our example dataset, we can search for places to eat near the center of Milan with `_geoRadius`: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "filter": "_geoRadius(45.472735, 9.184019, 2000)" }' ``` ```javascript JS theme={null} client.index('restaurants').search('', { filter: ['_geoRadius(45.472735, 9.184019, 2000)'], }) ``` ```python Python theme={null} client.index('restaurants').search('', { 'filter': '_geoRadius(45.472735, 9.184019, 2000)' }) ``` ```php PHP theme={null} $client->index('restaurants')->search('', [ 'filter' => '_geoRadius(45.472735, 9.184019, 2000)' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("").filter(new String[] {"_geoRadius(45.472735, 9.184019, 2000)"}).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('restaurants').search('', { filter: '_geoRadius(45.472735, 9.184019, 2000)' }) ``` ```go Go theme={null} resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoRadius(45.472735, 9.184019, 2000)", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = "_geoRadius(45.472735, 9.184019, 2000)" }; var restaurants = await client.Index("restaurants").SearchAsync("", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoRadius(45.472735, 9.184019, 2000)") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( filter: "_geoRadius(45.472735, 9.184019, 2000)" ) client.index("restaurants").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').search( '', SearchQuery( filterExpression: Meili.geoRadius( (lat: 45.472735, lng: 9.184019), 2000, ), ), ); ``` We also make a similar query using `_geoBoundingBox`: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "filter": "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])" }' ``` ```javascript JS theme={null} client.index('restaurants').search('', { filter: ['_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])'], }) ``` ```python Python theme={null} client.index('restaurants').search('Batman', { 'filter': '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])' }) ``` ```php PHP theme={null} $client->index('restaurants')->search('', [ 'filter' => '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q()("").filter(new String[] { "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])" }).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('restaurants').search('', { filter: ['_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])'] }) ``` ```go Go theme={null} client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])" }; var restaurants = await client.Index("restaurants").SearchAsync("restaurants", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( filter: "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])" ) client.index("restaurants").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').search( '', SearchQuery( filter: '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])', ), ); ``` And with `_geoPolygon`: ```json theme={null} [ { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9, "_geo": { "lat": 45.4777599, "lng": 9.1967508 } }, { "id": 3, "name": "Artico Gelateria Tradizionale", "address": "Via Dogana, 1, 20123 Milan, Italy", "type": "ice cream", "rating": 10, "_geo": { "lat": 45.4632046, "lng": 9.1719421 } } ] ``` It is also possible to combine `_geoRadius`, `_geoBoundingBox`, and `_geoPolygon` with other filters. We can narrow down our previous search so it only includes pizzerias: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "filter": "_geoRadius(45.472735, 9.184019, 2000) AND type = pizza" }' ``` ```javascript JS theme={null} client.index('restaurants').search('', { filter: ['_geoRadius(45.472735, 9.184019, 2000) AND type = pizza'], }) ``` ```python Python theme={null} client.index('restaurants').search('', { 'filter': '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' }) ``` ```php PHP theme={null} $client->index('restaurants')->search('', [ 'filter' => '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("").filter(new String[] {"_geoRadius(45.472735, 9.184019, 2000) AND type = pizza"}).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('restaurants').search('', { filter: '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' }) ``` ```go Go theme={null} resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoRadius(45.472735, 9.184019, 2000) AND type = pizza", }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Filter = new string[] { "_geoRadius(45.472735, 9.184019, 2000) AND type = pizza" } }; var restaurants = await client.Index("restaurants").SearchAsync("restaurants", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoRadius(45.472735, 9.184019, 2000) AND type = pizza") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( filter: "_geoRadius(45.472735, 9.184019, 2000) AND type = pizza" ) client.index("restaurants").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').search( '', SearchQuery( filterExpression: Meili.and([ Meili.geoRadius( (lat: 45.472735, lng: 9.184019), 2000, ), Meili.attr('type').eq('pizza'.toMeiliValue()) ]), ), ); ``` ```json theme={null} [ { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9, "_geo": { "lat": 45.4777599, "lng": 9.1967508 } } ] ``` `_geo`, `_geoDistance`, and `_geoPoint` are not valid filter rules. Trying to use any of them with the `filter` search parameter will result in an [`invalid_search_filter`](/reference/errors/error_codes#invalid_search_filter) error. ## Sorting results with `_geoPoint` ### Configuration Before using geosearch for sorting, you must add the `_geo` attribute to the [`sortableAttributes` list](/learn/filtering_and_sorting/sort_search_results): ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/restaurants/settings/sortable-attributes' \ -H 'Content-type:application/json' \ --data-binary '["_geo"]' ``` ```javascript JS theme={null} client.index('restaurants').updateSortableAttributes([ '_geo' ]) ``` ```python Python theme={null} client.index('restaurants').update_sortable_attributes([ '_geo' ]) ``` ```php PHP theme={null} $client->index('restaurants')->updateSortableAttributes([ '_geo' ]); ``` ```java Java theme={null} client.index("restaurants").updateSortableAttributesSettings(new String[] {"_geo"}); ``` ```ruby Ruby theme={null} client.index('restaurants').update_sortable_attributes(['_geo']) ``` ```go Go theme={null} sortableAttributes := []string{ "_geo", } client.Index("restaurants").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# theme={null} List attributes = new() { "_geo" }; TaskInfo result = await client.Index("restaurants").UpdateSortableAttributesAsync(attributes); ``` ```rust Rust theme={null} let task: TaskInfo = client .index("restaurants") .set_sortable_attributes(&["_geo"]) .await .unwrap(); ``` ```swift Swift theme={null} client.index("restaurants").updateSortableAttributes(["_geo"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').updateSortableAttributes(['_geo']); ``` It is not possible to sort documents based on the `_geojson` attribute. ### Usage ``` _geoPoint(0.0, 0.0):asc ``` ### Examples The `_geoPoint` sorting function can be used like any other sorting rule. We can order documents based on how close they are to the Eiffel Tower: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "sort": ["_geoPoint(48.8561446,2.2978204):asc"] }' ``` ```javascript JS theme={null} client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc'], }) ``` ```python Python theme={null} client.index('restaurants').search('', { 'sort': ['_geoPoint(48.8561446,2.2978204):asc'] }) ``` ```php PHP theme={null} $client->index('restaurants')->search('', [ 'sort' => ['_geoPoint(48.8561446,2.2978204):asc'] ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("").sort(new String[] {"_geoPoint(48.8561446,2.2978204):asc"}).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc'] }) ``` ```go Go theme={null} resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Sort: []string{ "_geoPoint(48.8561446,2.2978204):asc", }, }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Sort = new string[] { "_geoPoint(48.8561446,2.2978204):asc" } }; var restaurants = await client.Index("restaurants").SearchAsync("", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("restaurants") .search() .with_sort(&["_geoPoint(48.8561446, 2.2978204):asc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "", sort: ["_geoPoint(48.8561446, 2.2978204):asc"] ) client.index("restaurants").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').search( '', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc'])); ``` With our restaurants dataset, the results look like this: ```json theme={null} [ { "id": 2, "name": "Bouillon Pigalle", "address": "22 Bd de Clichy, 75018 Paris, France", "type": "french", "rating": 8, "_geo": { "lat": 48.8826517, "lng": 2.3352748 } }, { "id": 3, "name": "Artico Gelateria Tradizionale", "address": "Via Dogana, 1, 20123 Milan, Italy", "type": "ice cream", "rating": 10, "_geo": { "lat": 45.4632046, "lng": 9.1719421 } }, { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9, "_geo": { "lat": 45.4777599, "lng": 9.1967508 } } ] ``` `_geoPoint` also works when used together with other sorting rules. We can sort restaurants based on their proximity to the Eiffel Tower and their rating: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "sort": [ "_geoPoint(48.8561446,2.2978204):asc", "rating:desc" ] }' ``` ```javascript JS theme={null} client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'], }) ``` ```python Python theme={null} client.index('restaurants').search('', { 'sort': ['_geoPoint(48.8561446,2.2978204):asc', 'rating:desc'] }) ``` ```php PHP theme={null} $client->index('restaurants')->search('', [ 'sort' => ['_geoPoint(48.8561446,2.2978204):asc', 'rating:desc'] ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q()("").sort(new String[] { "_geoPoint(48.8561446,2.2978204):asc", "rating:desc", }).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'] }) ``` ```go Go theme={null} resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Sort: []string{ "_geoPoint(48.8561446,2.2978204):asc", "rating:desc", }, }) ``` ```csharp C# theme={null} SearchQuery filters = new SearchQuery() { Sort = new string[] { "_geoPoint(48.8561446,2.2978204):asc", "rating:desc" } }; var restaurants = await client.Index("restaurants").SearchAsync("restaurants", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("restaurants") .search() .with_sort(&["_geoPoint(48.8561446, 2.2978204):asc", "rating:desc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "", sort: ["_geoPoint(48.8561446, 2.2978204):asc", "rating:desc"] ) client.index("restaurants").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('restaurants').search( '', SearchQuery( sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'])); ``` ```json theme={null} [ { "id": 2, "name": "Bouillon Pigalle", "address": "22 Bd de Clichy, 75018 Paris, France", "type": "french", "rating": 8, "_geo": { "lat": 48.8826517, "lng": 2.3352748 } }, { "id": 3, "name": "Artico Gelateria Tradizionale", "address": "Via Dogana, 1, 20123 Milan, Italy", "type": "ice cream", "rating": 10, "_geo": { "lat": 45.4632046, "lng": 9.1719421 } }, { "id": 1, "name": "Nàpiz' Milano", "address": "Viale Vittorio Veneto, 30, 20124, Milan, Italy", "type": "pizza", "rating": 9, "_geo": { "lat": 45.4777599, "lng": 9.1967508 } } ] ``` # Search with facets Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters Faceted search interfaces provide users with a quick way to narrow down search results by selecting categories relevant to their query. In Meilisearch, facets are a specialized type of filter. This guide shows you how to configure facets and use them when searching a database of books. It also gives you instruction on how to get ## Requirements * a Meilisearch project * a command-line terminal ## Configure facet index settings First, create a new index using this books dataset. Documents in this dataset have the following fields: ```json theme={null} { "id": 5, "title": "Hard Times", "genres": ["Classics","Fiction", "Victorian", "Literature"], "publisher": "Penguin Classics", "language": "English", "author": "Charles Dickens", "description": "Hard Times is a novel of social […] ", "format": "Hardcover", "rating": 3 } ``` Next, add `genres`, `language`, and `rating` to the list of `filterableAttributes`: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/books/settings/filterable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "genres", "rating", "language" ]' ``` ```javascript JS theme={null} client.index('movie_ratings').updateFilterableAttributes(['genres', 'rating', 'language']) ``` ```python Python theme={null} client.index('movie_ratings').update_filterable_attributes([ 'genres', 'director', 'language' ]) ``` ```php PHP theme={null} $client->index('movie_ratings')->updateFilterableAttributes(['genres', 'rating', 'language']); ``` ```java Java theme={null} client.index("movie_ratings").updateFilterableAttributesSettings(new String[] { "genres", "director", "language" }); ``` ```ruby Ruby theme={null} client.index('movie_ratings').update_filterable_attributes(['genres', 'rating', 'language']) ``` ```go Go theme={null} filterableAttributes := []interface{}{ "genres", "rating", "language", } client.Index("movie_ratings").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# theme={null} List attributes = new() { "genres", "rating", "language" }; TaskInfo result = await client.Index("movie_ratings").UpdateFilterableAttributesAsync(attributes); ``` ```rust Rust theme={null} let task: TaskInfo = client .index("movie_ratings") .set_filterable_attributes(&["genres", "rating", "language"]) .await .unwrap(); ``` ```dart Dart theme={null} await client .index('movie_ratings') .updateFilterableAttributes(['genres', 'rating', 'language']); ``` You have now configured your index to use these attributes as filters. ## Use facets in a search query Make a search query setting the `facets` search parameter: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "classic", "facets": [ "genres", "rating", "language" ] }' ``` ```javascript JS theme={null} client.index('books').search('classic', { facets: ['genres', 'rating', 'language'] }) ``` ```python Python theme={null} client.index('books').search('classic', { 'facets': ['genres', 'rating', 'language'] }) ``` ```php PHP theme={null} $client->index('books')->search('classic', [ 'facets' => ['genres', 'rating', 'language'] ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("classic").facets(new String[] { "genres", "rating", "language" }).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('books').search('classic', { facets: ['genres', 'rating', 'language'] }) ``` ```go Go theme={null} resp, err := client.Index("books").Search("classic", &meilisearch.SearchRequest{ Facets: []string{ "genres", "rating", "language", }, }) ``` ```csharp C# theme={null} var sq = new SearchQuery { Facets = new string[] { "genres", "rating", "language" } }; await client.Index("books").SearchAsync("classic", sq); ``` ```rust Rust theme={null} let books = client.index("books"); let results: SearchResults = SearchQuery::new(&books) .with_query("classic") .with_facets(Selectors::Some(&["genres", "rating", "language"])) .execute() .await .unwrap(); ``` ```dart Dart theme={null} await client .index('books') .search('', SearchQuery(facets: ['genres', 'rating', 'language'])); ``` The response returns all books matching the query. It also returns two fields you can use to create a faceted search interface, `facetDistribution` and `facetStats`: ```json theme={null} { "hits": [ … ], … "facetDistribution": { "genres": { "Classics": 6, … }, "language": { "English": 6, "French": 1, "Spanish": 1 }, "rating": { "2.5": 1, … } }, "facetStats": { "rating": { "min": 2.5, "max": 4.7 } } } ``` `facetDistribution` lists all facets present in your search results, along with the number of documents returned for each facet. `facetStats` contains the highest and lowest values for all facets containing numeric values. ### Sorting facet values By default, all facet values are sorted in ascending alphanumeric order. You can change this using the `sortFacetValuesBy` property of the [`faceting` index settings](/reference/api/settings#faceting): ```bash cURL theme={null} curl \ -X PATCH 'MEILISEARCH_URL/indexes/books/settings/faceting' \ -H 'Content-Type: application/json' \ --data-binary '{ "sortFacetValuesBy": { "genres": "count" } }' ``` ```javascript JS theme={null} client.index('books').updateFaceting({ sortFacetValuesBy: { genres: 'count' } }) ``` ```python Python theme={null} client.index('books').update_faceting_settings({ 'sortFacetValuesBy': { 'genres': 'count' } }) ``` ```php PHP theme={null} $client->index('books')->updateFaceting(['sortFacetValuesBy' => ['genres' => 'count']]); ``` ```java Java theme={null} Faceting newFaceting = new Faceting(); HashMap facetSortValues = new HashMap<>(); facetSortValues.put("genres", FacetSortValue.COUNT); newFaceting.setSortFacetValuesBy(facetSortValues); client.index("books").updateFacetingSettings(newFaceting); ``` ```ruby Ruby theme={null} client.index('books').update_faceting( sort_facet_values_by: { genres: 'count' } ) ``` ```go Go theme={null} client.Index("books").UpdateFaceting(&meilisearch.Faceting{ SortFacetValuesBy: { "genres": SortFacetTypeCount, } }) ``` ```csharp C# theme={null} var newFaceting = new Faceting { SortFacetValuesBy = new Dictionary { ["genres"] = SortFacetValuesByType.Count } }; await client.Index("books").UpdateFacetingAsync(newFaceting); ``` ```rust Rust theme={null} let mut facet_sort_setting = BTreeMap::new(); facet_sort_setting.insert("genres".to_string(), FacetSortValue::Count); let faceting = FacetingSettings { max_values_per_facet: 100, sort_facet_values_by: Some(facet_sort_setting), }; let res = client.index("books") .set_faceting(&faceting) .await .unwrap(); ``` ```dart Dart theme={null} await client.index('books').updateFaceting( Faceting( sortFacetValuesBy: { 'genres': FacetingSortTypes.count, }, ), ); ``` The above code sample sorts the `genres` facet by descending value count. Repeating the previous query using the new settings will result in a different order in `facetsDistribution`: ```json theme={null} { … "facetDistribution": { "genres": { "Fiction": 8, "Literature": 7, "Classics": 6, "Novel": 2, "Horror": 2, "Fantasy": 2, "Victorian": 2, "Vampires": 1, "Tragedy": 1, "Satire": 1, "Romance": 1, "Historical Fiction": 1, "Coming-of-Age": 1, "Comedy": 1 }, … } } ``` ## Searching facet values You can also search for facet values with the [facet search endpoint](/reference/api/facet_search): ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/facet-search' \ -H 'Content-Type: application/json' \ --data-binary '{ "facetQuery": "c", "facetName": "genres" }' ``` ```javascript JS theme={null} client.index('books').searchForFacetValues({ facetQuery: 'c', facetName: 'genres' }) ``` ```python Python theme={null} client.index('books').facet_search('genres', 'c') ``` ```php PHP theme={null} $client->index('books')->facetSearch( (new FacetSearchQuery()) ->setFacetQuery('c') ->setFacetName('genres') ); ``` ```java Java theme={null} FacetSearchRequest fsr = FacetSearchRequest.builder().facetName("genres").facetQuery("c").build(); client.index("books").facetSearch(fsr); ``` ```ruby Ruby theme={null} client.index('books').facet_search('genres', 'c') ``` ```go Go theme={null} client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{ FacetQuery: "c", FacetName: "genres", ExhaustiveFacetCount: true }) ``` ```csharp C# theme={null} var query = new SearchFacetsQuery() { FacetQuery = "c", ExhaustiveFacetCount: true }; await client.Index("books").FacetSearchAsync("genres", query); ``` ```rust Rust theme={null} let res = client.index("books") .facet_search("genres") .with_facet_query("c") .execute() .await .unwrap(); ``` ```dart Dart theme={null} await client.index('books').facetSearch( FacetSearchQuery( facetQuery: 'c', facetName: 'genres', ), ); ``` The following code sample searches the `genres` facet for values starting with `c`: The response contains a `facetHits` array listing all matching facets, together with the total number of documents that include that facet: ```json theme={null} { … "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 refine results using the `q`, `filter`, and `matchingStrategy` parameters. [Learn more about them in the API reference.](/reference/api/facet_search) # Sort search results Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/sort_search_results By default, Meilisearch sorts results according to their relevancy. You can alter this behavior so users can decide at search time results they want to see first. By default, Meilisearch focuses on ordering results according to their relevancy. You can alter this sorting behavior so users can decide at search time what type of results they want to see first. This can be useful in many situations, such as when a user wants to see the cheapest products available in a webshop. Sorting at search time can be particularly effective when combined with [placeholder searches](/reference/api/search#placeholder-search). ## Configure Meilisearch for sorting at search time To allow your users to sort results at search time you must: 1. Decide which attributes you want to use for sorting 2. Add those attributes to the `sortableAttributes` index setting 3. Update Meilisearch's [ranking rules](/learn/relevancy/relevancy) (optional) Meilisearch sorts strings in lexicographic order based on their byte values. For example, `á`, which has a value of 225, will be sorted after `z`, which has a value of 122. Uppercase letters are sorted as if they were lowercase. They will still appear uppercase in search results. ### Add attributes to `sortableAttributes` Meilisearch allows you to sort results based on document fields. Only fields containing numbers, strings, arrays of numeric values, and arrays of string values can be used for sorting. After you have decided which fields you will allow your users to sort on, you must add their attributes to the [`sortableAttributes` index setting](/reference/api/settings#sortable-attributes). If a field has values of different types across documents, Meilisearch will give precedence to numbers over strings. This means documents with numeric field values will be ranked higher than those with string values. This can lead to unexpected behavior when sorting. For optimal user experience, only sort based on fields containing the same type of value. #### Example Suppose you have collection of books containing the following fields: ```json theme={null} [ { "id": 1, "title": "Solaris", "author": "Stanislaw Lem", "genres": [ "science fiction" ], "rating": { "critics": 95, "users": 87 }, "price": 5.00 }, … ] ``` If you are using this dataset in a webshop, you might want to allow your users to sort on `author` and `price`: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/books/settings/sortable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "author", "price" ]' ``` ```javascript JS theme={null} client.index('books').updateSortableAttributes([ 'author', 'price' ]) ``` ```python Python theme={null} client.index('books').update_sortable_attributes([ 'author', 'price' ]) ``` ```php PHP theme={null} $client->index('books')->updateSortableAttributes([ 'author', 'price' ]); ``` ```java Java theme={null} client.index("books").updateSortableAttributesSettings(new String[] {"price", "author"}); ``` ```ruby Ruby theme={null} client.index('books').update_sortable_attributes(['author', 'price']) ``` ```go Go theme={null} sortableAttributes := []string{ "author", "price", } client.Index("books").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# theme={null} await client.Index("books").UpdateSortableAttributesAsync(new [] { "price", "author" }); ``` ```rust Rust theme={null} let sortable_attributes = [ "author", "price" ]; let task: TaskInfo = client .index("books") .set_sortable_attributes(&sortable_attributes) .await .unwrap(); ``` ```swift Swift theme={null} client.index("books").updateSortableAttributes(["price", "author"]) { (result: Result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('books').updateSortableAttributes(['author', 'price']); ``` ### Customize ranking rule order (optional) When users sort results at search time, [Meilisearch's ranking rules](/learn/relevancy/relevancy) are set up so the top matches emphasize relevant results over sorting order. You might need to alter this behavior depending on your application's needs. This is the default configuration of Meilisearch's ranking rules: ```json theme={null} [ "words", "typo", "proximity", "attribute", "sort", "exactness" ] ``` `"sort"` is in fifth place. This means it acts as a tie-breaker rule: Meilisearch will first place results closely matching search terms at the top of the returned documents list and only then will apply the `"sort"` parameters as requested by the user. In other words, by default Meilisearch provides a very relevant sorting. Placing `"sort"` ranking rule higher in the list will emphasize exhaustive sorting over relevant sorting: your results will more closely follow the sorting order your user chose, but will not be as relevant. Sorting applies equally to all documents. Meilisearch does not offer native support for promoting, pinning, and boosting specific documents so they are displayed more prominently than other search results. Consult these Meilisearch blog articles for workarounds on [implementing promoted search results with React InstantSearch](https://blog.meilisearch.com/promoted-search-results-with-react-instantsearch) and [document boosting](https://blog.meilisearch.com/document-boosting). #### Example If your users care more about finding cheaper books than they care about finding specific matches to their queries, you can place `sort` much higher in the ranking rules: ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/books/settings/ranking-rules' \ -H 'Content-Type: application/json' \ --data-binary '[ "words", "sort", "typo", "proximity", "attribute", "exactness" ]' ``` ```javascript JS theme={null} client.index('books').updateRankingRules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```python Python theme={null} client.index('books').update_ranking_rules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```php PHP theme={null} $client->index('books')->updateRankingRules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]); ``` ```java Java theme={null} Settings settings = new Settings(); settings.setRankingRules(new String[] { "words", "sort", "typo", "proximity", "attribute", "exactness" }); client.index("books").updateSettings(settings); ``` ```ruby Ruby theme={null} client.index('books').update_ranking_rules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```go Go theme={null} rankingRules := []string{ "words", "sort", "typo", "proximity", "attribute", "exactness", } client.Index("books").UpdateRankingRules(&rankingRules) ``` ```csharp C# theme={null} await client.Index("books").UpdateRankingRulesAsync(new[] { "words", "sort", "typo", "proximity", "attribute", "exactness" }); ``` ```rust Rust theme={null} let ranking_rules = [ "words", "sort", "typo", "proximity", "attribute", "exactness" ]; let task: TaskInfo = client .index("books") .set_ranking_rules(&ranking_rules) .await .unwrap(); ``` ```swift Swift theme={null} let rankingRules: [String] = [ "words", "sort", "typo", "proximity", "attribute", "exactness" ] client.index("books").updateRankingRules(rankingRules) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('books').updateRankingRules( ['words', 'sort', 'typo', 'proximity', 'attribute', 'exactness']); ``` ## Sort results at search time After configuring `sortableAttributes`, you can use the [`sort` search parameter](/reference/api/search#sort) to control the sorting order of your search results. `sort` expects a list of attributes that have been added to the `sortableAttributes` list. Attributes must be given as `attribute:sorting_order`. In other words, each attribute must be followed by a colon (`:`) and a sorting order: either ascending (`asc`) or descending (`desc`). When using the `POST` route, `sort` expects an array of strings: ```json theme={null} "sort": [ "price:asc", "author:desc" ] ``` When using the `GET` route, `sort` expects a comma-separated string: ``` sort="price:desc,author:asc" ``` The order of `sort` values matter: the higher an attribute is in the search parameter value, the more Meilisearch will prioritize it over attributes placed lower. In our example, if multiple documents have the same value for `price`, Meilisearch will decide the order between these similarly-priced documents based on their `author`. ### Example Suppose you are searching for books in a webshop and want to see the cheapest science fiction titles. This query searches for `"science fiction"` books sorted from cheapest to most expensive: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "science fiction", "sort": ["price:asc"] }' ``` ```javascript JS theme={null} client.index('books').search('science fiction', { sort: ['price:asc'], }) ``` ```python Python theme={null} client.index('books').search('science fiction', { 'sort': ['price:asc'] }) ``` ```php PHP theme={null} $client->index('books')->search('science fiction', ['sort' => ['price:asc']]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("science fiction").sort(new String[] {"price:asc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('books').search('science fiction', { sort: ['price:asc'] }) ``` ```go Go theme={null} resp, err := client.Index("books").Search("science fiction", &meilisearch.SearchRequest{ Sort: []string{ "price:asc", }, }) ``` ```csharp C# theme={null} var sq = new SearchQuery { Sort = new[] { "price:asc" }, }; await client.Index("books").SearchAsync("science fiction", sq); ``` ```rust Rust theme={null} let results: SearchResults = client .index("books") .search() .with_query("science fiction") .with_sort(&["price:asc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "science fiction", sort: ["price:asc"] ) client.index("books").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('books') .search('science fiction', SearchQuery(sort: ['price:asc'])); ``` With our example dataset, the results look like this: ```json theme={null} [ { "id": 1, "title": "Solaris", "author": "Stanislaw Lem", "genres": [ "science fiction" ], "rating": { "critics": 95, "users": 87 }, "price": 5.00 }, { "id": 2, "title": "The Parable of the Sower", "author": "Octavia E. Butler", "genres": [ "science fiction" ], "rating": { "critics": 90, "users": 92 }, "price": 10.00 } ] ``` It is common to search books based on an author's name. `sort` can help grouping results from the same author. This query would only return books matching the query term `"butler"` and group results according to their authors: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "butler", "sort": ["author:desc"] }' ``` ```javascript JS theme={null} client.index('books').search('butler', { sort: ['author:desc'], }) ``` ```python Python theme={null} client.index('books').search('butler', { 'sort': ['author:desc'] }) ``` ```php PHP theme={null} $client->index('books')->search('butler', ['sort' => ['author:desc']]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("butler").sort(new String[] {"author:desc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('books').search('butler', { sort: ['author:desc'] }) ``` ```go Go theme={null} resp, err := client.Index("books").Search("butler", &meilisearch.SearchRequest{ Sort: []string{ "author:desc", }, }) ``` ```csharp C# theme={null} var sq = new SearchQuery { Sort = new[] { "author:desc" }, }; await client.Index("books").SearchAsync("butler", sq); ``` ```rust Rust theme={null} let results: SearchResults = client .index("books") .search() .with_query("butler") .with_sort(&["author:desc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "butler", sort: ["author:desc"] ) client.index("books").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('books') .search('butler', SearchQuery(sort: ['author:desc'])); ``` ```json theme={null} [ { "id": 2, "title": "The Parable of the Sower", "author": "Octavia E. Butler", "genres": [ "science fiction" ], "rating": { "critics": 90, "users": 92 }, "price": 10.00 }, { "id": 5, "title": "Wild Seed", "author": "Octavia E. Butler", "genres": [ "fantasy" ], "rating": { "critics": 84, "users": 80 }, "price": 5.00 }, { "id": 4, "title": "Gender Trouble", "author": "Judith Butler", "genres": [ "feminism", "philosophy" ], "rating": { "critics": 86, "users": 73 }, "price": 10.00 } ] ``` ### Sort by nested fields Use dot notation to sort results based on a document's nested fields. The following query sorts returned documents by their user review scores: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "science fiction", "sort": ["rating.users:asc"] }' ``` ```javascript JS theme={null} client.index('books').search('science fiction', { 'sort': ['rating.users:asc'], }) ``` ```python Python theme={null} client.index('books').search('science fiction', { 'sort': ['rating.users:asc'] }) ``` ```php PHP theme={null} $client->index('books')->search('science fiction', ['sort' => ['rating.users:asc']]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("science fiction").sort(new String[] {"rating.users:asc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('books').search('science fiction', { sort: ['rating.users:asc'] }) ``` ```go Go theme={null} resp, err := client.Index("books").Search("science fiction", &meilisearch.SearchRequest{ Sort: []string{ "rating.users:asc", }, }) ``` ```csharp C# theme={null} SearchQuery sort = new SearchQuery() { Sort = new string[] { "rating.users:asc" }}; await client.Index("books").SearchAsync("science fiction", sort); ``` ```rust Rust theme={null} let results: SearchResults = client .index("books") .search() .with_query("science fiction") .with_sort(&["rating.users:asc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "science fiction", sort: ["rating.users:asc"] ) client.index("books").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('movie_ratings') .search('thriller', SearchQuery(sort: ['rating.users:asc'])); ``` ## Sorting and custom ranking rules There is a lot of overlap between sorting and configuring [custom ranking rules](/learn/relevancy/custom_ranking_rules), as both can greatly influence which results a user will see first. Sorting is most useful when you want your users to be able to alter the order of returned results at query time. For example, webshop users might want to order results by price depending on what they are searching and to change whether they see the most expensive or the cheapest products first. Custom ranking rules, instead, establish a default sorting rule that is enforced in every search. This approach can be useful when you want to promote certain results above all others, regardless of a user's preferences. For example, you might want a webshop to always feature discounted products first, no matter what a user is searching for. ## Example application Take a look at our demos for examples of how to implement sorting: * **Ecommerce demo**: [preview](https://ecommerce.meilisearch.com/) • [GitHub repository](https://github.com/meilisearch/ecommerce-demo/) * **CRM SaaS demo**: [preview](https://saas.meilisearch.com/) • [GitHub repository](https://github.com/meilisearch/saas-demo/) # Filtering and sorting by date Source: https://www.meilisearch.com/docs/learn/filtering_and_sorting/working_with_dates Learn how to index documents with chronological data, and how to sort and filter search results based on time. In this guide, you will learn about Meilisearch's approach to date and time values, how to prepare your dataset for indexing, and how to chronologically sort and filter search results. ## Preparing your documents To filter and sort search results chronologically, your documents must have at least one field containing a [UNIX timestamp](https://kb.narrative.io/what-is-unix-time). You may also use a string with a date in a format that can be sorted lexicographically, such as `"2025-01-13"`. As an example, consider a database of video games. In this dataset, the release year is formatted as a timestamp: ```json theme={null} [ { "id": 0, "title": "Return of the Obra Dinn", "genre": "adventure", "release_timestamp": 1538949600 }, { "id": 1, "title": "The Excavation of Hob's Barrow", "genre": "adventure", "release_timestamp": 1664316000 }, { "id": 2, "title": "Bayonetta 2", "genre": "action", "release_timestamp": 1411164000 } ] ``` Once all documents in your dataset have a date field, [index your data](/reference/api/documents#add-or-replace-documents) as usual. The example below adds a videogame dataset to a `games` index: ```bash cURL theme={null} curl \ -x POST 'MEILISEARCH_URL/indexes/games/documents' \ -h 'content-type: application/json' \ --data-binary @games.json ``` ```javascript JS theme={null} const games = require('./games.json') client.index('games').addDocuments(games).then((res) => console.log(res)) ``` ```python Python theme={null} import json json_file = open('./games.json', encoding='utf-8') games = json.load(json_file) client.index('games').add_documents(games) ``` ```php PHP theme={null} $gamesJson = file_get_contents('games.json'); $games = json_decode($gamesJson); $client->index('games')->addDocuments($games); ``` ```java Java theme={null} import com.meilisearch.sdk; import org.json.JSONArray; import java.nio.file.Files; import java.nio.file.Path; Path fileName = Path.of("games.json"); String gamesJson = Files.readString(fileName); Index index = client.index("games"); index.addDocuments(gamesJson); ``` ```ruby Ruby theme={null} require 'json' games = JSON.parse(File.read('games.json')) client.index('games').add_documents(games) ``` ```go Go theme={null} jsonFile, _ := os.Open("games.json") defer jsonFile.Close() byteValue, _ := io.ReadAll(jsonFile) var games []map[string]interface{} json.Unmarshal(byteValue, &games) client.Index("games").AddDocuments(games, nil) ``` ```csharp C# theme={null} string jsonString = await File.ReadAllTextAsync("games.json"); var games = JsonSerializer.Deserialize>(jsonString, options); var index = client.Index("games"); await index.AddDocumentsAsync(games); ``` ```rust Rust theme={null} let mut file = File::open("games.json") .unwrap(); let mut content = String::new(); file .read_to_string(&mut content) .unwrap(); let docs: Vec = serde_json::from_str(&content) .unwrap(); client .index("games") .add_documents(&docs, None) .await .unwrap(); ``` ```swift Swift theme={null} let path = Bundle.main.url(forResource: "games", withExtension: "json")! let documents: Data = try Data(contentsOf: path) client.index("games").addDocuments(documents: documents) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} //import 'dart:io'; //import 'dart:convert'; final json = await File('games.json').readAsString(); await client.index('games').addDocumentsJson(json); ``` ## Filtering by date To filter search results based on their timestamp, add your document's timestamp field to the list of [`filterableAttributes`](/reference/api/settings#update-filterable-attributes): ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/games/settings/filterable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "release_timestamp" ]' ``` ```javascript JS theme={null} client.index('games').updateFilterableAttributes(['release_timestamp']) ``` ```python Python theme={null} client.index('games').update_filterable_attributes(['release_timestamp']) ``` ```php PHP theme={null} $client->index('games')->updateFilterableAttributes(['release_timestamp']); ``` ```java Java theme={null} client.index("movies").updateFilterableAttributesSettings(new String[] { "release_timestamp" }); ``` ```ruby Ruby theme={null} client.index('games').update_filterable_attributes(['release_timestamp']) ``` ```go Go theme={null} filterableAttributes := []interface{}{"release_timestamp"} client.Index("games").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# theme={null} await client.Index("games").UpdateFilterableAttributesAsync(new string[] { "release_timestamp" }); ``` ```rust Rust theme={null} let settings = Settings::new() .with_filterable_attributes(["release_timestamp"]); let task: TaskInfo = client .index("games") .set_settings(&settings) .await .unwrap(); ``` ```swift Swift theme={null} client.index("games").updateFilterableAttributes(["release_timestamp"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('games') .updateFilterableAttributes(['release_timestamp']); ``` Once you have configured `filterableAttributes`, you can filter search results by date. The following query only returns games released between 2018 and 2022: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/games/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "", "filter": "release_timestamp >= 1514761200 AND release_timestamp < 1672527600" }' ``` ```javascript JS theme={null} client.index('games').search('', { filter: 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```python Python theme={null} client.index('games').search('', { 'filter': 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```php PHP theme={null} $client->index('games')->search('', [ 'filter' => ['release_timestamp >= 1514761200 AND release_timestamp < 1672527600'] ]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("").filter(new String[] {"release_timestamp >= 1514761200 AND release_timestamp < 1672527600"}).build(); client.index("games").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('games').search('', { filter: 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```go Go theme={null} client.Index("games").Search("", &meilisearch.SearchRequest{ Filter: "release_timestamp >= 1514761200 AND release_timestamp < 1672527600", }) ``` ```csharp C# theme={null} var filters = new SearchQuery() { Filter = "release_timestamp >= 1514761200 AND release_timestamp < 1672527600" }; var games = await client.Index("games").SearchAsync("", filters); ``` ```rust Rust theme={null} let results: SearchResults = client .index("games") .search() .with_filter("release_timestamp >= 1514761200 AND release_timestamp < 1672527600") .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "", filter: "release_timestamp >= 1514761200 AND release_timestamp < 1672527600" ) client.index("games").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client.index('games').search( '', SearchQuery( filterExpression: Meili.and([ Meili.gte( 'release_timestamp'.toMeiliAttribute(), Meili.value(DateTime(2017, 12, 31, 23, 0)), ), Meili.lt( 'release_timestamp'.toMeiliAttribute(), Meili.value(DateTime(2022, 12, 31, 23, 0)), ), ]), ), ); ``` ## Sorting by date To sort search results chronologically, add your document's timestamp field to the list of [`sortableAttributes`](/reference/api/settings#update-sortable-attributes): ```bash cURL theme={null} curl \ -X PUT 'MEILISEARCH_URL/indexes/games/settings/sortable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "release_timestamp" ]' ``` ```javascript JS theme={null} client.index('games').updateSortableAttributes(['release_timestamp']) ``` ```python Python theme={null} client.index('games').update_sortable_attributes(['release_timestamp']) ``` ```php PHP theme={null} $client->index('games')->updateSortableAttributes(['release_timestamp']); ``` ```java Java theme={null} Settings settings = new Settings(); settings.setSortableAttributes(new String[] {"release_timestamp"}); client.index("games").updateSettings(settings); ``` ```ruby Ruby theme={null} client.index('games').update_sortable_attributes(['release_timestamp']) ``` ```go Go theme={null} sortableAttributes := []string{"release_timestamp","author"} client.Index("games").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# theme={null} await client.Index("games").UpdateSortableAttributesAsync(new string[] { "release_timestamp" }); ``` ```rust Rust theme={null} let settings = Settings::new() .with_sortable_attributes(["release_timestamp"]); let task: TaskInfo = client .index("games") .set_settings(&settings) .await .unwrap(); ``` ```swift Swift theme={null} client.index("games").updateSortableAttributes(["release_timestamp"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('games') .updateSortableAttributes(['release_timestamp']); ``` Once you have configured `sortableAttributes`, you can sort your search results based on their timestamp. The following query returns all games sorted from most recent to oldest: ```bash cURL theme={null} curl \ -X POST 'MEILISEARCH_URL/indexes/games/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "", "sort": ["release_timestamp:desc"] }' ``` ```javascript JS theme={null} client.index('games').search('', { sort: ['release_timestamp:desc'], }) ``` ```python Python theme={null} client.index('games').search('', { 'sort': ['release_timestamp:desc'] }) ``` ```php PHP theme={null} $client->index('games')->search('', ['sort' => ['release_timestamp:desc']]); ``` ```java Java theme={null} SearchRequest searchRequest = SearchRequest.builder().q("").sort(new String[] {"release_timestamp:desc"}).build(); client.index("games").search(searchRequest); ``` ```ruby Ruby theme={null} client.index('games').search('', sort: ['release_timestamp:desc']) ``` ```go Go theme={null} client.Index("games").Search("", &meilisearch.SearchRequest{ Sort: []string{ "release_timestamp:desc", }, }) ``` ```csharp C# theme={null} SearchQuery sort = new SearchQuery() { Sort = new string[] { "release_timestamp:desc" }}; await client.Index("games").SearchAsync("", sort); ``` ```rust Rust theme={null} let results: SearchResults = client .index("games") .search() .with_sort(["release_timestamp:desc"]) .execute() .await .unwrap(); ``` ```swift Swift theme={null} let searchParameters = SearchParameters( query: "", sort: ["release_timestamp:desc"], ) client.index("games").search(searchParameters) { (result: Result, Swift.Error>) in switch result { case .success(let searchResult): print(searchResult) case .failure(let error): print(error) } } ``` ```dart Dart theme={null} await client .index('games') .search('', SearchQuery(sort: ['release_timestamp:desc'])); ``` # Getting started with Meilisearch Cloud Source: https://www.meilisearch.com/docs/learn/getting_started/cloud_quick_start Learn how to create your first Meilisearch Cloud project. This tutorial walks you through setting up [Meilisearch Cloud](https://meilisearch.com/cloud), creating a project and an index, adding documents to it, and performing your first search with the default web interface. You need a Meilisearch Cloud account to follow along. If you don't have one, register for a 14-day free trial account at [https://cloud.meilisearch.com/register](https://cloud.meilisearch.com/register?utm_campaign=oss\&utm_source=docs\&utm_medium=cloud-quick-start).