1pub mod api_core;
7
8pub mod migration_online;
10
11pub mod session_variables;
14
15#[cfg(feature = "embedded")]
16pub mod embedded;
17
18pub use api_core::{CoreError, CoreResult, QueryApi, SchemaApi, SyncApi};
20
21#[cfg(feature = "vector")]
22pub use api_core::{VectorApi, VectorSearchResult};
23
24pub use linkrs_core::fusion::{
28 normalize_vector_hits, normalize_vector_point_id, rrf_fuse, weighted_fuse, FusedHit, ScoredHit,
29};
30
31#[cfg(feature = "vector")]
34pub mod vector_config {
35 use linkrs_config::LocalVectorConfig;
36
37 pub fn local_ivf_config(local: &LocalVectorConfig) -> Option<simvec::IvfConfig> {
39 let s = local.ivf.as_ref()?;
40 Some(simvec::IvfConfig {
41 lists: if s.lists == 0 {
42 None
43 } else {
44 Some(s.lists.max(1))
45 },
46 min_build_points: s.min_build_points,
47 sample_limit: s.sample_limit,
48 kmeans_max_iter: s.kmeans_max_iter,
49 drift_threshold: s.drift_threshold,
50 drift_check_interval: s.drift_check_interval,
51 default_nprobe: s.default_nprobe,
52 auto_promotion: s.auto_promotion,
53 max_probes: if s.max_probes == 0 {
54 None
55 } else {
56 Some(s.max_probes)
57 },
58 })
59 }
60
61 pub fn local_hnsw_config(local: &LocalVectorConfig) -> Option<simvec::HnswConfig> {
65 let s = local.hnsw.as_ref()?;
66 let defaults = simvec::HnswConfig::default();
67 Some(simvec::HnswConfig {
68 m: if s.m > 0 { s.m } else { defaults.m },
69 ef_construct: if s.ef_construct > 0 {
70 s.ef_construct
71 } else {
72 defaults.ef_construct
73 },
74 full_scan_threshold: (s.full_scan_threshold > 0).then_some(s.full_scan_threshold),
75 ef_search: if s.ef_search > 0 {
76 s.ef_search
77 } else {
78 defaults.ef_search
79 },
80 iterative_max_rounds: (s.iterative_max_rounds > 0).then_some(s.iterative_max_rounds),
81 max_scan_tuples: (s.max_scan_tuples > 0).then_some(s.max_scan_tuples),
82 ..defaults
83 })
84 }
85
86 pub fn local_quantization_config(
91 local: &LocalVectorConfig,
92 ) -> Option<simvec::QuantizationConfig> {
93 let s = local.quantization.as_ref()?;
94 if !s.enabled {
95 return None;
96 }
97 let type_str = s.quantization_type.as_deref()?.to_lowercase();
98 let always_ram = s.always_ram;
99 match type_str.as_str() {
100 "scalar" => {
101 let quantile = s.quantile.unwrap_or(0.99);
102 let mut cfg = simvec::QuantizationConfig::scalar(quantile);
103 if let Some(ar) = always_ram {
104 cfg = cfg.with_always_ram(ar);
105 }
106 Some(cfg)
107 }
108 "binary" => {
109 let mut cfg = simvec::QuantizationConfig::binary();
110 if let Some(ar) = always_ram {
111 cfg = cfg.with_always_ram(ar);
112 }
113 Some(cfg)
114 }
115 "product" | "pq" => {
116 let ratio = match s
117 .compression
118 .as_deref()
119 .unwrap_or("x4")
120 .to_lowercase()
121 .as_str()
122 {
123 "x4" | "4" => simvec::CompressionRatio::X4,
124 "x8" | "8" => simvec::CompressionRatio::X8,
125 "x16" | "16" => simvec::CompressionRatio::X16,
126 "x32" | "32" => simvec::CompressionRatio::X32,
127 "x64" | "64" => simvec::CompressionRatio::X64,
128 _ => simvec::CompressionRatio::X4,
129 };
130 let mut cfg = simvec::QuantizationConfig::product(ratio);
131 if let Some(ar) = always_ram {
132 cfg = cfg.with_always_ram(ar);
133 }
134 Some(cfg)
135 }
136 _ => None,
137 }
138 }
139}
140
141#[cfg(all(test, feature = "vector"))]
142mod vector_config_tests {
143 use super::vector_config::{local_hnsw_config, local_ivf_config, local_quantization_config};
144 use linkrs_config::{HnswSettings, IvfSettings, LocalVectorConfig};
145
146 #[test]
147 fn zero_toml_fields_map_to_engine_defaults() {
148 let local = LocalVectorConfig {
149 hnsw: Some(HnswSettings::default()),
150 ivf: Some(IvfSettings::default()),
151 ..LocalVectorConfig::default()
152 };
153
154 let hnsw = local_hnsw_config(&local).unwrap();
155 assert_eq!(hnsw.iterative_max_rounds, None);
156 assert_eq!(hnsw.max_scan_tuples, None);
157
158 let ivf = local_ivf_config(&local).unwrap();
159 assert_eq!(ivf.max_probes, None);
160 }
161
162 #[test]
163 fn scan_limit_toml_fields_flow_through() {
164 let local = LocalVectorConfig {
165 hnsw: Some(HnswSettings {
166 iterative_max_rounds: 5,
167 max_scan_tuples: 20_000,
168 ..HnswSettings::default()
169 }),
170 ivf: Some(IvfSettings {
171 max_probes: 16,
172 ..IvfSettings::default()
173 }),
174 ..LocalVectorConfig::default()
175 };
176
177 let hnsw = local_hnsw_config(&local).unwrap();
178 assert_eq!(hnsw.iterative_max_rounds, Some(5));
179 assert_eq!(hnsw.max_scan_tuples, Some(20_000));
180 assert!(hnsw.validate().is_ok());
181
182 let ivf = local_ivf_config(&local).unwrap();
183 assert_eq!(ivf.max_probes, Some(16));
184 assert!(ivf.validate().is_ok());
185 assert_eq!(
186 ivf.effective_max_probes(8),
187 8,
188 "probe cap clamped to list count"
189 );
190 }
191
192 #[test]
193 fn quantization_toml_maps_to_engine_config() {
194 let local = LocalVectorConfig {
195 quantization: Some(linkrs_config::QuantizationSettings {
196 enabled: true,
197 quantization_type: Some("scalar".to_string()),
198 quantile: Some(0.95),
199 compression: None,
200 always_ram: Some(false),
201 }),
202 ..LocalVectorConfig::default()
203 };
204 let qc = local_quantization_config(&local).unwrap();
205 assert!(qc.enabled);
206 assert_eq!(qc.quantile_or_default(), 0.95);
207 assert!(!qc.always_ram());
208
209 let local = LocalVectorConfig {
210 quantization: Some(linkrs_config::QuantizationSettings {
211 enabled: true,
212 quantization_type: Some("product".to_string()),
213 quantile: None,
214 compression: Some("x8".to_string()),
215 always_ram: None,
216 }),
217 ..LocalVectorConfig::default()
218 };
219 let qc = local_quantization_config(&local).unwrap();
220 assert!(qc.is_product());
221 assert_eq!(qc.quant_bytes_per_vector(128), 64); assert!(qc.validate(128).is_ok());
223
224 let disabled = LocalVectorConfig {
225 quantization: Some(linkrs_config::QuantizationSettings {
226 enabled: false,
227 quantization_type: Some("scalar".to_string()),
228 ..Default::default()
229 }),
230 ..LocalVectorConfig::default()
231 };
232 assert!(local_quantization_config(&disabled).is_none());
233 }
234}
235
236#[cfg(feature = "embedded")]
237pub use embedded::GraphDatabase;