vectorlite/lib.rs
1//! # VectorLite
2//!
3//! A high-performance, in-memory vector database optimized for AI agent workloads with HTTP API and thread-safe concurrency.
4//!
5//! ## Overview
6//!
7//! VectorLite is designed for **single-instance, low-latency vector operations** in AI agent environments. It prioritizes **sub-millisecond search performance** over distributed scalability, making it ideal for:
8//!
9//! - **AI Agent Sessions**: Session-scoped vector storage with fast retrieval
10//! - **Real-time Search**: Sub-millisecond response requirements
11//! - **Prototype Development**: Rapid iteration without infrastructure complexity
12//! - **Single-tenant Applications**: No multi-tenancy isolation requirements
13//!
14//! ## Key Features
15//!
16//! - **In-memory storage** for zero-latency access patterns
17//! - **Native Rust ML models** using Candle framework with pluggable architecture
18//! - **Thread-safe concurrency** with RwLock per collection and atomic ID generation
19//! - **HNSW indexing** for approximate nearest neighbor search with configurable accuracy
20//! - **HTTP API** for easy integration with AI agents and other services
21//!
22//! ## Quick Start
23//!
24//! ```rust
25//! use vectorlite::{VectorLiteClient, EmbeddingGenerator, IndexType, SimilarityMetric};
26//!
27//! // Create client with embedding function
28//! let client = VectorLiteClient::new(Box::new(EmbeddingGenerator::new()?));
29//!
30//! // Create collection
31//! client.create_collection("documents", IndexType::HNSW)?;
32//!
33//! // Add text (auto-generates embedding and ID)
34//! let id = client.add_text_to_collection("documents", "Hello world")?;
35//!
36//! // Search
37//! let results = client.search_text_in_collection(
38//! "documents",
39//! "hello",
40//! 5,
41//! SimilarityMetric::Cosine
42//! )?;
43//! ```
44//!
45//! ## Index Types
46//!
47//! ### FlatIndex
48//! - **Complexity**: O(n) search, O(1) insert
49//! - **Memory**: Linear with dataset size
50//! - **Use Case**: Small datasets (< 10K vectors) or exact search requirements
51//!
52//! ### HNSWIndex
53//! - **Complexity**: O(log n) search, O(log n) insert
54//! - **Memory**: ~2-3x vector size due to graph structure
55//! - **Use Case**: Large datasets with approximate search tolerance
56//!
57//! ## Similarity Metrics
58//!
59//! - **Cosine**: Default for normalized embeddings, scale-invariant
60//! - **Euclidean**: Geometric distance, sensitive to vector magnitude
61//! - **Manhattan**: L1 norm, robust to outliers
62//! - **Dot Product**: Raw similarity, requires consistent vector scaling
63//!
64//! ## HTTP Server
65//!
66//! ```rust
67//! use vectorlite::{VectorLiteClient, EmbeddingGenerator, start_server};
68//!
69//! #[tokio::main]
70//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
71//! let client = VectorLiteClient::new(Box::new(EmbeddingGenerator::new()?));
72//! start_server(client, "127.0.0.1", 3000).await?;
73//! Ok(())
74//! }
75//! ```
76//!
77//! ## Configuration Profiles
78//!
79//! ```bash
80//! # Balanced (default)
81//! cargo build
82//!
83//! # Memory-constrained environments
84//! cargo build --features memory-optimized
85//!
86//! # High-precision search
87//! cargo build --features high-accuracy
88//! ```
89
90pub mod index;
91pub mod embeddings;
92pub mod client;
93pub mod server;
94
95pub use index::flat::FlatIndex;
96pub use index::hnsw::HNSWIndex;
97pub use embeddings::{EmbeddingGenerator, EmbeddingFunction};
98pub use client::{VectorLiteClient, Collection, Settings, IndexType};
99pub use server::{create_app, start_server};
100
101use serde::{Serialize, Deserialize};
102
103/// Default vector dimension for embedding models
104pub const DEFAULT_VECTOR_DIMENSION: usize = 768;
105
106/// Represents a vector with an ID and floating-point values
107///
108/// # Examples
109///
110/// ```rust
111/// use vectorlite::Vector;
112///
113/// let vector = Vector {
114/// id: 1,
115/// values: vec![0.1, 0.2, 0.3, 0.4],
116/// };
117/// ```
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct Vector {
120 /// Unique identifier for the vector
121 pub id: u64,
122 /// The vector values (embedding coordinates)
123 pub values: Vec<f64>,
124}
125
126/// Search result containing a vector ID and similarity score
127///
128/// Results are typically sorted by score in descending order (highest similarity first).
129///
130/// # Examples
131///
132/// ```rust
133/// use vectorlite::SearchResult;
134///
135/// let result = SearchResult {
136/// id: 42,
137/// score: 0.95,
138/// };
139/// ```
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SearchResult {
142 /// The ID of the matching vector
143 pub id: u64,
144 /// Similarity score (higher is more similar)
145 pub score: f64,
146}
147
148/// Trait for vector indexing implementations
149///
150/// This trait defines the common interface for different vector indexing strategies,
151/// allowing for pluggable implementations like FlatIndex and HNSWIndex.
152///
153/// # Examples
154///
155/// ```rust
156/// use vectorlite::{VectorIndex, Vector, SimilarityMetric, FlatIndex};
157///
158/// let mut index = FlatIndex::new(3, Vec::new());
159/// let vector = Vector { id: 1, values: vec![1.0, 2.0, 3.0] };
160///
161/// index.add(vector)?;
162/// let results = index.search(&[1.1, 2.1, 3.1], 5, SimilarityMetric::Cosine);
163/// ```
164pub trait VectorIndex {
165 /// Add a vector to the index
166 fn add(&mut self, vector: Vector) -> Result<(), String>;
167
168 /// Remove a vector from the index by ID
169 fn delete(&mut self, id: u64) -> Result<(), String>;
170
171 /// Search for the k most similar vectors
172 fn search(&self, query: &[f64], k: usize, similarity_metric: SimilarityMetric) -> Vec<SearchResult>;
173
174 /// Get the number of vectors in the index
175 fn len(&self) -> usize;
176
177 /// Check if the index is empty
178 fn is_empty(&self) -> bool;
179
180 /// Get a vector by its ID
181 fn get_vector(&self, id: u64) -> Option<&Vector>;
182
183 /// Get the dimension of vectors in this index
184 fn dimension(&self) -> usize;
185}
186
187/// Wrapper enum for different vector index implementations
188///
189/// This allows for dynamic dispatch between different indexing strategies
190/// while maintaining a unified interface through the VectorIndex trait.
191///
192/// # Examples
193///
194/// ```rust
195/// use vectorlite::{VectorIndexWrapper, FlatIndex, HNSWIndex, Vector, SimilarityMetric};
196///
197/// // Create a flat index wrapper
198/// let mut wrapper = VectorIndexWrapper::Flat(FlatIndex::new(3, Vec::new()));
199///
200/// // Add a vector
201/// let vector = Vector { id: 1, values: vec![1.0, 2.0, 3.0] };
202/// wrapper.add(vector)?;
203///
204/// // Search using the wrapper
205/// let results = wrapper.search(&[1.1, 2.1, 3.1], 5, SimilarityMetric::Cosine);
206/// ```
207#[derive(Debug, Serialize, Deserialize)]
208pub enum VectorIndexWrapper {
209 /// Flat index for exact search (O(n) complexity)
210 Flat(FlatIndex),
211 /// HNSW index for approximate search (O(log n) complexity)
212 HNSW(HNSWIndex),
213}
214
215impl VectorIndex for VectorIndexWrapper {
216 fn add(&mut self, vector: Vector) -> Result<(), String> {
217 match self {
218 VectorIndexWrapper::Flat(index) => index.add(vector),
219 VectorIndexWrapper::HNSW(index) => index.add(vector),
220 }
221 }
222
223 fn delete(&mut self, id: u64) -> Result<(), String> {
224 match self {
225 VectorIndexWrapper::Flat(index) => index.delete(id),
226 VectorIndexWrapper::HNSW(index) => index.delete(id),
227 }
228 }
229
230 fn search(&self, query: &[f64], k: usize, s: SimilarityMetric) -> Vec<SearchResult> {
231 match self {
232 VectorIndexWrapper::Flat(index) => index.search(query, k, s),
233 VectorIndexWrapper::HNSW(index) => index.search(query, k, s),
234 }
235 }
236
237 fn len(&self) -> usize {
238 match self {
239 VectorIndexWrapper::Flat(index) => index.len(),
240 VectorIndexWrapper::HNSW(index) => index.len(),
241 }
242 }
243
244 fn is_empty(&self) -> bool {
245 match self {
246 VectorIndexWrapper::Flat(index) => index.is_empty(),
247 VectorIndexWrapper::HNSW(index) => index.is_empty(),
248 }
249 }
250
251 fn get_vector(&self, id: u64) -> Option<&Vector> {
252 match self {
253 VectorIndexWrapper::Flat(index) => index.get_vector(id),
254 VectorIndexWrapper::HNSW(index) => index.get_vector(id),
255 }
256 }
257
258 fn dimension(&self) -> usize {
259 match self {
260 VectorIndexWrapper::Flat(index) => index.dimension(),
261 VectorIndexWrapper::HNSW(index) => index.dimension(),
262 }
263 }
264}
265
266/// Similarity metrics for vector comparison
267///
268/// Different metrics are suitable for different use cases and vector characteristics.
269///
270/// # Examples
271///
272/// ```rust
273/// use vectorlite::SimilarityMetric;
274///
275/// let a = vec![1.0, 2.0, 3.0];
276/// let b = vec![1.1, 2.1, 3.1];
277///
278/// let cosine_score = SimilarityMetric::Cosine.calculate(&a, &b);
279/// let euclidean_score = SimilarityMetric::Euclidean.calculate(&a, &b);
280/// ```
281#[derive(Debug, Clone, Copy, PartialEq)]
282pub enum SimilarityMetric {
283 /// Cosine similarity - scale-invariant, good for normalized embeddings
284 /// Range: [-1, 1], where 1 is identical
285 Cosine,
286 /// Euclidean similarity - geometric distance converted to similarity
287 /// Range: [0, 1], where 1 is identical
288 Euclidean,
289 /// Manhattan similarity - L1 norm distance converted to similarity
290 /// Range: [0, 1], where 1 is identical, robust to outliers
291 Manhattan,
292 /// Dot product - raw similarity without normalization
293 /// Range: unbounded, requires consistent vector scaling
294 DotProduct,
295}
296
297impl SimilarityMetric {
298 pub fn calculate(&self, a: &[f64], b: &[f64]) -> f64 {
299 assert_eq!(a.len(), b.len(), "Vectors must have the same length");
300
301 match self {
302 SimilarityMetric::Cosine => cosine_similarity(a, b),
303 SimilarityMetric::Euclidean => euclidean_similarity(a, b),
304 SimilarityMetric::Manhattan => manhattan_similarity(a, b),
305 SimilarityMetric::DotProduct => dot_product(a, b),
306 }
307 }
308}
309
310impl Default for SimilarityMetric {
311 fn default() -> Self {
312 SimilarityMetric::Cosine
313 }
314}
315
316/// Calculate cosine similarity between two vectors
317///
318/// Cosine similarity measures the cosine of the angle between two vectors,
319/// making it scale-invariant and suitable for normalized embeddings.
320///
321/// # Arguments
322///
323/// * `a` - First vector
324/// * `b` - Second vector (must have same length as `a`)
325///
326/// # Returns
327///
328/// Similarity score in range [-1, 1] where:
329/// - 1.0 = identical vectors
330/// - 0.0 = orthogonal vectors
331/// - -1.0 = opposite vectors
332///
333/// # Panics
334///
335/// Panics if vectors have different lengths.
336///
337/// # Examples
338///
339/// ```rust
340/// use vectorlite::cosine_similarity;
341///
342/// let a = vec![1.0, 2.0, 3.0];
343/// let b = vec![1.0, 2.0, 3.0];
344/// let similarity = cosine_similarity(&a, &b);
345/// assert!((similarity - 1.0).abs() < 1e-10); // Identical vectors
346/// ```
347pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
348 assert_eq!(a.len(), b.len(), "Vectors must have the same length");
349
350 let (mut dot, mut norm_a_sq, mut norm_b_sq) = (0.0, 0.0, 0.0);
351
352 for (&x, &y) in a.iter().zip(b.iter()) {
353 dot += x * y;
354 norm_a_sq += x * x;
355 norm_b_sq += y * y;
356 }
357
358 let norm_a = norm_a_sq.sqrt();
359 let norm_b = norm_b_sq.sqrt();
360
361 if norm_a == 0.0 || norm_b == 0.0 {
362 0.0
363 } else {
364 dot / (norm_a * norm_b)
365 }
366}
367
368/// Calculate Euclidean similarity between two vectors
369///
370/// Euclidean similarity converts the Euclidean distance to a similarity score.
371/// It's sensitive to vector magnitude and suitable for vectors with consistent scaling.
372///
373/// # Arguments
374///
375/// * `a` - First vector
376/// * `b` - Second vector (must have same length as `a`)
377///
378/// # Returns
379///
380/// Similarity score in range [0, 1] where:
381/// - 1.0 = identical vectors
382/// - 0.0 = very distant vectors
383///
384/// # Panics
385///
386/// Panics if vectors have different lengths.
387///
388/// # Examples
389///
390/// ```rust
391/// use vectorlite::euclidean_similarity;
392///
393/// let a = vec![0.0, 0.0];
394/// let b = vec![3.0, 4.0];
395/// let similarity = euclidean_similarity(&a, &b);
396/// // Distance is 5.0, so similarity is 1/(1+5) = 1/6 ≈ 0.167
397/// ```
398pub fn euclidean_similarity(a: &[f64], b: &[f64]) -> f64 {
399 assert_eq!(a.len(), b.len(), "Vectors must have the same length");
400
401 let sum_sq = a.iter()
402 .zip(b.iter())
403 .map(|(x, y)| (x - y).powi(2))
404 .sum::<f64>();
405
406 let distance = sum_sq.sqrt();
407
408 // Convert distance to similarity: 1 / (1 + distance)
409 // This ensures similarity is in range [0, 1] with 1 being identical
410 1.0 / (1.0 + distance)
411}
412
413/// Calculate Manhattan similarity between two vectors
414///
415/// Manhattan similarity converts the L1 norm (Manhattan distance) to a similarity score.
416/// It's robust to outliers and suitable for high-dimensional sparse vectors.
417///
418/// # Arguments
419///
420/// * `a` - First vector
421/// * `b` - Second vector (must have same length as `a`)
422///
423/// # Returns
424///
425/// Similarity score in range [0, 1] where:
426/// - 1.0 = identical vectors
427/// - 0.0 = very distant vectors
428///
429/// # Panics
430///
431/// Panics if vectors have different lengths.
432///
433/// # Examples
434///
435/// ```rust
436/// use vectorlite::manhattan_similarity;
437///
438/// let a = vec![0.0, 0.0];
439/// let b = vec![3.0, 4.0];
440/// let similarity = manhattan_similarity(&a, &b);
441/// // Distance is 7.0, so similarity is 1/(1+7) = 1/8 = 0.125
442/// ```
443pub fn manhattan_similarity(a: &[f64], b: &[f64]) -> f64 {
444 assert_eq!(a.len(), b.len(), "Vectors must have the same length");
445
446 let distance = a.iter()
447 .zip(b.iter())
448 .map(|(x, y)| (x - y).abs())
449 .sum::<f64>();
450
451 // Convert distance to similarity: 1 / (1 + distance)
452 // This ensures similarity is in range [0, 1] with 1 being identical
453 1.0 / (1.0 + distance)
454}
455
456/// Calculate dot product between two vectors
457///
458/// Dot product is the raw similarity without normalization.
459/// It requires consistent vector scaling and can produce unbounded results.
460///
461/// # Arguments
462///
463/// * `a` - First vector
464/// * `b` - Second vector (must have same length as `a`)
465///
466/// # Returns
467///
468/// Dot product value (unbounded range):
469/// - Positive values indicate similar direction
470/// - Zero indicates orthogonal vectors
471/// - Negative values indicate opposite direction
472///
473/// # Panics
474///
475/// Panics if vectors have different lengths.
476///
477/// # Examples
478///
479/// ```rust
480/// use vectorlite::dot_product;
481///
482/// let a = vec![1.0, 2.0, 3.0];
483/// let b = vec![1.0, 2.0, 3.0];
484/// let product = dot_product(&a, &b);
485/// assert!((product - 14.0).abs() < 1e-10); // 1*1 + 2*2 + 3*3 = 14
486/// ```
487pub fn dot_product(a: &[f64], b: &[f64]) -> f64 {
488 assert_eq!(a.len(), b.len(), "Vectors must have the same length");
489
490 a.iter()
491 .zip(b.iter())
492 .map(|(x, y)| x * y)
493 .sum::<f64>()
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_cosine_similarity_identical_vectors() {
502 let a = vec![1.0, 2.0, 3.0];
503 let b = vec![1.0, 2.0, 3.0];
504 assert!((cosine_similarity(&a, &b) - 1.0).abs() < 1e-10);
505 }
506
507 #[test]
508 fn test_cosine_similarity_orthogonal_vectors() {
509 let a = vec![1.0, 0.0];
510 let b = vec![0.0, 1.0];
511 assert!((cosine_similarity(&a, &b) - 0.0).abs() < 1e-10);
512 }
513
514 #[test]
515 fn test_cosine_similarity_opposite_vectors() {
516 let a = vec![1.0, 2.0, 3.0];
517 let b = vec![-1.0, -2.0, -3.0];
518 assert!((cosine_similarity(&a, &b) - (-1.0)).abs() < 1e-10);
519 }
520
521 #[test]
522 fn test_euclidean_similarity_identical_vectors() {
523 let a = vec![1.0, 2.0, 3.0];
524 let b = vec![1.0, 2.0, 3.0];
525 assert!((euclidean_similarity(&a, &b) - 1.0).abs() < 1e-10);
526 }
527
528 #[test]
529 fn test_euclidean_similarity_different_vectors() {
530 let a = vec![0.0, 0.0];
531 let b = vec![3.0, 4.0];
532 let expected = 1.0 / (1.0 + 5.0); // 1 / (1 + sqrt(3^2 + 4^2))
533 assert!((euclidean_similarity(&a, &b) - expected).abs() < 1e-10);
534 }
535
536 #[test]
537 fn test_manhattan_similarity_identical_vectors() {
538 let a = vec![1.0, 2.0, 3.0];
539 let b = vec![1.0, 2.0, 3.0];
540 assert!((manhattan_similarity(&a, &b) - 1.0).abs() < 1e-10);
541 }
542
543 #[test]
544 fn test_manhattan_similarity_different_vectors() {
545 let a = vec![0.0, 0.0];
546 let b = vec![3.0, 4.0];
547 let expected = 1.0 / (1.0 + 7.0); // 1 / (1 + |0-3| + |0-4|)
548 assert!((manhattan_similarity(&a, &b) - expected).abs() < 1e-10);
549 }
550
551 #[test]
552 fn test_dot_product_identical_vectors() {
553 let a = vec![1.0, 2.0, 3.0];
554 let b = vec![1.0, 2.0, 3.0];
555 let expected = 1.0 + 4.0 + 9.0; // 14.0
556 assert!((dot_product(&a, &b) - expected).abs() < 1e-10);
557 }
558
559 #[test]
560 fn test_dot_product_orthogonal_vectors() {
561 let a = vec![1.0, 0.0];
562 let b = vec![0.0, 1.0];
563 assert!((dot_product(&a, &b) - 0.0).abs() < 1e-10);
564 }
565
566 #[test]
567 fn test_dot_product_opposite_vectors() {
568 let a = vec![1.0, 2.0, 3.0];
569 let b = vec![-1.0, -2.0, -3.0];
570 let expected = -1.0 - 4.0 - 9.0; // -14.0
571 assert!((dot_product(&a, &b) - expected).abs() < 1e-10);
572 }
573
574 #[test]
575 fn test_similarity_metric_enum() {
576 let a = vec![1.0, 2.0, 3.0];
577 let b = vec![1.0, 2.0, 3.0];
578
579 // Test that all metrics work
580 assert!((SimilarityMetric::Cosine.calculate(&a, &b) - 1.0).abs() < 1e-10);
581 assert!((SimilarityMetric::Euclidean.calculate(&a, &b) - 1.0).abs() < 1e-10);
582 assert!((SimilarityMetric::Manhattan.calculate(&a, &b) - 1.0).abs() < 1e-10);
583 assert!((SimilarityMetric::DotProduct.calculate(&a, &b) - 14.0).abs() < 1e-10);
584 }
585
586 #[test]
587 fn test_similarity_metric_default() {
588 assert_eq!(SimilarityMetric::default(), SimilarityMetric::Cosine);
589 }
590
591 #[test]
592 fn test_vector_store_creation() {
593 let vectors = vec![
594 Vector { id: 0, values: vec![1.0, 2.0, 3.0] },
595 Vector { id: 1, values: vec![4.0, 5.0, 6.0] },
596 ];
597 let store = FlatIndex::new(3, vectors);
598 assert_eq!(store.len(), 2);
599 assert!(!store.is_empty());
600 }
601
602 #[test]
603 fn test_vector_store_search() {
604 let vectors = vec![
605 Vector { id: 0, values: vec![1.0, 0.0, 0.0] },
606 Vector { id: 1, values: vec![0.0, 1.0, 0.0] },
607 Vector { id: 2, values: vec![0.0, 0.0, 1.0] },
608 ];
609 let store = FlatIndex::new(3, vectors);
610 let query = vec![1.0, 0.0, 0.0];
611 let results = store.search(&query, 2, SimilarityMetric::Cosine);
612
613 assert_eq!(results.len(), 2);
614 assert_eq!(results[0].id, 0);
615 assert!((results[0].score - 1.0).abs() < 1e-10);
616 }
617
618 #[test]
619 fn test_vector_index_wrapper_serialization() {
620 use serde_json;
621
622 // Test FlatIndex wrapper
623 let vectors = vec![
624 Vector { id: 1, values: vec![1.0, 0.0, 0.0] },
625 Vector { id: 2, values: vec![0.0, 1.0, 0.0] },
626 ];
627 let flat_index = FlatIndex::new(3, vectors);
628 let wrapper = VectorIndexWrapper::Flat(flat_index);
629
630 // Serialize wrapper
631 let serialized = serde_json::to_string(&wrapper).expect("Serialization should work");
632
633 // Deserialize wrapper
634 let deserialized: VectorIndexWrapper = serde_json::from_str(&serialized).expect("Deserialization should work");
635
636 // Verify the deserialized wrapper works
637 assert_eq!(deserialized.len(), 2);
638 assert_eq!(deserialized.dimension(), 3);
639 assert!(!deserialized.is_empty());
640
641 // Test search through the wrapper
642 let query = vec![1.1, 0.1, 0.1];
643 let results = deserialized.search(&query, 1, SimilarityMetric::Cosine);
644 assert_eq!(results.len(), 1);
645 assert_eq!(results[0].id, 1);
646 }
647
648}