Search
Search
Full-text search with support for filters, faceted search, sorting, highlighting, and hybrid vector search.
Search endpoint
/v1/searchSearch an index.
- Auth
- admin or search key
- Returns
- 200
{
"index": "products",
"q": "macbook",
"limit": 10,
"offset": 0,
"filter": "price < 2000",
"sort": ["price:asc"],
"facets": ["category"],
"attributesToHighlight": ["title"],
"attributesToRetrieve": [
"id",
"title",
"price"
]
}{
"hits": [
{
"id": 1,
"title": "MacBook Pro",
"price": 1999,
"_formatted": {
"title": "<em>MacBook</em> Pro"
}
}
],
"query": "macbook",
"processingTimeMs": 2,
"estimatedTotalHits": 42,
"facetDistribution": {
"category": {
"laptops": 12,
"phones": 30
}
}
}Parameters
A search key returns every field of each hit unless you ask for fewer. Anyone with the key can ask for all of them, so do not index private fields. See What a search key can read. To search several indexes in one call, see Multi-search.
Each search that answers counts as one search on your plan. Searches you run from the dashboard's Search tab are not counted or billed, though they still show in Analytics.
Matching every word
On Meilisearch, a query of several words does not need every word to match. The default, "last", drops words from the end until enough documents match, and typo tolerance then applies to what is left. The best matches still come first, but the total and the later pages fill up with weaker ones. On a test index, "beats headphones" matched 5 documents with the default: the two Beats headphones, a Beats speaker, "Vacuum belts" and "Coffee beans". With "all" it matched only the two headphones.
{
"index": "products",
"q": "beats headphones",
"matchingStrategy": "all"
}Use "all" for product and catalog search, where a shopper means every word. "frequency" drops the most common words first.
Paging and totals
Each engine stops somewhere. The answer tells you where, with fields that appear only when they apply.
{
"index": "products",
"q": "",
"page": 3,
"hitsPerPage": 20
}
{
"hits": [ ... ],
"page": 3,
"hitsPerPage": 20,
"totalHits": 10002,
"totalPages": 50,
"maxTotalHits": 1000
}Build page links from totalPages, not from the total. On Meilisearch you can raise the limit with the pagination.maxTotalHits setting. To read every document, use the cursor on GET /v1/documents with an admin key.
Search errors
A search the engine cannot run answers 400 with a code, and a message that names the field and the fix. Index names in messages are your index uid, never the engine's internal name.
{
"error": "Can't facet on `category`: …",
"code": "invalid_search_facets"
}In multi-search, a query that fails gets the same error and code in its own place in results, and the other queries still answer. All codes are on the Errors page.
Filters
Filter expressions let you narrow search results. Attributes must be listed in filterableAttributes in your index settings before they can be used in filters. That is the rule on Meilisearch and Visor. Typesense and Elasticsearch filter on every field without it, but on Elasticsearch an exact match on a text field needs <field>.keyword. See Index into more than one engine.
// Simple comparison
"price < 2000"
// Equality
"category = laptops"
// Combined with AND / OR
"price < 2000 AND category = laptops"
"category = laptops OR category = tv"
// IN operator
"category IN [laptops, phones, tablets]"
// Negation
"NOT category = accessories"
// Nested with parentheses
"(price < 500 OR price > 2000)" +
" AND category = laptops"A filter can also be an array of expressions, and every engine ANDs its items. Each item stays a whole clause: on Typesense, Elasticsearch and Visor each item is read on its own in the portable syntax (comparisons, IN, EXISTS, AND, OR and parentheses), and an item that does not parse is refused with 400. Meilisearch reads each item itself. To use an engine's own syntax, such as NOT on Visor or Typesense's filter_by, send one string.
"filter": [
"tenant_id = 42",
"category = laptops OR category = tv"
]Facets
Pass attribute names in the facets array to receive a distribution of values for each attribute. Useful for building filter UIs with counts.
"facetDistribution": {
"category": {
"laptops": 12,
"phones": 30,
"tablets": 8
}
}A number facet also comes back in facetStats with its lowest and highest value among the hits, for a price slider. Every engine returns it. On Typesense the field must be in filterableAttributes, as every facet must.
"facetStats": {
"price": { "min": 12.5, "max": 1999 }
}Sorting
Sort results by one or more attributes. Attributes must be listed in sortableAttributes. Use the format attribute:direction where direction is asc or desc.
"sort": ["price:asc", "title:desc"]Geo search
Find documents near a point, inside a box or inside a polygon, and sort them by distance. Send geo with /v1/search or with any query of /v1/multi-search. It works with your own filter and sort: the geo filter is added to yours, and the distance sort comes first.
{
"index": "stores",
"q": "",
"geo": {
"around": {
"lat": 48.8606,
"lng": 2.3376,
"radius": 5000
},
"sort": "asc"
}
}"hits": [
{ "id": "louvre", "_geoDistance": 0 },
{ "id": "notre-dame", "_geoDistance": 1234 }
]Each document keeps its point in a _geo field, in the shape its engine reads. Put _geo in filterableAttributes, and on Meilisearch in sortableAttributes too to sort by distance.
Declare _geo on Typesense and Elasticsearch before you send documents when you can. On Typesense, declaring it over documents that already hold [lat, lng] converts the field in place, with no reindex; any other shape is refused and nothing changes. On Elasticsearch, a field that documents have already typed cannot change: the settings call answers 400 reindex_required, and you need a new index with _geo declared before its first document.
A bad value answers 400 invalid_geo on every engine. A part the engine cannot run, such as a polygon on Meilisearch or a distance sort on Visor, answers 400 feature_not_supported.
Highlighting
Request highlighted versions of attributes to show which parts of the text matched the query. Matched text is wrapped in <em> tags, or in highlightPreTag and highlightPostTag where the engine applies them. Each hit then carries _formatted: its fields, with the highlighted ones marked and the cropped ones cut to a snippet. A field with no match keeps its value.
"_formatted": {
"title": "<em>MacBook</em> Pro 16-inch",
"description": "…the new <em>MacBook</em> Pro with a 16-inch…"
}