Skip to main content

gobby_code/search/
graph_boost.rs

1//! FalkorDB graph boost: find related symbols to boost in search ranking.
2//!
3//! Uses callers + usages as the boost set — symbols that are connected
4//! to the resolved query symbol in the call graph get a ranking boost via RRF.
5//!
6//! Source: src/gobby/code_index/searcher.py (_graph_boost method)
7
8use std::collections::HashSet;
9
10use crate::config::Context;
11use crate::db;
12use crate::graph::code_graph;
13use crate::search::fts;
14
15/// Get symbol IDs related to query via the call/import graph.
16///
17/// Returns a ranked list of symbol IDs for use as an RRF source.
18/// Returns empty vec when FalkorDB is unavailable (graceful degradation).
19pub fn graph_boost(ctx: &Context, query: &str) -> Vec<String> {
20    if ctx.falkordb.is_none() {
21        return vec![];
22    }
23
24    let mut conn = match db::connect_readonly(&ctx.database_url) {
25        Ok(conn) => conn,
26        Err(_) => return vec![],
27    };
28    let (resolved, _) = fts::resolve_graph_symbol(&mut conn, query, &ctx.project_id);
29    let Some(symbol) = resolved else {
30        return vec![];
31    };
32
33    let callers = code_graph::find_callers(ctx, &symbol.id, 0, 10).unwrap_or_default();
34    let usages = code_graph::find_usages(ctx, &symbol.id, 0, 10).unwrap_or_default();
35
36    let mut ids = Vec::new();
37    let mut seen = HashSet::new();
38    for r in callers.iter().chain(usages.iter()) {
39        if !r.id.is_empty() && seen.insert(r.id.clone()) {
40            ids.push(r.id.clone());
41        }
42    }
43    ids
44}
45
46/// Expand the graph neighborhood of seed symbols found by FTS/semantic search.
47///
48/// Takes symbol IDs from the top search results and queries FalkorDB for their
49/// callees (what they call) and callers (who calls them). Callees are ranked
50/// first since they represent implementation details more useful for conceptual
51/// queries. Returns deduplicated symbol IDs for use as an RRF source.
52pub fn graph_expand(ctx: &Context, seed_ids: &[String]) -> Vec<String> {
53    if seed_ids.is_empty() {
54        return vec![];
55    }
56
57    // Callees first — "what do these symbols call?" surfaces implementation details
58    let callees = code_graph::find_callees_batch(ctx, seed_ids, 30).unwrap_or_default();
59    // Callers second — "who calls these symbols?" surfaces broader context
60    let callers = code_graph::find_callers_batch(ctx, seed_ids, 30).unwrap_or_default();
61
62    let mut ids = Vec::new();
63    let mut seen = HashSet::new();
64    for r in callees.iter().chain(callers.iter()) {
65        if !r.id.is_empty() && seen.insert(r.id.clone()) {
66            ids.push(r.id.clone());
67        }
68    }
69    ids
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::path::PathBuf;
76
77    fn make_ctx_no_falkordb() -> Context {
78        Context {
79            database_url: "postgresql://localhost/nonexistent".to_string(),
80            project_root: PathBuf::from("/nonexistent"),
81            project_id: "test".to_string(),
82            quiet: true,
83            falkordb: None,
84            qdrant: None,
85            embedding: None,
86            code_vectors: crate::config::CodeVectorSettings::default(),
87            daemon_url: None,
88        }
89    }
90
91    #[test]
92    fn test_graph_boost_no_falkordb() {
93        let ctx = make_ctx_no_falkordb();
94        let result = graph_boost(&ctx, "some_function");
95        assert!(result.is_empty());
96    }
97
98    #[test]
99    fn test_graph_expand_no_falkordb() {
100        let ctx = make_ctx_no_falkordb();
101        let result = graph_expand(&ctx, &["some_function".to_string()]);
102        assert!(result.is_empty());
103    }
104
105    #[test]
106    fn test_graph_expand_empty_seeds() {
107        let ctx = make_ctx_no_falkordb();
108        let result = graph_expand(&ctx, &[]);
109        assert!(result.is_empty());
110    }
111}