Skip to main content

nodedb_types/config/tuning/
engines.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Per-engine tuning: Vector, Sparse, Graph, Timeseries.
4
5use serde::{Deserialize, Serialize};
6
7/// Vector engine tuning (HNSW, PQ, IVF).
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct VectorTuning {
10    #[serde(default = "default_flat_index_threshold")]
11    pub flat_index_threshold: usize,
12    #[serde(default = "default_seal_threshold")]
13    pub seal_threshold: usize,
14    #[serde(default = "default_pq_m")]
15    pub default_pq_m: usize,
16    #[serde(default = "default_ivf_cells")]
17    pub default_ivf_cells: usize,
18    #[serde(default = "default_ivf_nprobe")]
19    pub default_ivf_nprobe: usize,
20}
21
22impl Default for VectorTuning {
23    fn default() -> Self {
24        Self {
25            flat_index_threshold: default_flat_index_threshold(),
26            seal_threshold: default_seal_threshold(),
27            default_pq_m: default_pq_m(),
28            default_ivf_cells: default_ivf_cells(),
29            default_ivf_nprobe: default_ivf_nprobe(),
30        }
31    }
32}
33
34fn default_flat_index_threshold() -> usize {
35    10_000
36}
37fn default_seal_threshold() -> usize {
38    65_536
39}
40fn default_pq_m() -> usize {
41    8
42}
43fn default_ivf_cells() -> usize {
44    256
45}
46fn default_ivf_nprobe() -> usize {
47    16
48}
49
50/// Sparse/metadata engine tuning (BM25, GSI, HyperLogLog).
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct SparseTuning {
53    #[serde(default = "default_bm25_k1")]
54    pub bm25_k1: f32,
55    #[serde(default = "default_bm25_b")]
56    pub bm25_b: f32,
57    #[serde(default = "default_max_gsis_per_collection")]
58    pub max_gsis_per_collection: usize,
59    #[serde(default = "default_hll_m")]
60    pub hll_registers: usize,
61    #[serde(default = "default_hll_p")]
62    pub hll_precision: u32,
63}
64
65impl Default for SparseTuning {
66    fn default() -> Self {
67        Self {
68            bm25_k1: default_bm25_k1(),
69            bm25_b: default_bm25_b(),
70            max_gsis_per_collection: default_max_gsis_per_collection(),
71            hll_registers: default_hll_m(),
72            hll_precision: default_hll_p(),
73        }
74    }
75}
76
77fn default_bm25_k1() -> f32 {
78    1.2
79}
80fn default_bm25_b() -> f32 {
81    0.75
82}
83fn default_max_gsis_per_collection() -> usize {
84    4
85}
86fn default_hll_m() -> usize {
87    256
88}
89fn default_hll_p() -> u32 {
90    8
91}
92
93/// Default cap on visited nodes during BFS traversals.
94/// Prevents supernode fan-out explosion from consuming unbounded memory.
95pub const DEFAULT_MAX_VISITED: usize = 100_000;
96
97/// Default maximum BFS traversal depth.
98pub const DEFAULT_MAX_DEPTH: usize = 10;
99
100/// Default hard cap on results emitted from a single variable-length MATCH
101/// expansion before it must page via cross-shard resume.
102pub const DEFAULT_VARLEN_MAX_RESULTS: usize = 100_000;
103
104/// Default hard cap on the live frontier of a single variable-length MATCH
105/// expansion before it must page via cross-shard resume.
106pub const DEFAULT_VARLEN_MAX_FRONTIER: usize = 100_000;
107
108/// Graph engine tuning (traversal limits, LCC algorithm).
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct GraphTuning {
111    #[serde(default = "default_max_visited")]
112    pub max_visited: usize,
113    #[serde(default = "default_max_depth")]
114    pub max_depth: usize,
115    #[serde(default = "default_lcc_high_degree_threshold")]
116    pub lcc_high_degree_threshold: usize,
117    #[serde(default = "default_lcc_sample_pairs")]
118    pub lcc_sample_pairs: usize,
119    /// Hard cap on results emitted from a single variable-length MATCH
120    /// (`[*min..max]`) expansion. When an expansion would exceed this it
121    /// truncates at the current hop boundary and surfaces a resume cursor so
122    /// the remainder is paged across follow-up rounds (cross-shard or local) —
123    /// no row is silently dropped. Bounds peak per-expansion result allocation.
124    #[serde(default = "default_varlen_max_results")]
125    pub varlen_max_results: usize,
126    /// Hard cap on the live (per-hop) frontier of a single variable-length
127    /// MATCH expansion. When a single hop would grow the frontier past this it
128    /// truncates and pages via resume, bounding peak intermediate allocation on
129    /// dense / bidirectional traversals.
130    #[serde(default = "default_varlen_max_frontier")]
131    pub varlen_max_frontier: usize,
132}
133
134impl Default for GraphTuning {
135    fn default() -> Self {
136        Self {
137            max_visited: default_max_visited(),
138            max_depth: default_max_depth(),
139            lcc_high_degree_threshold: default_lcc_high_degree_threshold(),
140            lcc_sample_pairs: default_lcc_sample_pairs(),
141            varlen_max_results: default_varlen_max_results(),
142            varlen_max_frontier: default_varlen_max_frontier(),
143        }
144    }
145}
146
147fn default_max_visited() -> usize {
148    DEFAULT_MAX_VISITED
149}
150fn default_max_depth() -> usize {
151    DEFAULT_MAX_DEPTH
152}
153fn default_lcc_high_degree_threshold() -> usize {
154    2_000
155}
156fn default_lcc_sample_pairs() -> usize {
157    10_000
158}
159fn default_varlen_max_results() -> usize {
160    DEFAULT_VARLEN_MAX_RESULTS
161}
162fn default_varlen_max_frontier() -> usize {
163    DEFAULT_VARLEN_MAX_FRONTIER
164}
165
166/// Timeseries engine tuning (memtable budgets, block sizes).
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct TimeseriesToning {
169    /// Soft memtable budget: reaching it schedules a flush at the next record
170    /// boundary. Rows already in the memtable are never refused because of it.
171    #[serde(default = "default_memtable_budget_bytes")]
172    pub memtable_budget_bytes: usize,
173    /// Hard memtable ceiling. Reaching it forces a flush BEFORE the next WAL
174    /// record is ingested, never partway through one: by the time the Data
175    /// Plane sees a record its WAL append has already committed, so the
176    /// memtable must take the record whole or the write is silently lost.
177    /// Consequently a single record can carry the memtable up to its own
178    /// decoded size past this ceiling; see the admission gate in the
179    /// timeseries ingest handler for the bound that costs.
180    #[serde(default = "default_memtable_hard_limit_bytes")]
181    pub memtable_hard_limit_bytes: usize,
182    /// Maximum distinct values per symbol (tag / string) column in one
183    /// memtable generation. Flushing resets the dictionaries, so this bounds
184    /// dictionary size between flushes rather than over the collection's life.
185    #[serde(default = "default_max_tag_cardinality")]
186    pub max_tag_cardinality: u32,
187    #[serde(default = "default_total_budget_bytes")]
188    pub total_budget_bytes: usize,
189    #[serde(default = "default_ts_block_size")]
190    pub block_size: usize,
191}
192
193impl Default for TimeseriesToning {
194    fn default() -> Self {
195        Self {
196            memtable_budget_bytes: default_memtable_budget_bytes(),
197            memtable_hard_limit_bytes: default_memtable_hard_limit_bytes(),
198            max_tag_cardinality: default_max_tag_cardinality(),
199            total_budget_bytes: default_total_budget_bytes(),
200            block_size: default_ts_block_size(),
201        }
202    }
203}
204
205fn default_memtable_budget_bytes() -> usize {
206    64 * 1024 * 1024
207}
208fn default_memtable_hard_limit_bytes() -> usize {
209    80 * 1024 * 1024
210}
211fn default_max_tag_cardinality() -> u32 {
212    100_000
213}
214fn default_total_budget_bytes() -> usize {
215    100 * 1024 * 1024
216}
217fn default_ts_block_size() -> usize {
218    1024
219}
220
221/// KV engine tuning (hash table, expiry wheel, slab allocator).
222///
223/// Controls the per-core hash table parameters, incremental rehash behavior,
224/// expiry wheel tick rate, and the per-tick reap budget that prevents reactor
225/// stalls during mass-expiry events.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct KvTuning {
228    /// Default initial hash table capacity per collection (number of slots).
229    /// Should be a power of two. Larger values reduce early rehash churn for
230    /// collections that will grow quickly. Default: 16384.
231    #[serde(default = "default_kv_capacity")]
232    pub default_capacity: usize,
233
234    /// Hash table load factor threshold that triggers incremental rehash.
235    /// When `entries / capacity > rehash_load_factor`, the table begins
236    /// doubling. Range: 0.5–0.9. Default: 0.75 (standard Robin Hood threshold).
237    #[serde(default = "default_kv_rehash_load_factor")]
238    pub rehash_load_factor: f32,
239
240    /// Number of entries rehashed per PUT during incremental rehash.
241    /// Higher values complete rehash faster but add per-PUT latency.
242    /// Default: 8.
243    #[serde(default = "default_kv_rehash_batch_size")]
244    pub rehash_batch_size: usize,
245
246    /// Default inline value threshold in bytes. Values at or below this size
247    /// are stored directly in the hash entry (no pointer chase). Larger values
248    /// overflow to slab-allocated Binary Tuples. Default: 64.
249    #[serde(default = "default_kv_inline_threshold")]
250    pub default_inline_threshold: usize,
251
252    /// Maximum expirations processed per reactor tick (event loop iteration).
253    /// Prevents mass-expiry events (e.g., 10M keys with identical TTL) from
254    /// stalling the TPC core. Expired-but-not-yet-reaped keys are invisible
255    /// to GET (lazy fallback). Default: 1024.
256    #[serde(default = "default_kv_expiry_reap_budget")]
257    pub expiry_reap_budget: usize,
258
259    /// Expiry wheel tick interval in milliseconds. Determines the granularity
260    /// of TTL expiration. Lower = more precise but more CPU overhead.
261    /// Default: 1000 (1 second).
262    #[serde(default = "default_kv_expiry_tick_ms")]
263    pub expiry_tick_ms: u64,
264}
265
266impl Default for KvTuning {
267    fn default() -> Self {
268        Self {
269            default_capacity: default_kv_capacity(),
270            rehash_load_factor: default_kv_rehash_load_factor(),
271            rehash_batch_size: default_kv_rehash_batch_size(),
272            default_inline_threshold: default_kv_inline_threshold(),
273            expiry_reap_budget: default_kv_expiry_reap_budget(),
274            expiry_tick_ms: default_kv_expiry_tick_ms(),
275        }
276    }
277}
278
279fn default_kv_capacity() -> usize {
280    16_384
281}
282fn default_kv_rehash_load_factor() -> f32 {
283    0.75
284}
285fn default_kv_rehash_batch_size() -> usize {
286    8
287}
288fn default_kv_inline_threshold() -> usize {
289    64
290}
291fn default_kv_expiry_reap_budget() -> usize {
292    1024
293}
294fn default_kv_expiry_tick_ms() -> u64 {
295    1000
296}