remem/retrieval/search/memory/
explain.rs1use serde::{ser::SerializeStruct, Serialize, Serializer};
2
3#[derive(Debug, Clone, Serialize)]
4pub struct SearchExplain {
5 pub query: String,
6 pub project: Option<String>,
7 pub memory_type: Option<String>,
8 pub branch: Option<String>,
9 pub include_stale: bool,
10 pub limit: i64,
11 pub offset: i64,
12 pub fetch_limit: i64,
13 pub expanded_terms: Vec<String>,
14 pub core_terms: Vec<String>,
15 pub claim_terms: Vec<String>,
16 pub fts_query: Option<String>,
17 pub temporal_range: Option<(i64, i64)>,
18 pub temporal_field: Option<String>,
19 pub rrf_k: f64,
20 pub min_evidence_confidence: f64,
21 pub filtered_result_count: usize,
22 pub timings: Vec<crate::perf::PhaseTiming>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub rerank: Option<crate::retrieval::rerank::RerankExplain>,
26 pub channels: Vec<SearchExplainChannel>,
27 pub results: Vec<SearchExplainResult>,
28 pub has_more: bool,
29 pub raw_fallback_count: usize,
30}
31
32impl SearchExplain {
33 pub fn retain_result_ids(&mut self, result_ids: &[i64], has_more: bool, visible_limit: i64) {
34 self.has_more = has_more;
35 self.limit = visible_limit;
36 self.results
37 .retain(|result| result_ids.contains(&result.memory_id));
38 for (index, result) in self.results.iter_mut().enumerate() {
39 result.final_rank = index + 1;
40 }
41 }
42
43 pub fn set_raw_fallback_count(&mut self, count: usize) {
44 self.raw_fallback_count = count;
45 }
46}
47
48#[derive(Debug, Clone, Serialize)]
49pub struct SearchExplainDetails {
50 #[serde(flatten)]
51 pub explain: SearchExplain,
52 pub contribution_breakdowns: Vec<SearchExplainResultBreakdown>,
53}
54
55impl SearchExplainDetails {
56 pub fn retain_result_ids(&mut self, result_ids: &[i64], has_more: bool, visible_limit: i64) {
57 self.explain
58 .retain_result_ids(result_ids, has_more, visible_limit);
59 self.contribution_breakdowns
60 .retain(|result| result_ids.contains(&result.memory_id));
61 }
62
63 pub fn set_raw_fallback_count(&mut self, count: usize) {
64 self.explain.set_raw_fallback_count(count);
65 }
66}
67
68#[derive(Debug, Clone, Serialize)]
69pub struct SearchExplainChannel {
70 pub name: String,
71 pub enabled: bool,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub disabled_reason: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub candidates_scanned: Option<usize>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub embedding: Option<crate::retrieval::embedding::EmbeddingExecutionMetadata>,
78 pub hits: Vec<ChannelHit>,
79}
80
81#[derive(Debug, Clone, Serialize)]
82pub struct ChannelHit {
83 pub memory_id: i64,
84 pub rank: usize,
85}
86
87#[derive(Debug, Clone)]
88pub struct SearchExplainResult {
89 pub memory_id: i64,
90 pub final_rank: usize,
91 pub final_score: f64,
92 pub evidence_confidence: f64,
93 pub project: String,
94 pub scope: String,
95 pub visibility: String,
96 pub staleness: crate::memory::MemoryStalenessLabel,
97 pub contributions: Vec<ChannelContribution>,
98}
99
100impl SearchExplainResult {
101 pub fn fusion_score(&self) -> f64 {
103 self.contributions
104 .iter()
105 .map(|contribution| contribution.score)
106 .sum()
107 }
108
109 pub fn post_fusion_score_factor(&self) -> Option<f64> {
111 let fusion_score = self.fusion_score();
112 (fusion_score.is_finite() && fusion_score > 0.0)
113 .then_some(self.final_score / fusion_score)
114 .filter(|factor| factor.is_finite() && *factor >= 0.0)
115 }
116}
117
118impl Serialize for SearchExplainResult {
119 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
120 where
121 S: Serializer,
122 {
123 let mut state = serializer.serialize_struct("SearchExplainResult", 11)?;
124 state.serialize_field("memory_id", &self.memory_id)?;
125 state.serialize_field("final_rank", &self.final_rank)?;
126 state.serialize_field("final_score", &self.final_score)?;
127 state.serialize_field("evidence_confidence", &self.evidence_confidence)?;
128 state.serialize_field("project", &self.project)?;
129 state.serialize_field("scope", &self.scope)?;
130 state.serialize_field("visibility", &self.visibility)?;
131 state.serialize_field("staleness", &self.staleness)?;
132 state.serialize_field("contributions", &self.contributions)?;
133 state.serialize_field("fusion_score", &self.fusion_score())?;
134 state.serialize_field("post_fusion_score_factor", &self.post_fusion_score_factor())?;
135 state.end()
136 }
137}
138
139#[derive(Debug, Clone, Serialize)]
140pub struct ChannelContribution {
141 pub channel: String,
142 pub rank: usize,
143 pub score: f64,
144}
145
146#[derive(Debug, Clone, Serialize)]
147pub struct SearchExplainResultBreakdown {
148 pub memory_id: i64,
149 pub contributions: Vec<ChannelContributionBreakdown>,
150}
151
152#[derive(Debug, Clone, Serialize)]
153pub struct ChannelContributionBreakdown {
154 pub channel: String,
155 pub rank: usize,
156 pub weight: f64,
157 pub reciprocal_rank: f64,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub normalized_signal: Option<f64>,
160 pub total_score: f64,
161}