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}