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