vortex_layout/layouts/indexed/index.rs
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! The pluggable index-kind contract: what a kind must implement to be built at write time and
5//! probed at read time.
6
7use std::any::Any;
8use std::fmt::Debug;
9use std::ops::Range;
10use std::sync::Arc;
11
12use roaring::RoaringBitmap;
13use vortex_array::ArrayRef;
14use vortex_array::ExecutionCtx;
15use vortex_array::dtype::DType;
16use vortex_array::expr::BoundExpression;
17use vortex_buffer::BitBufferMut;
18use vortex_error::VortexResult;
19use vortex_mask::Mask;
20use vortex_session::VortexSession;
21use vortex_session::registry::Id;
22
23use crate::layouts::indexed::reader::OpenArgs;
24use crate::layouts::indexed::reader::OpenIndex;
25use crate::layouts::indexed::reader::open_index;
26use crate::layouts::indexed::writer::IndexWriter;
27use crate::layouts::indexed::writer::TypedIndexWriter;
28
29/// Stable registry id of an index kind, e.g. `vortex.idx.reverse_index`.
30pub type IndexId = Id;
31
32/// A pluggable index kind.
33///
34/// Mirrors the layout `VTable` machinery: implementations are registered in an
35/// [`IndexSession`](crate::layouts::indexed::session::IndexSession) under a stable string id,
36/// which is what gets written into the layout metadata. A reader that does not have the kind
37/// registered drops the index child and reads the data child directly.
38///
39/// The associated types are what keeps repeated work out of the read path. A reader parses a
40/// spec's [`Options`](Self::Options) once, and decodes each chunk of the index child into a
41/// [`Chunk`](Self::Chunk) at most once, sharing it between every expression that probes the
42/// chunk. Each expression is reduced by [`plan`](Self::plan) to a [`Query`](Self::Query) that
43/// [`resolve`](Self::resolve) answers against those decoded chunks.
44///
45/// # Serialization
46///
47/// A kind's on-disk format is exactly three things, and nothing else a kind does reaches the file:
48///
49/// - [`serialize_options`](Self::serialize_options) and
50/// [`deserialize_options`](Self::deserialize_options), for the options blob stored in the spec.
51/// The blob is the kind's to version.
52/// - [`index_dtype`](Self::index_dtype), the schema of the index child.
53/// - [`encode`](Self::encode) and [`decode`](Self::decode), between a [`Chunk`](Self::Chunk) and
54/// rows of that schema.
55///
56/// Builders produce chunks and options, never bytes, so these pairs can be tested without
57/// writing a layout. Re-encoding a decoded chunk must reproduce its encoding.
58pub trait IndexVTable: 'static + Send + Sync + Debug {
59 /// The kind's options, parsed from the self-versioned options blob.
60 type Options: 'static + Send + Sync;
61 /// Builds the index on the write path.
62 type Builder: IndexBuilder<Options = Self::Options, Chunk = Self::Chunk> + 'static;
63 /// What [`plan`](Self::plan) reduced one expression to, answered by
64 /// [`resolve`](Self::resolve).
65 type Query: 'static + Send + Sync;
66 /// One chunk of the index child, decoded into whatever form the kind answers queries from.
67 type Chunk: 'static + Send + Sync;
68
69 /// Stable string id, e.g. `vortex.idx.reverse_index`.
70 fn id(&self) -> IndexId;
71
72 /// Serialize options into the blob stored in the spec.
73 fn serialize_options(&self, options: &Self::Options) -> Vec<u8>;
74
75 /// Parse an options blob, as configured on the write side or stored in a spec.
76 fn deserialize_options(&self, options: &[u8]) -> VortexResult<Self::Options>;
77
78 /// The dtype of the index child this kind builds over values of `dtype`, or `None` if it
79 /// cannot index them.
80 ///
81 /// This is the index's schema: the writer rejects a builder whose output differs from it, and
82 /// the reader skips an index whose stored dtype no longer matches it.
83 fn index_dtype(&self, dtype: &DType, options: &Self::Options) -> Option<DType>;
84
85 /// Construct a builder for the write path.
86 ///
87 /// `data_block_len` is the data child's repartition block size when known. Kinds that emit
88 /// block-granular locators should default their block length to it so pruned blocks line up
89 /// with chunk and segment boundaries.
90 ///
91 /// A partitioned index calls this once per partition, and each builder sees only its
92 /// partition's rows.
93 fn builder(
94 &self,
95 dtype: &DType,
96 options: &Self::Options,
97 data_block_len: Option<u64>,
98 session: &VortexSession,
99 ) -> VortexResult<Self::Builder>;
100
101 /// Decide whether this index can serve `expr`, a single conjunct scoped to the data child's
102 /// `dtype`.
103 ///
104 /// `index_dtype` is the index child's dtype, as returned by [`IndexVTable::index_dtype`], and
105 /// the scope the plan's filter must be bound to.
106 ///
107 /// `None` means "no claim" and is always safe: the scan falls back to the data child.
108 fn plan(
109 &self,
110 expr: &BoundExpression,
111 dtype: &DType,
112 index_dtype: &DType,
113 options: &Self::Options,
114 ) -> VortexResult<Option<IndexQueryPlan<Self::Query>>>;
115
116 /// Encode a chunk a builder produced as rows of the index child, of the declared
117 /// [`index_dtype`](Self::index_dtype).
118 fn encode(
119 &self,
120 chunk: &Self::Chunk,
121 options: &Self::Options,
122 ctx: &mut ExecutionCtx,
123 ) -> VortexResult<ArrayRef>;
124
125 /// Decode rows of the index child, in its own schema, back into a chunk.
126 ///
127 /// The index child's layout strategy may chunk it differently than it was encoded, so this
128 /// must accept any contiguous run of encoded rows. Called at most once per chunk per reader,
129 /// however many expressions probe it.
130 fn decode(
131 &self,
132 chunk: ArrayRef,
133 options: &Self::Options,
134 ctx: &mut ExecutionCtx,
135 ) -> VortexResult<Self::Chunk>;
136
137 /// Answer `query` from one partition's decoded chunks, as a locator local to the partition.
138 ///
139 /// `chunks` are in index-row order and omit every chunk the plan's
140 /// [`filter`](IndexQueryPlan::filter) ruled out, so they hold everything the query can match
141 /// in this partition. `data_row_count` is the partition's row count in the data child.
142 fn resolve(
143 &self,
144 query: &Self::Query,
145 chunks: &[Arc<Self::Chunk>],
146 data_row_count: u64,
147 options: &Self::Options,
148 ctx: &mut ExecutionCtx,
149 ) -> VortexResult<RowLocator>;
150}
151
152/// Shared handle to a registered index kind, erasing its [`IndexVTable`] types.
153#[derive(Clone)]
154pub struct IndexVTableRef(Arc<dyn DynIndexVTable>);
155
156impl IndexVTableRef {
157 /// Erase an index kind so it can be registered and configured alongside others.
158 pub fn new<V: IndexVTable>(vtable: V) -> Self {
159 Self(Arc::new(vtable))
160 }
161
162 /// The kind's stable id.
163 pub fn id(&self) -> IndexId {
164 self.0.id()
165 }
166
167 /// The concrete kind, if it is a `V`.
168 pub fn as_opt<V: IndexVTable>(&self) -> Option<&V> {
169 self.0.as_any().downcast_ref()
170 }
171
172 /// Parse `options` once for writing.
173 pub(crate) fn open_writer(&self, options: &[u8]) -> VortexResult<Arc<dyn IndexWriter>> {
174 Arc::clone(&self.0).open_writer(options)
175 }
176
177 /// Parse a spec's options once for reading, or `None` if its stored dtype is not what the
178 /// kind now declares.
179 pub(crate) fn open_reader(
180 &self,
181 args: OpenArgs<'_>,
182 ) -> VortexResult<Option<Arc<dyn OpenIndex>>> {
183 Arc::clone(&self.0).open_reader(args)
184 }
185}
186
187impl Debug for IndexVTableRef {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 self.0.fmt(f)
190 }
191}
192
193/// The type-erased side of [`IndexVTable`], implemented for every kind.
194///
195/// Kept crate-private so the reader and writer can hand out their own typed state without it
196/// becoming public API; kinds only ever implement [`IndexVTable`].
197pub(crate) trait DynIndexVTable: 'static + Send + Sync + Debug {
198 fn id(&self) -> IndexId;
199
200 fn as_any(&self) -> &dyn Any;
201
202 fn open_writer(self: Arc<Self>, options: &[u8]) -> VortexResult<Arc<dyn IndexWriter>>;
203
204 fn open_reader(self: Arc<Self>, args: OpenArgs<'_>)
205 -> VortexResult<Option<Arc<dyn OpenIndex>>>;
206}
207
208impl<V: IndexVTable> DynIndexVTable for V {
209 fn id(&self) -> IndexId {
210 IndexVTable::id(self)
211 }
212
213 fn as_any(&self) -> &dyn Any {
214 self
215 }
216
217 fn open_writer(self: Arc<Self>, options: &[u8]) -> VortexResult<Arc<dyn IndexWriter>> {
218 Ok(Arc::new(TypedIndexWriter::try_new(self, options)?))
219 }
220
221 fn open_reader(
222 self: Arc<Self>,
223 args: OpenArgs<'_>,
224 ) -> VortexResult<Option<Arc<dyn OpenIndex>>> {
225 open_index(self, args)
226 }
227}
228
229/// What an [`IndexBuilder`] emits on finishing: its chunks in order, and its final options.
230pub type BuiltIndex<B> = (
231 Vec<<B as IndexBuilder>::Chunk>,
232 <B as IndexBuilder>::Options,
233);
234
235/// Accumulates index content while the data stream is written.
236pub trait IndexBuilder: Send {
237 /// The kind's options, as finally chosen by the build.
238 type Options;
239 /// The content the kind's [`IndexVTable::encode`] writes out.
240 type Chunk;
241
242 /// Chunks arrive in stream order with their row offset within this builder's partition, which
243 /// for an unpartitioned index is the whole layout.
244 fn push(
245 &mut self,
246 chunk: &ArrayRef,
247 row_offset: u64,
248 ctx: &mut ExecutionCtx,
249 ) -> VortexResult<()>;
250
251 /// Emit the index content as chunks, which the writer encodes and writes through a child
252 /// layout strategy in order.
253 ///
254 /// Returns the final options alongside them, so builders can record normalization choices or
255 /// block sizes discovered during the build.
256 ///
257 /// `None` declines: nothing worth keeping was built, so no index child and no spec are written,
258 /// and the wrapper collapses to the plain data layout if every builder declines. This is the
259 /// only point at which size can be judged — a builder is constructed before the first chunk
260 /// arrives, so row count and cardinality are not knowable earlier. Declining is always safe: an
261 /// absent index reads exactly like an unregistered one.
262 fn finish(self) -> VortexResult<Option<BuiltIndex<Self>>>;
263
264 /// Bytes currently buffered, reported up through the write context's buffered-bytes tracker.
265 fn buffered_bytes(&self) -> u64;
266}
267
268/// What a probe result means and how precisely it locates.
269#[derive(Copy, Clone, Debug, PartialEq, Eq)]
270pub enum IndexExactness {
271 /// True bits are exactly the matching rows, so the probe may serve `filter_evaluation`
272 /// directly and skip decoding the data child for that conjunct.
273 Exact,
274 /// False bits are proven non-matching; true bits are "maybe". Serves `pruning_evaluation`
275 /// only, and the real predicate re-checks the survivors.
276 Superset,
277}
278
279/// Where an index located its matches, in the data child's row space.
280///
281/// Both variants are roaring bitmaps: postings are naturally set-like, intersect and union
282/// cheaply, and expand into a [`Mask`] by walking runs in sorted order.
283///
284/// Roaring bitmaps are `u32`-keyed, which caps a single layout at `u32::MAX` rows. That is far
285/// above any practical Vortex file, and [`RowLocator::mask_for`] errors rather than truncating if
286/// it is ever exceeded.
287#[derive(Clone, Debug)]
288pub enum RowLocator {
289 /// Row positions local to the data child.
290 Rows(RoaringBitmap),
291 /// Ids of fixed `block_len`-row blocks, expanded by broadcasting each block's bit across its
292 /// rows — the same shape as the zoned reader's per-zone expansion.
293 Blocks { block_len: u64, ids: RoaringBitmap },
294}
295
296impl RowLocator {
297 /// An empty locator: nothing matches, so everything prunes.
298 pub fn empty_rows() -> Self {
299 RowLocator::Rows(RoaringBitmap::new())
300 }
301
302 /// Expand this locator into a mask covering `row_range` of the data child.
303 ///
304 /// The returned mask has length `row_range.len()` and is *not* intersected with any input
305 /// mask; callers do that.
306 pub fn mask_for(&self, row_range: &Range<u64>) -> VortexResult<Mask> {
307 let len = usize::try_from(row_range.end - row_range.start)?;
308 let mut bits = BitBufferMut::with_capacity(len);
309 self.append_to(row_range, &mut bits)?;
310 Ok(Mask::from(bits.freeze()))
311 }
312
313 /// Append this locator's bits for `row_range` to `bits`, one per row.
314 pub fn append_to(&self, row_range: &Range<u64>, bits: &mut BitBufferMut) -> VortexResult<()> {
315 match self {
316 // Walk the set bits in ascending order, emitting the false run before each one. The
317 // bitmap is sorted, so this is a single linear pass with no random access.
318 RowLocator::Rows(rows) => {
319 let start = u32::try_from(row_range.start)?;
320 let end = u32::try_from(row_range.end)?;
321 let mut pos = row_range.start;
322 for row in rows.range(start..end) {
323 let row = u64::from(row);
324 bits.append_n(false, usize::try_from(row - pos)?);
325 bits.append_n(true, 1);
326 pos = row + 1;
327 }
328 bits.append_n(false, usize::try_from(row_range.end - pos)?);
329 }
330 // Broadcast each block's bit across the rows it covers, clipped to `row_range`.
331 RowLocator::Blocks { block_len, ids } => {
332 let mut row = row_range.start;
333 while row < row_range.end {
334 let block = row / block_len;
335 let block_end = ((block + 1) * block_len).min(row_range.end);
336 let hit = ids.contains(u32::try_from(block)?);
337 bits.append_n(hit, usize::try_from(block_end - row)?);
338 row = block_end;
339 }
340 }
341 }
342 Ok(())
343 }
344}
345
346/// How an index intends to answer one expression.
347///
348/// The probe prunes the index child's chunks with `filter`, inheriting its zone maps, then decodes
349/// the surviving chunks (each at most once per reader) and hands them to
350/// [`IndexVTable::resolve`] to answer `query`.
351pub struct IndexQueryPlan<Q> {
352 /// Whether the resulting mask is exact or a superset.
353 pub exactness: IndexExactness,
354 /// Predicate bound to the index child's dtype. Chunks it proves hold no matching index rows
355 /// are neither loaded nor decoded.
356 pub filter: BoundExpression,
357 /// What [`IndexVTable::resolve`] answers from the decoded chunks.
358 pub query: Q,
359}