Skip to main content

linkrs_api/
lib.rs

1//! The Linkrs API module
2//!
3//! Provides the transport-independent core API consumed by the network
4//! service layer (`linkrs-server`) and library users.
5
6pub mod api_core;
7
8// Online migration drain fence shared by embedded and server callers.
9pub mod migration_online;
10
11// Transaction-aware session variable store shared by the server and
12// embedded session implementations.
13pub mod session_variables;
14
15#[cfg(feature = "embedded")]
16pub mod embedded;
17
18// ── Core re-exports ──────────────────────────────────────────────
19pub use api_core::{CoreError, CoreResult, QueryApi, SchemaApi, SyncApi};
20
21#[cfg(feature = "vector")]
22pub use api_core::{VectorApi, VectorSearchResult};
23
24// Hybrid retrieval helpers shared by fulltext and vector callers. Vector
25// point IDs carry index suffixes while fulltext doc IDs do not, so fusion
26// requires explicit normalization first.
27pub use linkrs_core::fusion::{
28    normalize_vector_hits, normalize_vector_point_id, rrf_fuse, weighted_fuse, FusedHit, ScoredHit,
29};
30
31/// Mapping helpers between raw linkrs-config settings and simvec
32/// types; keeps the two crates decoupled from each other.
33#[cfg(feature = "vector")]
34pub mod vector_config {
35    use linkrs_config::LocalVectorConfig;
36
37    /// Map raw TOML IVF settings to the local engine's IVF configuration.
38    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    /// Map raw TOML HNSW settings to the local engine's HNSW configuration.
62    /// A TOML value of `0` leaves the field at the engine default; nonzero
63    /// values override it.
64    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    /// Map raw TOML quantization settings to the local engine's quantization config.
87    ///
88    /// Mirrors Qdrant's scalar/product/binary builders;
89    /// `enabled=false` or missing/unknown type yields `None` (exact f32).
90    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); // X8: 128*4/8=64
222        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;