Please describe the end goal of this project
Summary
This is the DSL-compatibility fallback for the DSL Query Executor Plugin
(#20914). That plugin intercepts _search and routes
supported queries to the analytics engine (Calcite -> analytics executor); .This codec is used whenever a query is not routed
through #20914, so it still executes correctly on the native Lucene path.
1. Problem & goal
Goal: serve Parquet-resident columns as Lucene doc values at query time, so the standard DSL
aggregation path works unchanged. It is read-only — the composite indexing engine remains the
sole writer of Parquet files — and it is a permanent DSL-compatibility path, not scaffolding.
2. Where it lives
All new code lives in parquet-data-format:
- Types + native bridge:
ParquetPhysicalType, FieldTypeMapping, the read functions in ffm.rs,
and their RustBridge / NativeCall binds (NativeCall is in dataformat-native).
- Codec:
ParquetColumnReader, the page cache, the five DocValues iterators, OrdinalTable,
ParquetDocValuesProducer, ParquetDocValuesFormat.
- Routing:
ParquetDocValuesLeafReader + ParquetDocValuesDirectoryReader, installed by
ParquetDataFormatPlugin.onIndexModule. The leaf reader synthesizes a FieldInfo per
Parquet-resident field and serves its values from the producer.
3. Components
Lucene search / aggregation
│ asks for doc values of field "age"
▼
ParquetDocValuesDirectoryReader (wraps the DirectoryReader)
│ per leaf
▼
ParquetDocValuesLeafReader (synthesizes FieldInfo; overrides the 5 DV accessors)
│ getNumeric / getSortedSet / ...
▼
ParquetDocValuesProducer (one per segment; picks the iterator per field)
│
┌─────┴───────────────┬───────────────────────────┐
▼ ▼ ▼
iter/Parquet*DocValues OrdinalTable (keyword/ip) DocValuesSkipper (null; not served)
│ │
└─────────┬───────────┘
▼
bridge/ParquetColumnReader (Java side of the FFM bridge; page cache)
│ RustBridge.* (MethodHandles)
▼
ffm.rs (native) (opens & decodes the Parquet column)
Type/descriptor helpers used throughout: ParquetPhysicalType (the 0–5 code exchanged with
Rust) and FieldTypeMapping (OpenSearch type → Lucene DocValuesType + Parquet physical type).
4. Read flow
Aggregation over Parquet-only age (a long), size:0. There are two phases: setup (get the
iterator once per segment) and per-document reads (called for every matching doc).
Setup — the aggregator obtains a DocValues iterator once:
- Aggregator calls
leafReader.getSortedNumericDocValues("age").
- Leaf reader sees
age has a synthetic (Parquet) FieldInfo, so it calls
producer.getNumeric(fieldInfo).
- Producer opens a
ParquetColumnReader (→ native open_column_reader, which validates the
column's physical type) and constructs a ParquetNumericDocValues iterator over it.
- The iterator is returned up to the aggregator (wrapped as singleton
SortedNumeric).
Per-document — the aggregator drives the returned iterator:
- For each matching doc, the aggregator calls
iterator.advanceExact(doc) — this call lands on
the ParquetNumericDocValues from step 4, not on the leaf reader or producer. doc is used
directly as the Parquet row (Lucene doc N == Parquet row N in a finished segment, guaranteed
by the write/merge path — no docId→row translation).
- The iterator asks its
ParquetColumnReader for that row. If the row's page is not already cached,
the reader calls native decode_page_at_row, which decodes the whole page (~20k rows) into
the Java-side PageCache.
iterator.longValue() returns the value for doc from the cached page.
The first read of a page pays one FFM crossing; every subsequent row in that page is served from the
PageCache with no crossing. A retained forward cursor on the native side keeps an ascending scan
from reopening the row group (and re-decoding the dictionary) on the next page.
6. Native read path (ffm.rs)
The native side exposes a small C-ABI surface, additive to the existing writer/merge functions. All
row indices are global (file-relative).
Return convention. 0 = success (RC_OK); 1 = RC_OVERFLOW (a caller buffer was too small;
required sizes are written to the out-parameters so the caller grows and retries once); < 0 = a
negated error pointer. Overflow is a positive sentinel so it can never be mistaken for an error
pointer.
Functions.
parquet_open_column_reader / parquet_close_column_reader — open a per-column reader (validating
the column exists and its physical type matches), returning an opaque i64 handle; close it.
parquet_open_column_reader_count — number of open handles (leak diagnostics).
parquet_read_value_at_row / parquet_read_repeated_at_row — single-value / multi-value point
reads at one row.
parquet_get_column_num_pages / parquet_get_column_page_index — page count, then the per-page
row-range jump table + stats (first row, file offset, size, null count, min/max).
parquet_decode_page_at_row — decode the whole page containing a row into caller buffers: a
per-row value buffer plus a packed presence bitset (one bit per row; distinguishes a real 0 from
a null). Manages the retained forward cursor.
parquet_evict_file_metadata — drop a file's cached metadata on shard close.
State & caching. A node-level FileMetadataCache (keyed by path) parses the footer and every
column's page layout once; Parquet files are immutable, so entries never need invalidation. Each open
column reader owns a handle in a registry, its row-group layout, a retained decode cursor, and reused
scratch buffers so steady-state decoding is allocation-free.
Two outputs, two buffers. A page decode writes values and presence into separate caller-owned
(off-heap) buffers: the value buffer holds one i64 slot per row (nulls = 0); the presence bitset
holds one bit per row. Both are required because a stored 0 is otherwise indistinguishable from a
null. expand_to_outbuf writes the whole decoded page into the value buffer; the Java PageCache
then serves any row in that page by index with no further FFM crossing.
7. Key classes & methods (LLD)
ParquetPhysicalType (enum)
INT32(0) INT64(1) FLOAT(2) DOUBLE(3) BOOL(4) BYTE_ARRAY(5).
int code() — the discriminant sent to Rust (parquet_open_column_reader's expected_type); MUST
match ffm.rs TYPE_*. boolean isPrimitive() — false only for BYTE_ARRAY.
FieldTypeMapping
record Mapping(DocValuesType singleValued, DocValuesType multiValued, ParquetPhysicalType physical).
boolean isSupported(String type) — is there a mapping? (leaf reader gate)
Mapping forType(String type) — throws IllegalArgumentException for unmapped types
void validate(String field, String type, DocValuesType requested) — throws on incompatible DV type
Mapped: boolean/byte/short/integer/long/float/double/date/date_nanos → NUMERIC(+SORTED_NUMERIC);
keyword/ip → SORTED(+SORTED_SET); text/binary → BINARY. Unmapped types (half_float, unsigned_long,
scaled_float, token_count, version, wildcard, constant_keyword, flat_object, match_only_text) throw
rather than serve wrong values. (scaled_float is stored as a raw scaled long, so it is not a plain
INT64 mapping.)
ParquetColumnReader
static ParquetColumnReader open(Path, String column, ParquetPhysicalType, boolean repeated, BufferPool).
Value readValueAtRow(long row) — record Value(boolean present, long bits)
byte[] readBytesAtRow(long row) / byte[][] readRepeatedBytesAtRow(long)
RepeatedValues readRepeatedAtRow(long row) — record RepeatedValues(long[] bits)
void loadPageContaining(long row) → PageCache cache() — decode-a-page + cache
ColumnPageIndex pageIndex()
Owns the native handle and its lifecycle; the page cache and buffer pool live on the Java side.
OrdinalTable
static OrdinalTable buildSingleValued(reader, numRows) / buildMultiValued(...);
int ordForRow(int row) (and ordForRow(row, i) / countForRow for multi); BytesRef lookupOrd(int ord);
int valueCount(). Honors Lucene's per-segment-ordinal contract (ordinals ascend with term order).
The five iterators
ParquetNumericDocValues, ParquetSortedNumericDocValues, ParquetBinaryDocValues
(all take a ParquetColumnReader); ParquetSortedDocValues, ParquetSortedSetDocValues (take an
OrdinalTable). Each implements advanceExact / nextDoc / value accessor; nulls report absent.
ParquetDocValuesProducer extends DocValuesProducer
Built per segment; verifies numRows == maxDoc at construction (a mismatch throws
IllegalStateException). Accessors getNumeric, getSortedNumeric, getBinary, getSorted,
getSortedSet build the matching iterator; getSkipper returns null (no skipper is served);
checkIntegrity, close.
ParquetDocValuesFormat extends DocValuesFormat
fieldsProducer(SegmentReadState) → new ParquetDocValuesProducer(...);
fieldsConsumer(...) throws (read-only). SPI name "ParquetDocValues".
ParquetDocValuesLeafReader extends SequentialStoredFieldsLeafReader
static LeafReader wrapIfApplicable(LeafReader in, MapperService, QueryParquetStats) — wraps only
when a Parquet file resolves for the segment AND the mapping has a supported field missing Lucene DV.
- Synthesizes a
FieldInfo per Parquet-resident field (DV type from FieldTypeMapping;
skip-index type NONE, since no field claims a skipper the producer does not serve).
- Overrides the five DV accessors to serve from the producer (delegates all other fields);
getDocValuesSkipper → null for Parquet fields.
- Extends
SequentialStoredFieldsLeafReader so _source retrieval (size>0) works through the
derived-source path.
ParquetDocValuesDirectoryReader extends FilterDirectoryReader
static wrap(DirectoryReader, MapperService); its SubReaderWrapper calls
ParquetDocValuesLeafReader.wrapIfApplicable per leaf.
8. Design decisions & rationale
- Read a whole page per FFM crossing, cache it on the Java side. Parquet values are only
decodable in page-sized units, and the DocValues API is random-access. Decoding the page once and
serving subsequent rows from an off-heap PageCache amortizes the crossing over ~20k rows.
- Retained forward cursor. A Parquet column reader is a forward-only stream. Keeping it across
ascending page decodes lets a scan skip forward instead of reopening the row group and re-decoding
the dictionary each page; a backward jump or row-group change transparently reopens.
- Values and presence are separate outputs. A stored
0 is a valid value, so nullability is
carried in a parallel presence bitset rather than sentinel-encoded into the value.
- Unmapped types throw. The mapping table rejects types it cannot faithfully represent, so a
query never silently reads wrong values for an unsupported field.
- Off-heap value buffers, heap page index. The large per-page value buffer stays off-heap (stable
address for native writes, no GC copy); the small page-index arrays are copied to the Java heap
once at open, where they are convenient and GC-managed.
Supporting References
Parent issue: #19902
Issues
[]
Related component
Search
Please describe the end goal of this project
Summary
This is the DSL-compatibility fallback for the DSL Query Executor Plugin
(#20914). That plugin intercepts
_searchand routessupported queries to the analytics engine (Calcite -> analytics executor); .This codec is used whenever a query is not routed
through #20914, so it still executes correctly on the native Lucene path.
1. Problem & goal
Goal: serve Parquet-resident columns as Lucene doc values at query time, so the standard DSL
aggregation path works unchanged. It is read-only — the composite indexing engine remains the
sole writer of Parquet files — and it is a permanent DSL-compatibility path, not scaffolding.
2. Where it lives
All new code lives in
parquet-data-format:ParquetPhysicalType,FieldTypeMapping, the read functions inffm.rs,and their
RustBridge/NativeCallbinds (NativeCallis indataformat-native).ParquetColumnReader, the page cache, the five DocValues iterators,OrdinalTable,ParquetDocValuesProducer,ParquetDocValuesFormat.ParquetDocValuesLeafReader+ParquetDocValuesDirectoryReader, installed byParquetDataFormatPlugin.onIndexModule. The leaf reader synthesizes aFieldInfoperParquet-resident field and serves its values from the producer.
3. Components
Type/descriptor helpers used throughout:
ParquetPhysicalType(the 0–5 code exchanged withRust) and
FieldTypeMapping(OpenSearch type → Lucene DocValuesType + Parquet physical type).4. Read flow
Aggregation over Parquet-only
age(along),size:0. There are two phases: setup (get theiterator once per segment) and per-document reads (called for every matching doc).
Setup — the aggregator obtains a DocValues iterator once:
leafReader.getSortedNumericDocValues("age").agehas a synthetic (Parquet)FieldInfo, so it callsproducer.getNumeric(fieldInfo).ParquetColumnReader(→ nativeopen_column_reader, which validates thecolumn's physical type) and constructs a
ParquetNumericDocValuesiterator over it.SortedNumeric).Per-document — the aggregator drives the returned iterator:
iterator.advanceExact(doc)— this call lands onthe
ParquetNumericDocValuesfrom step 4, not on the leaf reader or producer.docis useddirectly as the Parquet row (Lucene doc
N== Parquet rowNin a finished segment, guaranteedby the write/merge path — no docId→row translation).
ParquetColumnReaderfor that row. If the row's page is not already cached,the reader calls native
decode_page_at_row, which decodes the whole page (~20k rows) intothe Java-side
PageCache.iterator.longValue()returns the value fordocfrom the cached page.The first read of a page pays one FFM crossing; every subsequent row in that page is served from the
PageCachewith no crossing. A retained forward cursor on the native side keeps an ascending scanfrom reopening the row group (and re-decoding the dictionary) on the next page.
6. Native read path (
ffm.rs)The native side exposes a small C-ABI surface, additive to the existing writer/merge functions. All
row indices are global (file-relative).
Return convention.
0= success (RC_OK);1=RC_OVERFLOW(a caller buffer was too small;required sizes are written to the out-parameters so the caller grows and retries once);
< 0= anegated error pointer. Overflow is a positive sentinel so it can never be mistaken for an error
pointer.
Functions.
parquet_open_column_reader/parquet_close_column_reader— open a per-column reader (validatingthe column exists and its physical type matches), returning an opaque
i64handle; close it.parquet_open_column_reader_count— number of open handles (leak diagnostics).parquet_read_value_at_row/parquet_read_repeated_at_row— single-value / multi-value pointreads at one row.
parquet_get_column_num_pages/parquet_get_column_page_index— page count, then the per-pagerow-range jump table + stats (first row, file offset, size, null count, min/max).
parquet_decode_page_at_row— decode the whole page containing a row into caller buffers: aper-row value buffer plus a packed presence bitset (one bit per row; distinguishes a real
0froma null). Manages the retained forward cursor.
parquet_evict_file_metadata— drop a file's cached metadata on shard close.State & caching. A node-level
FileMetadataCache(keyed by path) parses the footer and everycolumn's page layout once; Parquet files are immutable, so entries never need invalidation. Each open
column reader owns a handle in a registry, its row-group layout, a retained decode cursor, and reused
scratch buffers so steady-state decoding is allocation-free.
Two outputs, two buffers. A page decode writes values and presence into separate caller-owned
(off-heap) buffers: the value buffer holds one
i64slot per row (nulls = 0); the presence bitsetholds one bit per row. Both are required because a stored
0is otherwise indistinguishable from anull.
expand_to_outbufwrites the whole decoded page into the value buffer; the JavaPageCachethen serves any row in that page by index with no further FFM crossing.
7. Key classes & methods (LLD)
ParquetPhysicalType(enum)INT32(0) INT64(1) FLOAT(2) DOUBLE(3) BOOL(4) BYTE_ARRAY(5).int code()— the discriminant sent to Rust (parquet_open_column_reader'sexpected_type); MUSTmatch
ffm.rsTYPE_*.boolean isPrimitive()— false only forBYTE_ARRAY.FieldTypeMappingrecord Mapping(DocValuesType singleValued, DocValuesType multiValued, ParquetPhysicalType physical).boolean isSupported(String type)— is there a mapping? (leaf reader gate)Mapping forType(String type)— throwsIllegalArgumentExceptionfor unmapped typesvoid validate(String field, String type, DocValuesType requested)— throws on incompatible DV typeMapped: boolean/byte/short/integer/long/float/double/date/date_nanos → NUMERIC(+SORTED_NUMERIC);
keyword/ip → SORTED(+SORTED_SET); text/binary → BINARY. Unmapped types (half_float, unsigned_long,
scaled_float, token_count, version, wildcard, constant_keyword, flat_object, match_only_text) throw
rather than serve wrong values. (
scaled_floatis stored as a raw scaled long, so it is not a plainINT64 mapping.)
ParquetColumnReaderstatic ParquetColumnReader open(Path, String column, ParquetPhysicalType, boolean repeated, BufferPool).Value readValueAtRow(long row)—record Value(boolean present, long bits)byte[] readBytesAtRow(long row)/byte[][] readRepeatedBytesAtRow(long)RepeatedValues readRepeatedAtRow(long row)—record RepeatedValues(long[] bits)void loadPageContaining(long row)→PageCache cache()— decode-a-page + cacheColumnPageIndex pageIndex()Owns the native handle and its lifecycle; the page cache and buffer pool live on the Java side.
OrdinalTablestatic OrdinalTable buildSingleValued(reader, numRows)/buildMultiValued(...);int ordForRow(int row)(andordForRow(row, i)/countForRowfor multi);BytesRef lookupOrd(int ord);int valueCount(). Honors Lucene's per-segment-ordinal contract (ordinals ascend with term order).The five iterators
ParquetNumericDocValues,ParquetSortedNumericDocValues,ParquetBinaryDocValues(all take a
ParquetColumnReader);ParquetSortedDocValues,ParquetSortedSetDocValues(take anOrdinalTable). Each implementsadvanceExact/nextDoc/ value accessor; nulls report absent.ParquetDocValuesProducer extends DocValuesProducerBuilt per segment; verifies
numRows == maxDocat construction (a mismatch throwsIllegalStateException). AccessorsgetNumeric,getSortedNumeric,getBinary,getSorted,getSortedSetbuild the matching iterator;getSkipperreturns null (no skipper is served);checkIntegrity,close.ParquetDocValuesFormat extends DocValuesFormatfieldsProducer(SegmentReadState)→new ParquetDocValuesProducer(...);fieldsConsumer(...)throws (read-only). SPI name"ParquetDocValues".ParquetDocValuesLeafReader extends SequentialStoredFieldsLeafReaderstatic LeafReader wrapIfApplicable(LeafReader in, MapperService, QueryParquetStats)— wraps onlywhen a Parquet file resolves for the segment AND the mapping has a supported field missing Lucene DV.
FieldInfoper Parquet-resident field (DV type fromFieldTypeMapping;skip-index type NONE, since no field claims a skipper the producer does not serve).
getDocValuesSkipper→ null for Parquet fields.SequentialStoredFieldsLeafReaderso_sourceretrieval (size>0) works through thederived-source path.
ParquetDocValuesDirectoryReader extends FilterDirectoryReaderstatic wrap(DirectoryReader, MapperService); itsSubReaderWrappercallsParquetDocValuesLeafReader.wrapIfApplicableper leaf.8. Design decisions & rationale
decodable in page-sized units, and the DocValues API is random-access. Decoding the page once and
serving subsequent rows from an off-heap
PageCacheamortizes the crossing over ~20k rows.ascending page decodes lets a scan skip forward instead of reopening the row group and re-decoding
the dictionary each page; a backward jump or row-group change transparently reopens.
0is a valid value, so nullability iscarried in a parallel presence bitset rather than sentinel-encoded into the value.
query never silently reads wrong values for an unsupported field.
address for native writes, no GC copy); the small page-index arrays are copied to the Java heap
once at open, where they are convenient and GC-managed.
Supporting References
Parent issue: #19902
Issues
[]
Related component
Search