Skip to main content

sqlite_graphrag/commands/hybrid_search/
envelope.rs

1//! Serialisation shapes of the `hybrid-search` response.
2//!
3//! Only the wire contract lives here: the per-result item, the RRF weight pair
4//! and the top-level envelope with its degradation flags.
5
6use crate::output::RecallItem;
7
8/// Hybrid search item.
9#[derive(serde::Serialize)]
10pub struct HybridSearchItem {
11    /// Memory identifier.
12    pub memory_id: i64,
13    /// Name of this item.
14    pub name: String,
15    /// Namespace scope.
16    pub namespace: String,
17    /// Memory type classification.
18    #[serde(rename = "type")]
19    pub memory_type: String,
20    /// Human-readable description.
21    pub description: String,
22    /// Full text body.
23    pub body: String,
24    /// Snippet.
25    pub snippet: String,
26    /// Combined score.
27    pub combined_score: f64,
28    /// Alias of `combined_score` for the documented contract in SKILL.md.
29    pub score: f64,
30    /// Source of the match: always "hybrid" (RRF of vec + fts). Added in v2.0.1.
31    pub source: String,
32    /// VEC rank.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub vec_rank: Option<usize>,
35    /// FTS rank.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub fts_rank: Option<usize>,
38    /// Combined RRF score — explicit alias of `combined_score` for integration contracts.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub rrf_score: Option<f64>,
41    /// RRF score normalized to [0.0, 1.0] for cross-method comparability.
42    pub normalized_score: f64,
43    /// Raw KNN distance from the vector index (lower = more similar).
44    ///
45    /// Present when the result came from the vector search path; `None` when the
46    /// result appeared only in the FTS5 results and was not ranked by the KNN index.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub vec_distance: Option<f64>,
49    /// Raw BM25 score from the FTS5 index. Currently always `None`; reserved for
50    /// a future release when the FTS5 BM25 score is exposed by the storage layer.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub fts_bm25: Option<f64>,
53}
54
55/// RRF weights used in hybrid search: vec (vector) and fts (text).
56#[derive(serde::Serialize)]
57pub struct Weights {
58    /// VEC.
59    pub vec: f32,
60    /// FTS.
61    pub fts: f32,
62}
63
64/// Hybrid search response.
65#[derive(serde::Serialize)]
66pub struct HybridSearchResponse {
67    /// Search query text.
68    pub query: String,
69    /// Maximum number of results to return.
70    pub k: usize,
71    /// RRF k parameter used in the combined ranking.
72    pub rrf_k: u32,
73    /// Weights applied to vec and fts sources in the RRF fusion.
74    pub weights: Weights,
75    /// Results.
76    pub results: Vec<HybridSearchItem>,
77    /// Graph matches.
78    pub graph_matches: Vec<RecallItem>,
79    /// Ceiling applied to `graph_matches`; `null` when the cap is disabled.
80    ///
81    /// Echoed because the cap is ACTIVE by default
82    /// ([`crate::constants::DEFAULT_HYBRID_MAX_GRAPH_RESULTS`]): a truncation the
83    /// caller cannot see is worse than no truncation, since a short
84    /// `graph_matches` would otherwise be indistinguishable from a small
85    /// neighbourhood.
86    pub max_graph_results: Option<usize>,
87    /// True when FTS5 failed and the response is vec-only.
88    ///
89    /// Omitted from JSON when `false` to keep the happy-path envelope clean.
90    #[serde(skip_serializing_if = "std::ops::Not::not")]
91    pub fts_degraded: bool,
92    /// Human-readable description of the FTS5 failure when `fts_degraded` is true.
93    ///
94    /// Omitted from JSON when `None`.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub fts_error: Option<String>,
97    /// True when the FTS5 index was corrupted and successfully auto-rebuilt during this request.
98    ///
99    /// Omitted from JSON when `false` to keep the happy-path envelope clean.
100    #[serde(skip_serializing_if = "std::ops::Not::not")]
101    pub fts_auto_rebuilt: bool,
102    /// G58 (v1.0.80): symmetric to `fts_degraded`; `true` when the live query
103    /// embedding failed and the response degraded to FTS5-only. Absent on the
104    /// wire when false.
105    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
106    pub vec_degraded: bool,
107    /// G58 (v1.0.80): human-readable description of the embedding failure
108    /// that triggered the fallback. Absent on the wire when `vec_degraded` is
109    /// false.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub vec_error: Option<String>,
112    /// G58 (v1.0.80): advisory warning echoed for callers that branch on
113    /// top-level status. Distinguishes a FTS5-only fallback from a clean
114    /// hybrid response so downstream pipelines can lower their confidence.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub warning: Option<String>,
117    /// v1.0.84 (ADR-0042): discriminator of the embedding backend that actually
118    /// ran the live embedding. `"openrouter" | "none"`. Absent
119    /// on the wire when `None` (kept for happy-path envelope cleanliness).
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub backend_invoked: Option<&'static str>,
122    /// v1.0.84 (ADR-0042): reason code discriminating the degradation
123    /// (`"embedding_failed" | "cancelled" | "timeout"`). Absent when
124    /// `vec_degraded` is false.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub vec_degraded_reason: Option<String>,
127    /// Total execution time in milliseconds from handler start to serialisation.
128    pub elapsed_ms: u64,
129}