Introduction
Sorting results: Elasticsearch best practices explained—and done well, this can turn a sluggish search experience into one that feels snappy and relevant. Whether you're building a product listing, a news feed, or a high-velocity trading dashboard, how you sort your results fundamentally shapes what users see first. Getting it wrong means either wasting compute cycles on expensive operations, or returning results in an order that confuses your users. In this article, we'll walk through the proven patterns for sorting in Elasticsearch, covering keyword fields, numeric aggregations, date ranges, and the critical decisions around score, missing values, and field types.
Sort on keyword, number, or date fields
The most efficient sorts in Elasticsearch happen on fields that are already optimized for sorting: keyword, long, integer, double, date, and boolean types. These fielddata-friendly types allow Elasticsearch to build and cache sorted views without heavy computation. When you sort on these types, Elasticsearch loads the field values into memory once and reuses that structure across multiple queries—a huge win for throughput.
Let's say we're building an e-commerce catalog and need to sort products by price, then by newest first. Our mapping might look like:
{
"mappings": {
"properties": {
"product_name": { "type": "text" },
"price": { "type": "long" },
"created_at": { "type": "date" },
"category": { "type": "keyword" }
}
}
}A query that sorts by price ascending, then by creation date descending, would look like:
{
"query": {
"match_all": {}
},
"sort": [
{ "price": { "order": "asc" } },
{ "created_at": { "order": "desc" } }
],
"size": 20
}Notice the sort array maintains order: Elasticsearch groups results by the first sort criterion, then breaks ties with the second. This is efficient because both price and created_at are native numeric/date types with pre-built field data structures.
For numeric fields, be mindful of the actual range. If you're sorting on a long but your values fit comfortably in an integer, you'll save memory. Similarly, for timestamps, store them as date type—not as strings or unix millisecond numbers—so Elasticsearch can optimize comparisons.
Combine score with fields
One of the trickiest decisions in search UX is balancing relevance (search score) with business logic (price, date, popularity). A common mistake is to choose one or the other. Instead, we can sort by multiple criteria where _score is one component, weighted appropriately.
Imagine a travel search where we want to show the most relevant hotels first, but break ties by price. We'd query with a reasonable filter and sort:
{
"query": {
"bool": {
"must": [
{ "match": { "name": "luxury hotel in paris" } }
],
"filter": [
{ "range": { "rating": { "gte": 4.0 } } }
]
}
},
"sort": [
{ "_score": { "order": "desc" } },
{ "price": { "order": "asc" } }
]
}This tells Elasticsearch: "Give me hotels matching my query, sorted by how well they match, and if two hotels have identical scores, show the cheaper one first." The result feels both relevant and fair.
If you want to blend score and field values into a single sort key—say, favoring recent content but still respecting search relevance—use a function_score query or custom sorting with a script. Here's a simpler approach using _score with a secondary field:
{
"query": {
"function_score": {
"query": { "match": { "title": "elasticsearch sorting" } },
"functions": [
{ "weight": 1 }
],
"score_mode": "sum",
"boost_mode": "multiply"
}
},
"sort": [
{ "_score": { "order": "desc" } },
{ "published_at": { "order": "desc" } }
]
}The key insight: combining _score with field sorts lets you layer relevance, recency, and business rules without complexity. Sort by _score first, then by your secondary field. Users get relevant results, ordered sensibly.
Missing values first or last
In production data, missing fields are common. A product might not have a discount price, or a user profile might lack a creation date if migrated from a legacy system. Elasticsearch lets you control where missing values appear in your sort order using the missing parameter.
Here's a realistic scenario: you want to sort products by discount amount (descending), but products without a discount should appear last.
{
"query": { "match_all": {} },
"sort": [
{
"discount_amount": {
"order": "desc",
"missing": "_last"
}
}
],
"size": 50
}Setting "missing": "_last" ensures products without a discount field appear at the end of the result set. Conversely, "missing": "_first" would put them at the start. This prevents nulls from disrupting the perceived quality of your top results.
You can also assign a specific value for missing fields. If you want nulls treated as zero for a numeric field:
{
"sort": [
{
"stock_level": {
"order": "desc",
"missing": 0
}
}
]
}Now items with no stock_level are sorted as if they had a value of 0, which typically pushes them lower in the list when sorting descending. This approach is cleaner than filtering them out entirely, since you preserve them in results—users just see them lower down.
Avoid sorting on text fields
The most common performance pitfall we see is sorting on text fields. By design, text fields are analyzed—tokenized, lowercased, stemmed—making them unsuitable for precise sorting. Elasticsearch must load entire analyzed token arrays into memory, ballooning heap usage and query latency.
Here's an example of what not to do:
{
"mappings": {
"properties": {
"description": {
"type": "text"
}
}
}
}
If you then try to sort by description, Elasticsearch will either reject it or perform terribly:
{
"sort": [
{ "description": { "order": "asc" } }
]
}This is a recipe for high CPU and memory usage, especially at scale. Instead, if you need to sort on something that looks like a description, use a keyword subfield or a separate keyword mapping for the sortable variant:
{
"mappings": {
"properties": {
"description": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}Now you can sort on description.keyword safely:
{
"sort": [
{ "description.keyword": { "order": "asc" } }
]
}The ignore_above parameter truncates very long strings, keeping memory usage bounded. This pattern—analyzing for search via the main field, and sorting via a keyword subfield—is fundamental to Elasticsearch design and should be your default for any field you might sort on.
Common mistakes
- Sorting on text fields without a keyword subfield: This causes memory bloat and slow queries. Always add a keyword variant to text fields you might sort on.
- Using multiple sort clauses in the wrong order: Elasticsearch evaluates them left to right. Put your most important sort criterion first, or you'll get unexpected tie-breaking behavior.
- Ignoring missing values: Results with missing fields can cluster unexpectedly. Use
missing: "_last"ormissing: "_first"to be explicit. - Combining too many sort criteria: More than 2–3 sort fields usually signals you should revise your query logic or use scripting. Extra criteria add negligible accuracy but real latency.
- Not testing sort performance with realistic data volume: A sort that feels fast with 10,000 documents can crater with 10 million. Profile early with production-scale data.
Key takeaways
- Sort on
keyword, numeric, anddatefields; they're optimized for speed and memory efficiency. - Combine
_scorewith field sorts to balance relevance and business logic in a single query. - Use
missing: "_first"ormissing: "_last"to control where null values appear, preventing surprises in your result set. - Never sort on
textfields directly. Use akeywordsubfield or separate mapping so Elasticsearch can use fielddata efficiently. - Keep sort clauses minimal (2–3 max) and order them by importance. Test with production-scale data to catch performance issues early.






Comments
Loading comments…