High-performance Piecewise Geometric Model (PGM) learned index implementation in pure Rust with strict epsilon error guarantees
Rust
152
9 commits
updated Oct 19, 2026
A zero-dependency, production-grade implementation of Piecewise Geometric Model (PGM) Learned Index Structures in pure Rust.
This repository demonstrates the paradigm shift away from traditional comparison-based pointer hierarchies (such as B-Trees and LSM block indexes) to mathematical function approximations of Empirical Cumulative Distribution Functions (CDFs) with provable $\epsilon$ error bounds.
For more than 50 years, database indexing has relied on tree-based data structures (B-Tree, B+Tree, LSM-Tree). While mature, B-Trees possess inherent physical constraints on modern CPU architectures:
Learned Indexes (originally introduced by Kraska et al., MIT / Google) reframe indexing as a regression problem: Given a sorted sequence of keys $K = (k_0, k_1, \dots, k_{N-1})$, the position of any key $k$ in memory corresponds to the scaled Empirical Cumulative Distribution Function:
$$\text{Pos}(k) \approx N \times \text{CDF}(k)$$
Rather than allocating deep node hierarchies, a PGM-Index partitions the data into a minimal set of linear segments $y = ax + b$, guaranteeing that the true position of any indexed key is within a strict error window $[y - \epsilon, y + \epsilon]$.
The index builder executes in a single streaming pass ($O(N)$ time, $O(1)$ extra space).
For a segment starting at point $(x_0, y_0)$ and a given maximum error tolerance $\epsilon$: Each subsequent point $(x_i, y_i)$ restricts the feasible slope interval $[s_{\min}, s_{\max}]$:
$$s_{\min} = \max_{j} \left[ \frac{(y_j - \epsilon) - y_0}{x_j - x_0} \right]$$
$$s_{\max} = \min_{j} \left[ \frac{(y_j + \epsilon) - y_0}{x_j - x_0} \right]$$
As long as $s_{\min} \le s_{\max}$, all points observed so far can be represented by a single straight line with error at most $\epsilon$. Once $s_{\min} > s_{\max}$, the segment is finalized with slope $s = \frac{s_{\min} + s_{\max}}{2}$, and a new segment starts.
During point lookup:
| Dimension | Standard BTreeMap | PGM-Index (Learned) | Concrete Hardware Impact |
|---|---|---|---|
| Index Structure | Dynamic nodes with pointers | Contiguous array of $(a, b, k_0)$ segments | Entire index resides in CPU L1/L2 cache |
| Lookup Mechanism | 4–6 node hops across RAM | 1 affine math evaluation + local L1 search | Eliminates unpredictable pointer chasing |
| Build Time | $O(N \log N)$ repeated node rebalancing | $O(N)$ single-pass linear streaming | ~22x faster build times |
| Memory Footprint | ~45 MB per 1,000,000 keys | 3.1 KB – 48 KB per 1,000,000 keys | -99.89% to -99.99% memory reduction |
All benchmarks were measured locally on x86_64 hardware using Rust 1.99 in --release mode.
Each scenario was executed over 10 consecutive runs with distinct randomized seeds to eliminate CPU scheduling noise and warm-cache anomalies.
Simulates monotonically increasing timestamps, network log sequences, or primary auto-increment keys.
Distribution: Dense Timestamp | Tolerance: epsilon = 32
| Run # | Build Time (BTree / PGM) | Hit Latency (BTree / PGM) | Miss Latency (BTree / PGM) | Speedup (Hit) | Segments |
|---|---|---|---|---|---|
| 01 | 77.7 ms / 3.1 ms | 288.3 ns / 116.2 ns | 14.1 ns / 0.9 ns | 2.48x | 93 |
| 02 | 77.4 ms / 3.3 ms | 262.6 ns / 84.0 ns | 8.6 ns / 0.7 ns | 3.13x | 84 |
| 03 | 72.4 ms / 3.1 ms | 263.4 ns / 70.0 ns | 8.7 ns / 0.6 ns | 3.76x | 81 |
| 04 | 79.8 ms / 4.4 ms | 322.7 ns / 119.7 ns | 11.6 ns / 0.8 ns | 2.70x | 83 |
| 05 | 77.2 ms / 3.1 ms | 282.8 ns / 78.2 ns | 10.8 ns / 0.6 ns | 3.62x | 84 |
| 06 | 73.0 ms / 3.1 ms | 311.0 ns / 102.8 ns | 8.8 ns / 0.7 ns | 3.03x | 81 |
| 07 | 75.4 ms / 4.6 ms | 323.7 ns / 103.5 ns | 17.9 ns / 0.9 ns | 3.13x | 81 |
| 08 | 72.9 ms / 3.1 ms | 286.6 ns / 87.8 ns | 8.5 ns / 0.7 ns | 3.26x | 80 |
| 09 | 76.1 ms / 3.6 ms | 309.3 ns / 126.6 ns | 9.0 ns / 1.2 ns | 2.44x | 83 |
| 10 | 73.9 ms / 3.4 ms | 265.4 ns / 108.5 ns | 8.6 ns / 1.0 ns | 2.45x | 82 |
| AVG | 75.57 ms / 3.49 ms | 291.6 ns / 99.7 ns | 10.7 ns / 0.8 ns | 3.00x | 83 segments |
BTreeMap) $\to$ 3.1 KB (PGM-Index) (-99.99%).Simulates randomly distributed 64-bit object IDs, hashed identifiers, and sparse primary keys.
Distribution: Uniform Sparse Random | Tolerance: epsilon = 16
| Metric | BTreeMap (Standard) | PGM-Index (Learned) | Improvement Factor |
|---|---|---|---|
| Mean Build Time | 76.01 ms | 3.34 ms | 22.8x faster build |
| Index-Only Memory | 45.78 MB | 48.1 KB | -99.89% memory reduction |
| Segments Count | N/A (thousands of nodes) | 1,406 segments | 711x compression |
| Mean Hit Latency | 264.6 ns/op | 89.4 ns/op | 2.98x faster (Min: 73.2 ns, Max: 109.9 ns) |
| Mean Miss Latency | 251.7 ns/op | 81.6 ns/op | 3.08x faster |
| Verification | 100.0% match | 100.0% match | Zero discrepancies across 10 runs |
Simulates real-world power-law distributions (web traffic requests, financial transaction sizes, file size distributions).
Distribution: Lognormal Clustered | Tolerance: epsilon = 16
| Metric | BTreeMap (Standard) | PGM-Index (Learned) | Improvement Factor |
|---|---|---|---|
| Mean Build Time | 75.04 ms | 3.24 ms | 23.1x faster build |
| Index-Only Memory | 45.78 MB | 12.1 KB | -99.97% memory reduction |
| Segments Count | N/A | 455 segments | 2,197x compression |
| Mean Hit Latency | 260.2 ns/op | 81.3 ns/op | 3.21x faster (Min: 69.2 ns, Max: 92.1 ns) |
| Mean Miss Latency | 68.7 ns/op | 42.0 ns/op | 1.64x faster |
| Verification | 100.0% match | 100.0% match | Zero discrepancies across 10 runs |
Analytical engines ingest massive sorted blocks. PGM-Index acts as a lightweight zone map replacement, locating record offsets across millions of rows with minimal CPU instruction overhead.
In an LSM storage engine, SSTables written to NVMe are strictly immutable. Replacing traditional sparse block index tables with PGM segments allows billions of keys to be indexed entirely in host CPU cache or RAM, reducing NVMe read amplification to exactly 1 physical block I/O per query.
Fast local file indexers (e.g., Everything, NTFS USN Journal analyzers) can catalog tens of millions of records within a few megabytes of RAM while delivering sub-100ns path resolution.
Forwarding Information Bases (FIB) at 100+ Gbps line rates require deterministic sub-microsecond packet IP classification where L1/L2 cache saturation would immediately induce packet drops.
Add learned_index_lab to your Cargo.toml, or include pgm.rs directly in your project:
use pgm::PgmIndex;
fn main() {
// 1. Prepare sorted data
let data: Vec<u64> = vec![
100, 105, 110, 115, 120, 250, 500, 1000, 1500, 2000, 5000, 10000
];
// 2. Build the PGM index with maximum error epsilon (e.g. 16 or 32)
let pgm = PgmIndex::build(data, 16);
// 3. Point lookup (O(log S + log epsilon))
match pgm.find(1500) {
Some(index) => println!("Found key at position: {}", index),
None => println!("Key not present in dataset"),
}
// 4. Inspect memory overhead
println!("Total segments created: {}", pgm.segments.len());
println!("Index-only memory: {} bytes", pgm.index_only_bytes());
}
To verify the test suite and run the full 10-run statistical benchmark on your own machine:
# Clone the repository
git clone https://github.com/gilnett/learned_index_lab.git
cd learned_index_lab
# Run unit tests covering edge cases, gaps, and duplicates
cargo test
# Execute the 10-run release benchmark suite
cargo run --release
Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-2.0).
High-performance Piecewise Geometric Model (PGM) learned index implementation in pure Rust with strict epsilon error guarantees
Rust
152
9 commits
updated Oct 19, 2026
A zero-dependency, production-grade implementation of Piecewise Geometric Model (PGM) Learned Index Structures in pure Rust.
This repository demonstrates the paradigm shift away from traditional comparison-based pointer hierarchies (such as B-Trees and LSM block indexes) to mathematical function approximations of Empirical Cumulative Distribution Functions (CDFs) with provable $\epsilon$ error bounds.
For more than 50 years, database indexing has relied on tree-based data structures (B-Tree, B+Tree, LSM-Tree). While mature, B-Trees possess inherent physical constraints on modern CPU architectures:
Learned Indexes (originally introduced by Kraska et al., MIT / Google) reframe indexing as a regression problem: Given a sorted sequence of keys $K = (k_0, k_1, \dots, k_{N-1})$, the position of any key $k$ in memory corresponds to the scaled Empirical Cumulative Distribution Function:
$$\text{Pos}(k) \approx N \times \text{CDF}(k)$$
Rather than allocating deep node hierarchies, a PGM-Index partitions the data into a minimal set of linear segments $y = ax + b$, guaranteeing that the true position of any indexed key is within a strict error window $[y - \epsilon, y + \epsilon]$.
The index builder executes in a single streaming pass ($O(N)$ time, $O(1)$ extra space).
For a segment starting at point $(x_0, y_0)$ and a given maximum error tolerance $\epsilon$: Each subsequent point $(x_i, y_i)$ restricts the feasible slope interval $[s_{\min}, s_{\max}]$:
$$s_{\min} = \max_{j} \left[ \frac{(y_j - \epsilon) - y_0}{x_j - x_0} \right]$$
$$s_{\max} = \min_{j} \left[ \frac{(y_j + \epsilon) - y_0}{x_j - x_0} \right]$$
As long as $s_{\min} \le s_{\max}$, all points observed so far can be represented by a single straight line with error at most $\epsilon$. Once $s_{\min} > s_{\max}$, the segment is finalized with slope $s = \frac{s_{\min} + s_{\max}}{2}$, and a new segment starts.
During point lookup:
| Dimension | Standard BTreeMap | PGM-Index (Learned) | Concrete Hardware Impact |
|---|---|---|---|
| Index Structure | Dynamic nodes with pointers | Contiguous array of $(a, b, k_0)$ segments | Entire index resides in CPU L1/L2 cache |
| Lookup Mechanism | 4–6 node hops across RAM | 1 affine math evaluation + local L1 search | Eliminates unpredictable pointer chasing |
| Build Time | $O(N \log N)$ repeated node rebalancing | $O(N)$ single-pass linear streaming | ~22x faster build times |
| Memory Footprint | ~45 MB per 1,000,000 keys | 3.1 KB – 48 KB per 1,000,000 keys | -99.89% to -99.99% memory reduction |
All benchmarks were measured locally on x86_64 hardware using Rust 1.99 in --release mode.
Each scenario was executed over 10 consecutive runs with distinct randomized seeds to eliminate CPU scheduling noise and warm-cache anomalies.
Simulates monotonically increasing timestamps, network log sequences, or primary auto-increment keys.
Distribution: Dense Timestamp | Tolerance: epsilon = 32
| Run # | Build Time (BTree / PGM) | Hit Latency (BTree / PGM) | Miss Latency (BTree / PGM) | Speedup (Hit) | Segments |
|---|---|---|---|---|---|
| 01 | 77.7 ms / 3.1 ms | 288.3 ns / 116.2 ns | 14.1 ns / 0.9 ns | 2.48x | 93 |
| 02 | 77.4 ms / 3.3 ms | 262.6 ns / 84.0 ns | 8.6 ns / 0.7 ns | 3.13x | 84 |
| 03 | 72.4 ms / 3.1 ms | 263.4 ns / 70.0 ns | 8.7 ns / 0.6 ns | 3.76x | 81 |
| 04 | 79.8 ms / 4.4 ms | 322.7 ns / 119.7 ns | 11.6 ns / 0.8 ns | 2.70x | 83 |
| 05 | 77.2 ms / 3.1 ms | 282.8 ns / 78.2 ns | 10.8 ns / 0.6 ns | 3.62x | 84 |
| 06 | 73.0 ms / 3.1 ms | 311.0 ns / 102.8 ns | 8.8 ns / 0.7 ns | 3.03x | 81 |
| 07 | 75.4 ms / 4.6 ms | 323.7 ns / 103.5 ns | 17.9 ns / 0.9 ns | 3.13x | 81 |
| 08 | 72.9 ms / 3.1 ms | 286.6 ns / 87.8 ns | 8.5 ns / 0.7 ns | 3.26x | 80 |
| 09 | 76.1 ms / 3.6 ms | 309.3 ns / 126.6 ns | 9.0 ns / 1.2 ns | 2.44x | 83 |
| 10 | 73.9 ms / 3.4 ms | 265.4 ns / 108.5 ns | 8.6 ns / 1.0 ns | 2.45x | 82 |
| AVG | 75.57 ms / 3.49 ms | 291.6 ns / 99.7 ns | 10.7 ns / 0.8 ns | 3.00x | 83 segments |
BTreeMap) $\to$ 3.1 KB (PGM-Index) (-99.99%).Simulates randomly distributed 64-bit object IDs, hashed identifiers, and sparse primary keys.
Distribution: Uniform Sparse Random | Tolerance: epsilon = 16
| Metric | BTreeMap (Standard) | PGM-Index (Learned) | Improvement Factor |
|---|---|---|---|
| Mean Build Time | 76.01 ms | 3.34 ms | 22.8x faster build |
| Index-Only Memory | 45.78 MB | 48.1 KB | -99.89% memory reduction |
| Segments Count | N/A (thousands of nodes) | 1,406 segments | 711x compression |
| Mean Hit Latency | 264.6 ns/op | 89.4 ns/op | 2.98x faster (Min: 73.2 ns, Max: 109.9 ns) |
| Mean Miss Latency | 251.7 ns/op | 81.6 ns/op | 3.08x faster |
| Verification | 100.0% match | 100.0% match | Zero discrepancies across 10 runs |
Simulates real-world power-law distributions (web traffic requests, financial transaction sizes, file size distributions).
Distribution: Lognormal Clustered | Tolerance: epsilon = 16
| Metric | BTreeMap (Standard) | PGM-Index (Learned) | Improvement Factor |
|---|---|---|---|
| Mean Build Time | 75.04 ms | 3.24 ms | 23.1x faster build |
| Index-Only Memory | 45.78 MB | 12.1 KB | -99.97% memory reduction |
| Segments Count | N/A | 455 segments | 2,197x compression |
| Mean Hit Latency | 260.2 ns/op | 81.3 ns/op | 3.21x faster (Min: 69.2 ns, Max: 92.1 ns) |
| Mean Miss Latency | 68.7 ns/op | 42.0 ns/op | 1.64x faster |
| Verification | 100.0% match | 100.0% match | Zero discrepancies across 10 runs |
Analytical engines ingest massive sorted blocks. PGM-Index acts as a lightweight zone map replacement, locating record offsets across millions of rows with minimal CPU instruction overhead.
In an LSM storage engine, SSTables written to NVMe are strictly immutable. Replacing traditional sparse block index tables with PGM segments allows billions of keys to be indexed entirely in host CPU cache or RAM, reducing NVMe read amplification to exactly 1 physical block I/O per query.
Fast local file indexers (e.g., Everything, NTFS USN Journal analyzers) can catalog tens of millions of records within a few megabytes of RAM while delivering sub-100ns path resolution.
Forwarding Information Bases (FIB) at 100+ Gbps line rates require deterministic sub-microsecond packet IP classification where L1/L2 cache saturation would immediately induce packet drops.
Add learned_index_lab to your Cargo.toml, or include pgm.rs directly in your project:
use pgm::PgmIndex;
fn main() {
// 1. Prepare sorted data
let data: Vec<u64> = vec![
100, 105, 110, 115, 120, 250, 500, 1000, 1500, 2000, 5000, 10000
];
// 2. Build the PGM index with maximum error epsilon (e.g. 16 or 32)
let pgm = PgmIndex::build(data, 16);
// 3. Point lookup (O(log S + log epsilon))
match pgm.find(1500) {
Some(index) => println!("Found key at position: {}", index),
None => println!("Key not present in dataset"),
}
// 4. Inspect memory overhead
println!("Total segments created: {}", pgm.segments.len());
println!("Index-only memory: {} bytes", pgm.index_only_bytes());
}
To verify the test suite and run the full 10-run statistical benchmark on your own machine:
# Clone the repository
git clone https://github.com/gilnett/learned_index_lab.git
cd learned_index_lab
# Run unit tests covering edge cases, gaps, and duplicates
cargo test
# Execute the 10-run release benchmark suite
cargo run --release
Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-2.0).
376 followers · starred Oct 2026