#![allow(clippy::missing_inline_in_public_items)]
mod inc;
use api_huggingface::
{
Client,
environment::HuggingFaceEnvironmentImpl,
components::embeddings::EmbeddingResponse,
};
use std::collections::HashMap;
#[ cfg( feature = "integration" ) ]
use api_huggingface::
{
components::models::Models,
secret::Secret,
};
#[ cfg( feature = "integration" ) ]
use std::time::Instant;
#[ allow( missing_docs ) ]
#[ derive( Debug, Clone ) ]
pub struct Document
{
pub id : String,
pub title : String,
pub content : String,
pub metadata : HashMap< String, String >,
pub embedding : Option< Vec< f32 > >,
}
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum DocumentCategory
{
Technical,
Academic,
News,
Creative,
Legal,
}
#[ derive( Debug, Clone ) ]
pub struct SearchQuery
{
pub text : String,
pub limit : usize,
pub threshold : f32,
pub category_filter : Option< DocumentCategory >,
pub metadata_filters : HashMap< String, String >,
}
#[ derive( Debug, Clone ) ]
pub struct SearchResult
{
pub document : Document,
pub similarity : f32,
pub rank : usize,
}
#[ derive( Debug ) ]
pub struct DocumentSearchEngine
{
client : Client< HuggingFaceEnvironmentImpl >,
documents : HashMap< String, Document >,
embedding_model : String,
}
impl DocumentSearchEngine
{
#[ must_use ]
pub fn new( client : Client< HuggingFaceEnvironmentImpl >, embedding_model : String ) -> Self
{
Self
{
client,
documents : HashMap::new(),
embedding_model,
}
}
pub async fn add_document( &mut self, mut document : Document ) -> Result< String, Box< dyn std::error::Error > >
{
let response = self.client
.embeddings()
.create( document.content.clone(), &self.embedding_model )
.await?;
match response
{
EmbeddingResponse::Single( embedding_vectors ) =>
{
if let Some( embedding ) = embedding_vectors.first()
{
document.embedding = Some( embedding.clone() );
}
else
{
return Err( "No embedding generated".into() );
}
},
EmbeddingResponse::Batch( batch_vectors ) =>
{
if let Some( first_doc_vectors ) = batch_vectors.first()
{
if let Some( embedding ) = first_doc_vectors.first()
{
document.embedding = Some( embedding.clone() );
}
else
{
return Err( "No embedding in batch result".into() );
}
}
else
{
return Err( "No embedding generated".into() );
}
},
}
let doc_id = document.id.clone();
self.documents.insert( doc_id.clone(), document );
Ok( doc_id )
}
pub async fn add_documents_batch( &mut self, documents : Vec< Document > ) -> Result< Vec< String >, Box< dyn std::error::Error > >
{
let mut added_ids = Vec::new();
let batch_size = 5;
for batch in documents.chunks( batch_size )
{
let batch_contents : Vec< String > = batch.iter().map( | doc | doc.content.clone() ).collect();
let response = self.client
.embeddings()
.create_batch( batch_contents, &self.embedding_model )
.await?;
match response
{
EmbeddingResponse::Batch( batch_vectors ) =>
{
for ( i, doc_vectors ) in batch_vectors.iter().enumerate()
{
if let Some( mut doc ) = batch.get( i ).cloned()
{
if let Some( embedding ) = doc_vectors.first()
{
doc.embedding = Some( embedding.clone() );
let doc_id = doc.id.clone();
self.documents.insert( doc_id.clone(), doc );
added_ids.push( doc_id );
}
}
}
},
EmbeddingResponse::Single( embedding_vectors ) =>
{
if let Some( mut doc ) = batch.first().cloned()
{
if let Some( embedding ) = embedding_vectors.first()
{
doc.embedding = Some( embedding.clone() );
let doc_id = doc.id.clone();
self.documents.insert( doc_id.clone(), doc );
added_ids.push( doc_id );
}
}
},
}
}
Ok( added_ids )
}
pub async fn search( &self, query : SearchQuery ) -> Result< Vec< SearchResult >, Box< dyn std::error::Error > >
{
let response = self.client
.embeddings()
.create( query.text.clone(), &self.embedding_model )
.await?;
let query_embedding = match response
{
EmbeddingResponse::Single( embedding_vectors ) =>
{
embedding_vectors.first()
.ok_or( "No query embedding generated" )?
.clone()
},
EmbeddingResponse::Batch( batch_vectors ) =>
{
batch_vectors.first()
.and_then( | doc_vectors | doc_vectors.first() )
.ok_or( "No query embedding generated" )?
.clone()
},
};
let mut results = Vec::new();
for document in self.documents.values()
{
if let Some( doc_embedding ) = &document.embedding
{
let similarity = Self::cosine_similarity( &query_embedding, doc_embedding );
if similarity >= query.threshold
{
if let Some( category_filter ) = query.category_filter
{
if let Some( doc_category_str ) = document.metadata.get( "category" )
{
let doc_category = Self::parse_category( doc_category_str );
if doc_category != Some( category_filter )
{
continue;
}
}
else
{
continue; }
}
let mut passes_metadata_filters = true;
for ( key, value ) in &query.metadata_filters
{
if document.metadata.get( key ) != Some( value )
{
passes_metadata_filters = false;
break;
}
}
if passes_metadata_filters
{
results.push( SearchResult
{
document : document.clone(),
similarity,
rank : 0, } );
}
}
}
}
results.sort_by( | a, b | b.similarity.partial_cmp( &a.similarity ).unwrap_or( core::cmp::Ordering::Equal ) );
results.truncate( query.limit );
for ( i, result ) in results.iter_mut().enumerate()
{
result.rank = i + 1;
}
Ok( results )
}
fn cosine_similarity( a : &[ f32 ], b : &[ f32 ] ) -> f32
{
if a.len() != b.len()
{
return 0.0;
}
let dot_product : f32 = a.iter().zip( b.iter() ).map( | ( x, y ) | x * y ).sum();
let norm_a : f32 = a.iter().map( | x | x * x ).sum::< f32 >().sqrt();
let norm_b : f32 = b.iter().map( | x | x * x ).sum::< f32 >().sqrt();
if norm_a == 0.0 || norm_b == 0.0
{
0.0
}
else
{
dot_product / ( norm_a * norm_b )
}
}
fn parse_category( category_str : &str ) -> Option< DocumentCategory >
{
match category_str.to_lowercase().as_str()
{
"technical" => Some( DocumentCategory::Technical ),
"academic" => Some( DocumentCategory::Academic ),
"news" => Some( DocumentCategory::News ),
"creative" => Some( DocumentCategory::Creative ),
"legal" => Some( DocumentCategory::Legal ),
_ => None,
}
}
#[ must_use ]
pub fn get_stats( &self ) -> SearchIndexStats
{
let total_documents = self.documents.len();
let indexed_documents = self.documents.values()
.filter( | doc | doc.embedding.is_some() )
.count();
let mut category_counts = HashMap::new();
for document in self.documents.values()
{
if let Some( category_str ) = document.metadata.get( "category" )
{
*category_counts.entry( category_str.clone() ).or_insert( 0 ) += 1;
}
}
SearchIndexStats
{
total_documents,
indexed_documents,
category_counts,
embedding_model : self.embedding_model.clone(),
}
}
pub fn remove_document( &mut self, document_id : &str ) -> Option< Document >
{
self.documents.remove( document_id )
}
pub fn clear( &mut self )
{
self.documents.clear();
}
}
#[ derive( Debug, Clone ) ]
pub struct SearchIndexStats
{
pub total_documents : usize,
pub indexed_documents : usize,
pub category_counts : HashMap< String, usize >,
pub embedding_model : String,
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ cfg( feature = "integration" ) ]
fn create_integration_client() -> Client< HuggingFaceEnvironmentImpl >
{
let api_key = crate::inc::get_api_key_for_integration();
let secret = Secret::new( api_key );
let env = HuggingFaceEnvironmentImpl::build( secret, None )
.expect( "Failed to build environment" );
Client::build( env ).expect( "Failed to create client" )
}
fn create_sample_documents() -> Vec< Document >
{
vec!
[
Document
{
id : "doc1".to_string(),
title : "Rust Programming Guide".to_string(),
content : "Rust is a systems programming language focused on safety, speed, and concurrency.".to_string(),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "technical".to_string() ); m.insert( "author".to_string(), "tech_writer".to_string() ); m },
embedding : None,
},
Document
{
id : "doc2".to_string(),
title : "Machine Learning Fundamentals".to_string(),
content : "Machine learning involves algorithms that can learn patterns from data without explicit programming.".to_string(),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "academic".to_string() ); m.insert( "author".to_string(), "researcher".to_string() ); m },
embedding : None,
},
Document
{
id : "doc3".to_string(),
title : "Climate Change Report".to_string(),
content : "Global warming is causing significant changes to weather patterns and ecosystems worldwide.".to_string(),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "news".to_string() ); m.insert( "author".to_string(), "journalist".to_string() ); m },
embedding : None,
},
Document
{
id : "doc4".to_string(),
title : "Poetry Collection".to_string(),
content : "In whispered dreams and moonlit nights, the soul finds peace in gentle lights.".to_string(),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "creative".to_string() ); m.insert( "author".to_string(), "poet".to_string() ); m },
embedding : None,
},
Document
{
id : "doc5".to_string(),
title : "Legal Contract Template".to_string(),
content : "This agreement establishes the terms and conditions governing the relationship between parties.".to_string(),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "legal".to_string() ); m.insert( "author".to_string(), "lawyer".to_string() ); m },
embedding : None,
},
]
}
#[ test ]
fn test_document_structure()
{
let doc = Document
{
id : "test-id".to_string(),
title : "Test Document".to_string(),
content : "Test content for document".to_string(),
metadata : HashMap::new(),
embedding : None,
};
assert_eq!( doc.id, "test-id" );
assert_eq!( doc.title, "Test Document" );
assert_eq!( doc.content, "Test content for document" );
assert!( doc.metadata.is_empty() );
assert!( doc.embedding.is_none() );
}
#[ test ]
fn test_search_query_construction()
{
let query = SearchQuery
{
text : "machine learning".to_string(),
limit : 10,
threshold : 0.7,
category_filter : Some( DocumentCategory::Academic ),
metadata_filters : { let mut m = HashMap::new(); m.insert( "author".to_string(), "researcher".to_string() ); m },
};
assert_eq!( query.text, "machine learning" );
assert_eq!( query.limit, 10 );
assert!( ( query.threshold - 0.7 ).abs() < f32::EPSILON );
assert_eq!( query.category_filter, Some( DocumentCategory::Academic ) );
assert_eq!( query.metadata_filters.get( "author" ), Some( &"researcher".to_string() ) );
}
#[ test ]
fn test_document_category_parsing()
{
assert_eq!( DocumentSearchEngine::parse_category( "technical" ), Some( DocumentCategory::Technical ) );
assert_eq!( DocumentSearchEngine::parse_category( "Academic" ), Some( DocumentCategory::Academic ) );
assert_eq!( DocumentSearchEngine::parse_category( "NEWS" ), Some( DocumentCategory::News ) );
assert_eq!( DocumentSearchEngine::parse_category( "Creative" ), Some( DocumentCategory::Creative ) );
assert_eq!( DocumentSearchEngine::parse_category( "legal" ), Some( DocumentCategory::Legal ) );
assert_eq!( DocumentSearchEngine::parse_category( "unknown" ), None );
}
#[ test ]
fn test_cosine_similarity_calculation()
{
let vec_a = vec![ 1.0, 0.0, 0.0 ];
let vec_b = vec![ 1.0, 0.0, 0.0 ];
let vec_c = vec![ 0.0, 1.0, 0.0 ];
let sim_identical = DocumentSearchEngine::cosine_similarity( &vec_a, &vec_b );
assert!( ( sim_identical - 1.0 ).abs() < f32::EPSILON );
let sim_orthogonal = DocumentSearchEngine::cosine_similarity( &vec_a, &vec_c );
assert!( sim_orthogonal.abs() < f32::EPSILON );
let vec_d = vec![ 1.0, 0.0 ];
let sim_different_length = DocumentSearchEngine::cosine_similarity( &vec_a, &vec_d );
assert!( sim_different_length.abs() < f32::EPSILON );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_search_engine_creation()
{
let client = create_integration_client();
let engine = DocumentSearchEngine::new( client, Models::all_minilm_l6_v2().to_string() );
assert!( engine.documents.is_empty() );
assert_eq!( engine.embedding_model, Models::all_minilm_l6_v2() );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_search_index_stats()
{
let client = create_integration_client();
let mut engine = DocumentSearchEngine::new( client, Models::all_minilm_l6_v2().to_string() );
let docs = create_sample_documents();
for doc in docs
{
engine.documents.insert( doc.id.clone(), doc );
}
let stats = engine.get_stats();
assert_eq!( stats.total_documents, 5 );
assert_eq!( stats.indexed_documents, 0 ); assert_eq!( stats.embedding_model, Models::all_minilm_l6_v2() );
assert_eq!( stats.category_counts.get( "technical" ), Some( &1 ) );
assert_eq!( stats.category_counts.get( "academic" ), Some( &1 ) );
assert_eq!( stats.category_counts.get( "news" ), Some( &1 ) );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_document_management()
{
let client = create_integration_client();
let mut engine = DocumentSearchEngine::new( client, Models::all_minilm_l6_v2().to_string() );
let doc = create_sample_documents().into_iter().next().expect( "[test_document_management] create_sample_documents() should return at least 1 document - check create_sample_documents() implementation" );
let doc_id = doc.id.clone();
assert!( engine.remove_document( &doc_id ).is_none() );
engine.documents.insert( doc_id.clone(), doc );
assert_eq!( engine.documents.len(), 1 );
let removed_doc = engine.remove_document( &doc_id );
assert!( removed_doc.is_some() );
assert!( engine.documents.is_empty() );
let docs = create_sample_documents();
for doc in docs
{
engine.documents.insert( doc.id.clone(), doc );
}
assert_eq!( engine.documents.len(), 5 );
engine.clear();
assert!( engine.documents.is_empty() );
}
#[ tokio::test ]
async fn test_search_result_ranking()
{
let doc = create_sample_documents().into_iter().next().expect( "[test_search_result_ranking] create_sample_documents() should return at least 1 document - check create_sample_documents() implementation" );
let mut results =
[
SearchResult { document : doc.clone(), similarity : 0.8, rank : 0 },
SearchResult { document : doc.clone(), similarity : 0.9, rank : 0 },
SearchResult { document : doc, similarity : 0.7, rank : 0 },
].to_vec();
results.sort_by( | a, b | b.similarity.partial_cmp( &a.similarity ).unwrap_or( core::cmp::Ordering::Equal ) );
for ( i, result ) in results.iter_mut().enumerate()
{
result.rank = i + 1;
}
assert_eq!( results[ 0 ].rank, 1 );
assert!( ( results[ 0 ].similarity - 0.9 ).abs() < f32::EPSILON );
assert_eq!( results[ 1 ].rank, 2 );
assert!( ( results[ 1 ].similarity - 0.8 ).abs() < f32::EPSILON );
assert_eq!( results[ 2 ].rank, 3 );
assert!( ( results[ 2 ].similarity - 0.7 ).abs() < f32::EPSILON );
}
#[ tokio::test ]
async fn test_search_query_filtering()
{
let base_query = SearchQuery
{
text : "programming".to_string(),
limit : 5,
threshold : 0.5,
category_filter : None,
metadata_filters : HashMap::new(),
};
assert_eq!( base_query.limit, 5 );
assert!( ( base_query.threshold - 0.5 ).abs() < f32::EPSILON );
assert_eq!( base_query.category_filter, None );
let filtered_query = SearchQuery
{
text : "programming".to_string(),
limit : 3,
threshold : 0.8,
category_filter : Some( DocumentCategory::Technical ),
metadata_filters : { let mut m = HashMap::new(); m.insert( "author".to_string(), "tech_writer".to_string() ); m },
};
assert_eq!( filtered_query.category_filter, Some( DocumentCategory::Technical ) );
assert_eq!( filtered_query.metadata_filters.get( "author" ), Some( &"tech_writer".to_string() ) );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_embedding_model_variations()
{
let client = create_integration_client();
let models = vec!
[
Models::all_minilm_l6_v2(),
Models::all_minilm_l12_v2(),
Models::bge_large_en_v1_5(),
];
for model in models
{
let engine = DocumentSearchEngine::new( client.clone(), model.to_string() );
assert_eq!( engine.embedding_model, model );
}
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_performance_characteristics()
{
let client = create_integration_client();
let mut engine = DocumentSearchEngine::new( client, Models::all_minilm_l6_v2().to_string() );
let mut large_doc_set = Vec::new();
for i in 0..20
{
large_doc_set.push( Document
{
id : format!( "perf_doc_{i}" ),
title : format!( "Performance Test Document {i}" ),
content : format!( "This is test document {i} for performance evaluation with some meaningful content about topic {i}." ),
metadata : { let mut m = HashMap::new(); m.insert( "category".to_string(), "technical".to_string() ); m.insert( "batch".to_string(), format!( "{i}" ) ); m },
embedding : None,
} );
}
let start_time = Instant::now();
for doc in large_doc_set
{
engine.documents.insert( doc.id.clone(), doc );
}
let insertion_time = start_time.elapsed();
println!( "Document insertion time for 20 documents : {insertion_time:?}" );
assert_eq!( engine.documents.len(), 20 );
let stats = engine.get_stats();
assert_eq!( stats.total_documents, 20 );
assert_eq!( stats.indexed_documents, 0 ); }
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_error_handling_scenarios()
{
let client = create_integration_client();
let engine = DocumentSearchEngine::new( client, Models::bge_large_en_v1_5().to_string() );
let query = SearchQuery
{
text : "test query".to_string(),
limit : 5,
threshold : 0.5,
category_filter : None,
metadata_filters : HashMap::new(),
};
let results = engine.search( query ).await
.expect( "search on empty index should succeed (returns empty results)" );
assert!( results.is_empty(), "Search on empty index should return no results, got {}", results.len() );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_batch_processing_structure()
{
let client = create_integration_client();
let mut engine = DocumentSearchEngine::new( client, Models::bge_large_en_v1_5().to_string() );
let docs = create_sample_documents();
let ids = engine.add_documents_batch( docs ).await
.expect( "add_documents_batch should succeed with valid credentials" );
assert!( !ids.is_empty(), "Batch processing should return non-empty ID list" );
}
}