Skip to main content

linkrs_metrics/manager/
search.rs

1//! Search, index and vector observability.
2use serde::{Deserialize, Serialize};
3
4use super::core::StatsManager;
5use super::metric_type::MetricType;
6
7/// Per-index breakdown for handlers.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SearchIndexBreakdown {
10    pub index: String,
11    pub search_queries: u64,
12    pub search_errors: u64,
13    pub search_latency_ms: u64,
14    pub avg_search_latency_ms: f64,
15    pub index_operations: u64,
16    pub index_errors: u64,
17    pub index_latency_ms: u64,
18}
19
20impl StatsManager {
21    /// Record a search query operation.
22    /// Index metrics are keyed per-space (`space_{id}.{index}`) so same-named
23    /// indexes in different spaces no longer pollute each other.
24    pub fn record_search(&self, space_id: u64, index_name: &str, latency_ms: u64, success: bool) {
25        let space_key = Self::space_key(space_id);
26        let index_key = Self::index_key(space_id, index_name);
27        self.add_value(MetricType::NumSearchQueries);
28        self.add_space_metric(&space_key, MetricType::NumSearchQueries);
29        self.add_index_metric(&index_key, MetricType::NumSearchQueries);
30
31        if !success {
32            self.add_value(MetricType::NumSearchErrors);
33            self.add_space_metric(&space_key, MetricType::NumSearchErrors);
34            self.add_index_metric(&index_key, MetricType::NumSearchErrors);
35        }
36
37        self.add_value_with_amount(MetricType::SearchLatencyMs, latency_ms);
38        self.add_space_metric_with_amount(&space_key, MetricType::SearchLatencyMs, latency_ms);
39        self.add_index_metric_with_amount(&index_key, MetricType::SearchLatencyMs, latency_ms);
40
41        {
42            let mut histogram = self.search_latency_histogram.write();
43            histogram.record_micros(latency_ms * 1000);
44        }
45    }
46
47    /// Record an index operation
48    pub fn record_index_operation(
49        &self,
50        space_id: u64,
51        index_name: &str,
52        latency_ms: u64,
53        success: bool,
54    ) {
55        let space_key = Self::space_key(space_id);
56        let index_key = Self::index_key(space_id, index_name);
57        self.add_value(MetricType::NumIndexOperations);
58        self.add_space_metric(&space_key, MetricType::NumIndexOperations);
59        self.add_index_metric(&index_key, MetricType::NumIndexOperations);
60
61        if !success {
62            self.add_value(MetricType::NumIndexErrors);
63            self.add_space_metric(&space_key, MetricType::NumIndexErrors);
64            self.add_index_metric(&index_key, MetricType::NumIndexErrors);
65        }
66
67        self.add_value_with_amount(MetricType::IndexLatencyMs, latency_ms);
68        self.add_space_metric_with_amount(&space_key, MetricType::IndexLatencyMs, latency_ms);
69        self.add_index_metric_with_amount(&index_key, MetricType::IndexLatencyMs, latency_ms);
70    }
71
72    /// Record an index operation without a known space.
73    /// Used by storage paths that lack space context; records global and
74    /// `unknown.{index}` buckets only, never polluting real `space_{id}` keys.
75    pub fn record_index_operation_unknown_space(
76        &self,
77        index_name: &str,
78        latency_ms: u64,
79        success: bool,
80    ) {
81        let index_key = Self::unknown_index_key(index_name);
82        self.add_value(MetricType::NumIndexOperations);
83        self.add_index_metric(&index_key, MetricType::NumIndexOperations);
84        if !success {
85            self.add_value(MetricType::NumIndexErrors);
86            self.add_index_metric(&index_key, MetricType::NumIndexErrors);
87        }
88        self.add_value_with_amount(MetricType::IndexLatencyMs, latency_ms);
89        self.add_index_metric_with_amount(&index_key, MetricType::IndexLatencyMs, latency_ms);
90    }
91
92    /// Record a delete operation
93    pub fn record_delete_operation(
94        &self,
95        space_id: u64,
96        index_name: &str,
97        latency_ms: u64,
98        success: bool,
99    ) {
100        let space_key = Self::space_key(space_id);
101        let index_key = Self::index_key(space_id, index_name);
102        self.add_value(MetricType::NumDeleteOperations);
103        self.add_space_metric(&space_key, MetricType::NumDeleteOperations);
104        self.add_index_metric(&index_key, MetricType::NumDeleteOperations);
105
106        if !success {
107            self.add_value(MetricType::NumDeleteErrors);
108            self.add_space_metric(&space_key, MetricType::NumDeleteErrors);
109            self.add_index_metric(&index_key, MetricType::NumDeleteErrors);
110        }
111
112        self.add_value_with_amount(MetricType::DeleteLatencyMs, latency_ms);
113        self.add_space_metric_with_amount(&space_key, MetricType::DeleteLatencyMs, latency_ms);
114        self.add_index_metric_with_amount(&index_key, MetricType::DeleteLatencyMs, latency_ms);
115    }
116
117    /// Record search result count
118    pub fn record_search_result_count(&self, space_id: u64, count: u64) {
119        let space_key = Self::space_key(space_id);
120        self.add_value_with_amount(MetricType::SearchResultCount, count);
121        self.add_space_metric_with_amount(&space_key, MetricType::SearchResultCount, count);
122    }
123
124    /// Record cache hit or miss (global only).
125    pub fn record_cache_hit_global(&self, hit: bool) {
126        if hit {
127            self.add_value(MetricType::SearchCacheHitCount);
128        } else {
129            self.add_value(MetricType::SearchCacheMissCount);
130        }
131    }
132
133    pub fn record_vector_disabled_skips(&self, count: u64) {
134        self.set_value(MetricType::VectorDisabledSkips, count);
135    }
136
137    /// Record one embedding call (global bucket only).
138    ///
139    /// The embedding path has no space context, so only global counters are
140    /// updated; per-space keys are never synthesized here. Failed calls
141    /// carry zero token usage.
142    pub fn record_vector_embedding(
143        &self,
144        prompt_tokens: u64,
145        total_tokens: u64,
146        latency_ms: u64,
147        success: bool,
148    ) {
149        self.add_value(MetricType::VectorEmbeddingOps);
150        if !success {
151            self.add_value(MetricType::VectorEmbeddingErrors);
152        }
153        self.add_value_with_amount(MetricType::VectorEmbeddingLatencyMs, latency_ms);
154        self.add_value_with_amount(MetricType::VectorEmbeddingPromptTokens, prompt_tokens);
155        self.add_value_with_amount(MetricType::VectorEmbeddingTotalTokens, total_tokens);
156    }
157
158    /// Record one issued rerank call (global bucket only).
159    ///
160    /// Skipped reranks (no service, no query text, thin coverage) are not
161    /// recorded; only actual provider calls land here.
162    pub fn record_vector_rerank(&self, latency_ms: u64, success: bool) {
163        self.add_value(MetricType::VectorRerankOps);
164        if !success {
165            self.add_value(MetricType::VectorRerankErrors);
166        }
167        self.add_value_with_amount(MetricType::VectorRerankLatencyMs, latency_ms);
168    }
169
170    pub fn search_index_breakdown(&self) -> Vec<SearchIndexBreakdown> {
171        let mut out = Vec::new();
172        for entry in self.index_metrics.iter() {
173            let name = entry.key().clone();
174            let map = entry.value();
175            let get = |m: MetricType| map.get(&m).map(|v| v.get()).unwrap_or(0);
176            let search_queries = get(MetricType::NumSearchQueries);
177            let index_operations = get(MetricType::NumIndexOperations);
178            if search_queries == 0 && index_operations == 0 {
179                continue;
180            }
181            let search_latency_ms = get(MetricType::SearchLatencyMs);
182            out.push(SearchIndexBreakdown {
183                index: name,
184                search_queries,
185                search_errors: get(MetricType::NumSearchErrors),
186                search_latency_ms,
187                avg_search_latency_ms: if search_queries == 0 {
188                    0.0
189                } else {
190                    search_latency_ms as f64 / search_queries as f64
191                },
192                index_operations,
193                index_errors: get(MetricType::NumIndexErrors),
194                index_latency_ms: get(MetricType::IndexLatencyMs),
195            });
196        }
197        out.sort_by_key(|s| std::cmp::Reverse(s.search_queries));
198        out.truncate(50);
199        out
200    }
201}