1use crate::model::Symbol;
12use std::sync::{Arc, Mutex};
13
14use anyhow::{Context, Result};
15use model2vec_rs::model::StaticModel;
16
17pub const DEFAULT_MODEL: &str = "minishlab/potion-base-8M";
20
21pub struct Embedder {
23 model: StaticModel,
24}
25
26impl Embedder {
27 pub fn load(repo_or_path: &str) -> Result<Self> {
29 let model = StaticModel::from_pretrained(repo_or_path, None, Some(true), None)
30 .with_context(|| format!("load Model2Vec model `{repo_or_path}`"))?;
31 Ok(Self { model })
32 }
33
34 pub fn shared(repo_or_path: &str) -> Result<Arc<Self>> {
36 static LOADED: Mutex<Option<(String, Arc<Embedder>)>> = Mutex::new(None);
37 let mut loaded = LOADED.lock().unwrap_or_else(|e| e.into_inner());
38 if let Some((_, e)) = loaded.as_ref().filter(|(n, _)| n == repo_or_path) {
39 return Ok(e.clone());
40 }
41 let e = Arc::new(Self::load(repo_or_path)?);
42 *loaded = Some((repo_or_path.to_string(), e.clone()));
43 Ok(e)
44 }
45
46 pub fn embed(&self, texts: &[String]) -> Vec<Vec<f32>> {
48 self.model.encode(texts)
49 }
50
51 pub fn embed_query(&self, query: &str) -> Vec<f32> {
53 self.model.encode_single(query)
54 }
55}
56
57pub fn symbol_text(s: &Symbol) -> String {
61 let mut parts = vec![s.name.clone()];
62 if let Some(sig) = &s.signature {
63 parts.push(sig.clone());
64 }
65 parts.push(s.id.descriptor.replace(['/', '_', '.'], " "));
67 if let Some(doc) = &s.docstring {
68 parts.push(doc.clone());
69 }
70 parts.join(" ")
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn dot(a: &[f32], b: &[f32]) -> f32 {
78 a.iter().zip(b).map(|(x, y)| x * y).sum()
79 }
80
81 #[test]
83 #[ignore]
84 fn embeds_and_ranks_semantically() {
85 let e = Embedder::load(DEFAULT_MODEL).expect("load model");
86 let docs: Vec<String> = vec![
87 "open_database_connection def open_database_connection(host, port)".into(),
88 "parse_json def parse_json(text: str) -> dict".into(),
89 "UserAuthentication class UserAuthentication login verify password".into(),
90 ];
91 let vecs = e.embed(&docs);
92 assert_eq!(vecs.len(), 3);
93 assert!(!vecs[0].is_empty());
94
95 let q = e.embed_query("log in and check a user's password");
97 let best = (0..3)
98 .max_by(|&i, &j| dot(&q, &vecs[i]).partial_cmp(&dot(&q, &vecs[j])).unwrap())
99 .unwrap();
100 assert_eq!(best, 2, "auth doc should rank first");
101 }
102}
103
104pub mod rank {
108 use std::collections::BTreeSet;
109
110 pub fn split_identifier(ident: &str) -> Vec<String> {
114 let mut out: Vec<String> = vec![ident.to_lowercase()];
115 let snake: Vec<&str> = ident.split('_').filter(|s| !s.is_empty()).collect();
117 let mut parts: Vec<String> = Vec::new();
118 for seg in snake {
119 parts.extend(split_camel(seg));
120 }
121 for p in parts {
122 let p = p.to_lowercase();
123 if p.len() >= 2 && !out.contains(&p) {
124 out.push(p);
125 }
126 }
127 out
128 }
129
130 fn split_camel(s: &str) -> Vec<String> {
132 let mut words = Vec::new();
133 let chars: Vec<char> = s.chars().collect();
134 let mut start = 0;
135 for i in 1..chars.len() {
136 let (prev, cur) = (chars[i - 1], chars[i]);
137 let boundary = (prev.is_lowercase() && cur.is_uppercase())
139 || (prev.is_uppercase()
140 && cur.is_uppercase()
141 && i + 1 < chars.len()
142 && chars[i + 1].is_lowercase())
143 || (prev.is_alphabetic() != cur.is_alphabetic());
144 if boundary {
145 words.push(chars[start..i].iter().collect());
146 start = i;
147 }
148 }
149 if start < chars.len() {
150 words.push(chars[start..].iter().collect());
151 }
152 words
153 }
154
155 const STOPWORDS: &[&str] = &[
156 "the", "a", "an", "of", "to", "in", "and", "or", "for", "is", "how", "do", "does", "get",
157 "that", "this", "with", "on", "by",
158 ];
159
160 pub fn query_keywords(query: &str) -> Vec<String> {
162 query
163 .split(|c: char| !c.is_alphanumeric() && c != '_')
164 .flat_map(split_identifier)
165 .filter(|w| w.len() > 2 && !STOPWORDS.contains(&w.as_str()))
166 .collect::<BTreeSet<_>>()
167 .into_iter()
168 .collect()
169 }
170
171 pub fn is_symbol_query(query: &str) -> bool {
174 let q = query.trim();
175 if q.contains("::") || q.contains('.') || q.contains('_') || q.contains('/') {
176 return true;
177 }
178 let words: Vec<&str> = q.split_whitespace().collect();
179 if words.len() == 1 {
180 let w = words[0];
181 return w.chars().any(|c| c.is_uppercase()) || w.len() <= 24;
183 }
184 false
185 }
186
187 pub fn resolve_alpha(query: &str) -> f32 {
189 if is_symbol_query(query) {
190 0.3
191 } else {
192 0.5
193 }
194 }
195
196 pub fn lexical_score(keywords: &[String], name: &str, descriptor_tail: &str) -> f32 {
199 if keywords.is_empty() {
200 return 0.0;
201 }
202 let mut toks: BTreeSet<String> = BTreeSet::new();
203 toks.extend(split_identifier(name));
204 toks.extend(split_identifier(descriptor_tail));
205 let hits = keywords
206 .iter()
207 .filter(|k| {
208 toks.contains(*k)
209 || (k.len() >= 4 && toks.iter().any(|t| t.starts_with(k.as_str())))
210 })
211 .count();
212 hits as f32 / keywords.len() as f32
213 }
214
215 pub fn path_penalty(path: &str) -> f32 {
217 let p = path.to_lowercase();
218 let is_test = p.contains("/test")
219 || p.contains("test_")
220 || p.contains("_test.")
221 || p.contains(".test.")
222 || p.contains(".spec.")
223 || p.contains("/tests/")
224 || p.contains("/spec/")
225 || p.contains("__tests__");
226 if is_test
227 || p.contains("/examples")
228 || p.contains("/legacy")
229 || p.contains("/compat")
230 || p.contains("/migration")
231 || p.contains("/revision")
232 {
233 return 0.3;
234 }
235 if p.ends_with("/__init__.py") || p.ends_with("package-info.java") || p.ends_with("mod.rs")
236 {
237 return 0.5; }
239 if p.ends_with(".d.ts") {
240 return 0.7;
241 }
242 1.0
243 }
244
245 #[cfg(test)]
246 mod tests {
247 use super::*;
248
249 #[test]
250 fn splits_identifiers() {
251 let t = split_identifier("AsyncTaskRunner");
252 assert!(t.contains(&"async".to_string()));
253 assert!(t.contains(&"task".to_string()));
254 assert!(t.contains(&"runner".to_string()));
255 let s = split_identifier("get_user_id");
256 assert!(s.contains(&"user".to_string()) && s.contains(&"get_user_id".to_string()));
257 }
258
259 #[test]
260 fn detects_symbol_queries() {
261 assert!(is_symbol_query("AsyncTaskRunner"));
262 assert!(is_symbol_query("acme.const.Settings"));
263 assert!(is_symbol_query("get_config"));
264 assert!(!is_symbol_query("how do we connect to the database"));
265 }
266
267 #[test]
268 fn lexical_and_penalty() {
269 let kw = query_keywords("task runner");
270 assert!(lexical_score(&kw, "AsyncTaskRunner", "acme/database/runners") > 0.9);
271 assert_eq!(path_penalty("app/tests/test_api.py"), 0.3);
272 assert_eq!(path_penalty("acme/db.py"), 1.0);
273 }
274 }
275}