Skip to main content

velesdb_core/collection/search/
sparse.rs

1//! Public sparse and hybrid dense+sparse search methods for Collection.
2//!
3//! These methods provide a simpler API than the VelesQL-based internal
4//! `execute_sparse_search` / `execute_hybrid_search_with_strategy` methods,
5//! accepting raw `SparseVector` directly instead of VelesQL AST nodes.
6//! Designed for SDK wiring (Python, TypeScript, Mobile).
7
8use super::resolve;
9use crate::collection::types::Collection;
10use crate::error::{Error, Result};
11use crate::fusion::FusionStrategy;
12use crate::point::SearchResult;
13use crate::sparse_index::{search::sparse_search, SparseVector, DEFAULT_SPARSE_INDEX_NAME};
14
15impl Collection {
16    /// Sparse-only search on the default sparse index.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the default sparse index does not exist.
21    pub fn sparse_search_default(
22        &self,
23        query: &SparseVector,
24        k: usize,
25    ) -> Result<Vec<SearchResult>> {
26        self.sparse_search_named(query, k, DEFAULT_SPARSE_INDEX_NAME)
27    }
28
29    /// Sparse-only search on a named sparse index.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the named sparse index does not exist.
34    pub fn sparse_search_named(
35        &self,
36        query: &SparseVector,
37        k: usize,
38        index_name: &str,
39    ) -> Result<Vec<SearchResult>> {
40        let indexes = self.sparse_indexes.read();
41        let index = indexes
42            .get(index_name)
43            .ok_or_else(|| resolve::sparse_index_not_found(index_name))?;
44        let results = sparse_search(index, query, k);
45        // Explicit drop: `resolve_sparse_results` acquires the payload_storage read-lock,
46        // which is ordered after sparse_indexes in the Collection lock hierarchy.
47        // Releasing sparse_indexes here before entering resolve_sparse_results prevents
48        // a potential lock-ordering violation if the call path ever reacquires sparse_indexes.
49        drop(indexes);
50        Ok(self.resolve_sparse_results(&results, k))
51    }
52
53    /// Hybrid dense+sparse search with RRF fusion on the default sparse index.
54    ///
55    /// Runs both dense (HNSW) and sparse branches, then fuses using the
56    /// provided strategy (typically RRF with k=60).
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the sparse index does not exist or fusion fails.
61    pub fn hybrid_sparse_search(
62        &self,
63        dense_vector: &[f32],
64        sparse_query: &SparseVector,
65        k: usize,
66        strategy: &FusionStrategy,
67    ) -> Result<Vec<SearchResult>> {
68        self.hybrid_sparse_search_inner(
69            dense_vector,
70            sparse_query,
71            k,
72            strategy,
73            None,
74            DEFAULT_SPARSE_INDEX_NAME,
75        )
76    }
77
78    /// Hybrid dense+sparse search with metadata filtering.
79    ///
80    /// Same as [`hybrid_sparse_search`](Self::hybrid_sparse_search) but applies
81    /// a metadata filter to the sparse branch during candidate retrieval.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if the sparse index does not exist or fusion fails.
86    pub fn hybrid_sparse_search_with_filter(
87        &self,
88        dense_vector: &[f32],
89        sparse_query: &SparseVector,
90        k: usize,
91        strategy: &FusionStrategy,
92        filter: &crate::filter::Filter,
93    ) -> Result<Vec<SearchResult>> {
94        self.hybrid_sparse_search_inner(
95            dense_vector,
96            sparse_query,
97            k,
98            strategy,
99            Some(filter),
100            DEFAULT_SPARSE_INDEX_NAME,
101        )
102    }
103
104    /// Hybrid dense+sparse search on a named sparse index.
105    ///
106    /// Like [`hybrid_sparse_search`](Self::hybrid_sparse_search) but targets
107    /// a specific named sparse index (e.g. for BGE-M3 multi-model embeddings).
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the named sparse index does not exist or fusion fails.
112    pub fn hybrid_sparse_search_named(
113        &self,
114        dense_vector: &[f32],
115        sparse_query: &SparseVector,
116        k: usize,
117        strategy: &FusionStrategy,
118        index_name: &str,
119    ) -> Result<Vec<SearchResult>> {
120        self.hybrid_sparse_search_inner(dense_vector, sparse_query, k, strategy, None, index_name)
121    }
122
123    /// Hybrid dense+sparse search on a named sparse index with filtering.
124    ///
125    /// Combines [`hybrid_sparse_search_named`](Self::hybrid_sparse_search_named)
126    /// with metadata filtering on the sparse branch.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the named sparse index does not exist or fusion fails.
131    pub fn hybrid_sparse_search_named_with_filter(
132        &self,
133        dense_vector: &[f32],
134        sparse_query: &SparseVector,
135        k: usize,
136        strategy: &FusionStrategy,
137        index_name: &str,
138        filter: &crate::filter::Filter,
139    ) -> Result<Vec<SearchResult>> {
140        self.hybrid_sparse_search_inner(
141            dense_vector,
142            sparse_query,
143            k,
144            strategy,
145            Some(filter),
146            index_name,
147        )
148    }
149
150    /// Shared implementation for hybrid dense+sparse search with optional filter.
151    fn hybrid_sparse_search_inner(
152        &self,
153        dense_vector: &[f32],
154        sparse_query: &SparseVector,
155        k: usize,
156        strategy: &FusionStrategy,
157        filter: Option<&crate::filter::Filter>,
158        index_name: &str,
159    ) -> Result<Vec<SearchResult>> {
160        let candidate_k = k.saturating_mul(2).max(k.saturating_add(10));
161
162        let (dense_results, sparse_results) =
163            self.execute_both_branches(dense_vector, sparse_query, index_name, candidate_k, filter);
164
165        if dense_results.is_empty() && sparse_results.is_empty() {
166            return Ok(Vec::new());
167        }
168        if dense_results.is_empty() {
169            let scored: Vec<(u64, f32)> = sparse_results
170                .iter()
171                .map(|sd| (sd.doc_id, sd.score))
172                .collect();
173            return Ok(self.resolve_fused_results(&scored, k));
174        }
175        if sparse_results.is_empty() {
176            return Ok(self.resolve_fused_results(&dense_results, k));
177        }
178
179        let sparse_tuples: Vec<(u64, f32)> = sparse_results
180            .iter()
181            .map(|sd| (sd.doc_id, sd.score))
182            .collect();
183
184        let fused = strategy
185            .fuse(vec![dense_results, sparse_tuples])
186            .map_err(|e| Error::Config(format!("Fusion error: {e}")))?;
187
188        Ok(self.resolve_fused_results(&fused, k))
189    }
190}