1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLockReadGuard};
3
4use serde::Serialize;
5
6use super::plan_table::{SearchLaneKind, SearchShape};
7use super::{LaneExecution, LaneInput, SearchLane};
8use crate::context::SemanticIndexStatus;
9use crate::parser::SymbolCache;
10use crate::query_shape::{classify, looks_like_regex, QueryKind};
11use crate::search_index::{IndexStatus, SearchIndexSnapshot};
12use crate::semantic_index::SemanticIndex;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct RawQuery(String);
17
18impl RawQuery {
19 pub fn new(query: impl Into<String>) -> Self {
20 Self(query.into())
21 }
22
23 pub fn original_query(&self) -> &str {
24 &self.0
25 }
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30pub struct Span {
31 pub start: usize,
32 pub end: usize,
33}
34
35impl Span {
36 pub fn extract<'a>(&self, raw_query: &'a str) -> Option<&'a str> {
37 raw_query.get(self.start..self.end)
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
45pub struct QueryFacts {
46 pub embedded_span: Option<Span>,
47 pub exact_input_tokens: usize,
48 pub has_path_token: bool,
49 pub has_timestamp_or_pid: bool,
50 pub has_identifier_token: bool,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Token<'a> {
55 pub index: usize,
56 pub text: &'a str,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct TokenVariant {
61 pub token_index: usize,
62 pub text: String,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ReadinessWait {
68 Completed,
69 Cancelled,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum SymbolIndexStatus {
75 Ready,
76 Building,
77 Disabled,
78 Failed(String),
79}
80
81#[derive(Clone)]
83pub struct SemanticSnapshot<'a>(SemanticSnapshotStorage<'a>);
84
85#[derive(Clone)]
86enum SemanticSnapshotStorage<'a> {
87 Guard(Arc<RwLockReadGuard<'a, Option<SemanticIndex>>>),
88 Borrowed(Arc<SemanticIndex>),
89}
90
91impl std::fmt::Debug for SemanticSnapshot<'_> {
92 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 formatter
94 .debug_struct("SemanticSnapshot")
95 .field("present", &self.index().is_some())
96 .finish()
97 }
98}
99
100impl<'a> SemanticSnapshot<'a> {
101 pub fn from_guard(guard: RwLockReadGuard<'a, Option<SemanticIndex>>) -> Self {
102 Self(SemanticSnapshotStorage::Guard(Arc::new(guard)))
103 }
104
105 pub fn from_borrowed(index: Arc<SemanticIndex>) -> Self {
106 Self(SemanticSnapshotStorage::Borrowed(index))
107 }
108
109 pub fn index(&self) -> Option<&SemanticIndex> {
110 match &self.0 {
111 SemanticSnapshotStorage::Guard(guard) => guard.as_ref().as_ref(),
112 SemanticSnapshotStorage::Borrowed(index) => Some(index),
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct SemanticReadiness<'a> {
120 pub status: SemanticIndexStatus,
121 pub snapshot: Option<SemanticSnapshot<'a>>,
122 pub evicted: bool,
123 pub lock_contended: bool,
124}
125
126#[derive(Debug, Clone)]
128pub struct TrigramReadiness {
129 pub status: IndexStatus,
130 pub snapshot: Option<Arc<SearchIndexSnapshot>>,
131 pub evicted: bool,
132 pub lock_contended: bool,
133}
134
135#[derive(Clone)]
137pub struct SymbolReadiness {
138 pub status: SymbolIndexStatus,
139 pub snapshot: Option<Arc<SymbolCache>>,
140 pub evicted: bool,
141 pub lock_contended: bool,
142}
143
144impl std::fmt::Debug for SymbolReadiness {
145 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 formatter
147 .debug_struct("SymbolReadiness")
148 .field("status", &self.status)
149 .field("has_snapshot", &self.snapshot.is_some())
150 .field("evicted", &self.evicted)
151 .field("lock_contended", &self.lock_contended)
152 .finish()
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct ReadinessObservation<'a> {
159 pub semantic: SemanticReadiness<'a>,
160 pub trigram: TrigramReadiness,
161 pub symbol: SymbolReadiness,
162}
163
164pub trait ReadinessSource {
169 fn sample<'a>(&'a self) -> ReadinessObservation<'a>;
170 fn bounded_first_search_wait(&self) -> ReadinessWait;
171}
172
173#[doc(hidden)]
176pub enum RootReadiness<'a> {
177 Source(&'a dyn ReadinessSource),
178 Fixed(Readiness<'a>),
179}
180
181#[doc(hidden)]
182pub trait IntoRootReadiness<'a> {
183 fn into_root_readiness(self) -> RootReadiness<'a>;
184}
185
186impl<'a> IntoRootReadiness<'a> for &'a dyn ReadinessSource {
187 fn into_root_readiness(self) -> RootReadiness<'a> {
188 RootReadiness::Source(self)
189 }
190}
191
192impl<'a> IntoRootReadiness<'a> for Readiness<'a> {
193 fn into_root_readiness(self) -> RootReadiness<'a> {
194 RootReadiness::Fixed(self)
195 }
196}
197
198pub struct Root<'a> {
200 path: PathBuf,
201 readiness: RootReadiness<'a>,
202}
203
204impl std::fmt::Debug for Root<'_> {
205 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206 formatter
207 .debug_struct("Root")
208 .field("path", &self.path)
209 .finish()
210 }
211}
212
213impl<'a> Root<'a> {
214 pub fn new(path: impl Into<PathBuf>, readiness: impl IntoRootReadiness<'a>) -> Self {
215 Self {
216 path: path.into(),
217 readiness: readiness.into_root_readiness(),
218 }
219 }
220
221 pub fn path(&self) -> &Path {
222 &self.path
223 }
224
225 pub fn source(&self) -> Option<&'a dyn ReadinessSource> {
226 match &self.readiness {
227 RootReadiness::Source(source) => Some(*source),
228 RootReadiness::Fixed(_) => None,
229 }
230 }
231
232 pub fn fixed_readiness(&self) -> Option<&Readiness<'a>> {
233 match &self.readiness {
234 RootReadiness::Source(_) => None,
235 RootReadiness::Fixed(readiness) => Some(readiness),
236 }
237 }
238}
239
240#[derive(Clone, Default)]
241pub struct RetainedReadinessSnapshots<'a> {
242 semantic: Option<SemanticSnapshot<'a>>,
243 trigram: Option<Arc<SearchIndexSnapshot>>,
244 symbol: Option<Arc<SymbolCache>>,
245}
246
247impl std::fmt::Debug for RetainedReadinessSnapshots<'_> {
248 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 formatter
250 .debug_struct("RetainedReadinessSnapshots")
251 .field("semantic", &self.semantic.is_some())
252 .field("trigram", &self.trigram.is_some())
253 .field("symbol", &self.symbol.is_some())
254 .finish()
255 }
256}
257
258impl<'a> RetainedReadinessSnapshots<'a> {
259 pub fn new(
260 semantic: Option<SemanticSnapshot<'a>>,
261 trigram: Option<Arc<SearchIndexSnapshot>>,
262 symbol: Option<Arc<SymbolCache>>,
263 ) -> Self {
264 Self {
265 semantic,
266 trigram,
267 symbol,
268 }
269 }
270
271 pub fn semantic(&self) -> Option<&SemanticIndex> {
272 self.semantic.as_ref().and_then(SemanticSnapshot::index)
273 }
274
275 pub fn trigram(&self) -> Option<&SearchIndexSnapshot> {
276 self.trigram.as_deref()
277 }
278
279 pub fn symbol(&self) -> Option<&SymbolCache> {
280 self.symbol.as_deref()
281 }
282}
283
284#[derive(Debug, Clone, Serialize)]
285pub struct Readiness<'a> {
286 pub symbol_index: bool,
287 pub lexical_index: bool,
288 pub semantic_index: bool,
289 #[serde(skip_serializing_if = "Vec::is_empty")]
290 pub reasons: Vec<String>,
291 #[serde(skip)]
292 retained: RetainedReadinessSnapshots<'a>,
293 #[serde(skip)]
294 cancelled: bool,
295}
296
297impl PartialEq for Readiness<'_> {
298 fn eq(&self, other: &Self) -> bool {
299 self.symbol_index == other.symbol_index
300 && self.lexical_index == other.lexical_index
301 && self.semantic_index == other.semantic_index
302 && self.reasons == other.reasons
303 && self.cancelled == other.cancelled
304 }
305}
306
307impl Eq for Readiness<'_> {}
308
309impl<'a> Readiness<'a> {
310 pub fn new(symbol_index: bool, lexical_index: bool, semantic_index: bool) -> Self {
311 let mut reasons = Vec::new();
312 if !symbol_index {
313 reasons.push("symbol_index_unavailable".to_string());
314 }
315 if !lexical_index {
316 reasons.push("lexical_index_unavailable".to_string());
317 }
318 if !semantic_index {
319 reasons.push("semantic_index_unavailable".to_string());
320 }
321 Self::observed(
322 symbol_index,
323 lexical_index,
324 semantic_index,
325 reasons,
326 RetainedReadinessSnapshots::default(),
327 false,
328 )
329 }
330
331 pub fn observed(
332 symbol_index: bool,
333 lexical_index: bool,
334 semantic_index: bool,
335 reasons: Vec<String>,
336 retained: RetainedReadinessSnapshots<'a>,
337 cancelled: bool,
338 ) -> Self {
339 Self {
340 symbol_index,
341 lexical_index,
342 semantic_index,
343 reasons,
344 retained,
345 cancelled,
346 }
347 }
348
349 pub fn retained(&self) -> &RetainedReadinessSnapshots<'a> {
350 &self.retained
351 }
352
353 pub fn cancelled(&self) -> bool {
354 self.cancelled
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
359#[serde(rename_all = "snake_case")]
360pub enum ExactMode {
361 Ready,
362 Fallback,
363 #[serde(rename = "n/a")]
364 NotApplicable,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
368pub struct LanePlan<'a> {
369 pub shape: SearchShape,
370 pub query_facts: QueryFacts,
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub exact_input: Option<String>,
373 pub exact_mode: ExactMode,
374 #[serde(rename = "lanes_run")]
375 pub selected_lanes: Vec<SearchLaneKind>,
376 pub executed_callbacks: Vec<SearchLaneKind>,
377 pub readiness: Readiness<'a>,
378 pub variants: Vec<String>,
379}
380
381impl LanePlan<'_> {
382 pub fn contains(&self, lane: SearchLaneKind) -> bool {
383 self.selected_lanes.contains(&lane)
384 }
385}
386
387pub trait SearchExtensions: Send + Sync {
390 fn classify(&self, raw_query: &RawQuery) -> (SearchShape, QueryFacts) {
391 classify_raw_query(raw_query)
392 }
393
394 fn variants(&self, _token: Token<'_>) -> Vec<TokenVariant> {
395 Vec::new()
396 }
397
398 fn sample_readiness<'a>(&self, root: &Root<'a>) -> Readiness<'a> {
399 crate::search_b2::readiness::sample(root)
400 }
401
402 fn plan<'a>(
403 &self,
404 shape: &SearchShape,
405 facts: &QueryFacts,
406 readiness: &Readiness<'a>,
407 ) -> LanePlan<'a> {
408 default_lane_plan(*shape, facts, readiness.clone())
409 }
410
411 fn execute_lane(&self, lane: &dyn SearchLane, input: &LaneInput<'_>) -> LaneExecution {
412 lane.execute(input)
413 }
414}
415
416#[derive(Debug, Default, Clone, Copy)]
417pub struct DefaultSearchExtensions;
418
419impl SearchExtensions for DefaultSearchExtensions {}
420
421pub fn classify_raw_query(raw_query: &RawQuery) -> (SearchShape, QueryFacts) {
423 let query = raw_query.original_query().trim();
424 let shape = if looks_like_regex(query) {
425 SearchShape::Regex
426 } else if is_quoted(query) {
427 SearchShape::CodeLiteral
428 } else {
429 let query_shape = classify(query);
430 if query_shape.kind == QueryKind::ErrorCode || looks_like_log_excerpt(query) {
431 SearchShape::LogExcerpt
432 } else if query_shape.kind == QueryKind::Path {
433 SearchShape::Path
434 } else if query_shape.kind == QueryKind::Identifier {
435 SearchShape::Identifier
436 } else if query.split_whitespace().count() <= 2 {
437 SearchShape::Short
438 } else {
439 SearchShape::NaturalLanguage
440 }
441 };
442 let (_, facts) = crate::search_b2::router::classify(raw_query);
443 (shape, facts)
444}
445
446fn default_lane_plan<'a>(
447 shape: SearchShape,
448 facts: &QueryFacts,
449 readiness: Readiness<'a>,
450) -> LanePlan<'a> {
451 let mut selected_lanes = match shape {
452 SearchShape::Identifier => vec![
453 SearchLaneKind::Symbol,
454 SearchLaneKind::Exact,
455 SearchLaneKind::Lexical,
456 SearchLaneKind::Variants,
457 SearchLaneKind::Semantic,
458 ],
459 SearchShape::CodeLiteral => vec![SearchLaneKind::Exact, SearchLaneKind::Lexical],
460 SearchShape::Short => vec![
461 SearchLaneKind::Exact,
462 SearchLaneKind::Lexical,
463 SearchLaneKind::Semantic,
464 ],
465 SearchShape::NaturalLanguage => vec![
466 SearchLaneKind::Exact,
467 SearchLaneKind::Lexical,
468 SearchLaneKind::Variants,
469 SearchLaneKind::Semantic,
470 ],
471 SearchShape::LogExcerpt => vec![
472 SearchLaneKind::Anchored,
473 SearchLaneKind::Exact,
474 SearchLaneKind::Lexical,
475 ],
476 SearchShape::Path => vec![
477 SearchLaneKind::PathLookup,
478 SearchLaneKind::Exact,
479 SearchLaneKind::Lexical,
480 SearchLaneKind::FallbackWalk,
481 ],
482 SearchShape::Regex => vec![SearchLaneKind::FallbackWalk],
483 };
484
485 if !readiness.symbol_index {
486 selected_lanes.retain(|lane| *lane != SearchLaneKind::Symbol);
487 }
488 if !readiness.lexical_index {
489 selected_lanes.retain(|lane| {
490 !matches!(
491 lane,
492 SearchLaneKind::Exact | SearchLaneKind::Anchored | SearchLaneKind::Lexical
493 )
494 });
495 }
496 if !readiness.semantic_index {
497 selected_lanes.retain(|lane| *lane != SearchLaneKind::Semantic);
498 }
499 if !readiness.reasons.is_empty() {
500 selected_lanes.push(SearchLaneKind::ReadinessDisclosure);
501 }
502
503 let exact_mode = if !selected_lanes.contains(&SearchLaneKind::Exact) {
504 ExactMode::NotApplicable
505 } else if readiness.lexical_index && facts.exact_input_tokens > 0 {
506 ExactMode::Ready
507 } else {
508 ExactMode::Fallback
509 };
510 LanePlan {
511 shape,
512 query_facts: facts.clone(),
513 exact_input: None,
514 exact_mode,
515 executed_callbacks: selected_lanes.clone(),
516 selected_lanes,
517 readiness,
518 variants: Vec::new(),
519 }
520}
521
522fn is_quoted(query: &str) -> bool {
523 query.len() >= 2
524 && ((query.starts_with('"') && query.ends_with('"'))
525 || (query.starts_with('\'') && query.ends_with('\'')))
526}
527
528fn looks_like_log_excerpt(query: &str) -> bool {
529 let upper = query.to_ascii_uppercase();
530 [" ERROR ", " WARN ", " INFO ", " DEBUG ", " TRACE "]
531 .iter()
532 .any(|marker| upper.contains(marker))
533 || query.contains("::") && query.contains('[') && query.contains(']')
534}