Build an In-Memory Vector Table and Match Its Index
Chapter 1
Start from the two
*-startercrates. Finish with an exact SQL top-k query, an Arrow-backed table, and a safe DataFusion optimizer rule that can select a vector index.
Your first query asks for the three rows closest to one query vector:
SELECT id, payload
FROM points
ORDER BY cosine_distance(embedding, [1.0, 0.0, 0.0])
LIMIT 3;
Start from the exact plan. Scan every row, compute its distance to the query, and keep the nearest three with a bounded top-k sort:
SortExec: TopK(fetch=3), ...
VectorScanExec: rows=..., fetch=None
This plan is correct for every valid query shape because it does not skip any rows. A vector index can avoid scanning the
whole collection, but only when the query and the index describe the same ranking. The matcher must check the distance
function, embedding column, literal query vector, sort direction, dimension, and LIMIT; a mismatch keeps the exact
plan.
After your rule recognizes a compatible index, the physical plan becomes:
SortExec: TopK(fetch=3), ...
VectorIndexScanExec: index=flat, metric=Cosine, query_dim=3, fetch=Some(3), ordered=false
With FlatIndex, you can confirm both the exact result and the matched physical plan. In Chapter 2, index=ivf_flat will
appear behind the same rule.
Build the Exact Path and Matcher in Rust
You will modify:
rust/vector-starter/core/src/dataset.rs
rust/vector-starter/datafusion/src/lib.rs
Metric math, a FlatIndex that checks every vector, Arrow result execution, and all tests are ready for you to use. You
will build the storage and extension boundary around them: validated vectors, an Arrow-backed table, a physical scan, and
a rule that recognizes one safe top-k shape. Do not modify public APIs or tests.
Invariants
- I1 — Valid vectors: a dataset is non-empty, has nonzero fixed dimension, and contains only finite
f32values. - I2 — Consistent row identity: each external
idis unique, while a core row offset identifies the same row in the dataset and Arrow batch. - I3 — Faithful Arrow shape: the table schema is
id: UInt64,payload: Utf8, andembedding: FixedSizeList<Float32>with the dataset dimension. - I4 — Safe match: an index scan is selected only for one supported distance expression over
embedding, a literal query vector, a compatible metric and direction, and a valid dimension. - I5 — Exact fallback: filters, multiple sort keys, non-literal vectors, wrong metrics, wrong directions, and invalid
query vectors remain on
VectorScanExecplus DataFusion’s exact sort. - I6 — SQL owns ordering: unless the vector scan returns rows in the requested order, DataFusion retains its bounded sort after the index selects candidates.
Checkpoint 1: Validate the In-Memory Dataset
Implement the three TODOs in vector-starter/core/src/dataset.rs.
Dataset::try_new reads the first row to establish dimension, rejects an empty dataset or zero-dimensional vector, then
checks every row for equal length and finite components. Store the vectors as Arc<[Vec<f32>]>; later exact and
approximate indexes can cheaply share immutable data.
validate_for_metric rejects zero-norm stored rows for cosine distance. validate_query checks dimension, finiteness,
and the same cosine boundary for a query. Use the existing VectorError variants rather than panicking on input.
Prediction: For a two-dimensional dataset, should query [1.0] reach the DataFusion optimizer? It should be rejected
at the vector boundary; allowing a mismatched literal into an index scan would make the plan claim a contract the index
cannot satisfy.
Use the starter’s FlatIndex to exercise these checks:
cd rust
cargo test -p vector-core-starter --test indexes flat_search_is_deterministic_and_validates_queries
cargo test -p vector-core-starter --test indexes cosine_rejects_zero_norm_vectors
The exact-search and top-k helpers are already in place, so you can keep this checkpoint focused on the validation boundary.
Checkpoint 2: Turn Rows into an Arrow Table
Implement VectorTable::try_new in vector-starter/datafusion/src/lib.rs.
Preserve Identity
First reject duplicate external IDs with a HashSet. Then build Dataset from the embeddings in exactly the same row
order. If Arrow batch row 4 and dataset row 4 refer to different inputs, an index will return the wrong payload even when
its search result is otherwise correct.
Build the selected IndexConfig over that dataset. Chapter 1 passes IndexConfig::Flat; Chapter 2 will pass
IndexConfig::IvfFlat without changing table construction.
Define the Schema
DataFusion executes over Arrow arrays. Construct this schema:
id UInt64
payload Utf8
embedding FixedSizeList<Float32, dimension>
FixedSizeListArray stores all embedding components in one flat Float32Array; the list width tells Arrow where each
row begins and ends. For two three-dimensional rows, the child values are laid out as:
[x0, y0, z0, x1, y1, z1]
`---row 0--' `---row 1--'
Use i32::try_from(dataset.dimension()) for Arrow’s list width and return a plan error if the dimension cannot fit.
Create UInt64Array, StringArray, and FixedSizeListArray, then assemble one RecordBatch with the schema.
Prediction: What breaks if you sort the IDs before creating their Arrow array but leave embeddings in insertion order? Trace the row offset returned by an index to the payload DataFusion would emit.
Run the duplicate-ID boundary test:
cargo test -p vector-datafusion-starter --test sql table_rejects_duplicate_ids
Checkpoint 3: Expose a TableProvider and Exact Scan
DataFusion asks a TableProvider for a physical plan through scan. Implement the TODO in that method by creating the
existing VectorScanExec in ScanMode::Full.
Pass through:
- the Arrow batch and selected core index;
- DataFusion’s requested projection and limit;
- the session’s
vector_search.orderedoption; and - no ordering yet, because the initial scan has not accepted a sort.
The executor uses project_schema to preserve the requested column order, take to build result arrays from
row offsets, and MemoryStream to emit one batch. In full mode it returns ordinary table rows. DataFusion evaluates the
distance function and exact SortExec above that scan.
At this point, DataFusion can read your table and return exact top-k results. Run the query once and inspect how the scan, distance expression, and bounded sort fit together.
Checkpoint 4: Match One Safe Vector Ordering
Implement match_vector_order and try_pushdown_sort. DataFusion calls the latter while planning the physical query.
The matcher accepts only all of the following:
- exactly one
PhysicalSortExpr; - a supported scalar function: Euclidean
array_distance/list_distance,cosine_distance, orinner_product/dot_product; - ascending order for Euclidean/cosine or descending order for dot product;
- one
Columnand oneLiteralargument, allowing either argument order; - the column named
embeddingin the projected schema; - a literal vector that
scalar_vectorcan convert to finitef32values; - query dimension equal to the index dataset; and
- a nonzero cosine query.
uncast and scalar_vector are already implemented. They remove harmless cast wrappers and decode list literals backed
by integer, f32, or f64 Arrow arrays. Use them to build a conservative matching rule.
When matching fails, return SortOrderPushdownResult::Unsupported; DataFusion keeps the exact scan and sort. When it
succeeds, clone the scan into ScanMode::Vector { query }, retain the requested ordering, and return
SortOrderPushdownResult::Exact.
Prediction: A cosine index exists, but the query uses array_distance. Both functions accept the same vector shapes.
Should the rule select the index? No—the metric changes ranking, so I4 requires exact fallback.
Run the positive and negative plan tests:
cargo test -p vector-datafusion-starter --test sql compatible_top_k_uses_vector_index_scan_and_keeps_sort
cargo test -p vector-datafusion-starter --test sql unsafe_sort_shapes_are_not_lowered
cargo test -p vector-datafusion-starter --test sql filter_keeps_datafusion_exact_fallback
cargo test -p vector-datafusion-starter --test sql dot_product_requires_descending_order
Checkpoint 5: Push LIMIT Without Stealing ORDER BY
Implement ExecutionPlan::with_fetch. DataFusion calls it after sort pushdown and passes LIMIT k.
DataFusion 54.1.0 does not expose supported SQL optimizer hints for choosing this plan. Register
vector_search.ordered as a session option instead; DataFusion reads its value whenever it generates a new physical plan.
This keeps the SQL query portable while making the executor’s ordering guarantee explicit for the session.
Clone the scan and store the new fetch value. When the session option is ordered=true, return the scan directly; this mode
is valid only when the selected index returns rows in the accepted order. If no ordering or no fetch exists, also return
the scan.
For the default ordered=false path, clear the scan’s claimed ordering property and wrap it in DataFusion’s
SortExec::new(ordering, scan).with_fetch(Some(k)). The index chooses candidate row offsets; DataFusion still owns
nearest-first SQL output.
This detail prevents a subtle optimizer bug. Claiming exact ordering while an approximate executor returns candidates in heap or traversal order can produce the right set in the wrong order.
Verify both modes and the end-to-end SQL file:
cargo test -p vector-datafusion-starter --test sql ordered_session_mode_allows_sort_elision
cargo test -p vector-datafusion-starter --test sqllogictest day1_table_and_optimizer_sql
The SQLLogicTest asserts physical operators as well as rows. Its filtered case must remain exact; setting ordered mode may
remove the generic sort, and setting it back to false must restore that sort in the next generated plan.
Review Your Chapter 1 Result
After the two core tests, all sql.rs tests, and the Chapter 1 SQLLogicTest pass, choose one query and explain:
- how an input row becomes a dataset offset and three aligned Arrow arrays;
- where DataFusion performs exact distance, top-k, and final ordering;
- which comparison prevents a cosine query from using a Euclidean index;
- why a column-to-column distance expression stays exact; and
- how the same matching rule can later reach an approximate index without weakening exact fallback.
Keep your Chapter 1 changes in the two files named at the start. IVFFlat, filtered pushdown, DDL, and persistence remain outside this chapter.
Your feedback is greatly appreciated. Join our Discord community.
Found an issue? Open an issue or pull request at github.com/skyzh/write-you-a-vector-db.
write-you-a-vector-db © 2024-2026 by Alex Chi Z. All Rights Reserved.