Skip to main content

sqlite_graphrag/commands/hybrid_search/
args.rs

1//! CLI surface of the `hybrid-search` subcommand.
2
3use crate::cli::MemoryType;
4use crate::errors::AppError;
5use crate::output::JsonOutputFormat;
6
7/// Arguments for the `hybrid-search` subcommand.
8///
9/// When `--namespace` is omitted the search runs against the `global` namespace,
10/// which is the default namespace used by `remember` when no `--namespace` flag
11/// is provided. Pass an explicit `--namespace` value to search a different
12/// isolated namespace.
13#[derive(clap::Args)]
14#[command(after_long_help = "EXAMPLES:\n  \
15    # Basic hybrid search combining FTS5 + vector via RRF\n  \
16    sqlite-graphrag hybrid-search \"postgres migration deadlock\" --k 10\n\n  \
17    # Tune RRF weights to favor keyword matches over semantic similarity\n  \
18    sqlite-graphrag hybrid-search \"jwt auth\" --weight-fts 1.5 --weight-vec 0.5 --k 5\n\n  \
19    # Add graph traversal matches (entities connected to top results)\n  \
20    sqlite-graphrag hybrid-search \"frontend architecture\" --with-graph --k 10\n\n  \
21    # Graph traversal with custom depth and minimum edge weight\n  \
22    sqlite-graphrag hybrid-search \"auth design\" --with-graph --max-hops 3 --min-weight 0.5 --k 10\n\n  \
23NOTES:\n  \
24    --with-graph enables entity graph traversal seeded by the top RRF results.\n  \
25    Graph matches appear in the `graph_matches` array (separate from `results`).\n  \
26    Without --with-graph, `graph_matches` is always empty.")]
27pub struct HybridSearchArgs {
28    #[arg(
29        allow_hyphen_values = true,
30        help = "Hybrid search query (vector KNN + FTS5 BM25 fused via RRF)"
31    )]
32    /// Search query text.
33    pub query: String,
34    /// Maximum number of fused results to return after RRF combines vector + FTS5 candidates.
35    ///
36    /// Validated to the inclusive range `1..=4096` (the upper bound matches `sqlite-vec`'s knn
37    /// limit). Each underlying search fetches `k * 2` candidates before fusion.
38    #[arg(short = 'k', long, aliases = ["limit", "top-k"], default_value = "10", value_parser = crate::parsers::parse_k_range)]
39    pub k: usize,
40    /// Rrf k.
41    #[arg(long, default_value = "60")]
42    pub rrf_k: u32,
43    /// Weight VEC.
44    #[arg(long, default_value = "1.0")]
45    pub weight_vec: f32,
46    /// Weight FTS.
47    #[arg(long, default_value = "1.0")]
48    pub weight_fts: f32,
49    /// Filter by memory.type. Note: distinct from graph entity_type
50    /// (project/tool/person/file/concept/incident/decision/memory/dashboard/issue_tracker/organization/location/date)
51    /// used in --entities-file.
52    #[arg(long, value_enum)]
53    pub r#type: Option<MemoryType>,
54    /// Namespace scope.
55    #[arg(long)]
56    pub namespace: Option<String>,
57    /// With graph.
58    #[arg(long)]
59    pub with_graph: bool,
60    /// Cap the size of `graph_matches` to at most N entries; `0` removes the cap.
61    ///
62    /// Unlike the `recall` flag of the same name this one is ACTIVE by default
63    /// ([`crate::constants::DEFAULT_HYBRID_MAX_GRAPH_RESULTS`], overridable via
64    /// XDG `search.hybrid.max_graph_results`). The traversal is seeded by the
65    /// fused results AND by the entities nearest the query embedding, so its
66    /// size follows the graph, not `--k`: an uncapped `--k 3` measured a
67    /// 1 112 925 byte envelope.
68    #[arg(long, value_name = "N")]
69    pub max_graph_results: Option<usize>,
70    /// G58 (v1.0.80): skip the live query embedding and serve FTS5 BM25 only.
71    /// Useful in CI/CD with tight OAuth quota and in deterministic tests.
72    #[arg(long, help = "Skip live query embedding; serve FTS5 BM25 only")]
73    pub fallback_fts_only: bool,
74    /// Graph traversal depth (requires --with-graph; default 2 when active).
75    #[arg(long, value_parser = crate::parsers::parse_hops_range_u32)]
76    pub max_hops: Option<u32>,
77    /// Minimum edge weight for graph traversal (requires --with-graph; default 0.3 when active).
78    #[arg(long)]
79    pub min_weight: Option<f64>,
80    /// Output format.
81    #[arg(long, value_enum, default_value_t = JsonOutputFormat::Json)]
82    pub format: JsonOutputFormat,
83    /// Path to the SQLite database file.
84    #[arg(long)]
85    pub db: Option<String>,
86    /// Accept `--json` as a no-op because output is already JSON by default.
87    #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
88    pub json: bool,
89}
90
91impl HybridSearchArgs {
92    /// G20: reject graph-specific flags when `--with-graph` is not active.
93    ///
94    /// G48: `Option<T>` detects an explicitly provided flag even when the value
95    /// equals the old default (pre-fix, `--max-hops 2` was silently accepted).
96    pub(super) fn validate_graph_flags(&self) -> Result<(), AppError> {
97        if !self.with_graph {
98            if self.max_hops.is_some() {
99                return Err(AppError::Validation(
100                    "--max-hops requires --with-graph to be active".to_string(),
101                ));
102            }
103            if self.min_weight.is_some() {
104                return Err(AppError::Validation(
105                    "--min-weight requires --with-graph to be active".to_string(),
106                ));
107            }
108        }
109        Ok(())
110    }
111}