gitcortex_mcp/mcp/
search.rs1use gitcortex_core::{error::Result, graph::Node, schema::NodeKind, store::GraphStore};
14use serde::Serialize;
15
16#[derive(Debug, Clone, Serialize)]
17pub struct SearchHit {
18 pub name: String,
19 pub qualified_name: String,
20 pub kind: String,
21 pub file: String,
22 pub start_line: u32,
23 pub score: i32,
24}
25
26const DEFAULT_LIMIT: usize = 10;
28const MAX_LIMIT: usize = 200;
29
30pub fn search<S: GraphStore + ?Sized>(
34 store: &S,
35 branch: &str,
36 query: &str,
37 limit: Option<usize>,
38) -> Result<Vec<SearchHit>> {
39 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
40 let q = query.trim();
41 if q.is_empty() {
42 return Ok(Vec::new());
43 }
44
45 let nodes = store.list_all_nodes(branch)?;
48
49 let mut hits: Vec<SearchHit> = nodes
50 .into_iter()
51 .filter_map(|n| score(&n, q).map(|s| to_hit(n, s)))
52 .collect();
53
54 hits.sort_by(|a, b| {
55 b.score
56 .cmp(&a.score)
57 .then_with(|| a.name.len().cmp(&b.name.len()))
58 .then_with(|| a.qualified_name.cmp(&b.qualified_name))
59 });
60 hits.truncate(limit);
61 Ok(hits)
62}
63
64fn score(n: &Node, q: &str) -> Option<i32> {
65 let q_lower = q.to_ascii_lowercase();
69 let name_lower = n.name.to_ascii_lowercase();
70 let qname_lower = n.qualified_name.to_ascii_lowercase();
71 let base = if name_lower == q_lower {
72 100
73 } else if name_lower.starts_with(&q_lower) {
74 60
75 } else if name_lower.contains(&q_lower) {
76 30
77 } else if qname_lower.contains(&q_lower) {
78 10
79 } else {
80 return None;
81 };
82 Some(base + kind_boost(&n.kind))
83}
84
85fn kind_boost(k: &NodeKind) -> i32 {
86 match k {
87 NodeKind::Function | NodeKind::Method => 5,
88 NodeKind::Struct | NodeKind::Trait | NodeKind::Interface => 4,
89 NodeKind::Enum | NodeKind::TypeAlias => 3,
90 NodeKind::Constant | NodeKind::Macro | NodeKind::Annotation => 2,
91 _ => 0,
92 }
93}
94
95fn to_hit(n: Node, score: i32) -> SearchHit {
96 SearchHit {
97 name: n.name,
98 qualified_name: n.qualified_name,
99 kind: n.kind.to_string(),
100 file: n.file.display().to_string(),
101 start_line: n.span.start_line,
102 score,
103 }
104}