DocValues and Fielddata
A Lucene inverted index is excellent at answering "which documents contain term
X" and terrible at answering "for this document, what is the value of field Y."
Sorting, aggregating, and scripting all need the second access pattern — given a
doc, get its value — which is columnar. OpenSearch satisfies it two ways:
doc values (on disk, columnar, the default) and fielddata (in-heap, built
from the inverted index, the legacy fallback that exists mainly to make text
sortable/aggregatable and is off by default for good reason).
This chapter explains both, why keyword and text behave so differently, and
how global ordinals make terms aggregations fast. It underpins
Aggregations and Search Execution
(sorting), and connects to Circuit Breakers and Memory
(the fielddata breaker) and Mapping and Analysis
(which field types get doc values).
After this chapter you can:
- Explain why sorting/aggregating on a
textfield is blocked by default and whatfielddata: trueactually costs. - Name the four doc-values flavors and which field types use each.
- Describe global ordinals and why they make high-cardinality
keywordaggs viable. - Read the fielddata circuit breaker and diagnose a heap blow-up.
Two columnar stores, very different costs
| DocValues | Fielddata | |
|---|---|---|
| Where | on disk (columnar segment files), OS page cache | on JVM heap |
| Built | at index time, written with the segment | lazily at query time by uninverting the inverted index |
| Default for | keyword, numerics, date, boolean, ip, geo_point | nothing — must be enabled per text field |
| Cost | cheap, OS-cached, scales with data | expensive, heap-bound, can OOM the node |
| Used for | sort, aggregate, script field access, some queries | making text sortable/aggregatable (rarely a good idea) |
grep -rn "DocValuesType\|SortedSetDocValues\|hasDocValues\|docValues" \
server/src/main/java/org/opensearch/index/mapper/ | head
grep -rn "fielddata\|FieldDataType\|IndexFieldData" \
server/src/main/java/org/opensearch/index/fielddata/ | head
DocValues flavors
Lucene stores doc values in one of a few column types; OpenSearch picks the type from the field's mapping.
Lucene DocValuesType | Holds | OpenSearch field types |
|---|---|---|
NUMERIC | one number per doc | long, integer, double, date (as epoch), boolean |
SORTED_NUMERIC | multiple numbers per doc, sorted | numeric fields that are multi-valued |
SORTED | one term ordinal per doc | single-valued keyword-like |
SORTED_SET | a set of term ordinals per doc | multi-valued keyword, ip |
BINARY | raw bytes per doc | geo_point, custom binary |
The key idea for strings: doc values store ordinals, not the strings themselves. A per-segment sorted dictionary maps ordinal → term. To sort or bucket, you compare ordinals (cheap integers) and only resolve the actual term when you need to emit it.
grep -rn "SortedSetDocValues\|SortedNumericDocValues\|NumericDocValues\|BinaryDocValues" \
server/src/main/java/org/opensearch/index/fielddata/ | head
The IndexFieldData abstraction
OpenSearch reads doc values (and fielddata) through a uniform interface so aggregations and sorting don't care which backing store is used:
| Class | Role |
|---|---|
IndexFieldData<FD> | per-field, per-index accessor; produces LeafFieldData per segment |
LeafFieldData | per-segment view; yields SortedNumericDocValues / SortedSetDocValues-style accessors |
IndexFieldDataService | builds/caches IndexFieldData instances per field, wired to the breaker |
IndexFieldData.Builder | each MappedFieldType returns one via fielddataBuilder(...) |
grep -rn "interface IndexFieldData\|class LeafFieldData\|IndexFieldDataService\|fielddataBuilder" \
server/src/main/java/org/opensearch/index/fielddata/
grep -rn "fielddataBuilder" server/src/main/java/org/opensearch/index/mapper/ | head
A terms aggregator (see Aggregations) asks the field's
IndexFieldData for SortedSetDocValues, then buckets by ordinal. A sort on a
numeric field asks for SortedNumericDocValues. Same abstraction, different
backing.
Global ordinals — the trick that makes terms aggs fast
Ordinals are per-segment: ordinal 5 in segment A and ordinal 5 in segment B may be different terms. To aggregate across a whole shard you need a shard-global numbering: global ordinals, a mapping from each segment's local ordinals to a single shard-wide ordinal space.
flowchart LR
SA["Segment A local ords<br/>0=apple 1=pear"] --> GO[Global ordinal map]
SB["Segment B local ords<br/>0=pear 1=plum"] --> GO
GO --> G["Shard-global ords<br/>0=apple 1=pear 2=plum"]
G --> AGG[terms agg buckets by global ord]
Global ordinals are built lazily on first aggregation and cached per
shard, rebuilt when segments change (a refresh that adds segments). They cost
memory and a build pass, which is why the first terms agg after a refresh can
be slower than subsequent ones. eager_global_ordinals: true in the mapping
pre-builds them at refresh time to move that cost off the query path.
grep -rn "GlobalOrdinal\|buildGlobalOrdinals\|eager_global_ordinals\|OrdinalMap" \
server/src/main/java/org/opensearch/index/fielddata/
Note: High-cardinality
keywordaggregations live or die on global ordinals. If you aggregate a field with millions of distinct values on every query, considereager_global_ordinals, or reconsider whether you need a fulltermsagg vs acomposite/cardinalityapproach.
Why text is special — and why sorting/aggregating it is blocked
A text field is analyzed: "Open Source Engine" becomes tokens
[open, source, engine]. There is no single value to sort by, and text fields
do not get doc values (it wouldn't be meaningful for a token stream). So by
default:
// Sorting or aggregating a `text` field:
"Fielddata is disabled on text fields by default. Set fielddata=true on
[your_field] in order to load fielddata in memory by uninverting the inverted
index. Note that this can use significant memory."
That error is OpenSearch protecting your heap. Enabling fielddata: true makes
the node uninvert the entire inverted index for that field into heap at query
time — for a high-cardinality analyzed field across millions of docs, that is a
classic node-OOM recipe.
The right answer is the multi-field pattern: index the field as text for
search and as a keyword sub-field for sort/aggregate:
{
"title": {
"type": "text",
"fields": { "raw": { "type": "keyword" } }
}
}
Then search title (analyzed) and aggregate/sort title.raw (doc values, cheap).
This is the idiom; fielddata: true is the exception you reach for almost never.
grep -rn "Fielddata is disabled\|fielddata.*default\|TextFieldMapper" \
server/src/main/java/org/opensearch/index/mapper/TextFieldMapper.java | head
The fielddata circuit breaker
Because fielddata lives on heap and is built lazily, it is the most dangerous
memory consumer in the system. The fielddata circuit breaker
(indices.breaker.fielddata.limit, default ~40% of heap) caps total fielddata
heap; exceeding it throws CircuitBreakingException instead of OOM-ing the node.
DocValues do not count against it (they're off-heap / page cache). See
Circuit Breakers and Memory for the full breaker
hierarchy.
grep -rn "fielddata\|FIELDDATA\|CircuitBreaker.FIELDDATA" \
server/src/main/java/org/opensearch/indices/breaker/HierarchyCircuitBreakerService.java
curl -s 'localhost:9200/_nodes/stats/breaker?pretty' | grep -A6 fielddata
Inspect actual fielddata heap usage and evict it:
curl -s 'localhost:9200/_cat/fielddata?v&h=node,field,size'
curl -s -XPOST 'localhost:9200/idx/_cache/clear?fielddata=true'
Reading exercise
# 1. Where field types declare doc values and build fielddata
grep -rn "hasDocValues\|fielddataBuilder\|fielddata(" \
server/src/main/java/org/opensearch/index/mapper/KeywordFieldMapper.java \
server/src/main/java/org/opensearch/index/mapper/TextFieldMapper.java \
server/src/main/java/org/opensearch/index/mapper/NumberFieldMapper.java
# 2. The IndexFieldData abstraction + caching + breaker wiring
grep -rn "IndexFieldDataService\|CircuitBreakerService\|getForField\|clearField" \
server/src/main/java/org/opensearch/index/fielddata/IndexFieldDataService.java
# 3. Global ordinals
grep -rn "GlobalOrdinalsBuilder\|buildGlobalOrdinals\|eager_global_ordinals" \
server/src/main/java/org/opensearch/index/fielddata/
# 4. The famous error
grep -rn "Fielddata is disabled" server/src/main/java/org/opensearch/index/mapper/
Answer:
- Why can't a
textfield have doc values in any meaningful way? Tie it to analysis producing a token stream rather than a single value. - Walk the path a
termsaggregation takes to read akeywordfield's values: fromIndexFieldDatato the ordinals it buckets on. - What is a global ordinal and why is it needed in addition to per-segment ordinals? What event invalidates the cached global ordinals?
- What does
eager_global_ordinals: truechange about when the global ordinal build cost is paid, and why might you turn it on? - Compare where doc values and fielddata live (disk/page-cache vs heap) and explain why only one of them counts against the fielddata circuit breaker.
- Write the mapping that lets you full-text search a field and aggregate on it without ever enabling fielddata.
Common bugs and symptoms
| Symptom | Likely cause | Where to look |
|---|---|---|
Fielddata is disabled on text fields by default | sorting/aggregating a text field | use a keyword multi-field; TextFieldMapper |
Node OOM / GC death after enabling fielddata: true | uninverting a high-cardinality analyzed field into heap | revert; _cat/fielddata; multi-field pattern |
CircuitBreakingException: [fielddata] Data too large | fielddata exceeded indices.breaker.fielddata.limit | Circuit Breakers and Memory, _cache/clear?fielddata=true |
First terms agg after each refresh is slow, later ones fast | global ordinals rebuilt lazily after segment change | eager_global_ordinals: true |
| Sorting a numeric field returns nonsense / errors | field indexed without doc values (doc_values: false) | mapping; NumberFieldMapper.hasDocValues |
High-cardinality keyword agg slow + heavy | global ordinal build per shard dominates | eager_global_ordinals, reconsider composite/cardinality |
_cat/fielddata shows large entries you didn't expect | scripts or aggs forced fielddata on some field | audit aggs/scripts; clear cache |
Validation: prove you understand this
- State the one-sentence rule for when OpenSearch uses doc values vs fielddata,
and the default availability of each for
keyword, numeric, andtextfields. - Explain to a teammate why
fielddata: trueon atextfield is almost always the wrong fix for "I can't aggregate this field," and give the correct mapping. - Define local vs global ordinals with a two-segment example, and name the moment the global ordinal cache is invalidated.
- Trace, class by class (
MappedFieldType→IndexFieldData→LeafFieldData→ Lucene doc-values accessor), how atermsagg reads akeywordfield. - Explain why doc values do not trip the fielddata breaker but fielddata does, in terms of where each lives in memory.
- Given a 50-shard index where the first dashboard query each minute is slow, diagnose it as a global-ordinals symptom and propose the mapping change that fixes it.