1use crate::cross_modal_embeddings::{
42 AudioData, AudioEncoder, CrossModalConfig, CrossModalEncoder, GraphData, GraphEncoder,
43 ImageData, ImageEncoder, ImageFormat, Modality, ModalityData, MultiModalContent, TextEncoder,
44 VideoData, VideoEncoder,
45};
46use crate::Vector;
47use crate::VectorStore;
48use anyhow::{anyhow, Result};
49use parking_lot::RwLock;
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52use std::sync::Arc;
53
54pub struct MultiModalSearchEngine {
56 config: MultiModalConfig,
57 encoder: Arc<CrossModalEncoder>,
58 vector_store: Arc<RwLock<VectorStore>>,
59 modality_stores: HashMap<Modality, Arc<RwLock<VectorStore>>>,
60 query_cache: Arc<RwLock<HashMap<String, Vec<SearchResult>>>>,
61 total_indexed: Arc<RwLock<usize>>,
62 cache_hits: Arc<std::sync::atomic::AtomicU64>,
63 cache_queries: Arc<std::sync::atomic::AtomicU64>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct MultiModalConfig {
69 pub cross_modal_config: CrossModalConfig,
71 pub use_modality_specific_indices: bool,
73 pub enable_caching: bool,
75 pub cache_size_limit: usize,
77 pub search_strategy: SearchStrategy,
79 pub enable_query_expansion: bool,
81 pub query_expansion_factor: f32,
83}
84
85impl Default for MultiModalConfig {
86 fn default() -> Self {
87 Self {
88 cross_modal_config: CrossModalConfig::default(),
89 use_modality_specific_indices: true,
90 enable_caching: true,
91 cache_size_limit: 1000,
92 search_strategy: SearchStrategy::HybridFusion,
93 enable_query_expansion: true,
94 query_expansion_factor: 1.5,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum SearchStrategy {
102 JointSpaceOnly,
104 ModalitySpecific,
106 HybridFusion,
108 Adaptive,
110}
111
112#[derive(Debug, Clone)]
114pub struct MultiModalQuery {
115 pub modalities: Vec<QueryModality>,
116 pub weights: Option<HashMap<Modality, f32>>,
117 pub filters: Vec<QueryFilter>,
118 pub metadata: HashMap<String, String>,
119}
120
121#[derive(Debug, Clone)]
123pub enum QueryModality {
124 Text(String),
125 Image(Vec<u8>),
126 Audio(Vec<f32>, u32), Video(Vec<Vec<u8>>), Embedding(Vector), }
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct QueryFilter {
134 pub field: String,
135 pub operator: FilterOperator,
136 pub value: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub enum FilterOperator {
141 Equals,
142 NotEquals,
143 Contains,
144 GreaterThan,
145 LessThan,
146 Regex,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct SearchResult {
152 pub id: String,
153 pub score: f32,
154 pub modality: Modality,
155 pub metadata: HashMap<String, String>,
156 pub embedding: Option<Vec<f32>>,
157 pub modality_scores: HashMap<Modality, f32>,
158}
159
160impl MultiModalQuery {
161 pub fn text(text: impl Into<String>) -> Self {
163 Self {
164 modalities: vec![QueryModality::Text(text.into())],
165 weights: None,
166 filters: Vec::new(),
167 metadata: HashMap::new(),
168 }
169 }
170
171 pub fn image(image_data: Vec<u8>) -> Self {
173 Self {
174 modalities: vec![QueryModality::Image(image_data)],
175 weights: None,
176 filters: Vec::new(),
177 metadata: HashMap::new(),
178 }
179 }
180
181 pub fn audio(samples: Vec<f32>, sample_rate: u32) -> Self {
183 Self {
184 modalities: vec![QueryModality::Audio(samples, sample_rate)],
185 weights: None,
186 filters: Vec::new(),
187 metadata: HashMap::new(),
188 }
189 }
190
191 pub fn hybrid(modalities: Vec<QueryModality>) -> Self {
193 Self {
194 modalities,
195 weights: None,
196 filters: Vec::new(),
197 metadata: HashMap::new(),
198 }
199 }
200
201 pub fn with_filter(mut self, filter: QueryFilter) -> Self {
203 self.filters.push(filter);
204 self
205 }
206
207 pub fn with_weights(mut self, weights: HashMap<Modality, f32>) -> Self {
209 self.weights = Some(weights);
210 self
211 }
212
213 pub fn with_metadata(mut self, key: String, value: String) -> Self {
215 self.metadata.insert(key, value);
216 self
217 }
218}
219
220impl MultiModalSearchEngine {
221 pub fn new_default() -> Result<Self> {
223 Self::new(MultiModalConfig::default())
224 }
225
226 pub fn new(config: MultiModalConfig) -> Result<Self> {
228 let text_encoder = Box::new(ProductionTextEncoder::new(
230 config.cross_modal_config.joint_embedding_dim,
231 )?);
232 let image_encoder = Box::new(ProductionImageEncoder::new(
233 config.cross_modal_config.joint_embedding_dim,
234 )?);
235 let audio_encoder = Box::new(ProductionAudioEncoder::new(
236 config.cross_modal_config.joint_embedding_dim,
237 )?);
238 let video_encoder = Box::new(ProductionVideoEncoder::new(
239 config.cross_modal_config.joint_embedding_dim,
240 )?);
241 let graph_encoder = Box::new(ProductionGraphEncoder::new(
242 config.cross_modal_config.joint_embedding_dim,
243 )?);
244
245 let encoder = Arc::new(CrossModalEncoder::new(
246 config.cross_modal_config.clone(),
247 text_encoder,
248 image_encoder,
249 audio_encoder,
250 video_encoder,
251 graph_encoder,
252 ));
253
254 let vector_store = Arc::new(RwLock::new(VectorStore::new()));
256
257 let mut modality_stores = HashMap::new();
259 if config.use_modality_specific_indices {
260 for modality in &[
261 Modality::Text,
262 Modality::Image,
263 Modality::Audio,
264 Modality::Video,
265 ] {
266 let store = Arc::new(RwLock::new(VectorStore::new()));
267 modality_stores.insert(*modality, store);
268 }
269 }
270
271 Ok(Self {
272 config,
273 encoder,
274 vector_store,
275 modality_stores,
276 query_cache: Arc::new(RwLock::new(HashMap::new())),
277 total_indexed: Arc::new(RwLock::new(0)),
278 cache_hits: Arc::new(std::sync::atomic::AtomicU64::new(0)),
279 cache_queries: Arc::new(std::sync::atomic::AtomicU64::new(0)),
280 })
281 }
282
283 pub fn index_content(&self, id: String, content: MultiModalContent) -> Result<()> {
285 let embedding = self.encoder.encode(&content)?;
287
288 {
290 let mut store = self.vector_store.write();
291 store.index_vector(id.clone(), embedding.clone())?;
292 }
293
294 if self.config.use_modality_specific_indices {
296 for (modality, data) in &content.modalities {
297 if let Some(store) = self.modality_stores.get(modality) {
298 let modality_embedding = self.encode_modality(*modality, data)?;
300
301 let mut store = store.write();
302 store.index_vector(id.clone(), modality_embedding)?;
303 }
304 }
305 }
306
307 *self.total_indexed.write() += 1;
309
310 Ok(())
311 }
312
313 pub fn search(&self, query: &MultiModalQuery, k: usize) -> Result<Vec<SearchResult>> {
315 self.cache_queries
317 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
318
319 if self.config.enable_caching {
321 let cache_key = self.compute_cache_key(query);
322 if let Some(cached_results) = self.query_cache.read().get(&cache_key) {
323 self.cache_hits
324 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
325 return Ok(cached_results.clone());
326 }
327 }
328
329 let results = match self.config.search_strategy {
331 SearchStrategy::JointSpaceOnly => self.search_joint_space(query, k)?,
332 SearchStrategy::ModalitySpecific => self.search_modality_specific(query, k)?,
333 SearchStrategy::HybridFusion => self.search_hybrid(query, k)?,
334 SearchStrategy::Adaptive => self.search_adaptive(query, k)?,
335 };
336
337 let filtered_results = self.apply_filters(&results, &query.filters)?;
339
340 if self.config.enable_caching {
342 let cache_key = self.compute_cache_key(query);
343 let mut cache = self.query_cache.write();
344
345 if cache.len() >= self.config.cache_size_limit {
347 if let Some(first_key) = cache.keys().next().cloned() {
349 cache.remove(&first_key);
350 }
351 }
352
353 cache.insert(cache_key, filtered_results.clone());
354 }
355
356 Ok(filtered_results)
357 }
358
359 fn search_joint_space(&self, query: &MultiModalQuery, k: usize) -> Result<Vec<SearchResult>> {
361 let query_content = self.query_to_content(query)?;
363
364 let query_embedding = self.encoder.encode(&query_content)?;
366
367 let store = self.vector_store.read();
369 let results = store.similarity_search_vector(&query_embedding, k)?;
370
371 Ok(results
373 .into_iter()
374 .map(|(id, score)| SearchResult {
375 id,
376 score,
377 modality: Modality::Text, metadata: HashMap::new(),
379 embedding: None,
380 modality_scores: HashMap::new(),
381 })
382 .collect())
383 }
384
385 fn search_modality_specific(
387 &self,
388 query: &MultiModalQuery,
389 k: usize,
390 ) -> Result<Vec<SearchResult>> {
391 let mut all_results: Vec<SearchResult> = Vec::new();
392 let mut modality_results: HashMap<Modality, Vec<SearchResult>> = HashMap::new();
393
394 for query_modality in &query.modalities {
396 let (modality, data) = match query_modality {
397 QueryModality::Text(text) => (Modality::Text, ModalityData::Text(text.clone())),
398 QueryModality::Image(img_data) => {
399 let image_data = self.parse_image_data(img_data)?;
400 (Modality::Image, ModalityData::Image(image_data))
401 }
402 QueryModality::Audio(samples, rate) => {
403 let audio_data = AudioData {
404 samples: samples.clone(),
405 sample_rate: *rate,
406 channels: 1,
407 duration: samples.len() as f32 / *rate as f32,
408 features: None,
409 };
410 (Modality::Audio, ModalityData::Audio(audio_data))
411 }
412 QueryModality::Embedding(embedding) => {
413 let store = self.vector_store.read();
415 let results = store.similarity_search_vector(embedding, k)?;
416 all_results.extend(results.into_iter().map(|(id, score)| SearchResult {
417 id,
418 score,
419 modality: Modality::Numeric,
420 metadata: HashMap::new(),
421 embedding: None,
422 modality_scores: HashMap::new(),
423 }));
424 continue;
425 }
426 QueryModality::Video(_frames) => {
427 continue; }
430 };
431
432 if let Some(store) = self.modality_stores.get(&modality) {
433 let embedding = self.encode_modality(modality, &data)?;
434
435 let store = store.read();
436 let results = store.similarity_search_vector(&embedding, k)?;
437
438 let search_results: Vec<SearchResult> = results
439 .into_iter()
440 .map(|(id, score)| SearchResult {
441 id,
442 score,
443 modality,
444 metadata: HashMap::new(),
445 embedding: None,
446 modality_scores: HashMap::new(),
447 })
448 .collect();
449
450 modality_results.insert(modality, search_results);
451 }
452 }
453
454 let fused_results = self.fuse_modality_results(modality_results, query, k)?;
456
457 Ok(fused_results)
458 }
459
460 fn search_hybrid(&self, query: &MultiModalQuery, k: usize) -> Result<Vec<SearchResult>> {
462 let joint_results = self.search_joint_space(query, k * 2)?;
463 let modality_results = self.search_modality_specific(query, k * 2)?;
464
465 let fused = self.fuse_search_results(vec![joint_results, modality_results], &[0.6, 0.4])?;
467
468 Ok(fused.into_iter().take(k).collect())
470 }
471
472 fn search_adaptive(&self, query: &MultiModalQuery, k: usize) -> Result<Vec<SearchResult>> {
474 let num_modalities = query.modalities.len();
476
477 if num_modalities == 1 {
479 return self.search_modality_specific(query, k);
480 }
481
482 self.search_hybrid(query, k)
484 }
485
486 fn encode_modality(&self, _modality: Modality, data: &ModalityData) -> Result<Vector> {
488 let mut content_map = HashMap::new();
490
491 match data {
492 ModalityData::Text(_text) => {
493 content_map.insert(Modality::Text, data.clone());
494 }
495 ModalityData::Image(_image) => {
496 content_map.insert(Modality::Image, data.clone());
497 }
498 ModalityData::Audio(_audio) => {
499 content_map.insert(Modality::Audio, data.clone());
500 }
501 ModalityData::Video(_video) => {
502 content_map.insert(Modality::Video, data.clone());
503 }
504 ModalityData::Graph(_graph) => {
505 content_map.insert(Modality::Graph, data.clone());
506 }
507 ModalityData::Numeric(values) => {
508 return Ok(Vector::new(values.clone()));
510 }
511 ModalityData::Raw(_) => {
512 return Err(anyhow!("Raw data encoding not supported"));
513 }
514 }
515
516 let content = MultiModalContent {
517 modalities: content_map,
518 metadata: HashMap::new(),
519 temporal_info: None,
520 spatial_info: None,
521 };
522
523 self.encoder.encode(&content)
524 }
525
526 fn query_to_content(&self, query: &MultiModalQuery) -> Result<MultiModalContent> {
528 let mut modalities = HashMap::new();
529
530 for query_modality in &query.modalities {
531 match query_modality {
532 QueryModality::Text(text) => {
533 modalities.insert(Modality::Text, ModalityData::Text(text.clone()));
534 }
535 QueryModality::Image(img_data) => {
536 let image_data = self.parse_image_data(img_data)?;
537 modalities.insert(Modality::Image, ModalityData::Image(image_data));
538 }
539 QueryModality::Audio(samples, rate) => {
540 let audio_data = AudioData {
541 samples: samples.clone(),
542 sample_rate: *rate,
543 channels: 1,
544 duration: samples.len() as f32 / *rate as f32,
545 features: None,
546 };
547 modalities.insert(Modality::Audio, ModalityData::Audio(audio_data));
548 }
549 QueryModality::Embedding(_) => {
550 }
552 QueryModality::Video(frames) => {
553 let video_frames: Result<Vec<ImageData>> =
554 frames.iter().map(|f| self.parse_image_data(f)).collect();
555
556 let video_data = VideoData {
557 frames: video_frames?,
558 audio: None,
559 fps: 30.0,
560 duration: frames.len() as f32 / 30.0,
561 keyframes: vec![0],
562 };
563 modalities.insert(Modality::Video, ModalityData::Video(video_data));
564 }
565 }
566 }
567
568 Ok(MultiModalContent {
569 modalities,
570 metadata: query.metadata.clone(),
571 temporal_info: None,
572 spatial_info: None,
573 })
574 }
575
576 fn parse_image_data(&self, data: &[u8]) -> Result<ImageData> {
578 #[cfg(feature = "images")]
580 {
581 use image::GenericImageView;
582
583 let img = image::load_from_memory(data)
584 .map_err(|e| anyhow!("Failed to decode image: {}", e))?;
585
586 let (width, height) = img.dimensions();
587 let rgb_img = img.to_rgb8();
588 let raw_data = rgb_img.into_raw();
589
590 Ok(ImageData {
591 data: raw_data,
592 width,
593 height,
594 channels: 3,
595 format: ImageFormat::RGB,
596 features: None,
597 })
598 }
599
600 #[cfg(not(feature = "images"))]
601 {
602 Ok(ImageData {
604 data: data.to_vec(),
605 width: 0,
606 height: 0,
607 channels: 3,
608 format: ImageFormat::RGB,
609 features: None,
610 })
611 }
612 }
613
614 fn fuse_modality_results(
616 &self,
617 modality_results: HashMap<Modality, Vec<SearchResult>>,
618 query: &MultiModalQuery,
619 k: usize,
620 ) -> Result<Vec<SearchResult>> {
621 let mut score_map: HashMap<String, (f32, SearchResult)> = HashMap::new();
623
624 for (modality, results) in modality_results {
625 let weight = query
626 .weights
627 .as_ref()
628 .and_then(|w| w.get(&modality))
629 .copied()
630 .unwrap_or(1.0);
631
632 for (rank, result) in results.into_iter().enumerate() {
633 let rrf_score = weight / (60.0 + rank as f32 + 1.0);
634
635 score_map
636 .entry(result.id.clone())
637 .and_modify(|(score, existing)| {
638 *score += rrf_score;
639 existing.modality_scores.insert(modality, result.score);
640 })
641 .or_insert_with(|| {
642 let mut updated_result = result.clone();
643 updated_result
644 .modality_scores
645 .insert(modality, result.score);
646 (rrf_score, updated_result)
647 });
648 }
649 }
650
651 let mut fused_results: Vec<(f32, SearchResult)> = score_map.into_values().collect();
653 fused_results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
654
655 Ok(fused_results
657 .into_iter()
658 .take(k)
659 .map(|(score, mut result)| {
660 result.score = score;
661 result
662 })
663 .collect())
664 }
665
666 fn fuse_search_results(
668 &self,
669 result_sets: Vec<Vec<SearchResult>>,
670 weights: &[f32],
671 ) -> Result<Vec<SearchResult>> {
672 if result_sets.len() != weights.len() {
673 return Err(anyhow!("Weights length must match result sets length"));
674 }
675
676 let mut score_map: HashMap<String, (f32, SearchResult)> = HashMap::new();
677
678 for (results, &weight) in result_sets.into_iter().zip(weights.iter()) {
679 for (rank, result) in results.into_iter().enumerate() {
680 let rrf_score = weight / (60.0 + rank as f32 + 1.0);
681
682 score_map
683 .entry(result.id.clone())
684 .and_modify(|(score, _)| *score += rrf_score)
685 .or_insert_with(|| (rrf_score, result));
686 }
687 }
688
689 let mut fused_results: Vec<(f32, SearchResult)> = score_map.into_values().collect();
691 fused_results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
692
693 Ok(fused_results
695 .into_iter()
696 .map(|(score, mut result)| {
697 result.score = score;
698 result
699 })
700 .collect())
701 }
702
703 fn apply_filters(
705 &self,
706 results: &[SearchResult],
707 filters: &[QueryFilter],
708 ) -> Result<Vec<SearchResult>> {
709 if filters.is_empty() {
710 return Ok(results.to_vec());
711 }
712
713 let filtered: Vec<SearchResult> = results
714 .iter()
715 .filter(|result| self.matches_filters(result, filters))
716 .cloned()
717 .collect();
718
719 Ok(filtered)
720 }
721
722 fn matches_filters(&self, result: &SearchResult, filters: &[QueryFilter]) -> bool {
724 filters.iter().all(|filter| {
725 if let Some(value) = result.metadata.get(&filter.field) {
726 match filter.operator {
727 FilterOperator::Equals => value == &filter.value,
728 FilterOperator::NotEquals => value != &filter.value,
729 FilterOperator::Contains => value.contains(&filter.value),
730 FilterOperator::GreaterThan => value > &filter.value,
731 FilterOperator::LessThan => value < &filter.value,
732 FilterOperator::Regex => {
733 if let Ok(re) = regex::Regex::new(&filter.value) {
734 re.is_match(value)
735 } else {
736 false
737 }
738 }
739 }
740 } else {
741 false
742 }
743 })
744 }
745
746 fn compute_cache_key(&self, query: &MultiModalQuery) -> String {
748 use std::collections::hash_map::DefaultHasher;
749 use std::hash::{Hash, Hasher};
750
751 let mut hasher = DefaultHasher::new();
752
753 for modality in &query.modalities {
755 match modality {
756 QueryModality::Text(text) => text.hash(&mut hasher),
757 QueryModality::Image(data) => data.hash(&mut hasher),
758 QueryModality::Audio(samples, rate) => {
759 samples.len().hash(&mut hasher);
760 rate.hash(&mut hasher);
761 }
762 QueryModality::Video(frames) => frames.len().hash(&mut hasher),
763 QueryModality::Embedding(emb) => emb.dimensions.hash(&mut hasher),
764 }
765 }
766
767 format!("{:x}", hasher.finish())
768 }
769
770 pub fn get_statistics(&self) -> MultiModalStatistics {
772 let num_vectors = *self.total_indexed.read();
774
775 let mut modality_counts = HashMap::new();
776 for modality in self.modality_stores.keys() {
777 modality_counts.insert(*modality, 0);
779 }
780
781 MultiModalStatistics {
782 total_vectors: num_vectors,
783 modality_counts,
784 cache_size: self.query_cache.read().len(),
785 cache_hit_rate: {
786 let hits = self.cache_hits.load(std::sync::atomic::Ordering::Relaxed);
787 let queries = self
788 .cache_queries
789 .load(std::sync::atomic::Ordering::Relaxed);
790 if queries == 0 {
791 0.0_f32
792 } else {
793 hits as f32 / queries as f32
794 }
795 },
796 }
797 }
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
802pub struct MultiModalStatistics {
803 pub total_vectors: usize,
804 pub modality_counts: HashMap<Modality, usize>,
805 pub cache_size: usize,
806 pub cache_hit_rate: f32,
807}
808
809pub struct ProductionTextEncoder {
813 embedding_dim: usize,
814 vocab_size: usize,
815}
816
817impl ProductionTextEncoder {
818 pub fn new(embedding_dim: usize) -> Result<Self> {
819 Ok(Self {
820 embedding_dim,
821 vocab_size: 10000,
822 })
823 }
824
825 fn tokenize(&self, text: &str) -> Vec<String> {
827 text.to_lowercase()
828 .split_whitespace()
829 .map(|s| s.to_string())
830 .collect()
831 }
832
833 fn compute_embedding(&self, tokens: &[String]) -> Vec<f32> {
835 use std::collections::HashMap;
836
837 let mut freq_map = HashMap::new();
839 for token in tokens {
840 *freq_map.entry(token.clone()).or_insert(0) += 1;
841 }
842
843 let mut embedding = vec![0.0f32; self.embedding_dim];
845 for (token, count) in freq_map {
846 let hash = Self::hash_token(&token);
847 let index = (hash % self.embedding_dim as u64) as usize;
848 embedding[index] += count as f32 / tokens.len() as f32;
849 }
850
851 let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
853 if norm > 0.0 {
854 embedding.iter_mut().for_each(|x| *x /= norm);
855 }
856
857 embedding
858 }
859
860 fn hash_token(token: &str) -> u64 {
861 use std::collections::hash_map::DefaultHasher;
862 use std::hash::{Hash, Hasher};
863
864 let mut hasher = DefaultHasher::new();
865 token.hash(&mut hasher);
866 hasher.finish()
867 }
868}
869
870impl TextEncoder for ProductionTextEncoder {
871 fn encode(&self, text: &str) -> Result<Vector> {
872 let tokens = self.tokenize(text);
873 let embedding = self.compute_embedding(&tokens);
874 Ok(Vector::new(embedding))
875 }
876
877 fn encode_batch(&self, texts: &[String]) -> Result<Vec<Vector>> {
878 texts.iter().map(|text| self.encode(text)).collect()
879 }
880
881 fn get_embedding_dim(&self) -> usize {
882 self.embedding_dim
883 }
884}
885
886pub struct ProductionImageEncoder {
888 embedding_dim: usize,
889}
890
891impl ProductionImageEncoder {
892 pub fn new(embedding_dim: usize) -> Result<Self> {
893 Ok(Self { embedding_dim })
894 }
895
896 fn extract_image_features(&self, image: &ImageData) -> Result<Vec<f32>> {
898 let mut features = vec![0.0f32; self.embedding_dim];
902
903 let histogram_size = self.embedding_dim / 3;
905 for i in 0..histogram_size.min(image.data.len()) {
906 let pixel_value = image.data[i] as f32 / 255.0;
907 features[i % histogram_size] += pixel_value;
908 }
909
910 let spatial_offset = histogram_size;
912 features[spatial_offset] = image.width as f32 / 1000.0;
913 features[spatial_offset + 1] = image.height as f32 / 1000.0;
914 features[spatial_offset + 2] = (image.width * image.height) as f32 / 1_000_000.0;
915
916 let edge_offset = 2 * histogram_size;
918 for i in 0..(self.embedding_dim - edge_offset).min(100) {
919 if i + 1 < image.data.len() {
920 let gradient = (image.data[i + 1] as i32 - image.data[i] as i32).abs() as f32;
921 features[edge_offset + (i % (self.embedding_dim - edge_offset))] +=
922 gradient / 255.0;
923 }
924 }
925
926 let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
928 if norm > 0.0 {
929 features.iter_mut().for_each(|x| *x /= norm);
930 }
931
932 Ok(features)
933 }
934}
935
936impl ImageEncoder for ProductionImageEncoder {
937 fn encode(&self, image: &ImageData) -> Result<Vector> {
938 let features = self.extract_image_features(image)?;
939 Ok(Vector::new(features))
940 }
941
942 fn encode_batch(&self, images: &[ImageData]) -> Result<Vec<Vector>> {
943 images.iter().map(|img| self.encode(img)).collect()
944 }
945
946 fn get_embedding_dim(&self) -> usize {
947 self.embedding_dim
948 }
949
950 fn extract_features(&self, image: &ImageData) -> Result<Vec<f32>> {
951 self.extract_image_features(image)
952 }
953}
954
955pub struct ProductionAudioEncoder {
957 embedding_dim: usize,
958}
959
960impl ProductionAudioEncoder {
961 pub fn new(embedding_dim: usize) -> Result<Self> {
962 Ok(Self { embedding_dim })
963 }
964
965 fn extract_audio_features(&self, audio: &AudioData) -> Result<Vec<f32>> {
967 let mut features = vec![0.0f32; self.embedding_dim];
968
969 let chunk_size = audio.samples.len().max(1) / (self.embedding_dim / 4).max(1);
971 for (i, chunk) in audio.samples.chunks(chunk_size).enumerate() {
972 if i >= self.embedding_dim / 4 {
973 break;
974 }
975 let energy: f32 = chunk.iter().map(|x| x * x).sum::<f32>() / chunk.len() as f32;
976 features[i] = energy.sqrt();
977 }
978
979 let zcr_offset = self.embedding_dim / 4;
981 let mut zero_crossings = 0;
982 for i in 1..audio.samples.len() {
983 if (audio.samples[i] >= 0.0) != (audio.samples[i - 1] >= 0.0) {
984 zero_crossings += 1;
985 }
986 }
987 if zcr_offset < features.len() {
988 features[zcr_offset] = zero_crossings as f32 / audio.samples.len() as f32;
989 }
990
991 let spectral_offset = self.embedding_dim / 2;
993 for i in 0..(self.embedding_dim - spectral_offset).min(audio.samples.len()) {
994 features[spectral_offset + i] =
995 audio.samples[i].abs() * (i as f32 / audio.samples.len() as f32);
996 }
997
998 let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
1000 if norm > 0.0 {
1001 features.iter_mut().for_each(|x| *x /= norm);
1002 }
1003
1004 Ok(features)
1005 }
1006}
1007
1008impl AudioEncoder for ProductionAudioEncoder {
1009 fn encode(&self, audio: &AudioData) -> Result<Vector> {
1010 let features = self.extract_audio_features(audio)?;
1011 Ok(Vector::new(features))
1012 }
1013
1014 fn encode_batch(&self, audios: &[AudioData]) -> Result<Vec<Vector>> {
1015 audios.iter().map(|audio| self.encode(audio)).collect()
1016 }
1017
1018 fn get_embedding_dim(&self) -> usize {
1019 self.embedding_dim
1020 }
1021
1022 fn extract_features(&self, audio: &AudioData) -> Result<Vec<f32>> {
1023 self.extract_audio_features(audio)
1024 }
1025}
1026
1027pub struct ProductionVideoEncoder {
1029 embedding_dim: usize,
1030 image_encoder: ProductionImageEncoder,
1031}
1032
1033impl ProductionVideoEncoder {
1034 pub fn new(embedding_dim: usize) -> Result<Self> {
1035 Ok(Self {
1036 embedding_dim,
1037 image_encoder: ProductionImageEncoder::new(embedding_dim)?,
1038 })
1039 }
1040
1041 fn extract_video_features(&self, video: &VideoData) -> Result<Vec<f32>> {
1043 let mut all_features = Vec::new();
1044
1045 for keyframe_idx in &video.keyframes {
1047 if let Some(frame) = video.frames.get(*keyframe_idx) {
1048 let frame_features = self.image_encoder.extract_image_features(frame)?;
1049 all_features.extend(frame_features);
1050 }
1051 }
1052
1053 if all_features.is_empty() && !video.frames.is_empty() {
1055 let first_features = self
1056 .image_encoder
1057 .extract_image_features(&video.frames[0])?;
1058 all_features.extend(first_features);
1059
1060 if video.frames.len() > 1 {
1061 let last_features = self.image_encoder.extract_image_features(
1062 video
1063 .frames
1064 .last()
1065 .expect("video frames validated to have more than one element"),
1066 )?;
1067 all_features.extend(last_features);
1068 }
1069 }
1070
1071 let mut aggregated = vec![0.0f32; self.embedding_dim];
1073 if !all_features.is_empty() {
1074 let chunk_size = all_features.len() / self.embedding_dim.max(1);
1075 if chunk_size > 0 {
1076 for (i, chunk) in all_features.chunks(chunk_size).enumerate() {
1077 if i >= self.embedding_dim {
1078 break;
1079 }
1080 aggregated[i] = chunk.iter().sum::<f32>() / chunk.len() as f32;
1081 }
1082 }
1083 }
1084
1085 if self.embedding_dim > 3 {
1087 aggregated[self.embedding_dim - 3] = video.fps / 60.0;
1088 aggregated[self.embedding_dim - 2] = video.duration / 600.0;
1089 aggregated[self.embedding_dim - 1] = video.frames.len() as f32 / 1000.0;
1090 }
1091
1092 let norm: f32 = aggregated.iter().map(|x| x * x).sum::<f32>().sqrt();
1094 if norm > 0.0 {
1095 aggregated.iter_mut().for_each(|x| *x /= norm);
1096 }
1097
1098 Ok(aggregated)
1099 }
1100}
1101
1102impl VideoEncoder for ProductionVideoEncoder {
1103 fn encode(&self, video: &VideoData) -> Result<Vector> {
1104 let features = self.extract_video_features(video)?;
1105 Ok(Vector::new(features))
1106 }
1107
1108 fn encode_keyframes(&self, video: &VideoData) -> Result<Vec<Vector>> {
1109 video
1110 .keyframes
1111 .iter()
1112 .filter_map(|&idx| video.frames.get(idx))
1113 .map(|frame| self.image_encoder.encode(frame))
1114 .collect()
1115 }
1116
1117 fn get_embedding_dim(&self) -> usize {
1118 self.embedding_dim
1119 }
1120}
1121
1122pub struct ProductionGraphEncoder {
1124 embedding_dim: usize,
1125}
1126
1127impl ProductionGraphEncoder {
1128 pub fn new(embedding_dim: usize) -> Result<Self> {
1129 Ok(Self { embedding_dim })
1130 }
1131
1132 fn extract_graph_features(&self, graph: &GraphData) -> Result<Vec<f32>> {
1134 let mut features = vec![0.0f32; self.embedding_dim];
1135
1136 let mut node_degrees = HashMap::new();
1138 for edge in &graph.edges {
1139 *node_degrees.entry(edge.source.clone()).or_insert(0) += 1;
1140 *node_degrees.entry(edge.target.clone()).or_insert(0) += 1;
1141 }
1142
1143 let degrees: Vec<usize> = node_degrees.values().copied().collect();
1145 if !degrees.is_empty() {
1146 let avg_degree = degrees.iter().sum::<usize>() as f32 / degrees.len() as f32;
1147 let max_degree = *degrees.iter().max().unwrap_or(&0) as f32;
1148
1149 features[0] = avg_degree / 100.0;
1150 features[1] = max_degree / 100.0;
1151 features[2] = graph.nodes.len() as f32 / 1000.0;
1152 features[3] = graph.edges.len() as f32 / 1000.0;
1153 }
1154
1155 for (_i, node) in graph.nodes.iter().enumerate().take(self.embedding_dim / 2) {
1157 if !node.labels.is_empty() {
1158 let label_hash = Self::hash_string(&node.labels[0]);
1159 let idx = 4 + (label_hash % (self.embedding_dim as u64 / 2 - 4)) as usize;
1160 features[idx] += 1.0 / graph.nodes.len() as f32;
1161 }
1162 }
1163
1164 let norm: f32 = features.iter().map(|x| x * x).sum::<f32>().sqrt();
1166 if norm > 0.0 {
1167 features.iter_mut().for_each(|x| *x /= norm);
1168 }
1169
1170 Ok(features)
1171 }
1172
1173 fn hash_string(s: &str) -> u64 {
1174 use std::collections::hash_map::DefaultHasher;
1175 use std::hash::{Hash, Hasher};
1176
1177 let mut hasher = DefaultHasher::new();
1178 s.hash(&mut hasher);
1179 hasher.finish()
1180 }
1181}
1182
1183impl GraphEncoder for ProductionGraphEncoder {
1184 fn encode(&self, graph: &GraphData) -> Result<Vector> {
1185 let features = self.extract_graph_features(graph)?;
1186 Ok(Vector::new(features))
1187 }
1188
1189 fn encode_node(&self, node: &crate::cross_modal_embeddings::GraphNode) -> Result<Vector> {
1190 let graph = GraphData {
1192 nodes: vec![node.clone()],
1193 edges: Vec::new(),
1194 metadata: HashMap::new(),
1195 };
1196 self.encode(&graph)
1197 }
1198
1199 fn encode_subgraph(
1200 &self,
1201 nodes: &[crate::cross_modal_embeddings::GraphNode],
1202 edges: &[crate::cross_modal_embeddings::GraphEdge],
1203 ) -> Result<Vector> {
1204 let graph = GraphData {
1205 nodes: nodes.to_vec(),
1206 edges: edges.to_vec(),
1207 metadata: HashMap::new(),
1208 };
1209 self.encode(&graph)
1210 }
1211
1212 fn get_embedding_dim(&self) -> usize {
1213 self.embedding_dim
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220
1221 #[test]
1222 fn test_text_query() -> Result<()> {
1223 let _engine = MultiModalSearchEngine::new_default()?;
1224
1225 let query = MultiModalQuery::text("test query");
1226 assert_eq!(query.modalities.len(), 1);
1227
1228 Ok(())
1229 }
1230
1231 #[test]
1232 fn test_hybrid_query() -> Result<()> {
1233 let query = MultiModalQuery::hybrid(vec![
1234 QueryModality::Text("test".to_string()),
1235 QueryModality::Embedding(Vector::new(vec![0.0; 128])),
1236 ]);
1237
1238 assert_eq!(query.modalities.len(), 2);
1239
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn test_text_encoder() -> Result<()> {
1245 let encoder = ProductionTextEncoder::new(128)?;
1246
1247 let vector = encoder.encode("hello world")?;
1248 assert_eq!(vector.dimensions, 128);
1249
1250 let magnitude = vector.magnitude();
1252 assert!((magnitude - 1.0).abs() < 0.1);
1253
1254 Ok(())
1255 }
1256
1257 #[test]
1258 fn test_image_encoder() -> Result<()> {
1259 let encoder = ProductionImageEncoder::new(256)?;
1260
1261 let image_data = ImageData {
1262 data: vec![128; 1024],
1263 width: 32,
1264 height: 32,
1265 channels: 3,
1266 format: ImageFormat::RGB,
1267 features: None,
1268 };
1269
1270 let vector = encoder.encode(&image_data)?;
1271 assert_eq!(vector.dimensions, 256);
1272
1273 Ok(())
1274 }
1275
1276 #[test]
1277 fn test_audio_encoder() -> Result<()> {
1278 let encoder = ProductionAudioEncoder::new(128)?;
1279
1280 let audio_data = AudioData {
1281 samples: vec![0.5; 44100], sample_rate: 44100,
1283 channels: 1,
1284 duration: 1.0,
1285 features: None,
1286 };
1287
1288 let vector = encoder.encode(&audio_data)?;
1289 assert_eq!(vector.dimensions, 128);
1290
1291 Ok(())
1292 }
1293
1294 #[test]
1295 fn test_modality_fusion() -> Result<()> {
1296 let engine = MultiModalSearchEngine::new_default()?;
1297
1298 let mut modalities = HashMap::new();
1300 modalities.insert(Modality::Text, ModalityData::Text("test".to_string()));
1301
1302 let content = MultiModalContent {
1303 modalities,
1304 metadata: HashMap::new(),
1305 temporal_info: None,
1306 spatial_info: None,
1307 };
1308
1309 engine.index_content("test1".to_string(), content)?;
1310
1311 let stats = engine.get_statistics();
1312 assert_eq!(stats.total_vectors, 1);
1313
1314 Ok(())
1315 }
1316}