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 text field is blocked by default and what fielddata: true actually costs.
  • Name the four doc-values flavors and which field types use each.
  • Describe global ordinals and why they make high-cardinality keyword aggs viable.
  • Read the fielddata circuit breaker and diagnose a heap blow-up.

Two columnar stores, very different costs

DocValuesFielddata
Whereon disk (columnar segment files), OS page cacheon JVM heap
Builtat index time, written with the segmentlazily at query time by uninverting the inverted index
Default forkeyword, numerics, date, boolean, ip, geo_pointnothing — must be enabled per text field
Costcheap, OS-cached, scales with dataexpensive, heap-bound, can OOM the node
Used forsort, aggregate, script field access, some queriesmaking 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 DocValuesTypeHoldsOpenSearch field types
NUMERICone number per doclong, integer, double, date (as epoch), boolean
SORTED_NUMERICmultiple numbers per doc, sortednumeric fields that are multi-valued
SORTEDone term ordinal per docsingle-valued keyword-like
SORTED_SETa set of term ordinals per docmulti-valued keyword, ip
BINARYraw bytes per docgeo_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:

ClassRole
IndexFieldData<FD>per-field, per-index accessor; produces LeafFieldData per segment
LeafFieldDataper-segment view; yields SortedNumericDocValues / SortedSetDocValues-style accessors
IndexFieldDataServicebuilds/caches IndexFieldData instances per field, wired to the breaker
IndexFieldData.Buildereach 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 keyword aggregations live or die on global ordinals. If you aggregate a field with millions of distinct values on every query, consider eager_global_ordinals, or reconsider whether you need a full terms agg vs a composite/cardinality approach.


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:

  1. Why can't a text field have doc values in any meaningful way? Tie it to analysis producing a token stream rather than a single value.
  2. Walk the path a terms aggregation takes to read a keyword field's values: from IndexFieldData to the ordinals it buckets on.
  3. What is a global ordinal and why is it needed in addition to per-segment ordinals? What event invalidates the cached global ordinals?
  4. What does eager_global_ordinals: true change about when the global ordinal build cost is paid, and why might you turn it on?
  5. 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.
  6. Write the mapping that lets you full-text search a field and aggregate on it without ever enabling fielddata.

Common bugs and symptoms

SymptomLikely causeWhere to look
Fielddata is disabled on text fields by defaultsorting/aggregating a text fielduse a keyword multi-field; TextFieldMapper
Node OOM / GC death after enabling fielddata: trueuninverting a high-cardinality analyzed field into heaprevert; _cat/fielddata; multi-field pattern
CircuitBreakingException: [fielddata] Data too largefielddata exceeded indices.breaker.fielddata.limitCircuit Breakers and Memory, _cache/clear?fielddata=true
First terms agg after each refresh is slow, later ones fastglobal ordinals rebuilt lazily after segment changeeager_global_ordinals: true
Sorting a numeric field returns nonsense / errorsfield indexed without doc values (doc_values: false)mapping; NumberFieldMapper.hasDocValues
High-cardinality keyword agg slow + heavyglobal ordinal build per shard dominateseager_global_ordinals, reconsider composite/cardinality
_cat/fielddata shows large entries you didn't expectscripts or aggs forced fielddata on some fieldaudit aggs/scripts; clear cache

Validation: prove you understand this

  1. State the one-sentence rule for when OpenSearch uses doc values vs fielddata, and the default availability of each for keyword, numeric, and text fields.
  2. Explain to a teammate why fielddata: true on a text field is almost always the wrong fix for "I can't aggregate this field," and give the correct mapping.
  3. Define local vs global ordinals with a two-segment example, and name the moment the global ordinal cache is invalidated.
  4. Trace, class by class (MappedFieldTypeIndexFieldDataLeafFieldData → Lucene doc-values accessor), how a terms agg reads a keyword field.
  5. Explain why doc values do not trip the fielddata breaker but fielddata does, in terms of where each lives in memory.
  6. 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.