Tune for search speed
editTune for search speed
editGive memory to the filesystem cache
editElasticsearch heavily relies on the filesystem cache in order to make search fast. In general, you should make sure that at least half the available memory goes to the filesystem cache so that Elasticsearch can keep hot regions of the index in physical memory.
Use faster hardware
editIf your search is I/O bound, you should investigate giving more memory to the
filesystem cache (see above) or buying faster drives. In particular SSD drives
are known to perform better than spinning disks. Always use local storage,
remote filesystems such as NFS
or SMB
should be avoided. Also beware of
virtualized storage such as Amazon’s Elastic Block Storage
. Virtualized
storage works very well with Elasticsearch, and it is appealing since it is so
fast and simple to set up, but it is also unfortunately inherently slower on an
ongoing basis when compared to dedicated local storage. If you put an index on
EBS
, be sure to use provisioned IOPS otherwise operations could be quickly
throttled.
If your search is CPU-bound, you should investigate buying faster CPUs.
Document modeling
editDocuments should be modeled so that search-time operations are as cheap as possible.
In particular, joins should be avoided. nested
can make queries
several times slower and parent-child relations can make
queries hundreds of times slower. So if the same questions can be answered without
joins by denormalizing documents, significant speedups can be expected.
Search as few fields as possible
editThe more fields a query_string
or
multi_match
query targets, the slower it is.
A common technique to improve search speed over multiple fields is to copy
their values into a single field at index time, and then use this field at
search time. This can be automated with the copy-to
directive of
mappings without having to change the source of documents. Here is an example
of an index containing movies that optimizes queries that search over both the
name and the plot of the movie by indexing both values into the name_and_plot
field.
PUT movies { "mappings": { "properties": { "name_and_plot": { "type": "text" }, "name": { "type": "text", "copy_to": "name_and_plot" }, "plot": { "type": "text", "copy_to": "name_and_plot" } } } }
Pre-index data
editYou should leverage patterns in your queries to optimize the way data is indexed.
For instance, if all your documents have a price
field and most queries run
range
aggregations on a fixed
list of ranges, you could make this aggregation faster by pre-indexing the ranges
into the index and using a terms
aggregations.
For instance, if documents look like:
PUT index/_doc/1 { "designation": "spoon", "price": 13 }
and search requests look like:
GET index/_search { "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 10 }, { "from": 10, "to": 100 }, { "from": 100 } ] } } } }
Then documents could be enriched by a price_range
field at index time, which
should be mapped as a keyword
:
PUT index { "mappings": { "properties": { "price_range": { "type": "keyword" } } } } PUT index/_doc/1 { "designation": "spoon", "price": 13, "price_range": "10-100" }
And then search requests could aggregate this new field rather than running a
range
aggregation on the price
field.
GET index/_search { "aggs": { "price_ranges": { "terms": { "field": "price_range" } } } }
Consider mapping identifiers as keyword
editNot all numeric data should be mapped as a numeric field datatype.
Elasticsearch optimizes numeric fields, such as integer
or long
, for
range
queries. However, keyword
fields
are better for term
and other
term-level queries.
Identifiers, such as an ISBN or a product ID, are rarely used in range
queries. However, they are often retrieved using term-level queries.
Consider mapping a numeric identifier as a keyword
if:
-
You don’t plan to search for the identifier data using
range
queries. -
Fast retrieval is important.
term
query searches onkeyword
fields are often faster thanterm
searches on numeric fields.
If you’re unsure which to use, you can use a multi-field to map
the data as both a keyword
and a numeric datatype.
Avoid scripts
editIf possible, avoid using scripts or scripted fields in searches. Because scripts can’t make use of index structures, using scripts in search queries can result in slower search speeds.
If you often use scripts to transform indexed data, you can speed up search by making these changes during ingest instead. However, that often means slower index speeds.
Example
An index, my_test_scores
, contains two long
fields:
-
math_score
-
verbal_score
When running searches, users often use a script to sort results by the sum of these two field’s values.
GET /my_test_scores/_search { "query": { "term": { "grad_year": "2020" } }, "sort": [ { "_script": { "type": "number", "script": { "source": "doc['math_score'].value + doc['verbal_score'].value" }, "order": "desc" } } ] }
To speed up search, you can perform this calculation during ingest and index the sum to a field instead.
First, add a new field, total_score
, to the index. The
total_score
field will contain sum of the math_score
and verbal_score
field values.
PUT /my_test_scores/_mapping { "properties": { "total_score": { "type": "long" } } }
Next, use an ingest pipeline containing the
script
processor to calculate the sum of math_score
and
verbal_score
and index it in the total_score
field.
PUT _ingest/pipeline/my_test_scores_pipeline { "description": "Calculates the total test score", "processors": [ { "script": { "source": "ctx.total_score = (ctx.math_score + ctx.verbal_score)" } } ] }
To update existing data, use this pipeline to reindex any
documents from my_test_scores
to a new index, my_test_scores_2
.
POST /_reindex { "source": { "index": "my_test_scores" }, "dest": { "index": "my_test_scores_2", "pipeline": "my_test_scores_pipeline" } }
Continue using the pipeline to index any new documents to my_test_scores_2
.
POST /my_test_scores_2/_doc/?pipeline=my_test_scores_pipeline { "student": "kimchy", "grad_year": "2020", "math_score": 800, "verbal_score": 800 }
These changes may slow indexing but allow for faster searches. Users can now
sort searches made on my_test_scores_2
using the total_score
field instead
of using a script.
GET /my_test_scores_2/_search { "query": { "term": { "grad_year": "2020" } }, "sort": [ { "total_score": { "order": "desc" } } ] }
We recommend testing and benchmarking any indexing changes before deploying them in production.
Search rounded dates
editQueries on date fields that use now
are typically not cacheable since the
range that is being matched changes all the time. However switching to a
rounded date is often acceptable in terms of user experience, and has the
benefit of making better use of the query cache.
For instance the below query:
PUT index/_doc/1 { "my_date": "2016-05-11T16:30:55.328Z" } GET index/_search { "query": { "constant_score": { "filter": { "range": { "my_date": { "gte": "now-1h", "lte": "now" } } } } } }
could be replaced with the following query:
GET index/_search { "query": { "constant_score": { "filter": { "range": { "my_date": { "gte": "now-1h/m", "lte": "now/m" } } } } } }
In that case we rounded to the minute, so if the current time is 16:31:29
,
the range query will match everything whose value of the my_date
field is
between 15:31:00
and 16:31:59
. And if several users run a query that
contains this range in the same minute, the query cache could help speed things
up a bit. The longer the interval that is used for rounding, the more the query
cache can help, but beware that too aggressive rounding might also hurt user
experience.
It might be tempting to split ranges into a large cacheable part and smaller not cacheable parts in order to be able to leverage the query cache, as shown below:
GET index/_search { "query": { "constant_score": { "filter": { "bool": { "should": [ { "range": { "my_date": { "gte": "now-1h", "lte": "now-1h/m" } } }, { "range": { "my_date": { "gt": "now-1h/m", "lt": "now/m" } } }, { "range": { "my_date": { "gte": "now/m", "lte": "now" } } } ] } } } } }
However such practice might make the query run slower in some cases since the
overhead introduced by the bool
query may defeat the savings from better
leveraging the query cache.
Force-merge read-only indices
editIndices that are read-only may benefit from being merged down to a single segment. This is typically the case with time-based indices: only the index for the current time frame is getting new documents while older indices are read-only. Shards that have been force-merged into a single segment can use simpler and more efficient data structures to perform searches.
Do not force-merge indices to which you are still writing, or to which you will write again in the future. Instead, rely on the automatic background merge process to perform merges as needed to keep the index running smoothly. If you continue to write to a force-merged index then its performance may become much worse.
Warm up global ordinals
editGlobal ordinals are a data-structure that is used in order to run
terms
aggregations on
keyword
fields. They are loaded lazily in memory because
Elasticsearch does not know which fields will be used in terms
aggregations
and which fields won’t. You can tell Elasticsearch to load global ordinals
eagerly when starting or refreshing a shard by configuring mappings as
described below:
PUT index { "mappings": { "properties": { "foo": { "type": "keyword", "eager_global_ordinals": true } } } }
Warm up the filesystem cache
editIf the machine running Elasticsearch is restarted, the filesystem cache will be
empty, so it will take some time before the operating system loads hot regions
of the index into memory so that search operations are fast. You can explicitly
tell the operating system which files should be loaded into memory eagerly
depending on the file extension using the
index.store.preload
setting.
Loading data into the filesystem cache eagerly on too many indices or too many files will make search slower if the filesystem cache is not large enough to hold all the data. Use with caution.
Use index sorting to speed up conjunctions
editIndex sorting can be useful in order to make conjunctions faster at the cost of slightly slower indexing. Read more about it in the index sorting documentation.
Use preference
to optimize cache utilization
editThere are multiple caches that can help with search performance, such as the filesystem cache, the request cache or the query cache. Yet all these caches are maintained at the node level, meaning that if you run the same request twice in a row, have 1 replica or more and use round-robin, the default routing algorithm, then those two requests will go to different shard copies, preventing node-level caches from helping.
Since it is common for users of a search application to run similar requests one after another, for instance in order to analyze a narrower subset of the index, using a preference value that identifies the current user or session could help optimize usage of the caches.
Replicas might help with throughput, but not always
editIn addition to improving resiliency, replicas can help improve throughput. For instance if you have a single-shard index and three nodes, you will need to set the number of replicas to 2 in order to have 3 copies of your shard in total so that all nodes are utilized.
Now imagine that you have a 2-shards index and two nodes. In one case, the number of replicas is 0, meaning that each node holds a single shard. In the second case the number of replicas is 1, meaning that each node has two shards. Which setup is going to perform best in terms of search performance? Usually, the setup that has fewer shards per node in total will perform better. The reason for that is that it gives a greater share of the available filesystem cache to each shard, and the filesystem cache is probably Elasticsearch’s number 1 performance factor. At the same time, beware that a setup that does not have replicas is subject to failure in case of a single node failure, so there is a trade-off between throughput and availability.
So what is the right number of replicas? If you have a cluster that has
num_nodes
nodes, num_primaries
primary shards in total and if you want to
be able to cope with max_failures
node failures at once at most, then the
right number of replicas for you is
max(max_failures, ceil(num_nodes / num_primaries) - 1)
.