Skip to main content

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}