spatialrust_semantic/
search.rs1use crate::{
4 cosine_similarity, Embedding, EntityId, SemanticEntity, SemanticError, SemanticResult,
5};
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct FusionScore {
10 pub score: f32,
12 pub embedding: f32,
14 pub label: f32,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct MultimodalFusion {
21 pub embedding_weight: f32,
23 pub label_weight: f32,
25}
26
27impl Default for MultimodalFusion {
28 fn default() -> Self {
29 Self { embedding_weight: 0.7, label_weight: 0.3 }
30 }
31}
32
33impl MultimodalFusion {
34 pub fn score(&self, entity: &SemanticEntity, query: &Embedding) -> SemanticResult<FusionScore> {
36 let embedding = match &entity.embedding {
37 Some(values) => cosine_similarity(values, query)?,
38 None => 0.0,
39 };
40 let label = entity.labels.iter().map(|l| l.confidence).fold(0.0_f32, f32::max);
41 let score = self.embedding_weight * embedding + self.label_weight * label;
42 Ok(FusionScore { score, embedding, label })
43 }
44}
45
46#[derive(Clone, Debug, Default)]
48pub struct SemanticSearchIndex {
49 entities: Vec<SemanticEntity>,
50}
51
52impl SemanticSearchIndex {
53 #[must_use]
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn insert(&mut self, entity: SemanticEntity) {
61 self.entities.push(entity);
62 }
63
64 pub fn search(
66 &self,
67 query: &Embedding,
68 fusion: MultimodalFusion,
69 k: usize,
70 ) -> SemanticResult<Vec<(EntityId, FusionScore)>> {
71 if k == 0 {
72 return Err(SemanticError::InvalidConfiguration("k must be positive".into()));
73 }
74 let mut scored = self
75 .entities
76 .iter()
77 .map(|entity| fusion.score(entity, query).map(|score| (entity.id.clone(), score)))
78 .collect::<SemanticResult<Vec<_>>>()?;
79 scored
80 .sort_by(|a, b| b.1.score.partial_cmp(&a.1.score).unwrap_or(std::cmp::Ordering::Equal));
81 scored.truncate(k);
82 Ok(scored)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::{MultimodalFusion, SemanticSearchIndex};
89 use crate::{Embedding, EntityId, OpenVocabLabel, SemanticEntity};
90
91 #[test]
92 fn ranks_entities_by_fusion() {
93 let mut index = SemanticSearchIndex::new();
94 index.insert(SemanticEntity {
95 id: EntityId::new("a"),
96 centroid: None,
97 labels: vec![OpenVocabLabel { text: "chair".into(), confidence: 0.2 }],
98 embedding: Some(Embedding::try_new(vec![1.0, 0.0]).unwrap()),
99 });
100 index.insert(SemanticEntity {
101 id: EntityId::new("b"),
102 centroid: None,
103 labels: vec![OpenVocabLabel { text: "table".into(), confidence: 0.9 }],
104 embedding: Some(Embedding::try_new(vec![0.0, 1.0]).unwrap()),
105 });
106 let hits = index
107 .search(&Embedding::try_new(vec![1.0, 0.0]).unwrap(), MultimodalFusion::default(), 1)
108 .unwrap();
109 assert_eq!(hits[0].0, EntityId::new("a"));
110 }
111}