1use std::collections::HashSet;
2
3use postgres::Client;
4
5use crate::config::Context;
6use crate::models::{SearchResult, Symbol};
7use crate::visibility;
8
9use super::common::{
10 FILTERED_FETCH_CAP, PgParam, SymbolFilters, SymbolOrder, append_unique_symbols, escape_like,
11 push_param, query_symbols_by_conditions, sanitize_pg_search_query,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct VisibleSearchOutcome<T> {
16 pub results: Vec<T>,
17 pub degraded: bool,
18}
19
20impl<T> VisibleSearchOutcome<T> {
21 fn ok(results: Vec<T>) -> Self {
22 Self {
23 results,
24 degraded: false,
25 }
26 }
27
28 fn degraded(results: Vec<T>) -> Self {
29 Self {
30 results,
31 degraded: true,
32 }
33 }
34}
35
36pub fn search_symbols_fts(
37 conn: &mut Client,
38 query: &str,
39 project_id: &str,
40 kind: Option<&str>,
41 language: Option<&str>,
42 paths: &[String],
43 limit: usize,
44) -> Vec<Symbol> {
45 let bm25_query = sanitize_pg_search_query(query);
46 if bm25_query.is_empty() || limit == 0 {
47 return Vec::new();
48 }
49
50 let mut params = Vec::new();
51 let query_placeholder = push_param(&mut params, bm25_query);
52 let project_placeholder = push_param(&mut params, project_id.to_string());
53 let conditions = vec![
54 format!(
55 "(cs.name @@@ {q} OR cs.qualified_name @@@ {q} OR cs.signature @@@ {q} OR cs.docstring @@@ {q} OR cs.summary @@@ {q})",
56 q = query_placeholder
57 ),
58 format!("cs.project_id = {project_placeholder}"),
59 ];
60 let filters = SymbolFilters {
61 kind,
62 language,
63 paths,
64 };
65 query_symbols_by_conditions(
66 conn,
67 conditions,
68 params,
69 filters,
70 limit,
71 SymbolOrder::Bm25Score,
72 )
73}
74
75pub fn search_symbols_by_name(
77 conn: &mut Client,
78 query: &str,
79 project_id: &str,
80 kind: Option<&str>,
81 language: Option<&str>,
82 paths: &[String],
83 limit: usize,
84) -> Vec<Symbol> {
85 if query.trim().is_empty() || limit == 0 {
86 return Vec::new();
87 }
88 let escaped_query = escape_like(query);
89 let pattern = format!("%{escaped_query}%");
90 let mut params = Vec::new();
91 let project_placeholder = push_param(&mut params, project_id.to_string());
92 let name_placeholder = push_param(&mut params, pattern.clone());
93 let qualified_placeholder = push_param(&mut params, pattern);
94 let conditions = vec![
95 format!("cs.project_id = {project_placeholder}"),
96 format!(
97 "(cs.name LIKE {name_placeholder} ESCAPE '\\' OR cs.qualified_name LIKE {qualified_placeholder} ESCAPE '\\')"
98 ),
99 ];
100 query_symbols_by_conditions(
101 conn,
102 conditions,
103 params,
104 SymbolFilters {
105 kind,
106 language,
107 paths,
108 },
109 limit,
110 SymbolOrder::Name,
111 )
112}
113
114pub fn search_symbols_exact_first(
115 conn: &mut Client,
116 query: &str,
117 project_id: &str,
118 kind: Option<&str>,
119 language: Option<&str>,
120 paths: &[String],
121 limit: usize,
122) -> Vec<Symbol> {
123 if query.trim().is_empty() || limit == 0 {
124 return Vec::new();
125 }
126
127 let mut results = Vec::new();
128 let mut seen = HashSet::new();
129 let filters = SymbolFilters {
130 kind,
131 language,
132 paths,
133 };
134
135 let mut params = Vec::new();
136 let project = push_param(&mut params, project_id.to_string());
137 let query_param = push_param(&mut params, query.to_string());
138 let order = SymbolOrder::ExactCaseFirst(query_param.clone());
139 let exact = query_symbols_by_conditions(
140 conn,
141 vec![
142 format!("cs.project_id = {project}"),
143 format!(
144 "(cs.name = {q} OR cs.qualified_name = {q} OR lower(cs.name) = lower({q}) OR lower(cs.qualified_name) = lower({q}))",
145 q = query_param
146 ),
147 ],
148 params,
149 filters,
150 limit,
151 order,
152 );
153 append_unique_symbols(&mut results, &mut seen, exact, limit);
154 if results.len() >= limit {
155 return results;
156 }
157
158 let prefix_pattern = format!("{}%", escape_like(query));
159 let mut params = Vec::new();
160 let project = push_param(&mut params, project_id.to_string());
161 let prefix = push_param(&mut params, prefix_pattern);
162 let prefix_matches = query_symbols_by_conditions(
163 conn,
164 vec![
165 format!("cs.project_id = {project}"),
166 format!(
167 "(cs.name LIKE {prefix} ESCAPE '\\' OR cs.qualified_name LIKE {prefix} ESCAPE '\\')"
168 ),
169 ],
170 params,
171 filters,
172 limit,
173 SymbolOrder::Name,
174 );
175 append_unique_symbols(&mut results, &mut seen, prefix_matches, limit);
176 if results.len() >= limit {
177 return results;
178 }
179
180 let contains = search_symbols_by_name(conn, query, project_id, kind, language, paths, limit);
181 append_unique_symbols(&mut results, &mut seen, contains, limit);
182 if results.len() >= limit {
183 return results;
184 }
185
186 let fts = search_symbols_fts(conn, query, project_id, kind, language, paths, limit);
187 append_unique_symbols(&mut results, &mut seen, fts, limit);
188
189 results
190}
191
192pub fn search_symbols_fts_visible(
193 conn: &mut Client,
194 query: &str,
195 ctx: &Context,
196 kind: Option<&str>,
197 language: Option<&str>,
198 paths: &[String],
199 limit: usize,
200) -> VisibleSearchOutcome<Symbol> {
201 let bm25_query = sanitize_pg_search_query(query);
202 if bm25_query.is_empty() || limit == 0 {
203 return VisibleSearchOutcome::ok(Vec::new());
204 }
205
206 let mut params = Vec::new();
207 let query_placeholder = push_param(&mut params, bm25_query);
208 let conditions = vec![format!(
209 "(cs.name @@@ {q} OR cs.qualified_name @@@ {q} OR cs.signature @@@ {q} OR cs.docstring @@@ {q} OR cs.summary @@@ {q})",
210 q = query_placeholder
211 )];
212 query_visible_symbols_by_conditions(
213 conn,
214 ctx,
215 conditions,
216 params,
217 SymbolFilters {
218 kind,
219 language,
220 paths,
221 },
222 limit,
223 SymbolOrder::Bm25Score,
224 )
225}
226
227pub fn search_symbols_by_name_visible(
228 conn: &mut Client,
229 query: &str,
230 ctx: &Context,
231 kind: Option<&str>,
232 language: Option<&str>,
233 paths: &[String],
234 limit: usize,
235) -> VisibleSearchOutcome<Symbol> {
236 if query.trim().is_empty() || limit == 0 {
237 return VisibleSearchOutcome::ok(Vec::new());
238 }
239 let escaped_query = escape_like(query);
240 let pattern = format!("%{escaped_query}%");
241 let mut params = Vec::new();
242 let name_placeholder = push_param(&mut params, pattern.clone());
243 let qualified_placeholder = push_param(&mut params, pattern);
244 let conditions = vec![format!(
245 "(cs.name LIKE {name_placeholder} ESCAPE '\\' OR cs.qualified_name LIKE {qualified_placeholder} ESCAPE '\\')"
246 )];
247 query_visible_symbols_by_conditions(
248 conn,
249 ctx,
250 conditions,
251 params,
252 SymbolFilters {
253 kind,
254 language,
255 paths,
256 },
257 limit,
258 SymbolOrder::Name,
259 )
260}
261
262pub fn search_symbols_exact_first_visible(
263 conn: &mut Client,
264 query: &str,
265 ctx: &Context,
266 kind: Option<&str>,
267 language: Option<&str>,
268 paths: &[String],
269 limit: usize,
270) -> VisibleSearchOutcome<Symbol> {
271 if query.trim().is_empty() || limit == 0 {
272 return VisibleSearchOutcome::ok(Vec::new());
273 }
274
275 let mut results = Vec::new();
276 let mut seen = HashSet::new();
277 let mut degraded = false;
278 let filters = SymbolFilters {
279 kind,
280 language,
281 paths,
282 };
283
284 let mut params = Vec::new();
285 let query_param = push_param(&mut params, query.to_string());
286 let order = SymbolOrder::ExactCaseFirst(query_param.clone());
287 let exact = query_visible_symbols_by_conditions(
288 conn,
289 ctx,
290 vec![format!(
291 "(cs.name = {q} OR cs.qualified_name = {q} OR lower(cs.name) = lower({q}) OR lower(cs.qualified_name) = lower({q}))",
292 q = query_param
293 )],
294 params,
295 filters,
296 limit,
297 order,
298 );
299 degraded |= exact.degraded;
300 append_unique_symbols(&mut results, &mut seen, exact.results, limit);
301 if results.len() >= limit {
302 return VisibleSearchOutcome { results, degraded };
303 }
304
305 let prefix_pattern = format!("{}%", escape_like(query));
306 let mut params = Vec::new();
307 let prefix = push_param(&mut params, prefix_pattern);
308 let prefix_matches = query_visible_symbols_by_conditions(
309 conn,
310 ctx,
311 vec![format!(
312 "(cs.name LIKE {prefix} ESCAPE '\\' OR cs.qualified_name LIKE {prefix} ESCAPE '\\')"
313 )],
314 params,
315 filters,
316 limit,
317 SymbolOrder::Name,
318 );
319 degraded |= prefix_matches.degraded;
320 append_unique_symbols(&mut results, &mut seen, prefix_matches.results, limit);
321 if results.len() >= limit {
322 return VisibleSearchOutcome { results, degraded };
323 }
324
325 let contains = search_symbols_by_name_visible(conn, query, ctx, kind, language, paths, limit);
326 degraded |= contains.degraded;
327 append_unique_symbols(&mut results, &mut seen, contains.results, limit);
328 if results.len() >= limit {
329 return VisibleSearchOutcome { results, degraded };
330 }
331
332 let fts = search_symbols_fts_visible(conn, query, ctx, kind, language, paths, limit);
333 degraded |= fts.degraded;
334 append_unique_symbols(&mut results, &mut seen, fts.results, limit);
335
336 VisibleSearchOutcome { results, degraded }
337}
338
339fn query_visible_symbols_by_conditions(
340 conn: &mut Client,
341 ctx: &Context,
342 mut conditions: Vec<String>,
343 mut params: Vec<PgParam>,
344 filters: SymbolFilters<'_>,
345 limit: usize,
346 order: SymbolOrder,
347) -> VisibleSearchOutcome<Symbol> {
348 let project_ids = visibility::visible_project_ids(ctx);
349 if project_ids.is_empty() || limit == 0 {
350 return VisibleSearchOutcome::ok(Vec::new());
351 }
352 let project_placeholder = push_param(&mut params, project_ids);
353 conditions.push(format!("cs.project_id = ANY({project_placeholder})"));
354 let symbols = query_symbols_by_conditions(
355 conn,
356 conditions,
357 params,
358 filters,
359 limit.max(FILTERED_FETCH_CAP),
360 order,
361 );
362 let mut symbols = match visibility::filter_visible_symbols(conn, ctx, symbols) {
363 Ok(symbols) => symbols,
364 Err(error) => {
365 log::error!("visible symbol filtering failed: {error}");
366 return VisibleSearchOutcome::degraded(Vec::new());
367 }
368 };
369 symbols.truncate(limit);
370 VisibleSearchOutcome::ok(symbols)
371}
372
373pub fn search_text(
375 conn: &mut Client,
376 query: &str,
377 project_id: &str,
378 language: Option<&str>,
379 paths: &[String],
380 limit: usize,
381) -> Vec<SearchResult> {
382 search_symbols_fts(conn, query, project_id, None, language, paths, limit)
383 .into_iter()
384 .map(|s| s.to_brief())
385 .collect()
386}
387
388pub fn search_text_visible(
389 conn: &mut Client,
390 query: &str,
391 ctx: &Context,
392 language: Option<&str>,
393 paths: &[String],
394 limit: usize,
395) -> VisibleSearchOutcome<SearchResult> {
396 let results = search_symbols_fts_visible(conn, query, ctx, None, language, paths, limit);
397 VisibleSearchOutcome {
398 results: results.results.into_iter().map(|s| s.to_brief()).collect(),
399 degraded: results.degraded,
400 }
401}