# 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 { "EMBEDDER_NAME": { "source": "rest" } } ``` Next, configure the URL Meilisearch should use to contact the embedding provider: ```json { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 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 { "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 { "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 { "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 { "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 { "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 { "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 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 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 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 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 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 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 curl \ -X PATCH 'MEILISEARCH_URL/indexes/movies/settings' \ -H 'Content-Type: application/json' \ --data-binary '{ "embedders": { "image2text": { "source": "userProvided", "dimensions": 3 } } }' ``` ## 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 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 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 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 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 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 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 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 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 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 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 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 { "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 { "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 { "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 { "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 { "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 { "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 { "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 curl \ -X GET 'MEILISEARCH_URL/tasks?statuses=failed' ``` ```javascript JS client.tasks.getTasks({ statuses: ['failed', 'canceled'] }) ``` ```python Python client.get_tasks({'statuses': ['failed', 'canceled']}) ``` ```php PHP $client->getTasks((new TasksQuery())->setStatuses(['failed', 'canceled'])); ``` ```java Java TasksQuery query = new TasksQuery().setStatuses(new String[] {"failed", "canceled"}); client.getTasks(query); ``` ```ruby Ruby client.get_tasks(statuses: ['failed', 'canceled']) ``` ```go Go client.GetTasks(&meilisearch.TasksQuery{ Statuses: []meilisearch.TaskStatus{ meilisearch.TaskStatusFailed, meilisearch.TaskStatusCanceled, }, }) ``` ```csharp C# await client.GetTasksAsync(new TasksQuery { Statuses = new List { TaskInfoStatus.Failed, TaskInfoStatus.Canceled } }); ``` ```rust Rust let mut query = TasksQuery::new(&client); let tasks = query .with_statuses(["failed"]) .execute() .await .unwrap(); ``` ```swift Swift 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 await client.getTasks( params: TasksQuery( statuses: ['failed', 'canceled'], ), ); ``` Use a comma to separate multiple values and fetch both `canceled` and `failed` tasks: ```bash cURL curl \ -X GET 'MEILISEARCH_URL/tasks?statuses=failed,canceled' ``` ```rust Rust 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 curl \ -X GET 'MEILISEARCH_URL/tasks?indexUids=movies&types=documentAdditionOrUpdate,documentDeletion&statuses=processing' ``` ```javascript JS client.tasks.getTasks({ indexUids: ['movies'], types: ['documentAdditionOrUpdate','documentDeletion'], statuses: ['processing'] }) ``` ```python Python client.get_tasks( { 'indexUids': 'movies', 'types': ['documentAdditionOrUpdate', 'documentDeletion'], 'statuses': ['processing'], } ) ``` ```php PHP $client->getTasks( (new TasksQuery()) ->setStatuses(['processing']) ->setUids(['movies']) ->setTypes(['documentAdditionOrUpdate', 'documentDeletion']) ); ``` ```java Java TasksQuery query = new TasksQuery() .setStatuses(new String[] {"processing"}) .setTypes(new String[] {"documentAdditionOrUpdate", "documentDeletion"}) .setIndexUids(new String[] {"movies"}); client.getTasks(query); ``` ```ruby Ruby client.get_tasks(index_uids: ['movies'], types: ['documentAdditionOrUpdate', 'documentDeletion'], statuses: ['processing']) ``` ```go Go client.GetTasks(&meilisearch.TasksQuery{ IndexUIDS: []string{"movie"}, Types: []meilisearch.TaskType{ meilisearch.TaskTypeDocumentAdditionOrUpdate, meilisearch.TaskTypeDocumentDeletion, }, Statuses: []meilisearch.TaskStatus{ meilisearch.TaskStatusProcessing, }, }) ``` ```csharp C# 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 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 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 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 curl \ -X GET 'MEILISEARCH_URL/tasks?limit=2&from=10 ``` ```javascript JS client.tasks.getTasks({ limit: 2, from: 10 }) ``` ```python Python client.get_tasks({ 'limit': 2, 'from': 10 }) ``` ```php PHP $taskQuery = (new TasksQuery())->setLimit(2)->setFrom(10)); $client->getTasks($taskQuery); ``` ```java Java TasksQuery query = new TasksQuery() .setLimit(2) .setFrom(10); client.index("movies").getTasks(query); ``` ```ruby Ruby client.tasks(limit: 2, from: 10) ``` ```go Go client.GetTasks(&meilisearch.TasksQuery{ Limit: 2, From: 10, }); ``` ```csharp C# ResourceResults taskResult = await client.GetTasksAsync(new TasksQuery { Limit = 2, From = 10 }); ``` ```rust Rust let mut query = TasksSearchQuery::new(&client) .with_limit(2) .with_from(10) .execute() .await .unwrap(); ``` ```swift Swift 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 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 { "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 curl \ -X GET 'MEILISEARCH_URL/tasks?limit=2&from=8 ``` ```javascript JS client.tasks.getTasks({ limit: 2, from: 8 }) ``` ```python Python client.get_tasks({ 'limit': 2, 'from': 8 }) ``` ```php PHP $taskQuery = (new TasksQuery())->setLimit(2)->setFrom(8)); $client->getTasks($taskQuery); ``` ```java Java TasksQuery query = new TasksQuery() .setLimit(2) .setFrom(8); client.index("movies").getTasks(query); ``` ```ruby Ruby client.tasks(limit: 2, from: 8) ``` ```go Go client.GetTasks(&meilisearch.TasksQuery{ Limit: 2, From: 8, }); ``` ```csharp C# ResourceResults taskResult = await client.GetTasksAsync(new TasksQuery { Limit = 2, From = 8 }); ``` ```rust Rust let mut query = TasksSearchQuery::new(&client) .with_limit(2) .from(8) .execute() .await .unwrap(); ``` ```swift Swift 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 await client.getTasks(params: TasksQuery(limit: 2, from: 8)); ``` This will return a new batch of tasks: ```json { "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 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 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 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 {"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 {"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 curl \ -X POST 'MEILISEARCH_URL/indexes/movies/documents'\ -H 'Content-Type: application/json' \ --data-binary @movies.json ``` ```javascript JS const movies = require('./movies.json') client.index('movies').addDocuments(movies).then((res) => console.log(res)) ``` ```python Python import json json_file = open('movies.json', encoding='utf-8') movies = json.load(json_file) client.index('movies').add_documents(movies) ``` ```php PHP $moviesJson = file_get_contents('movies.json'); $movies = json_decode($moviesJson); $client->index('movies')->addDocuments($movies); ``` ```java Java 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 require 'json' movies_json = File.read('movies.json') movies = JSON.parse(movies_json) client.index('movies').add_documents(movies) ``` ```go Go 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# // 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 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 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 // 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 { "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?utm_campaign=oss\&utm_source=docs\&utm_medium=tasks-tutorial) 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. Look for your request's `taskUid` in the "Uid" column: A table listing multiple Meilisearch Cloud tasks When the task `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. ### Monitoring task status with the Meilisearch API Use the `taskUid` from your request's response to check the status of a task: ```bash cURL curl \ -X GET 'MEILISEARCH_URL/tasks/1' ``` ```javascript JS client.tasks.getTask(1) ``` ```python Python client.get_task(1) ``` ```php PHP $client->getTask(1); ``` ```java Java client.getTask(1); ``` ```ruby Ruby client.task(1) ``` ```go Go client.GetTask(1); ``` ```csharp C# TaskInfo task = await client.GetTaskAsync(1); ``` ```rust Rust let task: Task = client .get_task(1) .await .unwrap(); ``` ```swift Swift client.getTask(taskUid: 1) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart await client.getTask(1); ``` This will return the full task object: ```json { "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 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 { "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 { "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 { "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 { … "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 } } ] } ``` # Conversational search Source: https://www.meilisearch.com/docs/learn/chat/conversational_search Learn how to implement AI-powered conversational search using Meilisearch's chat feature Meilisearch's chat completions feature enables AI-powered conversational search, allowing users to ask questions in natural language and receive direct answers based on your indexed content. This feature transforms the traditional search experience into an interactive dialogue. This is an experimental feature. Use the Meilisearch Cloud UI or the experimental features endpoint to activate it: ```sh curl \ -X PATCH 'MEILISEARCH_URL/experimental-features/' \ -H 'Content-Type: application/json' \ --data-binary '{ "chatCompletions": true }' ``` ## What is conversational search? Conversational search interfaces allow users to: * Ask questions in natural language instead of using keywords * Receive direct answers rather than just document links * Maintain context across multiple questions * Get responses grounded in your actual content This approach bridges the gap between traditional search and modern AI experiences, making information more accessible and intuitive to find. ## How chat completions differs from traditional search ### 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 4. User can ask follow-up questions ## When to use chat completions vs traditional search ### Use conversational search when: * Users need direct answers to specific questions * Content is informational (documentation, knowledge bases, FAQs) * Users benefit from follow-up questions * Natural language interaction improves user experience ### Use traditional search when: * Users need to browse multiple options * Results require comparison (e-commerce products, listings) * Exact matching is critical * Response time is paramount ## Use chat completions to implement RAG pipelines The chat completions feature implements a complete Retrieval Augmented Generation (RAG) pipeline in a single API endpoint. Meilisearch's chat completions consolidates RAG creation into one streamlined 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 ### Alternative: MCP integration When integrating Meilisearch with AI assistants and automation tools, consider using [Meilisearch's Model Context Protocol (MCP) server](/guides/ai/mcp). MCP enables standardized tool integration across various AI platforms and applications. ## Architecture overview Chat completions operate through workspaces, which are isolated configurations for different use cases. Each workspace can: * Use different LLM sources (openAi, azureOpenAi, mistral, gemini, vLlm) * Apply custom prompts * Access specific indexes based on API keys * Maintain separate conversation contexts ### Key components 1. **Chat endpoint**: `/chats/{workspace}/chat/completions` * OpenAI-compatible interface * Supports streaming responses * Handles tool calling for index searches 2. **Workspace settings**: `/chats/{workspace}/settings` * Configure LLM provider and model * Set system prompts * Manage API credentials 3. **Index integration**: * Automatically searches relevant indexes * Uses existing Meilisearch search capabilities * Respects API key permissions ## Security considerations The chat completions feature integrates with Meilisearch's existing security model: * **API key permissions**: chat only accesses indexes visible to the provided API key * **Tenant tokens**: support for multi-tenant applications * **LLM credentials**: stored securely in workspace settings * **Content isolation**: responses based only on indexed content ## Next steps * [Get started with chat completions implementation](/learn/chat/getting_started_with_chat) * [Explore the chat completions API reference](/reference/api/chats) # 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 curl \ -X GET 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' ``` ```rust Rust 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 [ "*" ] ``` 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 curl \ -X PUT 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "title", "overview" ]' ``` ```rust Rust 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 { "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 curl \ -X GET 'MEILISEARCH_URL/tasks/TASK_UID' ``` ```rust Rust let task_status = index.get_task(&task).await.unwrap(); ``` Meilisearch will respond with a task object: ```json { "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 curl \ -X GET 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/searchable-attributes' ``` ```rust Rust let searchable_attributes: Vec = index .get_searchable_attributes() .await .unwrap(); ``` Meilisearch should return an array with the new values: ```json [ "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 curl \ -X POST 'MEILISEARCH_URL/dumps' ``` ```javascript JS client.createDump() ``` ```python Python client.create_dump() ``` ```php PHP $client->createDump(); ``` ```java Java client.createDump(); ``` ```ruby Ruby client.create_dump ``` ```go Go resp, err := client.CreateDump() ``` ```csharp C# await client.CreateDumpAsync(); ``` ```rust Rust client .create_dump() .await .unwrap(); ``` ```swift Swift client.createDump { result in switch result { case .success(let dumpStatus): print(dumpStatus) case .failure(let error): print(error) } } ``` ```dart Dart 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 { "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 curl \ -X GET 'MEILISEARCH_URL/tasks/1' ``` ```javascript JS client.tasks.getTask(1) ``` ```python Python client.get_task(1) ``` ```php PHP $client->getTask(1); ``` ```java Java client.getTask(1); ``` ```ruby Ruby client.task(1) ``` ```go Go client.GetTask(1); ``` ```csharp C# TaskInfo task = await client.GetTaskAsync(1); ``` ```rust Rust let task: Task = client .get_task(1) .await .unwrap(); ``` ```swift Swift client.getTask(taskUid: 1) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart await client.getTask(1); ``` This should return an object with detailed information about the dump operation: ```json { "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 ./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 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 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 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 [ { "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 [ [ "Bruce Willis", "Vin Diesel" ], "Kung Fu Panda" ] ``` Will be processed as if all elements were arranged at the same level: ```json "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 { "id": 0, "patient_name": { "forename": "Imogen", "surname": "Temult" } } ``` During indexing, Meilisearch uses dot notation to eliminate nested fields: ```json { "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 { "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 { "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 { "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 … "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 [ { "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 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 [ { "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 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 [ { "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 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 client.index('movies') .updateFilterableAttributes([ 'director', 'genres' ]) ``` ```python Python client.index('movies').update_filterable_attributes([ 'director', 'genres', ]) ``` ```php PHP $client->index('movies')->updateFilterableAttributes(['director', 'genres']); ``` ```java Java client.index("movies").updateFilterableAttributesSettings(new String[] { "genres", "director" }); ``` ```ruby Ruby client.index('movies').update_filterable_attributes([ 'director', 'genres' ]) ``` ```go Go resp, err := client.Index("movies").UpdateFilterableAttributes(&[]interface{}{ "director", "genres", }) ``` ```csharp C# await client.Index("movies").UpdateFilterableAttributesAsync(new [] { "director", "genres" }); ``` ```rust Rust let task: TaskInfo = client .index("movies") .set_filterable_attributes(["director", "genres"]) .await .unwrap(); ``` ```swift Swift client.index("movies").updateFilterableAttributes(["genre", "director"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart 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 curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "Avengers", "filter": "release_date > 795484800" }' ``` ```javascript JS client.index('movie_ratings').search('Avengers', { filter: 'release_date > 795484800' }) ``` ```python Python client.index('movie_ratings').search('Avengers', { 'filter': 'release_date > 795484800' }) ``` ```php PHP $client->index('movie_ratings')->search('Avengers', [ 'filter' => 'release_date > 795484800' ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("Avengers").filter(new String[] {"release_date > \"795484800\""}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby client.index('movie_ratings').search('Avengers', { filter: 'release_date > 795484800' }) ``` ```go Go resp, err := client.Index("movie_ratings").Search("Avengers", &meilisearch.SearchRequest{ Filter: "release_date > \"795484800\"", }) ``` ```csharp C# SearchQuery filters = new SearchQuery() { Filter = "release_date > \"795484800\"" }; var movies = await client.Index("movie_ratings").SearchAsync("Avengers", filters); ``` ```rust Rust let results: SearchResults = client .index("movie_ratings") .search() .with_query("Avengers") .with_filter("release_date > 795484800") .execute() .await .unwrap(); ``` ```swift Swift 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 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 curl \ -X POST 'MEILISEARCH_URL/indexes/movie_ratings/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "thriller", "filter": "rating.users >= 90" }' ``` ```javascript JS client.index('movie_ratings').search('thriller', { filter: 'rating.users >= 90' }) ``` ```python Python client.index('movie_ratings').search('thriller', { 'filter': 'rating.users >= 90' }) ``` ```php PHP $client->index('movie_ratings')->search('thriller', [ 'filter' => 'rating.users >= 90' ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("thriller").filter(new String[] {"rating.users >= 90"}).build(); client.index("movie_ratings").search(searchRequest); ``` ```ruby Ruby client.index('movies_ratings').search('thriller', { filter: 'rating.users >= 90' }) ``` ```go Go resp, err := client.Index("movie_ratings").Search("thriller", &meilisearch.SearchRequest{ Filter: "rating.users >= 90", }) ``` ```csharp C# var filters = new SearchQuery() { Filter = "rating.users >= 90" }; var movies = await client.Index("movie_ratings").SearchAsync("thriller", filters); ``` ```rust Rust let results: SearchResults = client .index("movie_rating") .search() .with_query("thriller") .with_filter("rating.users >= 90") .execute() .await .unwrap(); ``` ```swift Swift 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 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 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 client.index('movie_ratings').search('Batman', { filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```python Python client.index('movie_ratings').search('Batman', { 'filter': 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```php PHP $client->index('movie_ratings')->search('Batman', [ 'filter' => 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' ]); ``` ```java Java 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 client.index('movie_ratings').search('Batman', { filter: 'release_date > 795484800 AND (director = "Tim Burton" OR director = "Christopher Nolan")' }) ``` ```go Go resp, err := client.Index("movie_ratings").Search("Batman", &meilisearch.SearchRequest{ Filter: "release_date > 795484800 AND (director = \"Tim Burton\" OR director = \"Christopher Nolan\")", }) ``` ```csharp C# 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 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 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 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 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 client.index('movie_ratings').search('Planet of the Apes', { filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }) ``` ```python Python client.index('movie_ratings').search('Planet of the Apes', { 'filter': 'release_date > 1577884550 AND (NOT director = "Tim Burton"))' }) ``` ```php PHP $client->index('movie_ratings')->search('Planet of the Apes', [ 'filter' => 'release_date > 1577884550 AND (NOT director = "Tim Burton")' ]); ``` ```java Java 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 client.index('movie_ratings').search('Planet of the Apes', { filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")" }) ``` ```go Go resp, err := client.Index("movie_ratings").Search("Planet of the Apes", &meilisearch.SearchRequest{ Filter: "release_date > 1577884550 AND (NOT director = \"Tim Burton\")", }) ``` ```csharp C# 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 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 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 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 In order to start filtering and sorting documents based on their geographic location, you must make sure they contain a valid `_geo` field. `_geo` is a reserved field. If you include it 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 { … "_geo": { "lat": 0.0, "lng": "0.0" } } ``` ### Examples Suppose we have a JSON array containing a few restaurants: ```json [ { "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 geopositioning data: ```json [ { "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 "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" ``` ## Filtering results with `_geoRadius` and `_geoBoundingBox` You can use `_geo` data to filter queries so you only receive results located within a given geographic area. ### Configuration In order to filter results based on their location, you must add the `_geo` attribute to the `filterableAttributes` list: ```bash cURL curl \ -X PUT 'MEILISEARCH_URL/indexes/restaurants/settings/filterable-attributes' \ -H 'Content-type:application/json' \ --data-binary '["_geo"]' ``` ```javascript JS client.index('restaurants') .updateFilterableAttributes([ '_geo' ]) ``` ```python Python client.index('restaurants').update_filterable_attributes([ '_geo' ]) ``` ```php PHP $client->index('restaurants')->updateFilterableAttributes([ '_geo' ]); ``` ```java Java Settings settings = new Settings(); settings.setFilterableAttributes(new String[] {"_geo"}); client.index("restaurants").updateSettings(settings); ``` ```ruby Ruby client.index('restaurants').update_filterable_attributes(['_geo']) ``` ```go Go filterableAttributes := []interface{}{ "_geo", } client.Index("restaurants").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# List attributes = new() { "_geo" }; TaskInfo result = await client.Index("movies").UpdateFilterableAttributesAsync(attributes); ``` ```rust Rust let task: TaskInfo = client .index("restaurants") .set_filterable_attributes(&["_geo"]) .await .unwrap(); ``` ```swift Swift client.index("restaurants").updateFilterableAttributes(["_geo"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart 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` or `_geoBoundingBox`. These are special filter rules that ensure Meilisearch only returns results located within a specific geographic area. ### `_geoRadius` ``` _geoRadius(lat, lng, distance_in_meters) ``` ### `_geoBoundingBox` ``` _geoBoundingBox([{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 curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "filter": "_geoRadius(45.472735, 9.184019, 2000)" }' ``` ```javascript JS client.index('restaurants').search('', { filter: ['_geoRadius(45.472735, 9.184019, 2000)'], }) ``` ```python Python client.index('restaurants').search('', { 'filter': '_geoRadius(45.472735, 9.184019, 2000)' }) ``` ```php PHP $client->index('restaurants')->search('', [ 'filter' => '_geoRadius(45.472735, 9.184019, 2000)' ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("").filter(new String[] {"_geoRadius(45.472735, 9.184019, 2000)"}).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby client.index('restaurants').search('', { filter: '_geoRadius(45.472735, 9.184019, 2000)' }) ``` ```go Go resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoRadius(45.472735, 9.184019, 2000)", }) ``` ```csharp C# SearchQuery filters = new SearchQuery() { Filter = "_geoRadius(45.472735, 9.184019, 2000)" }; var restaurants = await client.Index("restaurants").SearchAsync("", filters); ``` ```rust Rust let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoRadius(45.472735, 9.184019, 2000)") .execute() .await .unwrap(); ``` ```swift Swift 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 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 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 client.index('restaurants').search('', { filter: ['_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])'], }) ``` ```python Python client.index('restaurants').search('Batman', { 'filter': '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])' }) ``` ```php PHP $client->index('restaurants')->search('', [ 'filter' => '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])' ]); ``` ```java Java 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 client.index('restaurants').search('', { filter: ['_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])'] }) ``` ```go Go client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])", }) ``` ```csharp C# 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 let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])") .execute() .await .unwrap(); ``` ```swift Swift 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 await client.index('restaurants').search( '', SearchQuery( filter: '_geoBoundingBox([45.494181, 9.214024], [45.449484, 9.179175])', ), ); ``` ```json [ { "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` and `_geoBoundingBox` with other filters. We can narrow down our previous search so it only includes pizzerias: ```bash cURL 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 client.index('restaurants').search('', { filter: ['_geoRadius(45.472735, 9.184019, 2000) AND type = pizza'], }) ``` ```python Python client.index('restaurants').search('', { 'filter': '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' }) ``` ```php PHP $client->index('restaurants')->search('', [ 'filter' => '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' ]); ``` ```java Java 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 client.index('restaurants').search('', { filter: '_geoRadius(45.472735, 9.184019, 2000) AND type = pizza' }) ``` ```go Go resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Filter: "_geoRadius(45.472735, 9.184019, 2000) AND type = pizza", }) ``` ```csharp C# 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 let results: SearchResults = client .index("restaurants") .search() .with_filter("_geoRadius(45.472735, 9.184019, 2000) AND type = pizza") .execute() .await .unwrap(); ``` ```swift Swift 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 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 [ { "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: ```bash cURL curl \ -X PUT 'MEILISEARCH_URL/indexes/restaurants/settings/sortable-attributes' \ -H 'Content-type:application/json' \ --data-binary '["_geo"]' ``` ```javascript JS client.index('restaurants').updateSortableAttributes([ '_geo' ]) ``` ```python Python client.index('restaurants').update_sortable_attributes([ '_geo' ]) ``` ```php PHP $client->index('restaurants')->updateSortableAttributes([ '_geo' ]); ``` ```java Java client.index("restaurants").updateSortableAttributesSettings(new String[] {"_geo"}); ``` ```ruby Ruby client.index('restaurants').update_sortable_attributes(['_geo']) ``` ```go Go sortableAttributes := []string{ "_geo", } client.Index("restaurants").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# List attributes = new() { "_geo" }; TaskInfo result = await client.Index("restaurants").UpdateSortableAttributesAsync(attributes); ``` ```rust Rust let task: TaskInfo = client .index("restaurants") .set_sortable_attributes(&["_geo"]) .await .unwrap(); ``` ```swift Swift client.index("restaurants").updateSortableAttributes(["_geo"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart await client.index('restaurants').updateSortableAttributes(['_geo']); ``` [Read more about `sortableAttributes` here.](/learn/filtering_and_sorting/sort_search_results) ### 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 curl \ -X POST 'MEILISEARCH_URL/indexes/restaurants/search' \ -H 'Content-type:application/json' \ --data-binary '{ "sort": ["_geoPoint(48.8561446,2.2978204):asc"] }' ``` ```javascript JS client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc'], }) ``` ```python Python client.index('restaurants').search('', { 'sort': ['_geoPoint(48.8561446,2.2978204):asc'] }) ``` ```php PHP $client->index('restaurants')->search('', [ 'sort' => ['_geoPoint(48.8561446,2.2978204):asc'] ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("").sort(new String[] {"_geoPoint(48.8561446,2.2978204):asc"}).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc'] }) ``` ```go Go resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Sort: []string{ "_geoPoint(48.8561446,2.2978204):asc", }, }) ``` ```csharp C# SearchQuery filters = new SearchQuery() { Sort = new string[] { "_geoPoint(48.8561446,2.2978204):asc" } }; var restaurants = await client.Index("restaurants").SearchAsync("", filters); ``` ```rust Rust let results: SearchResults = client .index("restaurants") .search() .with_sort(&["_geoPoint(48.8561446, 2.2978204):asc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 await client.index('restaurants').search( '', SearchQuery(sort: ['_geoPoint(48.8561446, 2.2978204):asc'])); ``` With our restaurants dataset, the results look like this: ```json [ { "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 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 client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'], }) ``` ```python Python client.index('restaurants').search('', { 'sort': ['_geoPoint(48.8561446,2.2978204):asc', 'rating:desc'] }) ``` ```php PHP $client->index('restaurants')->search('', [ 'sort' => ['_geoPoint(48.8561446,2.2978204):asc', 'rating:desc'] ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q()("").sort(new String[] { "_geoPoint(48.8561446,2.2978204):asc", "rating:desc", }).build(); client.index("restaurants").search(searchRequest); ``` ```ruby Ruby client.index('restaurants').search('', { sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'] }) ``` ```go Go resp, err := client.Index("restaurants").Search("", &meilisearch.SearchRequest{ Sort: []string{ "_geoPoint(48.8561446,2.2978204):asc", "rating:desc", }, }) ``` ```csharp C# 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 let results: SearchResults = client .index("restaurants") .search() .with_sort(&["_geoPoint(48.8561446, 2.2978204):asc", "rating:desc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 await client.index('restaurants').search( '', SearchQuery( sort: ['_geoPoint(48.8561446, 2.2978204):asc', 'rating:desc'])); ``` ```json [ { "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 { "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 curl \ -X PUT 'MEILISEARCH_URL/indexes/books/settings/filterable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "genres", "rating", "language" ]' ``` ```javascript JS client.index('movie_ratings').updateFilterableAttributes(['genres', 'rating', 'language']) ``` ```python Python client.index('movie_ratings').update_filterable_attributes([ 'genres', 'director', 'language' ]) ``` ```php PHP $client->index('movie_ratings')->updateFilterableAttributes(['genres', 'rating', 'language']); ``` ```java Java client.index("movie_ratings").updateFilterableAttributesSettings(new String[] { "genres", "director", "language" }); ``` ```ruby Ruby client.index('movie_ratings').update_filterable_attributes(['genres', 'rating', 'language']) ``` ```go Go filterableAttributes := []interface{}{ "genres", "rating", "language", } client.Index("movie_ratings").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# List attributes = new() { "genres", "rating", "language" }; TaskInfo result = await client.Index("movie_ratings").UpdateFilterableAttributesAsync(attributes); ``` ```rust Rust let task: TaskInfo = client .index("movie_ratings") .set_filterable_attributes(&["genres", "rating", "language"]) .await .unwrap(); ``` ```dart Dart 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 curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "classic", "facets": [ "genres", "rating", "language" ] }' ``` ```javascript JS client.index('books').search('classic', { facets: ['genres', 'rating', 'language'] }) ``` ```python Python client.index('books').search('classic', { 'facets': ['genres', 'rating', 'language'] }) ``` ```php PHP $client->index('books')->search('classic', [ 'facets' => ['genres', 'rating', 'language'] ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("classic").facets(new String[] { "genres", "rating", "language" }).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby client.index('books').search('classic', { facets: ['genres', 'rating', 'language'] }) ``` ```go Go resp, err := client.Index("books").Search("classic", &meilisearch.SearchRequest{ Facets: []string{ "genres", "rating", "language", }, }) ``` ```csharp C# var sq = new SearchQuery { Facets = new string[] { "genres", "rating", "language" } }; await client.Index("books").SearchAsync("classic", sq); ``` ```rust Rust 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 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 { "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 curl \ -X PATCH 'MEILISEARCH_URL/indexes/books/settings/faceting' \ -H 'Content-Type: application/json' \ --data-binary '{ "sortFacetValuesBy": { "genres": "count" } }' ``` ```javascript JS client.index('books').updateFaceting({ sortFacetValuesBy: { genres: 'count' } }) ``` ```python Python client.index('books').update_faceting_settings({ 'sortFacetValuesBy': { 'genres': 'count' } }) ``` ```php PHP $client->index('books')->updateFaceting(['sortFacetValuesBy' => ['genres' => 'count']]); ``` ```java Java Faceting newFaceting = new Faceting(); HashMap facetSortValues = new HashMap<>(); facetSortValues.put("genres", FacetSortValue.COUNT); newFaceting.setSortFacetValuesBy(facetSortValues); client.index("books").updateFacetingSettings(newFaceting); ``` ```ruby Ruby client.index('books').update_faceting( sort_facet_values_by: { genres: 'count' } ) ``` ```go Go client.Index("books").UpdateFaceting(&meilisearch.Faceting{ SortFacetValuesBy: { "genres": SortFacetTypeCount, } }) ``` ```csharp C# var newFaceting = new Faceting { SortFacetValuesBy = new Dictionary { ["genres"] = SortFacetValuesByType.Count } }; await client.Index("books").UpdateFacetingAsync(newFaceting); ``` ```rust Rust 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 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 { … "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 curl \ -X POST 'MEILISEARCH_URL/indexes/books/facet-search' \ -H 'Content-Type: application/json' \ --data-binary '{ "facetQuery": "c", "facetName": "genres" }' ``` ```javascript JS client.index('books').searchForFacetValues({ facetQuery: 'c', facetName: 'genres' }) ``` ```python Python client.index('books').facet_search('genres', 'c') ``` ```php PHP $client->index('books')->facetSearch( (new FacetSearchQuery()) ->setFacetQuery('c') ->setFacetName('genres') ); ``` ```java Java FacetSearchRequest fsr = FacetSearchRequest.builder().facetName("genres").facetQuery("c").build(); client.index("books").facetSearch(fsr); ``` ```ruby Ruby client.index('books').facet_search('genres', 'c') ``` ```go Go client.Index("books").FacetSearch(&meilisearch.FacetSearchRequest{ FacetQuery: "c", FacetName: "genres", ExhaustiveFacetCount: true }) ``` ```csharp C# var query = new SearchFacetsQuery() { FacetQuery = "c" }; await client.Index("books").FacetSearchAsync("genres", query); ``` ```rust Rust let res = client.index("books") .facet_search("genres") .with_facet_query("c") .execute() .await .unwrap(); ``` ```dart Dart 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 { … "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 [ { "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 curl \ -X PUT 'MEILISEARCH_URL/indexes/books/settings/sortable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "author", "price" ]' ``` ```javascript JS client.index('books').updateSortableAttributes([ 'author', 'price' ]) ``` ```python Python client.index('books').update_sortable_attributes([ 'author', 'price' ]) ``` ```php PHP $client->index('books')->updateSortableAttributes([ 'author', 'price' ]); ``` ```java Java client.index("books").updateSortableAttributesSettings(new String[] {"price", "author"}); ``` ```ruby Ruby client.index('books').update_sortable_attributes(['author', 'price']) ``` ```go Go sortableAttributes := []string{ "author", "price", } client.Index("books").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# await client.Index("books").UpdateSortableAttributesAsync(new [] { "price", "author" }); ``` ```rust Rust let sortable_attributes = [ "author", "price" ]; let task: TaskInfo = client .index("books") .set_sortable_attributes(&sortable_attributes) .await .unwrap(); ``` ```swift Swift 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 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 [ "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 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 client.index('books').updateRankingRules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```python Python client.index('books').update_ranking_rules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```php PHP $client->index('books')->updateRankingRules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]); ``` ```java Java Settings settings = new Settings(); settings.setRankingRules(new String[] { "words", "sort", "typo", "proximity", "attribute", "exactness" }); client.index("books").updateSettings(settings); ``` ```ruby Ruby client.index('books').update_ranking_rules([ 'words', 'sort', 'typo', 'proximity', 'attribute', 'exactness' ]) ``` ```go Go rankingRules := []string{ "words", "sort", "typo", "proximity", "attribute", "exactness", } client.Index("books").UpdateRankingRules(&rankingRules) ``` ```csharp C# await client.Index("books").UpdateRankingRulesAsync(new[] { "words", "sort", "typo", "proximity", "attribute", "exactness" }); ``` ```rust Rust let ranking_rules = [ "words", "sort", "typo", "proximity", "attribute", "exactness" ]; let task: TaskInfo = client .index("books") .set_ranking_rules(&ranking_rules) .await .unwrap(); ``` ```swift Swift 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 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 "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 curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "science fiction", "sort": ["price:asc"] }' ``` ```javascript JS client.index('books').search('science fiction', { sort: ['price:asc'], }) ``` ```python Python client.index('books').search('science fiction', { 'sort': ['price:asc'] }) ``` ```php PHP $client->index('books')->search('science fiction', ['sort' => ['price:asc']]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("science fiction").sort(new String[] {"price:asc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby client.index('books').search('science fiction', { sort: ['price:asc'] }) ``` ```go Go resp, err := client.Index("books").Search("science fiction", &meilisearch.SearchRequest{ Sort: []string{ "price:asc", }, }) ``` ```csharp C# var sq = new SearchQuery { Sort = new[] { "price:asc" }, }; await client.Index("books").SearchAsync("science fiction", sq); ``` ```rust Rust let results: SearchResults = client .index("books") .search() .with_query("science fiction") .with_sort(&["price:asc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 await client .index('books') .search('science fiction', SearchQuery(sort: ['price:asc'])); ``` With our example dataset, the results look like this: ```json [ { "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 curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "butler", "sort": ["author:desc"] }' ``` ```javascript JS client.index('books').search('butler', { sort: ['author:desc'], }) ``` ```python Python client.index('books').search('butler', { 'sort': ['author:desc'] }) ``` ```php PHP $client->index('books')->search('butler', ['sort' => ['author:desc']]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("butler").sort(new String[] {"author:desc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby client.index('books').search('butler', { sort: ['author:desc'] }) ``` ```go Go resp, err := client.Index("books").Search("butler", &meilisearch.SearchRequest{ Sort: []string{ "author:desc", }, }) ``` ```csharp C# var sq = new SearchQuery { Sort = new[] { "author:desc" }, }; await client.Index("books").SearchAsync("butler", sq); ``` ```rust Rust let results: SearchResults = client .index("books") .search() .with_query("butler") .with_sort(&["author:desc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 await client .index('books') .search('butler', SearchQuery(sort: ['author:desc'])); ``` ```json [ { "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 curl \ -X POST 'MEILISEARCH_URL/indexes/books/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "science fiction", "sort": ["rating.users:asc"] }' ``` ```javascript JS client.index('books').search('science fiction', { 'sort': ['rating.users:asc'], }) ``` ```python Python client.index('books').search('science fiction', { 'sort': ['rating.users:asc'] }) ``` ```php PHP $client->index('books')->search('science fiction', ['sort' => ['rating.users:asc']]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("science fiction").sort(new String[] {"rating.users:asc"}).build(); client.index("books").search(searchRequest); ``` ```ruby Ruby client.index('books').search('science fiction', { sort: ['rating.users:asc'] }) ``` ```go Go resp, err := client.Index("books").Search("science fiction", &meilisearch.SearchRequest{ Sort: []string{ "rating.users:asc", }, }) ``` ```csharp C# SearchQuery sort = new SearchQuery() { Sort = new string[] { "rating.users:asc" }}; await client.Index("books").SearchAsync("science fiction", sort); ``` ```rust Rust let results: SearchResults = client .index("books") .search() .with_query("science fiction") .with_sort(&["rating.users:asc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 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 [ { "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 curl \ -x POST 'MEILISEARCH_URL/indexes/games/documents' \ -h 'content-type: application/json' \ --data-binary @games.json ``` ```javascript JS const games = require('./games.json') client.index('games').addDocuments(games).then((res) => console.log(res)) ``` ```python Python import json json_file = open('./games.json', encoding='utf-8') games = json.load(json_file) client.index('games').add_documents(games) ``` ```php PHP $gamesJson = file_get_contents('games.json'); $games = json_decode($gamesJson); $client->index('games')->addDocuments($games); ``` ```java Java 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 require 'json' games = JSON.parse(File.read('games.json')) client.index('games').add_documents(games) ``` ```go Go 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# string jsonString = await File.ReadAllTextAsync("games.json"); var games = JsonSerializer.Deserialize>(jsonString, options); var index = client.Index("games"); await index.AddDocumentsAsync(games); ``` ```rust Rust 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 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 //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 curl \ -X PUT 'MEILISEARCH_URL/indexes/games/settings/filterable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "release_timestamp" ]' ``` ```javascript JS client.index('games').updateFilterableAttributes(['release_timestamp']) ``` ```python Python client.index('games').update_filterable_attributes(['release_timestamp']) ``` ```php PHP $client->index('games')->updateFilterableAttributes(['release_timestamp']); ``` ```java Java client.index("movies").updateFilterableAttributesSettings(new String[] { "release_timestamp" }); ``` ```ruby Ruby client.index('games').update_filterable_attributes(['release_timestamp']) ``` ```go Go filterableAttributes := []interface{}{"release_timestamp"} client.Index("games").UpdateFilterableAttributes(&filterableAttributes) ``` ```csharp C# await client.Index("games").UpdateFilterableAttributesAsync(new string[] { "release_timestamp" }); ``` ```rust Rust let settings = Settings::new() .with_filterable_attributes(["release_timestamp"]); let task: TaskInfo = client .index("games") .set_settings(&settings) .await .unwrap(); ``` ```swift Swift client.index("games").updateFilterableAttributes(["release_timestamp"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart 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 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 client.index('games').search('', { filter: 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```python Python client.index('games').search('', { 'filter': 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```php PHP $client->index('games')->search('', [ 'filter' => ['release_timestamp >= 1514761200 AND release_timestamp < 1672527600'] ]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("").filter(new String[] {"release_timestamp >= 1514761200 AND release_timestamp < 1672527600"}).build(); client.index("games").search(searchRequest); ``` ```ruby Ruby client.index('games').search('', { filter: 'release_timestamp >= 1514761200 AND release_timestamp < 1672527600' }) ``` ```go Go client.Index("games").Search("", &meilisearch.SearchRequest{ Filter: "release_timestamp >= 1514761200 AND release_timestamp < 1672527600", }) ``` ```csharp C# var filters = new SearchQuery() { Filter = "release_timestamp >= 1514761200 AND release_timestamp < 1672527600" }; var games = await client.Index("games").SearchAsync("", filters); ``` ```rust Rust let results: SearchResults = client .index("games") .search() .with_filter("release_timestamp >= 1514761200 AND release_timestamp < 1672527600") .execute() .await .unwrap(); ``` ```swift Swift 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 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 curl \ -X PUT 'MEILISEARCH_URL/indexes/games/settings/sortable-attributes' \ -H 'Content-Type: application/json' \ --data-binary '[ "release_timestamp" ]' ``` ```javascript JS client.index('games').updateSortableAttributes(['release_timestamp']) ``` ```python Python client.index('games').update_sortable_attributes(['release_timestamp']) ``` ```php PHP $client->index('games')->updateSortableAttributes(['release_timestamp']); ``` ```java Java Settings settings = new Settings(); settings.setSortableAttributes(new String[] {"release_timestamp"}); client.index("games").updateSettings(settings); ``` ```ruby Ruby client.index('games').update_sortable_attributes(['release_timestamp']) ``` ```go Go sortableAttributes := []string{"release_timestamp","author"} client.Index("games").UpdateSortableAttributes(&sortableAttributes) ``` ```csharp C# await client.Index("games").UpdateSortableAttributesAsync(new string[] { "release_timestamp" }); ``` ```rust Rust let settings = Settings::new() .with_sortable_attributes(["release_timestamp"]); let task: TaskInfo = client .index("games") .set_settings(&settings) .await .unwrap(); ``` ```swift Swift client.index("games").updateSortableAttributes(["release_timestamp"]) { (result) in switch result { case .success(let task): print(task) case .failure(let error): print(error) } } ``` ```dart Dart 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 curl \ -X POST 'MEILISEARCH_URL/indexes/games/search' \ -H 'Content-Type: application/json' \ --data-binary '{ "q": "", "sort": ["release_timestamp:desc"] }' ``` ```javascript JS client.index('games').search('', { sort: ['release_timestamp:desc'], }) ``` ```python Python client.index('games').search('', { 'sort': ['release_timestamp:desc'] }) ``` ```php PHP $client->index('games')->search('', ['sort' => ['release_timestamp:desc']]); ``` ```java Java SearchRequest searchRequest = SearchRequest.builder().q("").sort(new String[] {"release_timestamp:desc"}).build(); client.index("games").search(searchRequest); ``` ```ruby Ruby client.index('games').search('', sort: ['release_timestamp:desc']) ``` ```go Go client.Index("games").Search("", &meilisearch.SearchRequest{ Sort: []string{ "release_timestamp:desc", }, }) ``` ```csharp C# SearchQuery sort = new SearchQuery() { Sort = new string[] { "release_timestamp:desc" }}; await client.Index("games").SearchAsync("", sort); ``` ```rust Rust let results: SearchResults = client .index("games") .search() .with_sort(["release_timestamp:desc"]) .execute() .await .unwrap(); ``` ```swift Swift 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 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).