Skip to main content

velesdb_core/collection/vector_collection/
search.rs

1//! Search, match, aggregation, and query execution for `VectorCollection`.
2
3use std::collections::HashMap;
4
5use crate::error::Result;
6use crate::point::SearchResult;
7
8use super::VectorCollection;
9
10impl VectorCollection {
11    /// Performs kNN vector search using the HNSW index.
12    ///
13    /// Returns the `k` nearest neighbors ordered by ascending distance.
14    ///
15    /// # Errors
16    ///
17    /// - Returns an error if the query dimension does not match the collection.
18    /// - Returns an error if the HNSW index is not initialized.
19    ///
20    /// # Examples
21    ///
22    /// ```rust,no_run
23    /// # use velesdb_core::{VectorCollection, DistanceMetric, StorageMode};
24    /// # let coll = VectorCollection::create("./data/v".into(), "v", 128, DistanceMetric::Cosine, StorageMode::Full)?;
25    /// let results = coll.search(&vec![0.1; 128], 10)?;
26    /// for r in &results {
27    ///     println!("id={} score={}", r.point.id, r.score);
28    /// }
29    /// # Ok::<(), velesdb_core::Error>(())
30    /// ```
31    pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<SearchResult>> {
32        self.inner.search(query, k)
33    }
34
35    /// Performs full-text BM25 search over indexed payload fields.
36    ///
37    /// Returns up to `k` results ranked by BM25 relevance score.
38    ///
39    /// # Errors
40    ///
41    /// - Returns an error if storage retrieval fails.
42    ///
43    /// # Examples
44    ///
45    /// ```rust,no_run
46    /// # use velesdb_core::{VectorCollection, DistanceMetric, StorageMode};
47    /// # let coll = VectorCollection::create("./data/v".into(), "v", 128, DistanceMetric::Cosine, StorageMode::Full)?;
48    /// let results = coll.text_search("machine learning", 5)?;
49    /// # Ok::<(), velesdb_core::Error>(())
50    /// ```
51    pub fn text_search(&self, query: &str, k: usize) -> Result<Vec<SearchResult>> {
52        self.inner.text_search(query, k)
53    }
54
55    /// Performs kNN search with an explicit `ef_search` override.
56    ///
57    /// Higher `ef_search` values improve recall at the cost of latency.
58    ///
59    /// # Errors
60    ///
61    /// - Returns an error if the query dimension does not match the collection.
62    pub fn search_with_ef(
63        &self,
64        query: &[f32],
65        k: usize,
66        ef_search: usize,
67    ) -> Result<Vec<SearchResult>> {
68        self.inner.search_with_ef(query, k, ef_search)
69    }
70
71    /// Performs kNN search with a specific [`crate::SearchQuality`] profile.
72    ///
73    /// Use this instead of [`Self::search_with_ef`] when you want named
74    /// quality modes like [`crate::SearchQuality::AutoTune`] that compute ef
75    /// dynamically.
76    ///
77    /// # Errors
78    ///
79    /// - Returns an error if the query dimension does not match the collection.
80    pub fn search_with_quality(
81        &self,
82        query: &[f32],
83        k: usize,
84        quality: crate::SearchQuality,
85    ) -> Result<Vec<SearchResult>> {
86        self.inner.search_with_quality(query, k, quality)
87    }
88
89    /// Performs kNN search with a metadata filter applied post-retrieval.
90    ///
91    /// # Errors
92    ///
93    /// - Returns an error if the query dimension does not match the collection.
94    /// - Returns an error if the filter references an unsupported field type.
95    pub fn search_with_filter(
96        &self,
97        query: &[f32],
98        k: usize,
99        filter: &crate::filter::Filter,
100    ) -> Result<Vec<SearchResult>> {
101        self.inner.search_with_filter(query, k, filter)
102    }
103
104    /// Returns [`crate::ScoredResult`] pairs without payload hydration.
105    ///
106    /// Faster than [`search`](Self::search) when only IDs and scores are needed.
107    ///
108    /// # Errors
109    ///
110    /// - Returns an error if the query dimension does not match the collection.
111    pub fn search_ids(
112        &self,
113        query: &[f32],
114        k: usize,
115    ) -> Result<Vec<crate::scored_result::ScoredResult>> {
116        self.inner.search_ids(query, k)
117    }
118
119    /// Full-text search with metadata filter.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if storage retrieval fails.
124    pub fn text_search_with_filter(
125        &self,
126        query: &str,
127        k: usize,
128        filter: &crate::filter::Filter,
129    ) -> Result<Vec<SearchResult>> {
130        self.inner.text_search_with_filter(query, k, filter)
131    }
132
133    /// Performs hybrid search combining vector kNN and BM25 full-text via RRF fusion.
134    ///
135    /// When `alpha` is `None`, a default blending factor is used. Values closer
136    /// to `1.0` weight vector results more; values closer to `0.0` weight text.
137    ///
138    /// # Errors
139    ///
140    /// - Returns an error if the query dimension does not match the collection.
141    /// - Returns an error if text indexing or storage retrieval fails.
142    ///
143    /// # Examples
144    ///
145    /// ```rust,no_run
146    /// # use velesdb_core::{VectorCollection, DistanceMetric, StorageMode};
147    /// # let coll = VectorCollection::create("./data/v".into(), "v", 128, DistanceMetric::Cosine, StorageMode::Full)?;
148    /// let results = coll.hybrid_search(&vec![0.1; 128], "machine learning", 10, Some(0.7))?;
149    /// # Ok::<(), velesdb_core::Error>(())
150    /// ```
151    pub fn hybrid_search(
152        &self,
153        vector: &[f32],
154        text: &str,
155        k: usize,
156        alpha: Option<f32>,
157    ) -> Result<Vec<SearchResult>> {
158        self.inner.hybrid_search(vector, text, k, alpha, None)
159    }
160
161    /// Performs hybrid search (vector + BM25) with a metadata filter.
162    ///
163    /// # Errors
164    ///
165    /// - Returns an error if the query dimension does not match the collection.
166    /// - Returns an error if text indexing, storage, or filtering fails.
167    pub fn hybrid_search_with_filter(
168        &self,
169        vector: &[f32],
170        text: &str,
171        k: usize,
172        alpha: Option<f32>,
173        filter: &crate::filter::Filter,
174    ) -> Result<Vec<SearchResult>> {
175        self.inner
176            .hybrid_search_with_filter(vector, text, k, alpha, filter, None)
177    }
178
179    /// Performs batch kNN search with per-query metadata filters.
180    ///
181    /// Each query in `queries` is paired with the filter at the same index in
182    /// `filters`. Pass `None` for queries that should not be filtered.
183    ///
184    /// # Errors
185    ///
186    /// - Returns an error if any query dimension does not match the collection.
187    /// - Returns an error if `queries` and `filters` have different lengths.
188    ///
189    /// # Examples
190    ///
191    /// ```rust,no_run
192    /// # use velesdb_core::{VectorCollection, DistanceMetric, StorageMode};
193    /// # let coll = VectorCollection::create("./data/v".into(), "v", 128, DistanceMetric::Cosine, StorageMode::Full)?;
194    /// let q1 = vec![0.1; 128];
195    /// let q2 = vec![0.2; 128];
196    /// let results = coll.search_batch_with_filters(
197    ///     &[q1.as_slice(), q2.as_slice()],
198    ///     10,
199    ///     &[None, None],
200    /// )?;
201    /// assert_eq!(results.len(), 2);
202    /// # Ok::<(), velesdb_core::Error>(())
203    /// ```
204    pub fn search_batch_with_filters(
205        &self,
206        queries: &[&[f32]],
207        k: usize,
208        filters: &[Option<crate::filter::Filter>],
209    ) -> Result<Vec<Vec<SearchResult>>> {
210        self.inner.search_batch_with_filters(queries, k, filters)
211    }
212
213    /// Performs batch kNN search without filters, optimized for throughput.
214    ///
215    /// Uses rayon-parallelized HNSW search and result resolution for maximum
216    /// queries-per-second. Prefer this over calling [`search`](Self::search)
217    /// in a loop.
218    ///
219    /// # Errors
220    ///
221    /// - Returns an error if any query dimension does not match the collection.
222    ///
223    /// # Examples
224    ///
225    /// ```rust,no_run
226    /// # use velesdb_core::{VectorCollection, DistanceMetric, StorageMode};
227    /// # let coll = VectorCollection::create("./data/v".into(), "v", 128, DistanceMetric::Cosine, StorageMode::Full)?;
228    /// let q1 = vec![0.1; 128];
229    /// let q2 = vec![0.2; 128];
230    /// let results = coll.search_batch_parallel(&[q1.as_slice(), q2.as_slice()], 10)?;
231    /// assert_eq!(results.len(), 2);
232    /// # Ok::<(), velesdb_core::Error>(())
233    /// ```
234    pub fn search_batch_parallel(
235        &self,
236        queries: &[&[f32]],
237        k: usize,
238    ) -> Result<Vec<Vec<SearchResult>>> {
239        self.inner.search_batch_parallel(queries, k)
240    }
241
242    /// Performs multi-query search fusing results from multiple query vectors.
243    ///
244    /// # Errors
245    ///
246    /// - Returns an error if any query dimension does not match the collection.
247    /// - Returns an error if the fusion strategy fails.
248    pub fn multi_query_search(
249        &self,
250        queries: &[&[f32]],
251        k: usize,
252        strategy: crate::fusion::FusionStrategy,
253        filter: Option<&crate::filter::Filter>,
254    ) -> Result<Vec<SearchResult>> {
255        self.inner.multi_query_search(queries, k, strategy, filter)
256    }
257
258    /// Performs multi-query search returning only IDs and fused scores.
259    ///
260    /// # Errors
261    ///
262    /// - Returns an error if any query dimension does not match the collection.
263    /// - Returns an error if the fusion strategy fails.
264    pub fn multi_query_search_ids(
265        &self,
266        queries: &[&[f32]],
267        k: usize,
268        strategy: crate::fusion::FusionStrategy,
269    ) -> Result<Vec<(u64, f32)>> {
270        self.inner.multi_query_search_ids(queries, k, strategy)
271    }
272
273    /// Performs sparse-only search on the named index.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the named sparse index does not exist.
278    pub fn sparse_search(
279        &self,
280        query: &crate::index::sparse::SparseVector,
281        k: usize,
282        index_name: &str,
283    ) -> Result<Vec<SearchResult>> {
284        let indexes = self.inner.query.sparse_indexes.read();
285        let index = indexes.get(index_name).ok_or_else(|| {
286            crate::error::Error::Config(format!(
287                "Sparse index '{}' not found",
288                if index_name.is_empty() {
289                    "<default>"
290                } else {
291                    index_name
292                }
293            ))
294        })?;
295        let results = crate::index::sparse::sparse_search(index, query, k);
296        drop(indexes);
297        Ok(self.inner.resolve_sparse_results(&results, k))
298    }
299
300    /// Performs hybrid dense+sparse search with RRF fusion.
301    ///
302    /// # Errors
303    ///
304    /// Returns an error if dense or sparse search fails, or fusion errors.
305    #[allow(clippy::too_many_arguments)]
306    pub fn hybrid_sparse_search(
307        &self,
308        dense_vector: &[f32],
309        sparse_query: &crate::index::sparse::SparseVector,
310        k: usize,
311        index_name: &str,
312        strategy: &crate::fusion::FusionStrategy,
313    ) -> Result<Vec<SearchResult>> {
314        self.hybrid_sparse_search_filtered(
315            dense_vector,
316            sparse_query,
317            k,
318            index_name,
319            strategy,
320            None,
321        )
322    }
323
324    /// [`Self::hybrid_sparse_search`] with an optional metadata filter.
325    ///
326    /// The filter applies to **both** branches before fusion (dense
327    /// pre-filter, sparse post-filter — see `execute_both_branches`), so a
328    /// fused result never surfaces a point the filter excludes.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if dense or sparse search fails, or fusion errors.
333    #[allow(clippy::too_many_arguments)]
334    pub fn hybrid_sparse_search_filtered(
335        &self,
336        dense_vector: &[f32],
337        sparse_query: &crate::index::sparse::SparseVector,
338        k: usize,
339        index_name: &str,
340        strategy: &crate::fusion::FusionStrategy,
341        filter: Option<&crate::filter::Filter>,
342    ) -> Result<Vec<SearchResult>> {
343        let candidate_k = k.saturating_mul(2).max(k + 10);
344
345        let (dense_results, sparse_results) = self.inner.execute_both_branches(
346            dense_vector,
347            sparse_query,
348            index_name,
349            candidate_k,
350            filter,
351        );
352
353        if dense_results.is_empty() && sparse_results.is_empty() {
354            return Ok(Vec::new());
355        }
356        if dense_results.is_empty() {
357            let scored: Vec<(u64, f32)> = sparse_results
358                .iter()
359                .map(|sd| (sd.doc_id, sd.score))
360                .collect();
361            return Ok(self.inner.resolve_fused_results(&scored, k));
362        }
363        if sparse_results.is_empty() {
364            return Ok(self.inner.resolve_fused_results(&dense_results, k));
365        }
366
367        let sparse_tuples: Vec<(u64, f32)> = sparse_results
368            .iter()
369            .map(|sd| (sd.doc_id, sd.score))
370            .collect();
371
372        let fused = strategy
373            .fuse(vec![dense_results, sparse_tuples])
374            .map_err(|e| crate::error::Error::Config(format!("Fusion error: {e}")))?;
375
376        Ok(self.inner.resolve_fused_results(&fused, k))
377    }
378
379    /// Executes a graph MATCH query against the collection's edge store.
380    ///
381    /// # Errors
382    ///
383    /// - Returns an error if the match clause references an invalid label or property.
384    /// - Returns an error if the edge store is not initialized.
385    pub fn execute_match(
386        &self,
387        match_clause: &crate::velesql::MatchClause,
388        params: &std::collections::HashMap<String, serde_json::Value>,
389    ) -> crate::error::Result<Vec<crate::collection::search::query::match_exec::MatchResult>> {
390        self.inner.execute_match(match_clause, params)
391    }
392
393    /// Executes a MATCH query with vector similarity filtering.
394    ///
395    /// # Errors
396    ///
397    /// - Returns an error if the match clause is invalid or the query dimension mismatches.
398    pub fn execute_match_with_similarity(
399        &self,
400        match_clause: &crate::velesql::MatchClause,
401        query_vector: &[f32],
402        threshold: f32,
403        params: &std::collections::HashMap<String, serde_json::Value>,
404    ) -> crate::error::Result<Vec<crate::collection::search::query::match_exec::MatchResult>> {
405        self.inner
406            .execute_match_with_similarity(match_clause, query_vector, threshold, params)
407    }
408
409    /// Executes a MATCH query through the cost-based planner, returning ordered
410    /// [`MatchResult`](crate::collection::search::query::match_exec::MatchResult)s
411    /// with RETURN `ORDER BY`, deterministic tie-break, and post-sort LIMIT
412    /// applied — identical to the SQL `/query` path (backlog #1).
413    ///
414    /// # Errors
415    ///
416    /// - Returns an error if a guard-rail pre-check fails.
417    /// - Returns an error if traversal, ordering, or execution fails.
418    pub fn match_query_ordered(
419        &self,
420        match_clause: &crate::velesql::MatchClause,
421        params: &std::collections::HashMap<String, serde_json::Value>,
422    ) -> crate::error::Result<Vec<crate::collection::search::query::match_exec::MatchResult>> {
423        self.inner.match_query_ordered(match_clause, params)
424    }
425
426    /// Executes an aggregation query (GROUP BY / COUNT / SUM / AVG / MIN / MAX).
427    ///
428    /// # Errors
429    ///
430    /// - Returns an error if the query is invalid or aggregation computation fails.
431    pub fn execute_aggregate(
432        &self,
433        query: &crate::velesql::Query,
434        params: &std::collections::HashMap<String, serde_json::Value>,
435    ) -> Result<serde_json::Value> {
436        self.inner.execute_aggregate(query, params)
437    }
438
439    /// Executes a parsed `VelesQL` query.
440    ///
441    /// # Errors
442    ///
443    /// - Returns an error if the query references missing fields or execution fails.
444    pub fn execute_query(
445        &self,
446        query: &crate::velesql::Query,
447        params: &HashMap<String, serde_json::Value>,
448    ) -> Result<Vec<SearchResult>> {
449        self.inner.execute_query(query, params)
450    }
451
452    /// Executes a query with instrumentation and returns plan + actual stats.
453    ///
454    /// Delegates to [`crate::Database::explain_analyze_query`].
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if the query is invalid or execution fails.
459    pub fn explain_analyze_query(
460        &self,
461        query: &crate::velesql::Query,
462        params: &HashMap<String, serde_json::Value>,
463    ) -> Result<crate::velesql::ExplainOutput> {
464        self.inner.explain_analyze_query(query, params)
465    }
466
467    /// Sends a point into the streaming ingestion channel.
468    ///
469    /// Returns `Ok(())` on success (202 semantics). Returns
470    /// `BackpressureError::BufferFull` when the channel is at capacity, or
471    /// `BackpressureError::NotConfigured` if streaming is not active.
472    ///
473    /// # Errors
474    ///
475    /// Returns `BackpressureError` on buffer-full or not-configured.
476    #[cfg(feature = "persistence")]
477    pub fn stream_insert(
478        &self,
479        point: crate::point::Point,
480    ) -> std::result::Result<(), crate::collection::streaming::BackpressureError> {
481        self.inner.stream_insert(point)
482    }
483
484    /// Sends a batch of points into the streaming ingestion channel.
485    ///
486    /// Acquires the ingester lock once for the entire batch, eliminating
487    /// per-point lock overhead. Returns the number of points successfully
488    /// queued. Companion to [`Self::stream_insert`] for single-point sends.
489    ///
490    /// # Errors
491    ///
492    /// Returns `BackpressureError` on buffer-full, drain-dead, or not-configured.
493    #[cfg(feature = "persistence")]
494    pub fn stream_insert_batch(
495        &self,
496        points: Vec<crate::point::Point>,
497    ) -> std::result::Result<usize, crate::collection::streaming::BackpressureError> {
498        self.inner.stream_insert_batch(points)
499    }
500
501    /// Pushes `(id, vector)` entries into the delta buffer if it is active.
502    ///
503    /// No-op when the delta buffer is inactive. This is the public interface
504    /// used by streaming upsert handlers (e.g., NDJSON stream endpoint) to
505    /// keep the delta buffer in sync after a successful `upsert_bulk` call.
506    #[cfg(feature = "persistence")]
507    pub fn push_to_delta_if_active(&self, entries: &[(u64, Vec<f32>)]) {
508        self.inner.push_to_delta_if_active(entries);
509    }
510
511    /// Returns `true` if the delta buffer is currently active (HNSW rebuild
512    /// in progress). External callers can use this to decide whether to
513    /// snapshot entries for delta before a `upsert_bulk` call.
514    #[cfg(feature = "persistence")]
515    #[must_use]
516    pub fn is_delta_active(&self) -> bool {
517        self.inner.streaming.delta_buffer.is_active()
518    }
519
520    /// Enables streaming ingestion on this collection.
521    ///
522    /// Creates a [`StreamIngester`](crate::collection::streaming::StreamIngester) with
523    /// the given `config` and stores it internally. Points can then be submitted via
524    /// [`stream_insert`](Self::stream_insert) or [`stream_insert_batch`](Self::stream_insert_batch).
525    ///
526    /// Calling this when streaming is already active replaces the existing
527    /// ingester (the old drain task is aborted via `Drop`).
528    #[cfg(feature = "persistence")]
529    pub fn enable_streaming(&self, config: crate::collection::streaming::StreamingConfig) {
530        self.inner.enable_streaming(config);
531    }
532
533    /// Executes a raw VelesQL string, parsing it before execution.
534    ///
535    /// # Errors
536    ///
537    /// - Returns an error if the SQL string cannot be parsed.
538    /// - Returns an error if query execution fails.
539    pub fn execute_query_str(
540        &self,
541        sql: &str,
542        params: &HashMap<String, serde_json::Value>,
543    ) -> Result<Vec<SearchResult>> {
544        self.inner.execute_query_str(sql, params)
545    }
546
547    /// Reorders HNSW graph nodes in BFS traversal order for improved cache locality.
548    ///
549    /// After bulk insertion, nodes are stored in insertion order. Calling this
550    /// method once after loading vectors reorders both the vector buffer and all
551    /// adjacency lists so nodes traversed together during search are close in
552    /// memory, reducing L2/L3 cache misses by 15–30% on collections with ≥ 1 000
553    /// vectors (issue #377).
554    ///
555    /// Also builds a PDX block-columnar layout for SIMD-parallel distance
556    /// computation when the columnar search path is enabled.
557    ///
558    /// # When to call
559    ///
560    /// After [`Self::upsert`] bulk-loading for a new collection, before the
561    /// collection is opened for queries. No-op for collections with fewer than
562    /// 1 000 vectors.
563    ///
564    /// # Errors
565    ///
566    /// Returns an error if vector storage reordering fails.
567    pub fn reorder_for_locality(&self) -> Result<()> {
568        self.inner.reorder_for_locality()
569    }
570}