Segments and Codecs
A Lucene index is not one file — it is a pile of segments, each segment a
small, self-contained, immutable mini-index, plus one tiny commit file that
says "this set of segments, right now, is the index." Every byte Lucene reads at
query time comes from a segment; every byte it writes goes into a new segment.
Nothing is ever modified in place. Understanding this immutability, the commit
point that ties segments together, the per-segment file zoo, and the Codec SPI
that defines those files' formats is the foundation for every other chapter in
this section.
This is the Lucene-level companion to OpenSearch's
Refresh, Flush, and Merge: that chapter
told you a flush is an IndexWriter.commit() that writes segments_N; here you
learn what that file is and what the segments it points to actually contain.
After this chapter you can: list a segment's files and say what each holds;
explain why immutability shapes the entire engine (deletes, merges, NRT);
describe the Codec SPI and its five sub-formats; explain how a Codec is
discovered via META-INF/services; and set the codec on an OpenSearch index.
Immutable segments and the commit point
When IndexWriter flushes its in-RAM buffer, it writes a brand-new segment and
never touches it again. This single design choice explains an enormous amount:
| Consequence of immutability | Why |
|---|---|
| Deletes don't delete | You can't edit a segment, so a delete just marks the doc dead in a side liveDocs bitset (.liv). The bytes stay until a merge drops them. |
| Updates = delete + add | An update writes a new doc in a new segment and tombstones the old one. |
| Lock-free reads | A DirectoryReader over immutable segments needs no locking against writers — readers and writers never contend on the same bytes. |
| Cheap NRT | A refresh just opens a reader over the segments that exist now; no copying. |
| Merges are mandatory | Segment count and dead-doc debt grow forever without merging — see IndexWriter and Merge Internals. |
The index's current state is a commit point: a file named segments_N
(generation N, monotonically increasing) listing exactly which segments are
live and which codec wrote each. In Lucene this is the SegmentInfos object.
# In any index directory, the commit point is the segments_N file:
ls -la /path/to/index/ | grep segments
# segments_7 <- the current commit (generation 7)
# write.lock <- the IndexWriter lock
grep -rn "class SegmentInfos\|class SegmentCommitInfo\|class SegmentInfo " \
lucene/core/src/java/org/apache/lucene/index/
flowchart TD
SN["segments_7 (SegmentInfos / commit point)"] --> S0["_0 (segment)"]
SN --> S1["_1 (segment)"]
SN --> S2["_5 (segment, merged)"]
S0 --> F0["_0.si _0.cfs _0.cfe ..."]
S1 --> F1["_1.fnm _1.tim _1.doc ..."]
S2 --> F2["_5.dvd _5.kdd _5.vec ..."]
Note: A commit (
segments_N) is what survives a crash. Refreshed-but-not- committed segments are visible to search but recovered from the translog after a crash, not from a commit. Visibility (refresh) and durability (commit/flush) are independent — the OpenSearch engine chapter calls this out, and it is exactly the Lucene segment/commit distinction.
The per-segment file zoo
Each segment _N is a set of files sharing the prefix _N, one per data
structure. (When compound mode is on, most of them are packed into a single
.cfs with a .cfe table of contents — fewer file handles.) Memorize this
table; you'll read these extensions for the rest of your career.
| Extension(s) | Structure | Holds | Covered in |
|---|---|---|---|
.si | SegmentInfo | per-segment metadata: doc count, codec, files, diagnostics | this chapter |
.cfs / .cfe | Compound file + entries | all other files packed into one + its directory | this chapter |
.fnm | Field infos | the field list: name, number, index options, doc-values type, points/vector dims | this chapter |
.fdt / .fdx / .fdm | Stored fields data / index / meta | the original _source-style stored field values, compressed | this chapter |
.tim / .tip / .tmd | Terms dictionary / index / meta | the BlockTree terms dict + its FST index | Inverted Index |
.doc / .pos / .pay | Postings: docs / positions / payloads+offsets | the inverted lists per term | Inverted Index |
.dvd / .dvm | DocValues data / meta | columnar per-doc values | DocValues |
.nvd / .nvm | Norms data / meta | per-field length norms used in scoring | Inverted Index |
.kdd / .kdi / .kdm | Points data / index / meta | the BKD tree for numeric/geo points | Points & BKD |
.vec / .vex / .vem | Vector data / graph / meta | HNSW float/byte vectors + the graph | HNSW |
.liv | Live docs | the deletion bitset (which docs are still alive) | IndexWriter |
Go look at a real one. Index a few docs into OpenSearch, find the shard, and
find its files:
# 1. Locate the data path and a shard directory:
curl -s 'localhost:9200/_nodes/settings?filter_path=**.path.data' | python3 -m json.tool
# <data>/nodes/0/indices/<index-uuid>/<shard>/index
# 2. List the segment files by extension:
SHARD=/path/to/<data>/nodes/0/indices/<uuid>/0/index
find "$SHARD" -maxdepth 1 -type f | sed 's/.*\.//' | sort | uniq -c
ls -la "$SHARD"
# 3. Ask OpenSearch to describe the same segments:
curl -s 'localhost:9200/my-index/_segments?pretty' | head -60
curl -s 'localhost:9200/_cat/segments/my-index?v'
If you only see .cfs/.cfe/.si per segment, the segment is compound.
Small segments are compound by default; large merged ones are usually
non-compound (the per-file overhead amortizes away). Luke
(Lab L1) shows the same picture
visually.
The Codec SPI
A Codec defines the on-disk format of all those files. It is the central
extension point of Lucene's storage layer, wired through Java's
ServiceLoader (the SPI mechanism). A Codec is really a bundle of sub-format
factories:
| Sub-format | Defines the format of | Files |
|---|---|---|
PostingsFormat | terms dictionary + postings | .tim/.tip/.tmd/.doc/.pos/.pay |
DocValuesFormat | columnar doc values | .dvd/.dvm |
StoredFieldsFormat | stored field values | .fdt/.fdx/.fdm |
PointsFormat | BKD point trees | .kdd/.kdi/.kdm |
KnnVectorsFormat | HNSW vectors + graph | .vec/.vex/.vem |
NormsFormat | scoring norms | .nvd/.nvm |
FieldInfosFormat | the field metadata | .fnm |
SegmentInfoFormat | per-segment metadata | .si |
LiveDocsFormat / CompoundFormat | deletes / compound packing | .liv / .cfs/.cfe |
The default codec is versioned — LuceneNNNCodec, where NNN bumps when the
on-disk format changes. Do not hard-code the number; grep for it, because a
Lucene upgrade in OpenSearch is precisely "the default codec version moved."
# Find the current default codec class in your Lucene checkout:
ls lucene/core/src/java/org/apache/lucene/codecs/lucene*/
grep -rln "extends Codec\b" lucene/core/src/java/org/apache/lucene/codecs/
# The default is also declared as a service — see next section.
grep -rn "Codec.getDefault\|setDefault" lucene/core/src/java/org/apache/lucene/codecs/Codec.java
A codec's sub-formats are themselves SPI-pluggable and versioned independently.
This is why a single codec can read older segments written by an older codec
(each segment records the codec that wrote it, in its .si) — Lucene keeps the
old format readers around for one major version of back-compat. That back-compat
window is the deep reason an OpenSearch major can read indices from the previous
major but not from two majors ago.
SPI registration via META-INF/services
A Codec (or any sub-format) is discovered at runtime by listing its
fully-qualified class name in a META-INF/services file named for the SPI
interface. No central registry, no config — drop the jar on the classpath with
the right service file and Lucene finds it. This is exactly how OpenSearch's k-NN
plugin ships its own vector codec.
# The default codec registers itself here, in Lucene's core jar:
find lucene -path "*META-INF/services/org.apache.lucene.codecs.Codec"
cat lucene/core/src/resources/META-INF/services/org.apache.lucene.codecs.Codec
# And the sub-format services:
find lucene -path "*META-INF/services/org.apache.lucene.codecs.*Format"
# META-INF/services/org.apache.lucene.codecs.Codec
org.apache.lucene.codecs.lucene101.Lucene101Codec
# (number illustrative — grep your checkout for the real one)
To write your own, you extend Codec (usually FilterCodec, delegating most
sub-formats and overriding one), then add a line to the service file. That is the
whole of Lab L2: Write a custom codec.
Per-field codecs
A codec doesn't have to use one format for the entire index — it can choose a
different format per field. Lucene exposes this through
PerFieldPostingsFormat, PerFieldDocValuesFormat, and
PerFieldKnnVectorsFormat: the codec is asked, per field name, which format to
use. This is how a single index can have, say, a high-compression postings format
on a rarely-queried field and the fast default on a hot one — and how the k-NN
plugin attaches its HNSW format only to knn_vector fields while leaving the rest
on the standard codec.
grep -rn "PerFieldPostingsFormat\|PerFieldDocValuesFormat\|PerFieldKnnVectorsFormat\|getPostingsFormatForField" \
lucene/core/src/java/org/apache/lucene/codecs/perfield/
The mechanism: the codec returns a PerField*Format, whose getXForField(field)
returns the concrete format; Lucene records the chosen format name per field in
the .fnm so the reader can re-instantiate the right one. Override one method,
target one field — clean.
How OpenSearch chooses the codec
OpenSearch exposes the codec choice as the index setting index.codec. It
does not let you pick an arbitrary Lucene codec by class; it maps a few friendly
names to codec configurations (chiefly different stored-fields compression).
index.codec | Effect |
|---|---|
default | the standard Lucene codec, LZ4 stored-fields compression (fast) |
best_compression | the standard codec with DEFLATE/Zstd-style higher stored-fields compression (smaller, slower to fetch _source) |
| (others, version-dependent) | e.g. specialized compression — grep to see what your version registers |
# Set it at index creation:
curl -s -XPUT 'localhost:9200/logs' -H 'Content-Type: application/json' -d'
{ "settings": { "index.codec": "best_compression" } }'
# Where OpenSearch maps the name to a Lucene codec:
grep -rn "index.codec\|CodecService\|best_compression\|BEST_COMPRESSION\|Lucene.*Codec" \
server/src/main/java/org/opensearch/index/codec/CodecService.java \
server/src/main/java/org/opensearch/index/engine/EngineConfig.java 2>/dev/null
best_compression is the standard answer when disk is the constraint and _source
fetches are rare (log/metrics indices). It only affects stored fields
compression, not postings/points/docvalues — a common misconception. The k-NN
plugin layers its own codec on top of whichever index.codec you pick, adding
the vector format per knn_vector field.
Reading exercise
# 1. The commit point and its segments.
grep -rn "class SegmentInfos\|class SegmentInfo \|class SegmentCommitInfo" \
lucene/core/src/java/org/apache/lucene/index/
# 2. The default codec and its sub-formats.
ls lucene/core/src/java/org/apache/lucene/codecs/lucene*/
grep -rln "extends Codec\b\|extends FilterCodec" lucene/core/src/java/org/apache/lucene/codecs/
# 3. SPI registration files.
find lucene -path "*META-INF/services/org.apache.lucene.codecs.*"
# 4. Per-field formats.
grep -rn "getPostingsFormatForField\|getKnnVectorsFormatForField" \
lucene/core/src/java/org/apache/lucene/codecs/perfield/
# 5. Real files in a real shard.
find /path/to/shard/index -maxdepth 1 -type f | sed 's/.*\.//' | sort | uniq -c
curl -s 'localhost:9200/my-index/_segments?pretty' | head -40
# 6. OpenSearch's codec setting.
grep -rn "best_compression\|index.codec\|CodecService" \
server/src/main/java/org/opensearch/index/codec/
Answer:
- Why are segments immutable, and name three consequences (one of them about deletes, one about reads, one about merges).
- What does
segments_Ncontain, and what is the Lucene class for it? Why does each segment record the codec that wrote it? - Given the files
_3.si _3.tim _3.doc _3.kdd _3.dvd _3.vec, say what each one holds and which chapter covers it. - List the five most-used sub-formats of a
Codecand the file extensions each produces. - Explain SPI registration: how does Lucene find a custom codec at runtime without any central config?
- What exactly does
index.codec: best_compressionchange, and what does it not change? Why would you choose it for a logs index?
Common bugs and symptoms
| Symptom | Root cause | Where to look |
|---|---|---|
| Index won't open: "format version is not supported" | a segment written by a newer/older codec than this Lucene can read (back-compat window exceeded) | the segment's .si codec; the Lucene upgrade story |
| Too many open files | non-compound segments × many segments; file-descriptor limit | enable compound; merge; _cat/segments |
best_compression didn't shrink the index | it only compresses stored fields; your size is postings/docvalues/points | profile per-file sizes with find/du; _segments |
| Custom codec "not found" at runtime | missing/typo'd META-INF/services line, or jar not on classpath | the service file; the SPI loader |
k-NN vector files (.vec) missing on a knn_vector field | per-field vector format not attached; wrong codec wired | the plugin's codec; index.knn; per-field format |
| Disk grows without bound | dead docs never reclaimed; merges starved | _cat/segments deleted.docs; merge internals |
Validation: prove you understand this
- Draw an index as a
segments_Ncommit pointing at three segments, and one segment as its set of extension files; label what each file holds. - Explain immutability and derive from it: how deletes work, why reads need no lock against writes, and why merges are mandatory.
- Define the
CodecSPI and list its sub-formats and their file extensions. Show theMETA-INF/servicesline that registers a codec. - Explain per-field formats and give the k-NN-plugin example of why you'd want one format on one field only.
- State precisely what
index.codec: best_compressiondoes and does not affect, and the workload it suits. - From a
findover a real shard, classify every file extension you see and say which chapter of this section owns it.