Skip to main content

gobby_code/vector/code_symbols/
search.rs

1use crate::config::{CODE_SYMBOL_COLLECTION_PREFIX, Context};
2
3use super::embedding::{embed_query_with_source, embedding_source_from_context};
4use super::qdrant::{collection_name, vector_search};
5use super::types::{CodeSymbolVectorSearchHit, CodeSymbolVectorSearchRequest};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum SearchError {
9    MissingQdrantConfig,
10    MissingEmbeddingConfig,
11    QueryEmbeddingFailed,
12    VectorSearch(String),
13}
14
15impl std::fmt::Display for SearchError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::MissingQdrantConfig => write!(f, "Qdrant config is missing"),
19            Self::MissingEmbeddingConfig => write!(f, "embedding config is missing"),
20            Self::QueryEmbeddingFailed => write!(f, "query embedding failed"),
21            Self::VectorSearch(error) => write!(f, "semantic vector search failed: {error}"),
22        }
23    }
24}
25
26impl std::error::Error for SearchError {}
27
28pub fn search_code_symbols(
29    ctx: &Context,
30    request: &CodeSymbolVectorSearchRequest,
31) -> Result<Vec<CodeSymbolVectorSearchHit>, SearchError> {
32    let qdrant_config = match &ctx.qdrant {
33        Some(config) => config,
34        None => return Err(SearchError::MissingQdrantConfig),
35    };
36
37    let embedding_source = match embedding_source_from_context(ctx) {
38        Some(source) => source,
39        None => return Err(SearchError::MissingEmbeddingConfig),
40    };
41
42    let embedding = match embed_query_with_source(&embedding_source, &request.query) {
43        Some(embedding) => embedding,
44        None => return Err(SearchError::QueryEmbeddingFailed),
45    };
46
47    let collection = collection_name(&request.collection_prefix, &request.project_id);
48    match vector_search(qdrant_config, &collection, &embedding, request.limit) {
49        Ok(hits) => Ok(hits
50            .into_iter()
51            .map(|(symbol_id, score)| CodeSymbolVectorSearchHit { symbol_id, score })
52            .collect()),
53        Err(error) => Err(SearchError::VectorSearch(error.to_string())),
54    }
55}
56
57/// Semantic search is an optional ranking signal. Returning an empty result on
58/// transport/config errors lets hybrid search degrade to lexical/graph sources
59/// instead of failing the whole user query.
60pub fn semantic_search(ctx: &Context, query: &str, limit: usize) -> Vec<(String, f64)> {
61    let request = CodeSymbolVectorSearchRequest {
62        project_id: ctx.project_id.clone(),
63        query: query.to_string(),
64        limit,
65        collection_prefix: CODE_SYMBOL_COLLECTION_PREFIX.to_string(),
66    };
67
68    match search_code_symbols(ctx, &request) {
69        Ok(hits) => hits
70            .into_iter()
71            .map(|hit| (hit.symbol_id, hit.score))
72            .collect(),
73        Err(error) => {
74            log::warn!("semantic vector search skipped: {error}");
75            Vec::new()
76        }
77    }
78}