use std::sync::Arc;
use rustc_hash::FxHashMap;
use crate::dsl::Schema;
use crate::error::Result;
use crate::structures::{CoarseCentroids, PQCodebook};
#[cfg(feature = "native")]
mod reader;
#[cfg(feature = "native")]
mod vector_builder;
#[cfg(feature = "native")]
mod writer;
#[cfg(feature = "native")]
pub use reader::{IndexReader, Searcher};
#[cfg(feature = "native")]
pub use writer::IndexWriter;
mod metadata;
pub use metadata::{FieldVectorMeta, INDEX_META_FILENAME, IndexMetadata, VectorIndexState};
#[cfg(feature = "native")]
mod helpers;
#[cfg(feature = "native")]
pub use helpers::{
IndexingStats, SchemaConfig, SchemaFieldConfig, create_index_at_path, create_index_from_sdl,
index_documents_from_reader, index_json_document, parse_schema,
};
pub const SLICE_CACHE_FILENAME: &str = "index.slicecache";
#[derive(Debug, Clone)]
pub struct IndexConfig {
pub num_threads: usize,
pub num_indexing_threads: usize,
pub num_compression_threads: usize,
pub term_cache_blocks: usize,
pub store_cache_blocks: usize,
pub max_indexing_memory_bytes: usize,
pub merge_policy: Box<dyn crate::merge::MergePolicy>,
pub optimization: crate::structures::IndexOptimization,
}
impl Default for IndexConfig {
fn default() -> Self {
#[cfg(feature = "native")]
let cpus = num_cpus::get().max(1);
#[cfg(not(feature = "native"))]
let cpus = 1;
Self {
num_threads: cpus,
num_indexing_threads: 1,
num_compression_threads: cpus,
term_cache_blocks: 256,
store_cache_blocks: 32,
max_indexing_memory_bytes: 2 * 1024 * 1024 * 1024, merge_policy: Box::new(crate::merge::TieredMergePolicy::default()),
optimization: crate::structures::IndexOptimization::default(),
}
}
}
#[cfg(feature = "native")]
pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
directory: Arc<D>,
schema: Arc<Schema>,
config: IndexConfig,
segment_manager: Arc<crate::merge::SegmentManager<D>>,
trained_centroids: FxHashMap<u32, Arc<CoarseCentroids>>,
trained_codebooks: FxHashMap<u32, Arc<PQCodebook>>,
}
#[cfg(feature = "native")]
impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
let directory = Arc::new(directory);
let schema = Arc::new(schema);
let metadata = IndexMetadata::new((*schema).clone());
let segment_manager = Arc::new(crate::merge::SegmentManager::new(
Arc::clone(&directory),
Arc::clone(&schema),
metadata,
config.merge_policy.clone_box(),
config.term_cache_blocks,
));
segment_manager.update_metadata(|_| {}).await?;
Ok(Self {
directory,
schema,
config,
segment_manager,
trained_centroids: FxHashMap::default(),
trained_codebooks: FxHashMap::default(),
})
}
pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
let directory = Arc::new(directory);
let metadata = IndexMetadata::load(directory.as_ref()).await?;
let schema = Arc::new(metadata.schema.clone());
let (trained_centroids, trained_codebooks) =
metadata.load_trained_structures(directory.as_ref()).await;
let segment_manager = Arc::new(crate::merge::SegmentManager::new(
Arc::clone(&directory),
Arc::clone(&schema),
metadata,
config.merge_policy.clone_box(),
config.term_cache_blocks,
));
Ok(Self {
directory,
schema,
config,
segment_manager,
trained_centroids,
trained_codebooks,
})
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn directory(&self) -> &D {
&self.directory
}
pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
&self.segment_manager
}
pub fn writer(&self) -> writer::IndexWriter<D> {
writer::IndexWriter::from_index(self)
}
pub async fn reader(&self) -> Result<IndexReader<D>> {
IndexReader::from_segment_manager(
Arc::clone(&self.schema),
Arc::clone(&self.segment_manager),
self.trained_centroids.clone(),
self.trained_codebooks.clone(),
self.config.term_cache_blocks,
)
.await
}
pub fn config(&self) -> &IndexConfig {
&self.config
}
pub fn trained_centroids(&self) -> &FxHashMap<u32, Arc<CoarseCentroids>> {
&self.trained_centroids
}
pub fn trained_codebooks(&self) -> &FxHashMap<u32, Arc<PQCodebook>> {
&self.trained_codebooks
}
pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
let reader = self.reader().await?;
let searcher = reader.searcher().await?;
Ok(searcher.segment_readers().to_vec())
}
pub async fn num_docs(&self) -> Result<u32> {
let reader = self.reader().await?;
let searcher = reader.searcher().await?;
Ok(searcher.num_docs())
}
pub async fn doc(&self, doc_id: crate::DocId) -> Result<Option<crate::dsl::Document>> {
let reader = self.reader().await?;
let searcher = reader.searcher().await?;
searcher.doc(doc_id).await
}
pub fn default_fields(&self) -> Vec<crate::Field> {
if !self.schema.default_fields().is_empty() {
self.schema.default_fields().to_vec()
} else {
self.schema
.fields()
.filter(|(_, entry)| {
entry.indexed && entry.field_type == crate::dsl::FieldType::Text
})
.map(|(field, _)| field)
.collect()
}
}
pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
Arc::new(crate::tokenizer::TokenizerRegistry::default())
}
pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
let default_fields = self.default_fields();
let tokenizers = self.tokenizers();
let query_routers = self.schema.query_routers();
if !query_routers.is_empty()
&& let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
{
return crate::dsl::QueryLanguageParser::with_router(
Arc::clone(&self.schema),
default_fields,
tokenizers,
router,
);
}
crate::dsl::QueryLanguageParser::new(Arc::clone(&self.schema), default_fields, tokenizers)
}
pub async fn query(
&self,
query_str: &str,
limit: usize,
) -> Result<crate::query::SearchResponse> {
self.query_offset(query_str, limit, 0).await
}
pub async fn query_offset(
&self,
query_str: &str,
limit: usize,
offset: usize,
) -> Result<crate::query::SearchResponse> {
let parser = self.query_parser();
let query = parser
.parse(query_str)
.map_err(crate::error::Error::Query)?;
self.search_offset(query.as_ref(), limit, offset).await
}
pub async fn search(
&self,
query: &dyn crate::query::Query,
limit: usize,
) -> Result<crate::query::SearchResponse> {
self.search_offset(query, limit, 0).await
}
pub async fn search_offset(
&self,
query: &dyn crate::query::Query,
limit: usize,
offset: usize,
) -> Result<crate::query::SearchResponse> {
let reader = self.reader().await?;
let searcher = reader.searcher().await?;
let segments = searcher.segment_readers();
let mut all_results: Vec<(u128, crate::query::SearchResult)> = Vec::new();
let fetch_limit = offset + limit;
for segment in segments {
let segment_id = segment.meta().id;
let results =
crate::query::search_segment(segment.as_ref(), query, fetch_limit).await?;
for result in results {
all_results.push((segment_id, result));
}
}
all_results.sort_by(|a, b| {
b.1.score
.partial_cmp(&a.1.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let total_hits = all_results.len() as u32;
let hits: Vec<crate::query::SearchHit> = all_results
.into_iter()
.skip(offset)
.take(limit)
.map(|(segment_id, result)| crate::query::SearchHit {
address: crate::query::DocAddress::new(segment_id, result.doc_id),
score: result.score,
matched_fields: result.extract_ordinals(),
})
.collect();
Ok(crate::query::SearchResponse { hits, total_hits })
}
pub async fn get_document(
&self,
address: &crate::query::DocAddress,
) -> Result<Option<crate::dsl::Document>> {
let segment_id = address.segment_id_u128().ok_or_else(|| {
crate::error::Error::Query(format!("Invalid segment ID: {}", address.segment_id))
})?;
let reader = self.reader().await?;
let searcher = reader.searcher().await?;
for segment in searcher.segment_readers() {
if segment.meta().id == segment_id {
return segment.doc(address.doc_id).await;
}
}
Ok(None)
}
pub async fn reload(&self) -> Result<()> {
Ok(())
}
pub async fn get_postings(
&self,
field: crate::Field,
term: &[u8],
) -> Result<
Vec<(
Arc<crate::segment::SegmentReader>,
crate::structures::BlockPostingList,
)>,
> {
let segments = self.segment_readers().await?;
let mut results = Vec::new();
for segment in segments {
if let Some(postings) = segment.get_postings(field, term).await? {
results.push((segment, postings));
}
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::directories::RamDirectory;
use crate::dsl::{Document, SchemaBuilder};
#[tokio::test]
async fn test_index_create_and_search() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let body = schema_builder.add_text_field("body", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
let mut doc1 = Document::new();
doc1.add_text(title, "Hello World");
doc1.add_text(body, "This is the first document");
writer.add_document(doc1).unwrap();
let mut doc2 = Document::new();
doc2.add_text(title, "Goodbye World");
doc2.add_text(body, "This is the second document");
writer.add_document(doc2).unwrap();
writer.commit().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.num_docs().await.unwrap(), 2);
let postings = index.get_postings(title, b"world").await.unwrap();
assert_eq!(postings.len(), 1); assert_eq!(postings[0].1.doc_count(), 2);
let doc = index.doc(0).await.unwrap().unwrap();
assert_eq!(doc.get_first(title).unwrap().as_text(), Some("Hello World"));
}
#[tokio::test]
async fn test_multiple_segments() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig {
max_indexing_memory_bytes: 1024, ..Default::default()
};
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
for batch in 0..3 {
for i in 0..5 {
let mut doc = Document::new();
doc.add_text(title, format!("Document {} batch {}", i, batch));
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
}
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.num_docs().await.unwrap(), 15);
assert!(
index.segment_readers().await.unwrap().len() >= 2,
"Expected multiple segments"
);
}
#[tokio::test]
async fn test_segment_merge() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig {
max_indexing_memory_bytes: 512, ..Default::default()
};
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
for batch in 0..3 {
for i in 0..3 {
let mut doc = Document::new();
doc.add_text(title, format!("Document {} batch {}", i, batch));
writer.add_document(doc).unwrap();
}
writer.flush().await.unwrap();
}
writer.commit().await.unwrap();
let index = Index::open(dir.clone(), config.clone()).await.unwrap();
assert!(
index.segment_readers().await.unwrap().len() >= 2,
"Expected multiple segments"
);
let writer = IndexWriter::open(dir.clone(), config.clone())
.await
.unwrap();
writer.force_merge().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.segment_readers().await.unwrap().len(), 1);
assert_eq!(index.num_docs().await.unwrap(), 9);
let mut found_docs = 0;
for i in 0..9 {
if index.doc(i).await.unwrap().is_some() {
found_docs += 1;
}
}
assert_eq!(found_docs, 9);
}
#[tokio::test]
async fn test_match_query() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let body = schema_builder.add_text_field("body", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
let mut doc1 = Document::new();
doc1.add_text(title, "rust programming");
doc1.add_text(body, "Learn rust language");
writer.add_document(doc1).unwrap();
let mut doc2 = Document::new();
doc2.add_text(title, "python programming");
doc2.add_text(body, "Learn python language");
writer.add_document(doc2).unwrap();
writer.commit().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
let results = index.query("rust", 10).await.unwrap();
assert_eq!(results.hits.len(), 1);
let results = index.query("rust programming", 10).await.unwrap();
assert!(!results.hits.is_empty());
let hit = &results.hits[0];
assert!(!hit.address.segment_id.is_empty(), "Should have segment_id");
let doc = index.get_document(&hit.address).await.unwrap().unwrap();
assert!(
!doc.field_values().is_empty(),
"Doc should have field values"
);
let doc = index.doc(0).await.unwrap().unwrap();
assert!(
!doc.field_values().is_empty(),
"Doc should have field values"
);
}
#[tokio::test]
async fn test_slice_cache_warmup_and_load() {
use crate::directories::SliceCachingDirectory;
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let body = schema_builder.add_text_field("body", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
for i in 0..10 {
let mut doc = Document::new();
doc.add_text(title, format!("Document {} about rust", i));
doc.add_text(body, format!("This is body text number {}", i));
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let caching_dir = SliceCachingDirectory::new(dir.clone(), 1024 * 1024);
let index = Index::open(caching_dir, config.clone()).await.unwrap();
let results = index.query("rust", 10).await.unwrap();
assert!(!results.hits.is_empty());
let stats = index.directory.stats();
assert!(stats.total_bytes > 0, "Cache should have data after search");
}
#[tokio::test]
async fn test_multivalue_field_indexing_and_search() {
let mut schema_builder = SchemaBuilder::default();
let uris = schema_builder.add_text_field("uris", true, true);
let title = schema_builder.add_text_field("title", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
let mut doc = Document::new();
doc.add_text(uris, "one");
doc.add_text(uris, "two");
doc.add_text(title, "Test Document");
writer.add_document(doc).unwrap();
let mut doc2 = Document::new();
doc2.add_text(uris, "three");
doc2.add_text(title, "Another Document");
writer.add_document(doc2).unwrap();
writer.commit().await.unwrap();
let index = Index::open(dir, config).await.unwrap();
assert_eq!(index.num_docs().await.unwrap(), 2);
let doc = index.doc(0).await.unwrap().unwrap();
let all_uris: Vec<_> = doc.get_all(uris).collect();
assert_eq!(all_uris.len(), 2, "Should have 2 uris values");
assert_eq!(all_uris[0].as_text(), Some("one"));
assert_eq!(all_uris[1].as_text(), Some("two"));
let json = doc.to_json(index.schema());
let uris_json = json.get("uris").unwrap();
assert!(uris_json.is_array(), "Multi-value field should be an array");
let uris_arr = uris_json.as_array().unwrap();
assert_eq!(uris_arr.len(), 2);
assert_eq!(uris_arr[0].as_str(), Some("one"));
assert_eq!(uris_arr[1].as_str(), Some("two"));
let results = index.query("uris:one", 10).await.unwrap();
assert_eq!(results.hits.len(), 1, "Should find doc with 'one'");
assert_eq!(results.hits[0].address.doc_id, 0);
let results = index.query("uris:two", 10).await.unwrap();
assert_eq!(results.hits.len(), 1, "Should find doc with 'two'");
assert_eq!(results.hits[0].address.doc_id, 0);
let results = index.query("uris:three", 10).await.unwrap();
assert_eq!(results.hits.len(), 1, "Should find doc with 'three'");
assert_eq!(results.hits[0].address.doc_id, 1);
let results = index.query("uris:nonexistent", 10).await.unwrap();
assert_eq!(results.hits.len(), 0, "Should not find non-existent value");
}
#[tokio::test]
async fn test_wand_optimization_for_or_queries() {
use crate::query::{BooleanQuery, TermQuery};
let mut schema_builder = SchemaBuilder::default();
let content = schema_builder.add_text_field("content", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
let mut doc = Document::new();
doc.add_text(content, "rust programming language is fast");
writer.add_document(doc).unwrap();
let mut doc = Document::new();
doc.add_text(content, "rust is a systems language");
writer.add_document(doc).unwrap();
let mut doc = Document::new();
doc.add_text(content, "programming is fun");
writer.add_document(doc).unwrap();
let mut doc = Document::new();
doc.add_text(content, "python is easy to learn");
writer.add_document(doc).unwrap();
let mut doc = Document::new();
doc.add_text(content, "rust rust programming programming systems");
writer.add_document(doc).unwrap();
writer.commit().await.unwrap();
let index = Index::open(dir.clone(), config.clone()).await.unwrap();
let or_query = BooleanQuery::new()
.should(TermQuery::text(content, "rust"))
.should(TermQuery::text(content, "programming"));
let results = index.search(&or_query, 10).await.unwrap();
assert_eq!(results.hits.len(), 4, "Should find exactly 4 documents");
let doc_ids: Vec<u32> = results.hits.iter().map(|h| h.address.doc_id).collect();
assert!(doc_ids.contains(&0), "Should find doc 0");
assert!(doc_ids.contains(&1), "Should find doc 1");
assert!(doc_ids.contains(&2), "Should find doc 2");
assert!(doc_ids.contains(&4), "Should find doc 4");
assert!(
!doc_ids.contains(&3),
"Should NOT find doc 3 (only has 'python')"
);
let single_query = BooleanQuery::new().should(TermQuery::text(content, "rust"));
let results = index.search(&single_query, 10).await.unwrap();
assert_eq!(results.hits.len(), 3, "Should find 3 documents with 'rust'");
let must_query = BooleanQuery::new()
.must(TermQuery::text(content, "rust"))
.should(TermQuery::text(content, "programming"));
let results = index.search(&must_query, 10).await.unwrap();
assert_eq!(results.hits.len(), 3, "Should find 3 documents with 'rust'");
let must_not_query = BooleanQuery::new()
.should(TermQuery::text(content, "rust"))
.should(TermQuery::text(content, "programming"))
.must_not(TermQuery::text(content, "systems"));
let results = index.search(&must_not_query, 10).await.unwrap();
let doc_ids: Vec<u32> = results.hits.iter().map(|h| h.address.doc_id).collect();
assert!(
!doc_ids.contains(&1),
"Should NOT find doc 1 (has 'systems')"
);
assert!(
!doc_ids.contains(&4),
"Should NOT find doc 4 (has 'systems')"
);
let or_query = BooleanQuery::new()
.should(TermQuery::text(content, "rust"))
.should(TermQuery::text(content, "programming"));
let results = index.search(&or_query, 2).await.unwrap();
assert_eq!(results.hits.len(), 2, "Should return only top 2 results");
}
#[tokio::test]
async fn test_wand_results_match_standard_boolean() {
use crate::query::{BooleanQuery, TermQuery, WandOrQuery};
let mut schema_builder = SchemaBuilder::default();
let content = schema_builder.add_text_field("content", true, true);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
for i in 0..10 {
let mut doc = Document::new();
let text = match i % 4 {
0 => "apple banana cherry",
1 => "apple orange",
2 => "banana grape",
_ => "cherry date",
};
doc.add_text(content, text);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let index = Index::open(dir.clone(), config.clone()).await.unwrap();
let wand_query = WandOrQuery::new(content).term("apple").term("banana");
let bool_query = BooleanQuery::new()
.should(TermQuery::text(content, "apple"))
.should(TermQuery::text(content, "banana"));
let wand_results = index.search(&wand_query, 10).await.unwrap();
let bool_results = index.search(&bool_query, 10).await.unwrap();
assert_eq!(
wand_results.hits.len(),
bool_results.hits.len(),
"WAND and Boolean should find same number of docs"
);
let wand_docs: std::collections::HashSet<u32> =
wand_results.hits.iter().map(|h| h.address.doc_id).collect();
let bool_docs: std::collections::HashSet<u32> =
bool_results.hits.iter().map(|h| h.address.doc_id).collect();
assert_eq!(
wand_docs, bool_docs,
"WAND and Boolean should find same documents"
);
}
#[tokio::test]
async fn test_vector_index_threshold_switch() {
use crate::dsl::{DenseVectorConfig, VectorIndexType};
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let embedding = schema_builder.add_dense_vector_field_with_config(
"embedding",
true, true, DenseVectorConfig {
dim: 8,
index_type: VectorIndexType::IvfRaBitQ,
store_raw: true,
num_clusters: Some(4), nprobe: 2,
mrl_dim: None,
build_threshold: Some(50), },
);
let schema = schema_builder.build();
let dir = RamDirectory::new();
let config = IndexConfig::default();
let writer = IndexWriter::create(dir.clone(), schema.clone(), config.clone())
.await
.unwrap();
for i in 0..30 {
let mut doc = Document::new();
doc.add_text(title, format!("Document {}", i));
let vec: Vec<f32> = (0..8).map(|_| (i as f32) / 30.0).collect();
doc.add_dense_vector(embedding, vec);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
let index = Index::open(dir.clone(), config.clone()).await.unwrap();
assert!(
index.trained_centroids.is_empty(),
"Should not have trained centroids below threshold"
);
let query_vec: Vec<f32> = vec![0.5; 8];
let segments = index.segment_readers().await.unwrap();
assert!(!segments.is_empty());
let results = segments[0]
.search_dense_vector(
embedding,
&query_vec,
5,
1,
crate::query::MultiValueCombiner::Max,
)
.unwrap();
assert!(!results.is_empty(), "Flat search should return results");
let writer = IndexWriter::open(dir.clone(), config.clone())
.await
.unwrap();
for i in 30..60 {
let mut doc = Document::new();
doc.add_text(title, format!("Document {}", i));
let vec: Vec<f32> = (0..8).map(|_| (i as f32) / 60.0).collect();
doc.add_dense_vector(embedding, vec);
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
assert!(
writer.is_vector_index_built(embedding).await,
"Vector index should be built after crossing threshold"
);
let index = Index::open(dir.clone(), config.clone()).await.unwrap();
assert!(
index.trained_centroids.contains_key(&embedding.0),
"Should have loaded trained centroids for embedding field"
);
let segments = index.segment_readers().await.unwrap();
let results = segments[0]
.search_dense_vector(
embedding,
&query_vec,
5,
1,
crate::query::MultiValueCombiner::Max,
)
.unwrap();
assert!(
!results.is_empty(),
"Search should return results after build"
);
let writer = IndexWriter::open(dir.clone(), config.clone())
.await
.unwrap();
writer.build_vector_index().await.unwrap();
assert!(writer.is_vector_index_built(embedding).await);
}
}