The Elasticsearch terms query returns documents whose indexed value for a field exactly matches one of the supplied terms. It is the multi-value equivalent of term - effectively a WHERE field IN (...) clause in SQL. The query performs no analysis on the input, so it matches against the exact tokens already in the inverted index. Use it on keyword, numeric, date, or ip fields, or on text fields where you know the post-analysis token.
Syntax
GET /index/_search
{
"query": {
"terms": {
"field_name": ["value1", "value2", "value3"],
"boost": 1.0
}
}
}
Terms-lookup form (fetch the list from another document):
GET /index/_search
{
"query": {
"terms": {
"field_name": {
"index": "lookups",
"id": "user-42",
"path": "allowed_tags",
"routing": "user-42"
}
}
}
}
Parameters
| Parameter | Description | Required | Default |
|---|---|---|---|
<field> |
Field name. Its value is either an array of terms or a terms-lookup object. | Yes | - |
boost |
Score multiplier applied at the top of the query body. | No | 1.0 |
<field>.index |
(Terms lookup) Index that holds the document with the terms list. | Yes for lookup | - |
<field>.id |
(Terms lookup) Document ID containing the terms list. | Yes for lookup | - |
<field>.path |
(Terms lookup) Field path inside the source document. Supports dot notation. | Yes for lookup | - |
<field>.routing |
(Terms lookup) Custom routing value for fetching the source document. | No | - |
The number of terms is capped by index.max_terms_count, default 65536. Exceeding it raises an error.
Examples
Basic multi-value match on a keyword field:
GET /products/_search
{
"query": {
"terms": {
"color.keyword": ["red", "blue", "green"]
}
}
}
In a filter context so the query is unscored and cacheable:
GET /events/_search
{
"query": {
"bool": {
"filter": [
{ "terms": { "status": ["active", "pending"] } },
{ "range": { "@timestamp": { "gte": "now-1d/d" } } }
]
}
}
}
Terms lookup - pull the values from another document (typical pattern: per-user allowlists):
GET /articles/_search
{
"query": {
"terms": {
"tags": {
"index": "user-prefs",
"id": "user-42",
"path": "followed_tags"
}
}
}
}
Combine with must_not to exclude a set of values:
GET /events/_search
{
"query": {
"bool": {
"filter": [ { "range": { "@timestamp": { "gte": "now-7d/d" } } } ],
"must_not": [ { "terms": { "level": ["debug", "trace"] } } ]
}
}
}
Performance and Use Notes
The terms query is implemented as a disjunction of term queries with shared scoring. On keyword and numeric fields, lookups against the term dictionary are fast and the query enters the per-node query cache when used in filter context. The terms-lookup form executes an extra GET against the source index on every search; that GET is also cached when the source document is unchanged, but it adds latency for cold lookups and depends on the source index's availability.
The terms count limit (index.max_terms_count, default 65536) protects against pathologically large queries. Hitting the limit means you should either move the list into a terms-lookup document or restructure the query - for hundreds of thousands of values, denormalize a tag into the indexed document instead of filtering at search time. The query is case-sensitive: the supplied terms must match the indexed terms byte for byte.
Bloated terms lists are a common cause of slow searches and cluster CPU pressure. Pulse inspects Elasticsearch query traffic and flags terms queries with large value sets or expensive terms-lookups, with suggestions to consolidate into a join or denormalized field.
Common Mistakes
- Sending mixed-case terms against a
keywordfield indexed without alowercasenormalizer. Match is byte-exact -"Red"will not match"red". - Running
termsagainst an analyzedtextfield. The query bypasses analysis, so multi-word values rarely match. Use the.keywordsub-field. - Hitting
index.max_terms_countwith a huge IN list. Switch to a terms lookup or denormalize. - Placing
termsin amustclause when no score is needed. Move it tofilterto skip scoring and enable caching. - Using a terms lookup against a high-churn document. Each update invalidates the cached result and the next query pays a fresh GET.
Frequently Asked Questions
Q: How is the terms query different from a match query?
A: A terms query performs exact, byte-for-byte matching against indexed terms with no analysis. A match query analyzes the input with the field's search analyzer before matching. Use terms for keyword/exact-value fields; use match for full-text search on analyzed text.
Q: Is there a limit on the number of terms in a terms query?
A: Yes. index.max_terms_count defaults to 65536. Exceeding the limit raises an error. For large lists, prefer a terms lookup or restructure the data so the predicate becomes a single field test.
Q: How do I make a terms query case-insensitive?
A: Apply a lowercase normalizer to the keyword field at mapping time and lowercase the supplied terms in your application. Unlike term, the terms query has no case_insensitive parameter as of the current stable docs.
Q: What does the terms-lookup mechanism do?
A: Terms lookup tells Elasticsearch to fetch the list of values from a field in another document at query time. It is the idiomatic way to express per-user allowlists, follow graphs, or any "include only X's tags" predicate without inlining the full list in every request.
Q: Can the terms query match values inside nested objects?
A: Only when wrapped in a nested query pointing at the nested path. A bare terms query against nested_field.value will not respect the per-element relationship guarantees of nested mappings.
Q: Should I use a terms query or many should clauses in a bool query?
A: A single terms query is faster and cleaner than a bool with many should term clauses, both because it produces one disjunction internally and because it does not consume should clause budget against indices.query.bool.max_clause_count.
Related Reading
- Elasticsearch Query Language: overview of the query DSL.
- Bool Query: combine
termswith other predicates. - Range Query: pair with
termsfilters in time-bounded searches. - Exists Query: test field presence before matching values.
- Nested Query: use when target values live inside nested objects.