Skip to main content

vortex_layout/layouts/indexed/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! A prototype of the `vortex.indexed` layout proposed in
5//! [vortex-data/vortex#9024](https://github.com/vortex-data/vortex/issues/9024).
6//!
7//! A `vortex.indexed` layout wraps a data layout with zero or more *locating indexes* held as
8//! auxiliary children. Writers build indexes through the pluggable [`IndexVTable`] registry while
9//! streaming; readers probe those indexes to prune (or outright answer) filter predicates, and
10//! fall back to a plain scan of the data child whenever an index is missing, unknown, or has no
11//! claim on the expression.
12//!
13//! Indexes are optional at every stage. A builder that finds nothing worth keeping declines once
14//! the stream is drained — see [`IndexBuilder::finish`] — and if every builder declines, no wrapper
15//! is written at all and the file carries the plain data layout.
16//!
17//! # Shape
18//!
19//! ```text
20//! vortex.indexed
21//! ├── child 0: data      Transparent("data")
22//! ├── child 1: index #0  Auxiliary("index:<id>")
23//! └── child n: index #n  Auxiliary("index:<id>")
24//! ```
25//!
26//! Index content is written through an ordinary layout strategy, so it is chunked, zone-mapped,
27//! and compressed by the same machinery as data. Probing an index prunes the index child's chunks
28//! through the generic reader, so a sorted key column's zone map narrows the probe to a handful of
29//! chunks, and each surviving chunk is decoded at most once per reader, however many expressions
30//! probe it.
31//!
32//! # Serialization
33//!
34//! An indexed layout is serialized in three layers, each with one place that defines it:
35//!
36//! - **The layout's own metadata**, the [`IndexedMetadata`] protobuf behind a version byte: one
37//!   spec per index child, recording the kind's id, its options blob, the index child's dtype and
38//!   its partitioning. Pinned by the `indexed.metadata` goldenfile.
39//! - **Each kind's options and content**, defined by the kind's [`IndexVTable`] and nothing else:
40//!   `serialize_options`/`deserialize_options`, the schema `index_dtype`, and `encode`/`decode`
41//!   between the kind's chunks and rows of that schema. Builders hand the writer chunks, never
42//!   bytes. `test_harness::check_roundtrip` checks a kind's pairs without writing a layout.
43//! - **The index child's layout**, which is whatever layout strategy the writer chose, serialized
44//!   like any other layout. Any layout is allowed, so long as its dtype is the spec's.
45//!
46//! The dtype is what ties the layers together, and it is checked at every boundary: the writer
47//! rejects an encoded chunk that differs from the kind's declared dtype, deserializing rejects an
48//! index child that differs from its spec's dtype, and a reader skips an index whose spec's dtype
49//! the kind no longer declares, so a kind can change its schema without misreading old files.
50//!
51//! # Nesting
52//!
53//! Both child slots hold an ordinary [`LayoutRef`], so nesting is not special-cased anywhere in
54//! this module — it falls out of every child being read and written through the generic
55//! [`crate::LayoutReader`] and [`crate::LayoutStrategy`] traits:
56//!
57//! - **Data child.** Slot 0 can be any layout — chunked, zoned, struct, flat, or another
58//!   `vortex.indexed` — since [`writer::IndexedStrategy`] writes it through a plain
59//!   `Arc<dyn LayoutStrategy>` and `IndexedReader` reads it back through the generic
60//!   reader trait rather than assuming a concrete kind. A data child that is itself wrapped in
61//!   `vortex.indexed` prunes with its own indexes when the outer wrapper delegates a
62//!   `pruning_evaluation`/`filter_evaluation` call to it, so a struct layout whose fields are
63//!   independently indexed, or two stacked `vortex.indexed` layers with different index kinds,
64//!   both work without this layout knowing about it.
65//! - **Index child.** Index content is written through the same kind of `Arc<dyn LayoutStrategy>`
66//!   and probed through the generic reader trait, so it too can be chunked, zone-mapped, or itself
67//!   wrapped in `vortex.indexed`. Wrapping an index child in another index (for example, a small
68//!   index over keys that point into a large posting list) prunes the probe the same way it would
69//!   prune a plain data scan.
70//! - **`vortex.indexed` as someone else's child.** Since it is an ordinary [`LayoutRef`], this
71//!   layout is not restricted to the top of a file — it can equally be a struct field, one chunk
72//!   of a chunked layout, or the child of any other layout that composes over children.
73//!
74//! The one thing that does not nest is the [`IndexSession`] registry itself: every level of
75//! nesting is resolved against the same session passed down from the reader, so an index kind
76//! unregistered at any level degrades to inert there, independent of the other levels.
77//!
78//! # What ships here
79//!
80//! The generic wrapper only: [`Indexed`], [`writer::IndexedStrategy`] and `IndexedReader`,
81//! plus the [`IndexVTable`] contract that index kinds implement. Concrete kinds are registered into
82//! an [`session::IndexSession`] — see the `reverse_index` example (`examples/reverse_index/`) for
83//! a worked example, a minimal equality index over any column.
84
85pub mod index;
86pub(crate) mod reader;
87pub mod session;
88#[cfg(any(test, feature = "_test-harness"))]
89pub mod test_harness;
90pub mod writer;
91
92use std::sync::Arc;
93
94use prost::Message;
95use roaring::RoaringBitmap;
96use vortex_array::DeserializeMetadata;
97use vortex_array::SerializeMetadata;
98use vortex_array::dtype::DType;
99use vortex_array::dtype::proto::dtype as pb;
100use vortex_error::VortexExpect;
101use vortex_error::VortexResult;
102use vortex_error::vortex_bail;
103use vortex_error::vortex_ensure;
104use vortex_error::vortex_err;
105use vortex_error::vortex_panic;
106use vortex_session::VortexSession;
107use vortex_session::registry::CachedId;
108
109pub use self::index::BuiltIndex;
110pub use self::index::IndexBuilder;
111pub use self::index::IndexExactness;
112pub use self::index::IndexId;
113pub use self::index::IndexQueryPlan;
114pub use self::index::IndexVTable;
115pub use self::index::IndexVTableRef;
116pub use self::index::RowLocator;
117pub use self::session::IndexSession;
118pub use self::session::IndexSessionExt;
119pub use self::writer::IndexConfig;
120pub use self::writer::IndexedStrategy;
121use crate::Layout;
122use crate::LayoutChildType;
123use crate::LayoutDeserializeArgs;
124use crate::LayoutId;
125use crate::LayoutParts;
126use crate::LayoutReaderContext;
127use crate::LayoutReaderRef;
128use crate::LayoutRef;
129use crate::VTable;
130use crate::children::layout_children;
131use crate::layouts::indexed::reader::IndexedReader;
132use crate::segments::SegmentSource;
133
134/// Registry id of this layout encoding.
135pub const INDEXED_LAYOUT_ID: &str = "vortex.indexed";
136
137/// Leading byte of the serialized metadata, so the protobuf can be re-shaped later.
138const INDEXED_METADATA_VERSION: u8 = 1;
139
140/// Layout vtable for the `vortex.indexed` layout.
141#[derive(Clone, Debug)]
142pub struct Indexed;
143
144/// How an index's rows are split into independently built and probed partitions.
145///
146/// Partition `p` covers data rows `p * partition_len .. (p + 1) * partition_len` (the last one
147/// clipped to the data row count) and index-child rows `index_ends[p - 1] .. index_ends[p]`. Its
148/// locators are local to the partition, so a probe restricted to one partition reads only that
149/// partition's slice of the index child.
150///
151/// This is what lets an index's granularity differ from the data child's chunking, the same way a
152/// zone map's `zone_len` does.
153#[derive(Clone, Debug)]
154pub struct IndexPartitioning {
155    partition_len: u64,
156    index_ends: Arc<[u64]>,
157    declined: RoaringBitmap,
158}
159
160impl IndexPartitioning {
161    /// Create a partitioning from each partition's exclusive end row in the index child, and the
162    /// partitions whose builder declined.
163    pub fn new(partition_len: u64, index_ends: Vec<u64>, declined: RoaringBitmap) -> Self {
164        Self {
165            partition_len,
166            index_ends: index_ends.into(),
167            declined,
168        }
169    }
170
171    /// Rows of the data child covered by each partition, except possibly the last.
172    pub fn partition_len(&self) -> u64 {
173        self.partition_len
174    }
175
176    /// Each partition's exclusive end row in the index child.
177    pub fn index_ends(&self) -> &[u64] {
178        &self.index_ends
179    }
180
181    /// Partitions whose builder declined. They hold no index rows and never prune.
182    pub fn declined(&self) -> &RoaringBitmap {
183        &self.declined
184    }
185
186    fn validate(&self, id: IndexId, data_row_count: u64, index_row_count: u64) -> VortexResult<()> {
187        vortex_ensure!(
188            self.partition_len > 0,
189            "Index {id} has a zero partition length"
190        );
191        let expected = data_row_count.div_ceil(self.partition_len);
192        vortex_ensure!(
193            self.index_ends.len() as u64 == expected,
194            "Index {id} has {} partitions, expected {expected} for {data_row_count} rows at {} rows each",
195            self.index_ends.len(),
196            self.partition_len
197        );
198        vortex_ensure!(
199            self.index_ends.is_sorted(),
200            "Index {id} partition ends must be non-decreasing"
201        );
202        vortex_ensure!(
203            self.index_ends.last().copied().unwrap_or(0) == index_row_count,
204            "Index {id} partition ends do not cover its {index_row_count}-row child"
205        );
206        vortex_ensure!(
207            self.declined.max().is_none_or(|p| u64::from(p) < expected),
208            "Index {id} declines a partition past its last"
209        );
210        Ok(())
211    }
212}
213
214/// One index attached to the data child.
215#[derive(Clone, Debug)]
216pub struct IndexSpec {
217    id: IndexId,
218    options: Arc<[u8]>,
219    index_dtype: DType,
220    /// `None` means a single partition covering the whole data child.
221    partitioning: Option<IndexPartitioning>,
222    /// The resolved kind, or `None` when it is not registered in this session. An unresolved spec
223    /// is inert: its child is never probed and reads fall through to the data child.
224    vtable: Option<IndexVTableRef>,
225}
226
227impl IndexSpec {
228    /// Create a spec for an index that was just built by `vtable`.
229    pub fn new(
230        vtable: IndexVTableRef,
231        options: Vec<u8>,
232        index_dtype: DType,
233        partitioning: Option<IndexPartitioning>,
234    ) -> Self {
235        Self {
236            id: vtable.id(),
237            options: options.into(),
238            index_dtype,
239            partitioning,
240            vtable: Some(vtable),
241        }
242    }
243
244    /// How this index is partitioned, or `None` for a single partition over the whole data child.
245    pub fn partitioning(&self) -> Option<&IndexPartitioning> {
246        self.partitioning.as_ref()
247    }
248
249    /// The registry id of this index's kind.
250    pub fn id(&self) -> IndexId {
251        self.id
252    }
253
254    /// The kind-defined, self-versioned options blob.
255    pub fn options(&self) -> &[u8] {
256        &self.options
257    }
258
259    /// The dtype of this index's layout child.
260    pub fn index_dtype(&self) -> &DType {
261        &self.index_dtype
262    }
263
264    /// The resolved kind, or `None` if it is not registered in this session.
265    pub fn vtable(&self) -> Option<&IndexVTableRef> {
266        self.vtable.as_ref()
267    }
268}
269
270/// Layout-specific data for the [`IndexedLayout`].
271///
272/// Child 0 is the data, sharing this layout's dtype and row space. Children 1.. are index content,
273/// one per entry in `IndexedData::indexes`.
274#[derive(Clone, Debug)]
275pub struct IndexedData {
276    indexes: Arc<[IndexSpec]>,
277}
278
279/// A layout that attaches locating indexes to a data child.
280pub type IndexedLayout = Layout<Indexed>;
281
282impl IndexedLayout {
283    /// Assemble an indexed layout from a data child and one layout child per index spec.
284    pub fn try_new(
285        data: LayoutRef,
286        index_layouts: Vec<LayoutRef>,
287        indexes: Vec<IndexSpec>,
288    ) -> VortexResult<Self> {
289        vortex_ensure!(
290            index_layouts.len() == indexes.len(),
291            "IndexedLayout got {} index children for {} specs",
292            index_layouts.len(),
293            indexes.len()
294        );
295        for (layout, spec) in index_layouts.iter().zip(&indexes) {
296            vortex_ensure!(
297                layout.dtype() == &spec.index_dtype,
298                "Index child dtype {} does not match spec dtype {} for {}",
299                layout.dtype(),
300                spec.index_dtype,
301                spec.id
302            );
303            if let Some(partitioning) = &spec.partitioning {
304                partitioning.validate(spec.id, data.row_count(), layout.row_count())?;
305            }
306        }
307
308        let dtype = data.dtype().clone();
309        let row_count = data.row_count();
310        let mut children = Vec::with_capacity(1 + index_layouts.len());
311        children.push(data);
312        children.extend(index_layouts);
313
314        Ok(LayoutParts::new(
315            Indexed,
316            dtype,
317            row_count,
318            Vec::new(),
319            layout_children(children),
320            IndexedData {
321                indexes: indexes.into(),
322            },
323        )
324        .into_typed())
325    }
326
327    /// The indexes attached to the data child.
328    pub fn indexes(&self) -> &Arc<[IndexSpec]> {
329        &self.indexes
330    }
331}
332
333impl VTable for Indexed {
334    type LayoutData = IndexedData;
335    type Metadata = IndexedMetadata;
336
337    fn id(&self) -> LayoutId {
338        static ID: CachedId = CachedId::new(INDEXED_LAYOUT_ID);
339        *ID
340    }
341
342    fn metadata(layout: &Layout<Self>) -> Self::Metadata {
343        IndexedMetadata {
344            indexes: layout
345                .indexes
346                .iter()
347                .map(|spec| IndexSpecProto {
348                    id: spec.id.to_string(),
349                    options: spec.options.to_vec(),
350                    index_dtype: Some(
351                        pb::DType::try_from(&spec.index_dtype)
352                            .vortex_expect("index child dtype should be serializable"),
353                    ),
354                    partition_len: spec.partitioning.as_ref().map(|p| p.partition_len),
355                    partition_index_ends: spec
356                        .partitioning
357                        .as_ref()
358                        .map(|p| p.index_ends.to_vec())
359                        .unwrap_or_default(),
360                    declined_partitions: spec
361                        .partitioning
362                        .as_ref()
363                        .map(|p| p.declined.iter().collect())
364                        .unwrap_or_default(),
365                })
366                .collect::<Vec<_>>()
367                .into(),
368        }
369    }
370
371    fn deserialize(
372        &self,
373        args: &LayoutDeserializeArgs<'_>,
374        metadata: &IndexedMetadata,
375    ) -> VortexResult<Self::LayoutData> {
376        vortex_ensure!(
377            args.children.nchildren() == 1 + metadata.indexes.len(),
378            "IndexedLayout expects {} children (data + {} indexes), got {}",
379            1 + metadata.indexes.len(),
380            metadata.indexes.len(),
381            args.children.nchildren()
382        );
383
384        let registry = args.session.indexes();
385        let indexes = metadata
386            .indexes
387            .iter()
388            .enumerate()
389            .map(|(idx, spec)| {
390                let index_dtype = spec
391                    .index_dtype
392                    .as_ref()
393                    .map(|dtype| DType::from_proto(dtype, args.session))
394                    .transpose()?
395                    .ok_or_else(|| vortex_err!("Index spec {} is missing its dtype", spec.id))?;
396                let id = IndexId::from(spec.id.as_str());
397
398                let partitioning = spec
399                    .partition_len
400                    .map(|partition_len| {
401                        let partitioning = IndexPartitioning::new(
402                            partition_len,
403                            spec.partition_index_ends.clone(),
404                            spec.declined_partitions.iter().copied().collect(),
405                        );
406                        partitioning.validate(
407                            id,
408                            args.row_count,
409                            args.children.child_row_count(idx + 1),
410                        )?;
411                        Ok::<_, vortex_error::VortexError>(partitioning)
412                    })
413                    .transpose()?;
414
415                // An unknown kind degrades to an inert spec rather than failing the read: the
416                // child stays addressable so child counts and dtypes still line up, but nothing
417                // ever probes it.
418                Ok(IndexSpec {
419                    id,
420                    options: spec.options.as_slice().into(),
421                    index_dtype,
422                    partitioning,
423                    vtable: registry.find(&id),
424                })
425            })
426            .collect::<VortexResult<Vec<_>>>()?;
427
428        args.children.child(0, args.dtype)?;
429        for (idx, spec) in indexes.iter().enumerate() {
430            args.children.child(idx + 1, &spec.index_dtype)?;
431        }
432
433        Ok(IndexedData {
434            indexes: indexes.into(),
435        })
436    }
437
438    fn child_dtype(layout: &Layout<Self>, slot: usize) -> VortexResult<DType> {
439        match slot {
440            0 => Ok(layout.dtype().clone()),
441            _ => {
442                let Some(spec) = layout.indexes.get(slot - 1) else {
443                    vortex_bail!("Invalid child index: {}", slot);
444                };
445                Ok(spec.index_dtype.clone())
446            }
447        }
448    }
449
450    fn child_type(layout: &Layout<Self>, slot: usize) -> LayoutChildType {
451        match slot {
452            0 => LayoutChildType::Transparent("data".into()),
453            _ => match layout.indexes.get(slot - 1) {
454                Some(spec) => LayoutChildType::Auxiliary(format!("index:{}", spec.id).into()),
455                None => vortex_panic!("Invalid child index: {}", slot),
456            },
457        }
458    }
459
460    fn new_reader(
461        layout: &Layout<Self>,
462        name: Arc<str>,
463        segment_source: Arc<dyn SegmentSource>,
464        session: &VortexSession,
465        ctx: &LayoutReaderContext,
466    ) -> VortexResult<LayoutReaderRef> {
467        Ok(Arc::new(IndexedReader::try_new(
468            layout.clone(),
469            name,
470            segment_source,
471            session.clone(),
472            ctx.clone(),
473        )?))
474    }
475}
476
477/// Serialized indexed-layout metadata: one entry per index child, in child order.
478#[derive(Debug, Clone, PartialEq)]
479pub struct IndexedMetadata {
480    indexes: Arc<[IndexSpecProto]>,
481}
482
483#[derive(Clone, PartialEq, Message)]
484struct IndexedMetadataProto {
485    #[prost(message, repeated, tag = "1")]
486    indexes: Vec<IndexSpecProto>,
487}
488
489#[derive(Clone, PartialEq, Message)]
490struct IndexSpecProto {
491    /// Registry id of the index kind, e.g. `vortex.idx.reverse_index`.
492    #[prost(string, tag = "1")]
493    id: String,
494    /// Kind-defined, self-versioned options.
495    #[prost(bytes = "vec", tag = "2")]
496    options: Vec<u8>,
497    /// The dtype of the index child. Layout nodes carry no dtype of their own — it flows top-down
498    /// during deserialization — so an auxiliary child's dtype has to be recorded here.
499    #[prost(message, optional, tag = "3")]
500    index_dtype: Option<pb::DType>,
501    /// Data rows per partition; absent for a single partition over the whole data child.
502    #[prost(uint64, optional, tag = "4")]
503    partition_len: Option<u64>,
504    /// Each partition's exclusive end row in the index child.
505    #[prost(uint64, repeated, tag = "5")]
506    partition_index_ends: Vec<u64>,
507    /// Partitions whose builder declined.
508    #[prost(uint32, repeated, tag = "6")]
509    declined_partitions: Vec<u32>,
510}
511
512impl SerializeMetadata for IndexedMetadata {
513    fn serialize(self) -> Vec<u8> {
514        let proto = IndexedMetadataProto {
515            indexes: self.indexes.to_vec(),
516        };
517        let mut metadata = vec![INDEXED_METADATA_VERSION];
518        metadata.extend(proto.encode_to_vec());
519        metadata
520    }
521}
522
523impl DeserializeMetadata for IndexedMetadata {
524    type Output = Self;
525
526    fn deserialize(metadata: &[u8]) -> VortexResult<Self::Output> {
527        let Some((&version, proto_bytes)) = metadata.split_first() else {
528            vortex_bail!("Indexed metadata missing protobuf version");
529        };
530        vortex_ensure!(
531            version == INDEXED_METADATA_VERSION,
532            "Unsupported indexed metadata version: {}",
533            version
534        );
535
536        let proto = IndexedMetadataProto::decode(proto_bytes)
537            .map_err(|err| vortex_err!("Failed to decode indexed metadata: {err}"))?;
538        Ok(Self {
539            indexes: proto.indexes.into(),
540        })
541    }
542}
543
544#[cfg(test)]
545mod tests;