sqlite_graphrag/commands/hybrid_search/
mod.rs1use crate::errors::AppError;
10use crate::output::{self, RecallItem};
11use crate::paths::AppPaths;
12use crate::storage::connection::open_ro;
13
14mod args;
15mod envelope;
16mod fusion;
17mod graph_expansion;
18mod retrieval;
19
20pub use args::HybridSearchArgs;
21pub use envelope::{HybridSearchItem, HybridSearchResponse, Weights};
22
23#[tracing::instrument(skip_all, level = "debug", name = "hybrid_search")]
25pub fn run(
26 args: HybridSearchArgs,
27 backends: crate::cli::BackendChoice,
28 fail_on_degraded: bool,
29) -> Result<(), AppError> {
30 let start = std::time::Instant::now();
31 let _ = args.format;
32 tracing::debug!(target: "hybrid_search", query = %args.query, k = args.k, "fusing results");
33 crate::agent_surface::universe::record(crate::agent_surface::universe::QueryCeiling {
36 applied: args.k,
37 offset: 0,
38 source: crate::agent_surface::universe::CeilingSource::Flag,
39 kind: crate::agent_surface::universe::CeilingKind::TopK,
40 universe_total: None,
41 });
42
43 args.validate_graph_flags()?;
44
45 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
46 let paths = AppPaths::resolve(args.db.as_deref())?;
47 crate::storage::connection::ensure_db_ready(&paths)?;
48
49 output::emit_progress_i18n(
50 "Computing query embedding...",
51 "Calculando embedding da consulta...",
52 );
53 let conn = open_ro(&paths.db)?;
54 let resolved = retrieval::resolve_query_embedding(&args, &paths.models, backends);
55 if let Some(err) = crate::query_embedding::degradation_failure(
60 fail_on_degraded,
61 resolved.degraded,
62 resolved.reason_code,
63 ) {
64 return Err(err);
65 }
66 let crate::query_embedding::QueryEmbedding {
67 embedding,
68 degraded: vec_degraded,
69 error: vec_error,
70 backend_invoked,
71 reason_code: vec_degraded_code,
72 } = resolved;
73
74 let memory_type_str = args.r#type.map(|t| t.as_str());
75
76 let vec_results = retrieval::vector_candidates(
77 &conn,
78 embedding.as_ref(),
79 std::slice::from_ref(&namespace),
80 memory_type_str,
81 args.k,
82 )?;
83
84 let (fts_results, fts_degraded, fts_error, fts_auto_rebuilt) =
85 retrieval::fts_candidates(&conn, &args, &namespace, memory_type_str);
86
87 let results = fusion::fuse_candidates(&conn, &args, &vec_results, &fts_results)?;
88
89 let graph_matches: Vec<RecallItem> =
90 graph_expansion::expand(&conn, &args, embedding.as_ref(), &namespace, &results)?;
91
92 output::emit_json(&HybridSearchResponse {
93 query: args.query,
94 k: args.k,
95 rrf_k: args.rrf_k,
96 weights: Weights {
97 vec: args.weight_vec,
98 fts: args.weight_fts,
99 },
100 results,
101 graph_matches,
102 max_graph_results: crate::constants::hybrid_search_max_graph_results(
103 args.max_graph_results,
104 ),
105 fts_degraded,
106 fts_error,
107 fts_auto_rebuilt,
108 vec_degraded,
109 vec_error: vec_error.clone(),
110 warning: if vec_degraded {
111 Some(
112 "live query embedding unavailable; results are FTS5 BM25 only (semantic relevance reduced)"
113 .to_string(),
114 )
115 } else {
116 None
117 },
118 backend_invoked,
119 vec_degraded_reason: if vec_degraded { vec_error } else { None },
120 vec_degraded_code: if vec_degraded {
121 vec_degraded_code
122 } else {
123 None
124 },
125 elapsed_ms: start.elapsed().as_millis() as u64,
126 })?;
127
128 Ok(())
129}
130
131#[cfg(test)]
132mod tests;