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}