use std::collections::HashSet;
use std::path::Path;
use std::time::{Duration, Instant};
use crate::distance::DistanceMetric;
use crate::engine::SynaDB;
use crate::error::{Result, SynaError};
#[cfg(feature = "faiss")]
use crate::faiss_index::FaissConfig;
use crate::hnsw::{HnswConfig, HnswIndex};
use crate::types::Atom;
#[derive(Debug, Clone)]
pub struct VectorConfig {
pub dimensions: u16,
pub metric: DistanceMetric,
pub key_prefix: String,
pub index_threshold: usize,
pub backend: IndexBackend,
pub sync_on_write: bool,
pub checkpoint_interval_secs: u64,
}
impl Default for VectorConfig {
fn default() -> Self {
Self {
dimensions: 768,
metric: DistanceMetric::Cosine,
key_prefix: "vec/".to_string(),
index_threshold: 10000,
backend: IndexBackend::default(),
sync_on_write: true,
checkpoint_interval_secs: 30,
}
}
}
#[derive(Debug, Clone)]
pub enum IndexBackend {
Hnsw(HnswConfig),
#[cfg(feature = "faiss")]
Faiss(FaissConfig),
None,
}
impl Default for IndexBackend {
fn default() -> Self {
IndexBackend::Hnsw(HnswConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub key: String,
pub score: f32,
pub vector: Vec<f32>,
}
pub struct VectorStore {
db: SynaDB,
db_path: std::path::PathBuf,
config: VectorConfig,
vector_keys: HashSet<String>,
vector_keys_ordered: Vec<String>,
hnsw_index: Option<HnswIndex>,
#[cfg(feature = "faiss")]
faiss_index: Option<crate::faiss_index::FaissIndex>,
index_dirty: bool,
last_checkpoint: Instant,
checkpoint_interval: Duration,
}
impl VectorStore {
pub fn new<P: AsRef<Path>>(path: P, config: VectorConfig) -> Result<Self> {
if config.dimensions < 64 || config.dimensions > 8192 {
return Err(SynaError::InvalidDimensions(config.dimensions));
}
let db_path = path.as_ref().to_path_buf();
let db_config = crate::engine::DbConfig {
sync_on_write: config.sync_on_write,
..Default::default()
};
let db = SynaDB::with_config(&db_path, db_config)?;
let vector_keys_ordered: Vec<String> = db
.keys()
.into_iter()
.filter(|k| k.starts_with(&config.key_prefix))
.collect();
let vector_keys: HashSet<String> = vector_keys_ordered.iter().cloned().collect();
let hnsw_index = match &config.backend {
IndexBackend::Hnsw(_) | IndexBackend::None => {
let hnsw_path = Self::hnsw_index_path(&db_path);
if hnsw_path.exists() {
match HnswIndex::load_validated(&hnsw_path, config.dimensions, config.metric) {
Ok(index) => {
if index.len() == vector_keys.len() {
Some(index)
} else {
None
}
}
Err(_) => None, }
} else {
None
}
}
#[cfg(feature = "faiss")]
IndexBackend::Faiss(_) => None, };
#[cfg(feature = "faiss")]
let faiss_index = match &config.backend {
IndexBackend::Faiss(faiss_config) => Some(crate::faiss_index::FaissIndex::new(
config.dimensions,
config.metric,
faiss_config.clone(),
)?),
_ => None,
};
let checkpoint_interval = Duration::from_secs(config.checkpoint_interval_secs);
Ok(Self {
db,
db_path,
config,
vector_keys,
vector_keys_ordered,
hnsw_index,
#[cfg(feature = "faiss")]
faiss_index,
index_dirty: false,
last_checkpoint: Instant::now(),
checkpoint_interval,
})
}
fn hnsw_index_path(db_path: &Path) -> std::path::PathBuf {
let mut hnsw_path = db_path.to_path_buf();
let extension = match hnsw_path.extension() {
Some(ext) => format!("{}.hnsw", ext.to_string_lossy()),
None => "hnsw".to_string(),
};
hnsw_path.set_extension(extension);
hnsw_path
}
pub fn insert(&mut self, key: &str, vector: &[f32]) -> Result<()> {
if vector.len() != self.config.dimensions as usize {
return Err(SynaError::DimensionMismatch {
expected: self.config.dimensions,
got: vector.len() as u16,
});
}
let full_key = format!("{}{}", self.config.key_prefix, key);
let atom = Atom::Vector(vector.to_vec(), self.config.dimensions);
self.db.append(&full_key, atom)?;
let is_new_key = self.vector_keys.insert(full_key.clone());
if is_new_key {
self.vector_keys_ordered.push(full_key.clone());
}
if self.hnsw_index.is_some() && is_new_key {
self.insert_to_hnsw_incremental(&full_key, vector);
self.index_dirty = true;
} else if self.hnsw_index.is_none() {
if matches!(self.config.backend, IndexBackend::Hnsw(_))
&& self.config.index_threshold > 0
&& self.vector_keys.len() >= self.config.index_threshold
{
self.build_index()?;
}
}
if self.index_dirty
&& self.checkpoint_interval.as_secs() > 0
&& self.last_checkpoint.elapsed() >= self.checkpoint_interval
{
self.checkpoint_index()?;
}
Ok(())
}
pub fn insert_batch(&mut self, keys: &[&str], vectors: &[&[f32]]) -> Result<usize> {
if keys.len() != vectors.len() {
return Err(SynaError::ShapeMismatch {
data_size: vectors.len(),
expected_size: keys.len(),
});
}
let mut inserted = 0;
let should_build_index = self.hnsw_index.is_none()
&& matches!(self.config.backend, IndexBackend::Hnsw(_))
&& self.config.index_threshold > 0;
for (key, vector) in keys.iter().zip(vectors.iter()) {
if vector.len() != self.config.dimensions as usize {
return Err(SynaError::DimensionMismatch {
expected: self.config.dimensions,
got: vector.len() as u16,
});
}
let full_key = format!("{}{}", self.config.key_prefix, key);
let atom = Atom::Vector(vector.to_vec(), self.config.dimensions);
self.db.append(&full_key, atom)?;
let is_new_key = self.vector_keys.insert(full_key.clone());
if is_new_key {
self.vector_keys_ordered.push(full_key.clone());
inserted += 1;
}
}
if should_build_index && self.vector_keys.len() >= self.config.index_threshold {
self.build_index()?;
}
if self.index_dirty
&& self.checkpoint_interval.as_secs() > 0
&& self.last_checkpoint.elapsed() >= self.checkpoint_interval
{
self.checkpoint_index()?;
}
Ok(inserted)
}
pub fn insert_batch_fast(
&mut self,
keys: &[&str],
vectors: &[&[f32]],
update_index: bool,
) -> Result<usize> {
if keys.len() != vectors.len() {
return Err(SynaError::ShapeMismatch {
data_size: vectors.len(),
expected_size: keys.len(),
});
}
let mut inserted = 0;
for (key, vector) in keys.iter().zip(vectors.iter()) {
if vector.len() != self.config.dimensions as usize {
return Err(SynaError::DimensionMismatch {
expected: self.config.dimensions,
got: vector.len() as u16,
});
}
let full_key = format!("{}{}", self.config.key_prefix, key);
let atom = Atom::Vector(vector.to_vec(), self.config.dimensions);
self.db.append(&full_key, atom)?;
let is_new_key = self.vector_keys.insert(full_key.clone());
if is_new_key {
self.vector_keys_ordered.push(full_key);
inserted += 1;
}
}
if update_index {
let should_build = self.hnsw_index.is_none()
&& matches!(self.config.backend, IndexBackend::Hnsw(_))
&& self.config.index_threshold > 0
&& self.vector_keys.len() >= self.config.index_threshold;
if should_build {
self.build_index()?;
}
}
Ok(inserted)
}
fn insert_to_hnsw_incremental(&mut self, key: &str, vector: &[f32]) {
use crate::hnsw::HnswNode;
let index = match self.hnsw_index.as_mut() {
Some(idx) => idx,
None => return,
};
if index.key_to_id.contains_key(key) {
return;
}
let level = index.random_level();
let node = HnswNode::new(key.to_string(), vector.to_vec(), level);
let node_id = index.nodes.len();
index.nodes.push(node);
index.key_to_id.insert(key.to_string(), node_id);
if node_id == 0 {
index.entry_point = Some(node_id);
return;
}
let m = index.config().m;
let m_max = index.config().m_max;
let ef_construction = index.config().ef_construction;
let mut ep = index.entry_point.unwrap_or(0);
let current_max_level = index.max_level();
for lc in ((level + 1)..=current_max_level).rev() {
let results = index.search_layer(vector, ep, 1, lc);
if !results.is_empty() {
ep = results[0].0;
}
}
let start_level = level.min(current_max_level);
for l in (0..=start_level).rev() {
let candidates = index.search_layer(vector, ep, ef_construction, l);
let max_neighbors = if l == 0 { m } else { m_max };
let neighbors: Vec<(usize, f32)> = candidates.into_iter().take(max_neighbors).collect();
if !neighbors.is_empty() {
ep = neighbors[0].0;
}
if l < index.nodes[node_id].neighbors.len() {
index.nodes[node_id].neighbors[l] = neighbors.clone();
}
for (neighbor_id, dist) in neighbors {
if l < index.nodes[neighbor_id].neighbors.len() {
index.nodes[neighbor_id].neighbors[l].push((node_id, dist));
if index.nodes[neighbor_id].neighbors[l].len() > max_neighbors {
index.nodes[neighbor_id].neighbors[l].sort_by(|a, b| a.1.total_cmp(&b.1));
index.nodes[neighbor_id].neighbors[l].truncate(max_neighbors);
}
}
}
}
if level > current_max_level || index.entry_point.is_none() {
index.entry_point = Some(node_id);
}
}
pub fn checkpoint_index(&mut self) -> Result<()> {
if !self.index_dirty {
return Ok(());
}
if let Some(ref index) = self.hnsw_index {
let hnsw_path = Self::hnsw_index_path(&self.db_path);
index.save(&hnsw_path)?;
}
self.index_dirty = false;
self.last_checkpoint = Instant::now();
Ok(())
}
pub fn search(&mut self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
if query.len() != self.config.dimensions as usize {
return Err(SynaError::DimensionMismatch {
expected: self.config.dimensions,
got: query.len() as u16,
});
}
match &self.config.backend {
#[cfg(feature = "faiss")]
IndexBackend::Faiss(_) => {
if self.faiss_index.is_some() {
return self.search_faiss(query, k);
}
self.search_brute_force(query, k)
}
IndexBackend::Hnsw(_) => {
if self.hnsw_index.is_some()
&& self.config.index_threshold > 0
&& self.vector_keys.len() >= self.config.index_threshold
{
return self.search_hnsw(query, k);
}
self.search_brute_force(query, k)
}
IndexBackend::None => self.search_brute_force(query, k),
}
}
fn search_brute_force(&mut self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
let mut results: Vec<SearchResult> = Vec::new();
let keys = self.vector_keys_ordered.clone();
for full_key in &keys {
if let Some(Atom::Vector(vec, _)) = self.db.get(full_key)? {
let score = self.config.metric.distance(query, &vec);
let key = full_key
.strip_prefix(&self.config.key_prefix)
.unwrap_or(full_key)
.to_string();
results.push(SearchResult {
key,
score,
vector: vec,
});
}
}
results.sort_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
results.truncate(k);
Ok(results)
}
fn search_hnsw(&mut self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
let index = self
.hnsw_index
.as_ref()
.ok_or_else(|| SynaError::CorruptedIndex("HNSW index not available".to_string()))?;
let hnsw_results = index.search(query, k);
let mut results = Vec::with_capacity(hnsw_results.len());
for (full_key, score) in hnsw_results {
let key = full_key
.strip_prefix(&self.config.key_prefix)
.unwrap_or(&full_key)
.to_string();
let vector = if let Some(Atom::Vector(vec, _)) = self.db.get(&full_key)? {
vec
} else {
continue;
};
results.push(SearchResult { key, score, vector });
}
Ok(results)
}
#[cfg(feature = "faiss")]
fn search_faiss(&mut self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
let index = self
.faiss_index
.as_mut()
.ok_or_else(|| SynaError::CorruptedIndex("FAISS index not available".to_string()))?;
let faiss_results = index.search(query, k)?;
let mut results = Vec::with_capacity(faiss_results.len());
for (key_without_prefix, score) in faiss_results {
let full_key = format!("{}{}", self.config.key_prefix, key_without_prefix);
let vector = if let Some(Atom::Vector(vec, _)) = self.db.get(&full_key)? {
vec
} else {
continue;
};
results.push(SearchResult {
key: key_without_prefix,
score,
vector,
});
}
Ok(results)
}
pub fn build_index(&mut self) -> Result<()> {
let mut index = HnswIndex::new(
self.config.dimensions,
self.config.metric,
HnswConfig::default(),
);
let keys = self.vector_keys_ordered.clone();
for full_key in &keys {
if let Some(Atom::Vector(vec, _)) = self.db.get(full_key)? {
self.add_node_to_index(&mut index, full_key, &vec);
}
}
let hnsw_path = Self::hnsw_index_path(&self.db_path);
index.save(&hnsw_path)?;
self.hnsw_index = Some(index);
self.index_dirty = false;
self.last_checkpoint = Instant::now();
Ok(())
}
pub fn save_index(&self) -> Result<()> {
let index = self
.hnsw_index
.as_ref()
.ok_or_else(|| SynaError::CorruptedIndex("No HNSW index to save".to_string()))?;
let hnsw_path = Self::hnsw_index_path(&self.db_path);
index.save(&hnsw_path)?;
Ok(())
}
fn add_node_to_index(&self, index: &mut HnswIndex, key: &str, vector: &[f32]) {
use crate::hnsw::HnswNode;
if index.key_to_id.contains_key(key) {
return;
}
let level = index.random_level();
let node = HnswNode::new(key.to_string(), vector.to_vec(), level);
let node_id = index.nodes.len();
index.nodes.push(node);
index.key_to_id.insert(key.to_string(), node_id);
let current_max_level = index.max_level();
if index.entry_point.is_none() || level > current_max_level {
index.entry_point = Some(node_id);
index.set_max_level(level);
}
if node_id > 0 {
let m = index.config().m;
let m_max = index.config().m_max;
for l in 0..=level {
let max_neighbors = if l == 0 { m } else { m_max };
let mut neighbors = Vec::new();
let mut distances: Vec<(usize, f32)> = index
.nodes
.iter()
.enumerate()
.filter(|(id, n)| *id != node_id && n.neighbors.len() > l)
.map(|(id, n)| (id, index.metric().distance(vector, &n.vector)))
.collect();
distances.sort_by(|a, b| a.1.total_cmp(&b.1));
for (neighbor_id, dist) in distances.into_iter().take(max_neighbors) {
neighbors.push((neighbor_id, dist));
if l < index.nodes[neighbor_id].neighbors.len() {
index.nodes[neighbor_id].neighbors[l].push((node_id, dist));
if index.nodes[neighbor_id].neighbors[l].len() > max_neighbors {
index.nodes[neighbor_id].neighbors[l]
.sort_by(|a, b| a.1.total_cmp(&b.1));
index.nodes[neighbor_id].neighbors[l].truncate(max_neighbors);
}
}
}
if l < index.nodes[node_id].neighbors.len() {
index.nodes[node_id].neighbors[l] = neighbors;
}
}
}
}
pub fn has_index(&self) -> bool {
self.hnsw_index.is_some()
}
pub fn index_threshold(&self) -> usize {
self.config.index_threshold
}
pub fn get(&mut self, key: &str) -> Result<Option<Vec<f32>>> {
let full_key = format!("{}{}", self.config.key_prefix, key);
match self.db.get(&full_key)? {
Some(Atom::Vector(vec, _)) => Ok(Some(vec)),
_ => Ok(None),
}
}
pub fn delete(&mut self, key: &str) -> Result<()> {
let full_key = format!("{}{}", self.config.key_prefix, key);
self.db.delete(&full_key)?;
self.vector_keys.remove(&full_key);
self.vector_keys_ordered.retain(|k| k != &full_key);
Ok(())
}
pub fn len(&self) -> usize {
self.vector_keys.len()
}
pub fn is_empty(&self) -> bool {
self.vector_keys.is_empty()
}
pub fn is_dirty(&self) -> bool {
self.index_dirty
}
pub fn dimensions(&self) -> u16 {
self.config.dimensions
}
pub fn metric(&self) -> DistanceMetric {
self.config.metric
}
pub fn flush(&mut self) -> Result<()> {
self.checkpoint_index()
}
}
impl Drop for VectorStore {
fn drop(&mut self) {
if self.index_dirty {
if let Err(e) = self.checkpoint_index() {
eprintln!("Warning: Failed to save HNSW index on drop: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_vector_store_basic() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 128,
metric: DistanceMetric::Cosine,
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
let vec1: Vec<f32> = (0..128).map(|i| i as f32 * 0.01).collect();
store.insert("v1", &vec1).unwrap();
let retrieved = store.get("v1").unwrap().unwrap();
assert_eq!(retrieved.len(), 128);
assert_eq!(store.len(), 1);
}
#[test]
fn test_vector_store_search() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.001).collect();
let results = store.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].key, "v0");
assert!(results[0].score < 0.001);
}
#[test]
fn test_dimension_validation() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 32,
..Default::default()
};
assert!(VectorStore::new(&db_path, config).is_err());
let config = VectorConfig {
dimensions: 9000,
..Default::default()
};
assert!(VectorStore::new(&db_path, config).is_err());
let config = VectorConfig {
dimensions: 128,
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
let wrong_vec = vec![0.1f32; 64];
assert!(store.insert("v1", &wrong_vec).is_err());
}
#[test]
fn test_vector_store_delete() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
let vec1: Vec<f32> = vec![0.1; 64];
store.insert("v1", &vec1).unwrap();
assert_eq!(store.len(), 1);
store.delete("v1").unwrap();
assert_eq!(store.len(), 0);
assert!(store.get("v1").unwrap().is_none());
}
#[test]
fn test_hnsw_integration_build_index() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
index_threshold: 0, ..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
assert!(!store.has_index());
store.build_index().unwrap();
assert!(store.has_index());
}
#[test]
fn test_hnsw_integration_auto_build() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
index_threshold: 5, ..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..4 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
assert!(!store.has_index());
let vec: Vec<f32> = (0..64).map(|j| (4 * 64 + j) as f32 * 0.001).collect();
store.insert("v4", &vec).unwrap();
assert!(store.has_index());
}
#[test]
fn test_hnsw_integration_search_with_index() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
index_threshold: 5, ..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
store.build_index().unwrap();
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.001).collect();
let results = store.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].key, "v0");
assert!(results[0].score < 0.001);
}
#[test]
fn test_hnsw_integration_search_below_threshold() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
index_threshold: 100, ..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
store.build_index().unwrap();
assert!(store.has_index());
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.001).collect();
let results = store.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].key, "v0");
}
#[test]
fn test_index_threshold_config() {
let config = VectorConfig::default();
assert_eq!(config.index_threshold, 10000);
let custom_config = VectorConfig {
index_threshold: 5000,
..Default::default()
};
assert_eq!(custom_config.index_threshold, 5000);
}
#[test]
fn test_backend_selection_default() {
let config = VectorConfig::default();
match config.backend {
IndexBackend::Hnsw(_) => {} _ => panic!("Default backend should be HNSW"),
}
}
#[test]
fn test_backend_selection_none() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
backend: IndexBackend::None,
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.001).collect();
let results = store.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].key, "v0");
assert!(results[0].score < 0.001);
}
#[test]
fn test_backend_selection_hnsw_custom() {
use crate::hnsw::HnswConfig;
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
let config = VectorConfig {
dimensions: 64,
metric: DistanceMetric::Euclidean,
index_threshold: 5,
backend: IndexBackend::Hnsw(HnswConfig {
m: 32,
ef_construction: 400,
..Default::default()
}),
..Default::default()
};
let mut store = VectorStore::new(&db_path, config).unwrap();
for i in 0..10 {
let vec: Vec<f32> = (0..64).map(|j| (i * 64 + j) as f32 * 0.001).collect();
store.insert(&format!("v{}", i), &vec).unwrap();
}
store.build_index().unwrap();
assert!(store.has_index());
let query: Vec<f32> = (0..64).map(|j| j as f32 * 0.001).collect();
let results = store.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].key, "v0");
}
}