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}
50
51/// RRF weights used in hybrid search: vec (vector) and fts (text).
52#[derive(serde::Serialize)]
53pub struct Weights {
54    /// VEC.
55    pub vec: f32,
56    /// FTS.
57    pub fts: f32,
58}
59
60/// Hybrid search response.
61#[derive(serde::Serialize)]
62pub struct HybridSearchResponse {
63    /// Search query text.
64    pub query: String,
65    /// Maximum number of results to return.
66    pub k: usize,
67    /// RRF k parameter used in the combined ranking.
68    pub rrf_k: u32,
69    /// Weights applied to vec and fts sources in the RRF fusion.
70    pub weights: Weights,
71    /// Results.
72    pub results: Vec<HybridSearchItem>,
73    /// Graph matches.
74    pub graph_matches: Vec<RecallItem>,
75    /// Ceiling applied to `graph_matches`; `null` when the cap is disabled.
76    ///
77    /// Echoed because the cap is ACTIVE by default
78    /// ([`crate::constants::DEFAULT_HYBRID_MAX_GRAPH_RESULTS`]): a truncation the
79    /// caller cannot see is worse than no truncation, since a short
80    /// `graph_matches` would otherwise be indistinguishable from a small
81    /// neighbourhood.
82    pub max_graph_results: Option<usize>,
83    /// True when FTS5 failed and the response is vec-only.
84    ///
85    /// Omitted from JSON when `false` to keep the happy-path envelope clean.
86    #[serde(skip_serializing_if = "std::ops::Not::not")]
87    pub fts_degraded: bool,
88    /// Human-readable description of the FTS5 failure when `fts_degraded` is true.
89    ///
90    /// Omitted from JSON when `None`.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub fts_error: Option<String>,
93    /// True when the FTS5 index was corrupted and successfully auto-rebuilt during this request.
94    ///
95    /// Omitted from JSON when `false` to keep the happy-path envelope clean.
96    #[serde(skip_serializing_if = "std::ops::Not::not")]
97    pub fts_auto_rebuilt: bool,
98    /// G58 (v1.0.80): symmetric to `fts_degraded`; `true` when the live query
99    /// embedding failed and the response degraded to FTS5-only. Absent on the
100    /// wire when false.
101    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
102    pub vec_degraded: bool,
103    /// G58 (v1.0.80): human-readable description of the embedding failure
104    /// that triggered the fallback. Absent on the wire when `vec_degraded` is
105    /// false.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub vec_error: Option<String>,
108    /// G58 (v1.0.80): advisory warning echoed for callers that branch on
109    /// top-level status. Distinguishes a FTS5-only fallback from a clean
110    /// hybrid response so downstream pipelines can lower their confidence.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub warning: Option<String>,
113    /// v1.0.84 (ADR-0042): discriminator of the embedding backend that actually
114    /// ran the live embedding. `"openrouter" | "none"`. Absent
115    /// on the wire when `None` (kept for happy-path envelope cleanliness).
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub backend_invoked: Option<&'static str>,
118    /// Operator-facing PROSE for the degradation, not a closed set.
119    ///
120    /// What lands here is `FallbackReason`'s `Display`, carrying the provider's
121    /// own message, so no enum could ever have held it. GAP-SG-290 measured
122    /// this; the machine-readable half travels beside it in
123    /// [`Self::vec_degraded_code`] rather than replacing this field.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub vec_degraded_reason: Option<String>,
126    /// v1.2.8 (GAP-SG-290): stable, machine-readable code for the degradation.
127    ///
128    /// `FallbackReason::reason_code()` plus `FALLBACK_FTS_ONLY_CODE`, which is
129    /// the eight-value set a consumer can match on. Absent when `vec_degraded`
130    /// is false, so the happy-path envelope is byte-identical.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub vec_degraded_code: Option<&'static str>,
133    /// Total execution time in milliseconds from handler start to serialisation.
134    pub elapsed_ms: u64,
135}