khive_retrieval/hit.rs
1//! Search result types returned by hybrid retrieval pipelines.
2//!
3//! These types depend only on `uuid` and `khive-score`, so any pipeline that produces
4//! ranked hits can return them without depending on a higher layer.
5
6use khive_score::DeterministicScore;
7use uuid::Uuid;
8
9/// The strategy that produced a hit's ordering score, including local modifiers.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum RankScoreKind {
12 /// Reciprocal rank fusion of the retrieval legs.
13 Rrf,
14 /// Vector similarity alone.
15 Vector,
16 /// Keyword score alone.
17 Keyword,
18 /// Weighted combination of the retrieval legs.
19 Weighted,
20 /// Union of the retrieval legs.
21 Union,
22}
23
24impl RankScoreKind {
25 /// Lowercase wire representation used by search serializers.
26 #[must_use]
27 pub const fn as_str(self) -> &'static str {
28 match self {
29 Self::Rrf => "rrf",
30 Self::Vector => "vector",
31 Self::Keyword => "keyword",
32 Self::Weighted => "weighted",
33 Self::Union => "union",
34 }
35 }
36}
37
38/// Retained component scores before fusion and strategy-local modifiers.
39/// An absent retrieval leg has no score, which is distinct from a measured zero.
40/// Scores belong to the backend and model that produced the retained hit;
41/// vector similarities from different embedding models are not comparable.
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
43pub struct SearchSignals {
44 /// Vector similarity, or `None` when the vector leg did not return the hit.
45 pub vector_similarity: Option<DeterministicScore>,
46 /// Keyword score, or `None` when the text leg did not return the hit.
47 pub keyword_score: Option<DeterministicScore>,
48}
49
50/// A unified search result combining vector and text signals.
51#[derive(Clone, Debug)]
52pub struct SearchHit {
53 /// Identifier of the matched record.
54 pub entity_id: Uuid,
55 /// Ordering score assigned by the fusion strategy.
56 pub score: DeterministicScore,
57 /// The strategy that produced `score`.
58 pub rank_score_kind: RankScoreKind,
59 /// Component scores retained before fusion.
60 pub signals: SearchSignals,
61 /// The retrieval leg or legs that returned the hit.
62 pub source: SearchSource,
63 /// Title of the matched record, when the text leg carried one.
64 pub title: Option<String>,
65 /// Excerpt of the matched text, when the text leg carried one.
66 pub snippet: Option<String>,
67}
68
69/// Result of a hybrid search: the fused hits — text hits alone when the vector
70/// arm failed — plus the vector arm's error, if any.
71#[derive(Clone, Debug)]
72pub struct HybridSearchOutcome {
73 /// The fused hits.
74 pub hits: Vec<SearchHit>,
75 /// The vector arm's error message, when that arm failed.
76 pub vector_error: Option<String>,
77}
78
79/// Which retrieval path(s) contributed to a hit.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum SearchSource {
82 /// Returned by the vector leg only.
83 Vector,
84 /// Returned by the text leg only.
85 Text,
86 /// Returned by both legs.
87 Both,
88}
89
90impl SearchSource {
91 /// Combine retrieval-leg membership from two appearances of the same hit.
92 #[must_use]
93 pub const fn union(self, other: Self) -> Self {
94 match (self, other) {
95 (Self::Text, Self::Text) => Self::Text,
96 (Self::Vector, Self::Vector) => Self::Vector,
97 _ => Self::Both,
98 }
99 }
100
101 /// Lowercase wire representation used by search serializers.
102 #[must_use]
103 pub const fn as_str(self) -> &'static str {
104 match self {
105 Self::Vector => "vector",
106 Self::Text => "text",
107 Self::Both => "both",
108 }
109 }
110}