1pub(crate) mod bmp;
4pub(crate) mod loader;
5mod types;
6
7pub use bmp::BmpIndex;
8#[cfg(feature = "diagnostics")]
9pub use types::DimRawData;
10pub use types::{SparseIndex, VectorIndex, VectorSearchResult};
11
12#[derive(Debug, Clone, Default)]
14pub struct SegmentMemoryStats {
15 pub segment_id: u128,
17 pub num_docs: u32,
19 pub term_dict_cache_bytes: usize,
21 pub store_cache_bytes: usize,
23 pub sparse_index_bytes: usize,
25 pub dense_index_bytes: usize,
27 pub bloom_filter_bytes: usize,
29 pub pinned_metadata_bytes: u64,
31 pub pin_intended_bytes: u64,
34}
35
36impl SegmentMemoryStats {
37 pub fn total_bytes(&self) -> usize {
39 self.term_dict_cache_bytes
40 + self.store_cache_bytes
41 + self.sparse_index_bytes
42 + self.dense_index_bytes
43 + self.bloom_filter_bytes
44 }
45}
46
47use std::sync::Arc;
48
49use rustc_hash::FxHashMap;
50
51use super::vector_data::LazyFlatVectorData;
52use crate::directories::{Directory, FileHandle};
53use crate::dsl::{DenseVectorQuantization, Document, Field, Schema};
54use crate::structures::{
55 AsyncSSTableReader, BlockPostingList, CoarseCentroids, IVFPQIndex, IVFRaBitQIndex, PQCodebook,
56 RaBitQIndex, SSTableStats, TermInfo,
57};
58use crate::{DocId, Error, Result};
59
60use super::store::{AsyncStoreReader, RawStoreBlock};
61use super::types::{SegmentFiles, SegmentId, SegmentMeta};
62
63pub(crate) fn combine_ordinal_results(
69 raw: impl IntoIterator<Item = (u32, u16, f32)>,
70 combiner: crate::query::MultiValueCombiner,
71 limit: usize,
72) -> Vec<VectorSearchResult> {
73 let collected: Vec<(u32, u16, f32)> = raw.into_iter().collect();
74
75 let num_raw = collected.len();
76 if log::log_enabled!(log::Level::Debug) {
77 let mut ids: Vec<u32> = collected.iter().map(|(d, _, _)| *d).collect();
78 ids.sort_unstable();
79 ids.dedup();
80 log::debug!(
81 "combine_ordinal_results: {} raw entries, {} unique docs, combiner={:?}, limit={}",
82 num_raw,
83 ids.len(),
84 combiner,
85 limit
86 );
87 }
88
89 let all_single = collected.iter().all(|&(_, ord, _)| ord == 0);
91 if all_single {
92 let mut results: Vec<VectorSearchResult> = collected
93 .into_iter()
94 .map(|(doc_id, _, score)| VectorSearchResult::new(doc_id, score, vec![(0, score)]))
95 .collect();
96 results.sort_unstable_by(|a, b| {
97 b.score
98 .partial_cmp(&a.score)
99 .unwrap_or(std::cmp::Ordering::Equal)
100 });
101 results.truncate(limit);
102 return results;
103 }
104
105 let mut doc_ordinals: rustc_hash::FxHashMap<DocId, Vec<(u32, f32)>> =
107 rustc_hash::FxHashMap::default();
108 for (doc_id, ordinal, score) in collected {
109 doc_ordinals
110 .entry(doc_id as DocId)
111 .or_default()
112 .push((ordinal as u32, score));
113 }
114 let mut results: Vec<VectorSearchResult> = doc_ordinals
115 .into_iter()
116 .map(|(doc_id, ordinals)| {
117 let combined_score = combiner.combine(&ordinals);
118 VectorSearchResult::new(doc_id, combined_score, ordinals)
119 })
120 .collect();
121 results.sort_unstable_by(|a, b| {
122 b.score
123 .partial_cmp(&a.score)
124 .unwrap_or(std::cmp::Ordering::Equal)
125 });
126 results.truncate(limit);
127 results
128}
129
130pub struct SegmentReader {
136 meta: SegmentMeta,
137 term_dict: Arc<AsyncSSTableReader<TermInfo>>,
139 postings_handle: FileHandle,
141 store: Arc<AsyncStoreReader>,
143 schema: Arc<Schema>,
144 vector_indexes: FxHashMap<u32, VectorIndex>,
146 flat_vectors: FxHashMap<u32, LazyFlatVectorData>,
148 coarse_centroids: FxHashMap<u32, Arc<CoarseCentroids>>,
150 sparse_indexes: FxHashMap<u32, SparseIndex>,
152 bmp_indexes: FxHashMap<u32, BmpIndex>,
154 positions_handle: Option<FileHandle>,
156 fast_fields: FxHashMap<u32, crate::structures::fast_field::FastFieldReader>,
158 #[cfg(feature = "native")]
163 pin_report: crate::segment::pin::PinReport,
164}
165
166impl SegmentReader {
167 pub async fn open<D: Directory>(
169 dir: &D,
170 segment_id: SegmentId,
171 schema: Arc<Schema>,
172 cache_blocks: usize,
173 ) -> Result<Self> {
174 let files = SegmentFiles::new(segment_id.0);
175
176 let meta_slice = dir.open_read(&files.meta).await?;
178 let meta_bytes = meta_slice.read_bytes().await?;
179 let meta = SegmentMeta::deserialize(meta_bytes.as_slice())?;
180 debug_assert_eq!(meta.id, segment_id.0);
181
182 let term_dict_handle = dir.open_lazy(&files.term_dict).await?;
184 let term_dict = AsyncSSTableReader::open(term_dict_handle, cache_blocks).await?;
185
186 let postings_handle = dir.open_lazy(&files.postings).await?;
188
189 let store_handle = dir.open_lazy(&files.store).await?;
191 let store = AsyncStoreReader::open(store_handle, cache_blocks).await?;
192
193 let vectors_data = loader::load_vectors_file(dir, &files, &schema).await?;
195 let vector_indexes = vectors_data.indexes;
196 let flat_vectors = vectors_data.flat_vectors;
197
198 #[cfg(feature = "native")]
203 for (field_id, lazy_flat) in &flat_vectors {
204 if vector_indexes.contains_key(field_id) {
205 lazy_flat.advise_random_access();
206 }
207 }
208
209 let sparse_data = loader::load_sparse_file(dir, &files, meta.num_docs, &schema).await?;
211 let sparse_indexes = sparse_data.maxscore_indexes;
212 let bmp_indexes = sparse_data.bmp_indexes;
213
214 let positions_handle = loader::open_positions_file(dir, &files, &schema).await?;
216
217 let fast_fields = loader::load_fast_fields_file(dir, &files, &schema).await?;
219
220 {
222 let mut parts = vec![format!(
223 "[segment] loaded {:016x}: docs={}",
224 segment_id.0, meta.num_docs
225 )];
226 if !vector_indexes.is_empty() || !flat_vectors.is_empty() {
227 parts.push(format!(
228 "dense: {} ann + {} flat fields",
229 vector_indexes.len(),
230 flat_vectors.len()
231 ));
232 }
233 for (field_id, idx) in &sparse_indexes {
234 parts.push(format!(
235 "sparse field {}: {} dims, ~{:.1} KB",
236 field_id,
237 idx.num_dimensions(),
238 idx.num_dimensions() as f64 * 24.0 / 1024.0
239 ));
240 }
241 for (field_id, idx) in &bmp_indexes {
242 parts.push(format!(
243 "bmp field {}: {} dims, {} blocks",
244 field_id,
245 idx.dims(),
246 idx.num_blocks
247 ));
248 }
249 if !fast_fields.is_empty() {
250 parts.push(format!("fast: {} fields", fast_fields.len()));
251 }
252 log::debug!("{}", parts.join(", "));
253 }
254
255 #[allow(unused_mut)]
256 let mut reader = Self {
257 meta,
258 term_dict: Arc::new(term_dict),
259 postings_handle,
260 store: Arc::new(store),
261 schema,
262 vector_indexes,
263 flat_vectors,
264 coarse_centroids: FxHashMap::default(),
265 sparse_indexes,
266 bmp_indexes,
267 positions_handle,
268 fast_fields,
269 #[cfg(feature = "native")]
270 pin_report: Default::default(),
271 };
272
273 #[cfg(feature = "native")]
275 reader.apply_pin_policy(&crate::segment::pin::pin_policy().to_owned());
276
277 Ok(reader)
278 }
279
280 #[cfg(feature = "native")]
289 pub(crate) fn apply_pin_policy(&mut self, policy: &crate::segment::pin::PinPolicy) {
290 use crate::segment::pin::PinReport;
291
292 if !policy.is_enabled() {
293 return;
294 }
295 let mut remaining = policy.budget_bytes;
296 let mut report = PinReport::default();
297
298 for bmp in self.bmp_indexes.values_mut() {
300 bmp.pin_block_starts(policy.mode, &mut remaining, &mut report);
301 }
302 for sparse in self.sparse_indexes.values_mut() {
304 sparse.pin_skip_section(policy.mode, &mut remaining, &mut report);
305 }
306 for flat in self.flat_vectors.values_mut() {
308 flat.pin_doc_ids(policy.mode, &mut remaining, &mut report);
309 }
310 for bmp in self.bmp_indexes.values_mut() {
311 bmp.pin_doc_maps(policy.mode, &mut remaining, &mut report);
312 }
313 for bmp in self.bmp_indexes.values_mut() {
315 bmp.pin_sb_grid(policy.mode, &mut remaining, &mut report);
316 }
317
318 if report.skipped_budget_bytes > 0 || report.failed_bytes > 0 {
319 log::warn!(
320 "[pin] segment {:016x}: pinned {}/{} bytes (budget skipped {}, mlock failed {}) — raise HERMES_PIN_METADATA_BUDGET_MB or RLIMIT_MEMLOCK for full coverage",
321 self.meta.id,
322 report.pinned_bytes,
323 report.intended_bytes,
324 report.skipped_budget_bytes,
325 report.failed_bytes,
326 );
327 } else if report.pinned_bytes > 0 {
328 log::info!(
329 "[pin] segment {:016x}: pinned {} bytes of hot metadata ({:?})",
330 self.meta.id,
331 report.pinned_bytes,
332 policy.mode,
333 );
334 }
335 self.pin_report = report;
336 }
337
338 pub fn meta(&self) -> &SegmentMeta {
344 &self.meta
345 }
346
347 pub fn num_docs(&self) -> u32 {
348 self.meta.num_docs
349 }
350
351 pub fn avg_field_len(&self, field: Field) -> f32 {
353 self.meta.avg_field_len(field)
354 }
355
356 pub fn schema(&self) -> &Schema {
357 &self.schema
358 }
359
360 pub fn sparse_indexes(&self) -> &FxHashMap<u32, SparseIndex> {
362 &self.sparse_indexes
363 }
364
365 pub fn sparse_index(&self, field: Field) -> Option<&SparseIndex> {
367 self.sparse_indexes.get(&field.0)
368 }
369
370 pub fn bmp_index(&self, field: Field) -> Option<&BmpIndex> {
372 self.bmp_indexes.get(&field.0)
373 }
374
375 pub fn bmp_indexes(&self) -> &FxHashMap<u32, BmpIndex> {
377 &self.bmp_indexes
378 }
379
380 pub fn vector_indexes(&self) -> &FxHashMap<u32, VectorIndex> {
382 &self.vector_indexes
383 }
384
385 pub fn flat_vectors(&self) -> &FxHashMap<u32, LazyFlatVectorData> {
387 &self.flat_vectors
388 }
389
390 pub fn fast_field(
392 &self,
393 field_id: u32,
394 ) -> Option<&crate::structures::fast_field::FastFieldReader> {
395 self.fast_fields.get(&field_id)
396 }
397
398 pub fn fast_fields(&self) -> &FxHashMap<u32, crate::structures::fast_field::FastFieldReader> {
400 &self.fast_fields
401 }
402
403 pub fn term_dict_stats(&self) -> SSTableStats {
405 self.term_dict.stats()
406 }
407
408 pub fn memory_stats(&self) -> SegmentMemoryStats {
410 let term_dict_stats = self.term_dict.stats();
411
412 let term_dict_cache_bytes = self.term_dict.cached_blocks() * 4096;
414
415 let store_cache_bytes = self.store.cached_blocks() * 4096;
417
418 let sparse_index_bytes: usize = self
420 .sparse_indexes
421 .values()
422 .map(|s| s.estimated_memory_bytes())
423 .sum::<usize>()
424 + self
425 .bmp_indexes
426 .values()
427 .map(|b| b.estimated_memory_bytes())
428 .sum::<usize>();
429
430 let dense_index_bytes: usize = self
433 .vector_indexes
434 .values()
435 .map(|v| v.estimated_memory_bytes())
436 .sum();
437
438 #[cfg(feature = "native")]
439 let (pinned_metadata_bytes, pin_intended_bytes) =
440 (self.pin_report.pinned_bytes, self.pin_report.intended_bytes);
441 #[cfg(not(feature = "native"))]
442 let (pinned_metadata_bytes, pin_intended_bytes) = (0u64, 0u64);
443
444 SegmentMemoryStats {
445 segment_id: self.meta.id,
446 num_docs: self.meta.num_docs,
447 term_dict_cache_bytes,
448 store_cache_bytes,
449 sparse_index_bytes,
450 dense_index_bytes,
451 bloom_filter_bytes: term_dict_stats.bloom_filter_size,
452 pinned_metadata_bytes,
453 pin_intended_bytes,
454 }
455 }
456
457 pub async fn get_postings(
462 &self,
463 field: Field,
464 term: &[u8],
465 ) -> Result<Option<BlockPostingList>> {
466 log::debug!(
467 "SegmentReader::get_postings field={} term_len={}",
468 field.0,
469 term.len()
470 );
471
472 let mut key = Vec::with_capacity(4 + term.len());
474 key.extend_from_slice(&field.0.to_le_bytes());
475 key.extend_from_slice(term);
476
477 let term_info = match self.term_dict.get(&key).await? {
479 Some(info) => {
480 log::debug!("SegmentReader::get_postings found term_info");
481 info
482 }
483 None => {
484 log::debug!("SegmentReader::get_postings term not found");
485 return Ok(None);
486 }
487 };
488
489 if let Some((doc_ids, term_freqs)) = term_info.decode_inline() {
491 let mut posting_list = crate::structures::PostingList::with_capacity(doc_ids.len());
493 for (doc_id, tf) in doc_ids.into_iter().zip(term_freqs) {
494 posting_list.push(doc_id, tf);
495 }
496 let block_list = BlockPostingList::from_posting_list(&posting_list)?;
497 return Ok(Some(block_list));
498 }
499
500 let (posting_offset, posting_len) = term_info.external_info().ok_or_else(|| {
502 Error::Corruption("TermInfo has neither inline nor external data".to_string())
503 })?;
504
505 let start = posting_offset;
506 let end = start + posting_len;
507
508 if end > self.postings_handle.len() {
509 return Err(Error::Corruption(
510 "Posting offset out of bounds".to_string(),
511 ));
512 }
513
514 let posting_bytes = self.postings_handle.read_bytes_range(start..end).await?;
515 let block_list = BlockPostingList::deserialize_zero_copy(posting_bytes)?;
516
517 Ok(Some(block_list))
518 }
519
520 pub async fn get_prefix_postings(
522 &self,
523 field: Field,
524 prefix: &[u8],
525 ) -> Result<Vec<BlockPostingList>> {
526 let mut key_prefix = Vec::with_capacity(4 + prefix.len());
528 key_prefix.extend_from_slice(&field.0.to_le_bytes());
529 key_prefix.extend_from_slice(prefix);
530
531 let entries = self.term_dict.prefix_scan(&key_prefix).await?;
532 let mut results = Vec::with_capacity(entries.len());
533
534 for (_key, term_info) in entries {
535 if let Some((doc_ids, term_freqs)) = term_info.decode_inline() {
536 let mut posting_list = crate::structures::PostingList::with_capacity(doc_ids.len());
537 for (doc_id, tf) in doc_ids.into_iter().zip(term_freqs) {
538 posting_list.push(doc_id, tf);
539 }
540 results.push(BlockPostingList::from_posting_list(&posting_list)?);
541 } else if let Some((posting_offset, posting_len)) = term_info.external_info() {
542 let start = posting_offset;
543 let end = start + posting_len;
544 if end > self.postings_handle.len() {
545 continue;
546 }
547 let posting_bytes = self.postings_handle.read_bytes_range(start..end).await?;
548 results.push(BlockPostingList::deserialize_zero_copy(posting_bytes)?);
549 }
550 }
551
552 Ok(results)
553 }
554
555 pub async fn doc(&self, local_doc_id: DocId) -> Result<Option<Document>> {
560 self.doc_with_fields(local_doc_id, None).await
561 }
562
563 pub async fn doc_with_fields(
569 &self,
570 local_doc_id: DocId,
571 fields: Option<&rustc_hash::FxHashSet<u32>>,
572 ) -> Result<Option<Document>> {
573 let mut doc = match fields {
574 Some(set) => {
575 let field_ids: Vec<u32> = set.iter().copied().collect();
576 match self
577 .store
578 .get_fields(local_doc_id, &self.schema, &field_ids)
579 .await
580 {
581 Ok(Some(d)) => d,
582 Ok(None) => return Ok(None),
583 Err(e) => return Err(Error::from(e)),
584 }
585 }
586 None => match self.store.get(local_doc_id, &self.schema).await {
587 Ok(Some(d)) => d,
588 Ok(None) => return Ok(None),
589 Err(e) => return Err(Error::from(e)),
590 },
591 };
592
593 for (&field_id, lazy_flat) in &self.flat_vectors {
595 if let Some(set) = fields
597 && !set.contains(&field_id)
598 {
599 continue;
600 }
601
602 let is_binary = lazy_flat.quantization == DenseVectorQuantization::Binary;
603 let (start, entries) = lazy_flat.flat_indexes_for_doc(local_doc_id);
604 for (j, &(_doc_id, _ordinal)) in entries.iter().enumerate() {
605 let flat_idx = start + j;
606 if is_binary {
607 let vbs = lazy_flat.vector_byte_size();
608 let mut raw = vec![0u8; vbs];
609 match lazy_flat.read_vector_raw_into(flat_idx, &mut raw).await {
610 Ok(()) => {
611 doc.add_binary_dense_vector(Field(field_id), raw);
612 }
613 Err(e) => {
614 log::warn!("Failed to hydrate binary vector field {}: {}", field_id, e);
615 }
616 }
617 } else {
618 match lazy_flat.get_vector(flat_idx).await {
619 Ok(vec) => {
620 doc.add_dense_vector(Field(field_id), vec);
621 }
622 Err(e) => {
623 log::warn!("Failed to hydrate vector field {}: {}", field_id, e);
624 }
625 }
626 }
627 }
628 }
629
630 Ok(Some(doc))
631 }
632
633 pub async fn prefetch_terms(
635 &self,
636 field: Field,
637 start_term: &[u8],
638 end_term: &[u8],
639 ) -> Result<()> {
640 let mut start_key = Vec::with_capacity(4 + start_term.len());
641 start_key.extend_from_slice(&field.0.to_le_bytes());
642 start_key.extend_from_slice(start_term);
643
644 let mut end_key = Vec::with_capacity(4 + end_term.len());
645 end_key.extend_from_slice(&field.0.to_le_bytes());
646 end_key.extend_from_slice(end_term);
647
648 self.term_dict.prefetch_range(&start_key, &end_key).await?;
649 Ok(())
650 }
651
652 pub fn store_has_dict(&self) -> bool {
654 self.store.has_dict()
655 }
656
657 pub fn store(&self) -> &super::store::AsyncStoreReader {
659 &self.store
660 }
661
662 pub fn store_raw_blocks(&self) -> Vec<RawStoreBlock> {
664 self.store.raw_blocks()
665 }
666
667 pub fn store_data_slice(&self) -> &FileHandle {
669 self.store.data_slice()
670 }
671
672 pub async fn all_terms(&self) -> Result<Vec<(Vec<u8>, TermInfo)>> {
674 self.term_dict.all_entries().await.map_err(Error::from)
675 }
676
677 pub async fn all_terms_with_stats(&self) -> Result<Vec<(Field, String, u32)>> {
682 let entries = self.term_dict.all_entries().await?;
683 let mut result = Vec::with_capacity(entries.len());
684
685 for (key, term_info) in entries {
686 if key.len() > 4 {
688 let field_id = u32::from_le_bytes([key[0], key[1], key[2], key[3]]);
689 let term_bytes = &key[4..];
690 if let Ok(term_str) = std::str::from_utf8(term_bytes) {
691 result.push((Field(field_id), term_str.to_string(), term_info.doc_freq()));
692 }
693 }
694 }
695
696 Ok(result)
697 }
698
699 pub fn term_dict_iter(&self) -> crate::structures::AsyncSSTableIterator<'_, TermInfo> {
701 self.term_dict.iter()
702 }
703
704 pub async fn prefetch_term_dict(&self) -> crate::Result<()> {
708 self.term_dict
709 .prefetch_all_data_bulk()
710 .await
711 .map_err(crate::Error::from)
712 }
713
714 pub async fn read_postings(&self, offset: u64, len: u64) -> Result<Vec<u8>> {
716 let start = offset;
717 let end = start + len;
718 let bytes = self.postings_handle.read_bytes_range(start..end).await?;
719 Ok(bytes.to_vec())
720 }
721
722 pub async fn read_position_bytes(&self, offset: u64, len: u64) -> Result<Option<Vec<u8>>> {
724 let handle = match &self.positions_handle {
725 Some(h) => h,
726 None => return Ok(None),
727 };
728 let start = offset;
729 let end = start + len;
730 let bytes = handle.read_bytes_range(start..end).await?;
731 Ok(Some(bytes.to_vec()))
732 }
733
734 pub fn has_positions_file(&self) -> bool {
736 self.positions_handle.is_some()
737 }
738
739 fn score_quantized_batch(
745 query: &[f32],
746 raw: &[u8],
747 quant: crate::dsl::DenseVectorQuantization,
748 dim: usize,
749 scores: &mut [f32],
750 unit_norm: bool,
751 ) {
752 use crate::dsl::DenseVectorQuantization;
753 use crate::structures::simd;
754 match (quant, unit_norm) {
755 (DenseVectorQuantization::F32, false) => {
756 let num_floats = scores.len() * dim;
757 debug_assert!(
758 (raw.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>()),
759 "f32 vector data not 4-byte aligned — vectors file may use legacy format"
760 );
761 let vectors: &[f32] =
762 unsafe { std::slice::from_raw_parts(raw.as_ptr() as *const f32, num_floats) };
763 simd::batch_cosine_scores(query, vectors, dim, scores);
764 }
765 (DenseVectorQuantization::F32, true) => {
766 let num_floats = scores.len() * dim;
767 debug_assert!(
768 (raw.as_ptr() as usize).is_multiple_of(std::mem::align_of::<f32>()),
769 "f32 vector data not 4-byte aligned"
770 );
771 let vectors: &[f32] =
772 unsafe { std::slice::from_raw_parts(raw.as_ptr() as *const f32, num_floats) };
773 simd::batch_dot_scores(query, vectors, dim, scores);
774 }
775 (DenseVectorQuantization::F16, false) => {
776 simd::batch_cosine_scores_f16(query, raw, dim, scores);
777 }
778 (DenseVectorQuantization::F16, true) => {
779 simd::batch_dot_scores_f16(query, raw, dim, scores);
780 }
781 (DenseVectorQuantization::UInt8, false) => {
782 simd::batch_cosine_scores_u8(query, raw, dim, scores);
783 }
784 (DenseVectorQuantization::UInt8, true) => {
785 simd::batch_dot_scores_u8(query, raw, dim, scores);
786 }
787 (DenseVectorQuantization::Binary, _) => {
788 unreachable!("Binary quantization should not reach score_quantized_batch");
790 }
791 }
792 }
793
794 pub async fn search_dense_vector(
800 &self,
801 field: Field,
802 query: &[f32],
803 k: usize,
804 nprobe: usize,
805 rerank_factor: f32,
806 combiner: crate::query::MultiValueCombiner,
807 ) -> Result<Vec<VectorSearchResult>> {
808 let ann_index = self.vector_indexes.get(&field.0);
809 let lazy_flat = self.flat_vectors.get(&field.0);
810
811 if ann_index.is_none() && lazy_flat.is_none() {
813 return Ok(Vec::new());
814 }
815
816 let unit_norm = self
818 .schema
819 .get_field_entry(field)
820 .and_then(|e| e.dense_vector_config.as_ref())
821 .is_some_and(|c| c.unit_norm);
822
823 const BRUTE_FORCE_BATCH: usize = 4096;
825
826 let fetch_k = (k as f32 * rerank_factor.max(1.0)).ceil() as usize;
827
828 let t0 = std::time::Instant::now();
830 let mut results: Vec<(u32, u16, f32)> = if let Some(index) = ann_index {
831 match index {
833 VectorIndex::RaBitQ(lazy) => {
834 let rabitq = lazy.get().ok_or_else(|| {
835 Error::Schema("RaBitQ index deserialization failed".to_string())
836 })?;
837 rabitq
838 .search(query, fetch_k)
839 .into_iter()
840 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
841 .collect()
842 }
843 VectorIndex::IVF(lazy) => {
844 let (index, codebook) = lazy.get().ok_or_else(|| {
845 Error::Schema("IVF index deserialization failed".to_string())
846 })?;
847 let centroids = self.coarse_centroids.get(&field.0).ok_or_else(|| {
848 Error::Schema(format!(
849 "IVF index requires coarse centroids for field {}",
850 field.0
851 ))
852 })?;
853 let effective_nprobe = if nprobe > 0 { nprobe } else { 32 };
854 index
855 .search(centroids, codebook, query, fetch_k, Some(effective_nprobe))
856 .into_iter()
857 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
858 .collect()
859 }
860 VectorIndex::ScaNN(lazy) => {
861 let (index, codebook) = lazy.get().ok_or_else(|| {
862 Error::Schema("ScaNN index deserialization failed".to_string())
863 })?;
864 let centroids = self.coarse_centroids.get(&field.0).ok_or_else(|| {
865 Error::Schema(format!(
866 "ScaNN index requires coarse centroids for field {}",
867 field.0
868 ))
869 })?;
870 let effective_nprobe = if nprobe > 0 { nprobe } else { 32 };
871 index
872 .search(centroids, codebook, query, fetch_k, Some(effective_nprobe))
873 .into_iter()
874 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
875 .collect()
876 }
877 VectorIndex::BinaryIvf(_) => {
878 Vec::new()
880 }
881 }
882 } else if let Some(lazy_flat) = lazy_flat {
883 log::debug!(
886 "[search_dense] field {}: brute-force on {} vectors (dim={}, quant={:?})",
887 field.0,
888 lazy_flat.num_vectors,
889 lazy_flat.dim,
890 lazy_flat.quantization
891 );
892 let dim = lazy_flat.dim;
893 let n = lazy_flat.num_vectors;
894 let quant = lazy_flat.quantization;
895 let mut collector = crate::query::ScoreCollector::new(fetch_k);
896 let mut scores = vec![0f32; BRUTE_FORCE_BATCH];
897
898 for batch_start in (0..n).step_by(BRUTE_FORCE_BATCH) {
899 let batch_count = BRUTE_FORCE_BATCH.min(n - batch_start);
900 let batch_bytes = lazy_flat
901 .read_vectors_batch(batch_start, batch_count)
902 .await
903 .map_err(crate::Error::Io)?;
904 let raw = batch_bytes.as_slice();
905
906 Self::score_quantized_batch(
907 query,
908 raw,
909 quant,
910 dim,
911 &mut scores[..batch_count],
912 unit_norm,
913 );
914
915 for (i, &score) in scores.iter().enumerate().take(batch_count) {
916 let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
917 collector.insert_with_ordinal(doc_id, score, ordinal);
918 }
919 }
920
921 collector
922 .into_sorted_results()
923 .into_iter()
924 .map(|(doc_id, score, ordinal)| (doc_id, ordinal, score))
925 .collect()
926 } else {
927 return Ok(Vec::new());
928 };
929 let l1_elapsed = t0.elapsed();
930 {
931 let kind = match ann_index {
932 Some(VectorIndex::RaBitQ(_)) => "rabitq",
933 Some(VectorIndex::IVF(_)) => "ivf_rabitq",
934 Some(VectorIndex::ScaNN(_)) => "scann",
935 Some(VectorIndex::BinaryIvf(_)) => "binary_ivf",
936 None => "flat",
937 };
938 crate::observe::dense_l1(field.0, kind, l1_elapsed.as_secs_f64(), results.len());
939 }
940 log::debug!(
941 "[search_dense] field {}: L1 returned {} candidates in {:.1}ms",
942 field.0,
943 results.len(),
944 l1_elapsed.as_secs_f64() * 1000.0
945 );
946
947 if ann_index.is_some()
950 && !results.is_empty()
951 && let Some(lazy_flat) = lazy_flat
952 {
953 let t_rerank = std::time::Instant::now();
954 let dim = lazy_flat.dim;
955 let quant = lazy_flat.quantization;
956 let vbs = lazy_flat.vector_byte_size();
957
958 let mut resolved: Vec<(usize, usize)> = Vec::new(); for (ri, c) in results.iter().enumerate() {
961 let (start, entries) = lazy_flat.flat_indexes_for_doc(c.0);
962 for (j, &(_, ord)) in entries.iter().enumerate() {
963 if ord == c.1 {
964 resolved.push((ri, start + j));
965 break;
966 }
967 }
968 }
969
970 let t_resolve = t_rerank.elapsed();
971 if !resolved.is_empty() {
972 resolved.sort_unstable_by_key(|&(_, flat_idx)| flat_idx);
974
975 #[cfg(feature = "native")]
980 lazy_flat.prefetch_vectors(resolved.iter().map(|&(_, flat_idx)| flat_idx));
981
982 let t_read = std::time::Instant::now();
984 let mut raw_buf = vec![0u8; resolved.len() * vbs];
985 for (buf_idx, &(_, flat_idx)) in resolved.iter().enumerate() {
986 let _ = lazy_flat
987 .read_vector_raw_into(
988 flat_idx,
989 &mut raw_buf[buf_idx * vbs..(buf_idx + 1) * vbs],
990 )
991 .await;
992 }
993
994 let read_elapsed = t_read.elapsed();
995
996 let t_score = std::time::Instant::now();
998 let mut scores = vec![0f32; resolved.len()];
999 Self::score_quantized_batch(query, &raw_buf, quant, dim, &mut scores, unit_norm);
1000 let score_elapsed = t_score.elapsed();
1001
1002 for (buf_idx, &(ri, _)) in resolved.iter().enumerate() {
1004 results[ri].2 = scores[buf_idx];
1005 }
1006
1007 crate::observe::dense_rerank(
1008 field.0,
1009 t_rerank.elapsed().as_secs_f64(),
1010 t_resolve.as_secs_f64(),
1011 read_elapsed.as_secs_f64(),
1012 resolved.len(),
1013 );
1014 log::debug!(
1015 "[search_dense] field {}: rerank {} vectors (dim={}, quant={:?}, {}B/vec): resolve={:.1}ms read={:.1}ms score={:.1}ms",
1016 field.0,
1017 resolved.len(),
1018 dim,
1019 quant,
1020 vbs,
1021 t_resolve.as_secs_f64() * 1000.0,
1022 read_elapsed.as_secs_f64() * 1000.0,
1023 score_elapsed.as_secs_f64() * 1000.0,
1024 );
1025 }
1026
1027 if results.len() > fetch_k {
1028 results.select_nth_unstable_by(fetch_k, |a, b| b.2.total_cmp(&a.2));
1029 results.truncate(fetch_k);
1030 }
1031 results.sort_unstable_by(|a, b| b.2.total_cmp(&a.2));
1032 log::debug!(
1033 "[search_dense] field {}: rerank total={:.1}ms",
1034 field.0,
1035 t_rerank.elapsed().as_secs_f64() * 1000.0
1036 );
1037 }
1038
1039 Ok(combine_ordinal_results(results, combiner, k))
1040 }
1041
1042 fn binary_ivf_nprobe(&self, field: Field) -> Option<usize> {
1044 self.schema
1045 .get_field_entry(field)
1046 .and_then(|e| e.binary_dense_vector_config.as_ref())
1047 .map(|c| c.nprobe)
1048 .filter(|&n| n > 0)
1049 }
1050
1051 pub async fn search_binary_dense_vector(
1055 &self,
1056 field: Field,
1057 query: &[u8],
1058 k: usize,
1059 combiner: crate::query::MultiValueCombiner,
1060 ) -> Result<Vec<VectorSearchResult>> {
1061 if let Some(VectorIndex::BinaryIvf(lazy)) = self.vector_indexes.get(&field.0)
1064 && let Some(ivf) = lazy.get()
1065 {
1066 let nprobe = self.binary_ivf_nprobe(field);
1067 let results = ivf.search(query, k, nprobe);
1068 return Ok(combine_ordinal_results(results, combiner, k));
1069 }
1070
1071 let lazy_flat = match self.flat_vectors.get(&field.0) {
1072 Some(f) => f,
1073 None => return Ok(Vec::new()),
1074 };
1075
1076 const BRUTE_FORCE_BATCH: usize = 8192; let dim_bits = lazy_flat.dim;
1079 let byte_len = lazy_flat.vector_byte_size();
1080 let n = lazy_flat.num_vectors;
1081
1082 if byte_len != query.len() {
1083 return Err(Error::Schema(format!(
1084 "Binary query vector byte length {} != field byte length {}",
1085 query.len(),
1086 byte_len
1087 )));
1088 }
1089
1090 let mut collector = crate::query::ScoreCollector::new(k);
1091 let mut scores = vec![0f32; BRUTE_FORCE_BATCH];
1092
1093 for batch_start in (0..n).step_by(BRUTE_FORCE_BATCH) {
1094 let batch_count = BRUTE_FORCE_BATCH.min(n - batch_start);
1095 let batch_bytes = lazy_flat
1096 .read_vectors_batch(batch_start, batch_count)
1097 .await
1098 .map_err(crate::Error::Io)?;
1099 let raw = batch_bytes.as_slice();
1100
1101 crate::structures::simd::batch_hamming_scores(
1102 query,
1103 raw,
1104 byte_len,
1105 dim_bits,
1106 &mut scores[..batch_count],
1107 );
1108
1109 let threshold = collector.threshold();
1110 for (i, &score) in scores.iter().enumerate().take(batch_count) {
1111 if score > threshold {
1112 let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
1113 collector.insert_with_ordinal(doc_id, score, ordinal);
1114 }
1115 }
1116 }
1117
1118 let results: Vec<(u32, u16, f32)> = collector
1119 .into_sorted_results()
1120 .into_iter()
1121 .map(|(doc_id, score, ordinal)| (doc_id, ordinal, score))
1122 .collect();
1123
1124 Ok(combine_ordinal_results(results, combiner, k))
1125 }
1126
1127 pub fn has_dense_vector_index(&self, field: Field) -> bool {
1129 self.vector_indexes.contains_key(&field.0) || self.flat_vectors.contains_key(&field.0)
1130 }
1131
1132 pub fn get_dense_vector_index(&self, field: Field) -> Option<Arc<RaBitQIndex>> {
1134 match self.vector_indexes.get(&field.0) {
1135 Some(VectorIndex::RaBitQ(lazy)) => lazy.get().cloned(),
1136 _ => None,
1137 }
1138 }
1139
1140 pub fn get_ivf_vector_index(
1142 &self,
1143 field: Field,
1144 ) -> Option<(Arc<IVFRaBitQIndex>, Arc<crate::structures::RaBitQCodebook>)> {
1145 match self.vector_indexes.get(&field.0) {
1146 Some(VectorIndex::IVF(lazy)) => lazy.get().map(|(i, c)| (i.clone(), c.clone())),
1147 _ => None,
1148 }
1149 }
1150
1151 pub fn coarse_centroids(&self, field_id: u32) -> Option<&Arc<CoarseCentroids>> {
1153 self.coarse_centroids.get(&field_id)
1154 }
1155
1156 pub fn set_coarse_centroids(&mut self, centroids: FxHashMap<u32, Arc<CoarseCentroids>>) {
1158 self.coarse_centroids = centroids;
1159 }
1160
1161 pub fn get_scann_vector_index(
1163 &self,
1164 field: Field,
1165 ) -> Option<(Arc<IVFPQIndex>, Arc<PQCodebook>)> {
1166 match self.vector_indexes.get(&field.0) {
1167 Some(VectorIndex::ScaNN(lazy)) => lazy.get().map(|(i, c)| (i.clone(), c.clone())),
1168 _ => None,
1169 }
1170 }
1171
1172 pub fn get_vector_index(&self, field: Field) -> Option<&VectorIndex> {
1174 self.vector_indexes.get(&field.0)
1175 }
1176
1177 pub async fn get_positions(
1182 &self,
1183 field: Field,
1184 term: &[u8],
1185 ) -> Result<Option<crate::structures::PositionPostingList>> {
1186 let handle = match &self.positions_handle {
1188 Some(h) => h,
1189 None => return Ok(None),
1190 };
1191
1192 let mut key = Vec::with_capacity(4 + term.len());
1194 key.extend_from_slice(&field.0.to_le_bytes());
1195 key.extend_from_slice(term);
1196
1197 let term_info = match self.term_dict.get(&key).await? {
1199 Some(info) => info,
1200 None => return Ok(None),
1201 };
1202
1203 let (offset, length) = match term_info.position_info() {
1205 Some((o, l)) => (o, l),
1206 None => return Ok(None),
1207 };
1208
1209 let slice = handle.slice(offset..offset + length);
1211 let data = slice.read_bytes().await?;
1212
1213 let pos_list = crate::structures::PositionPostingList::deserialize(data.as_slice())?;
1215
1216 Ok(Some(pos_list))
1217 }
1218
1219 pub fn has_positions(&self, field: Field) -> bool {
1221 if let Some(entry) = self.schema.get_field_entry(field) {
1223 entry.positions.is_some()
1224 } else {
1225 false
1226 }
1227 }
1228}
1229
1230#[cfg(feature = "sync")]
1232impl SegmentReader {
1233 pub fn get_postings_sync(&self, field: Field, term: &[u8]) -> Result<Option<BlockPostingList>> {
1235 let mut key = Vec::with_capacity(4 + term.len());
1237 key.extend_from_slice(&field.0.to_le_bytes());
1238 key.extend_from_slice(term);
1239
1240 let term_info = match self.term_dict.get_sync(&key)? {
1242 Some(info) => info,
1243 None => return Ok(None),
1244 };
1245
1246 if let Some((doc_ids, term_freqs)) = term_info.decode_inline() {
1248 let mut posting_list = crate::structures::PostingList::with_capacity(doc_ids.len());
1249 for (doc_id, tf) in doc_ids.into_iter().zip(term_freqs) {
1250 posting_list.push(doc_id, tf);
1251 }
1252 let block_list = BlockPostingList::from_posting_list(&posting_list)?;
1253 return Ok(Some(block_list));
1254 }
1255
1256 let (posting_offset, posting_len) = term_info.external_info().ok_or_else(|| {
1258 Error::Corruption("TermInfo has neither inline nor external data".to_string())
1259 })?;
1260
1261 let start = posting_offset;
1262 let end = start + posting_len;
1263
1264 if end > self.postings_handle.len() {
1265 return Err(Error::Corruption(
1266 "Posting offset out of bounds".to_string(),
1267 ));
1268 }
1269
1270 let posting_bytes = self.postings_handle.read_bytes_range_sync(start..end)?;
1271 let block_list = BlockPostingList::deserialize_zero_copy(posting_bytes)?;
1272
1273 Ok(Some(block_list))
1274 }
1275
1276 pub fn get_prefix_postings_sync(
1278 &self,
1279 field: Field,
1280 prefix: &[u8],
1281 ) -> Result<Vec<BlockPostingList>> {
1282 let mut key_prefix = Vec::with_capacity(4 + prefix.len());
1283 key_prefix.extend_from_slice(&field.0.to_le_bytes());
1284 key_prefix.extend_from_slice(prefix);
1285
1286 let entries = self.term_dict.prefix_scan_sync(&key_prefix)?;
1287 let mut results = Vec::with_capacity(entries.len());
1288
1289 for (_key, term_info) in entries {
1290 if let Some((doc_ids, term_freqs)) = term_info.decode_inline() {
1291 let mut posting_list = crate::structures::PostingList::with_capacity(doc_ids.len());
1292 for (doc_id, tf) in doc_ids.into_iter().zip(term_freqs) {
1293 posting_list.push(doc_id, tf);
1294 }
1295 results.push(BlockPostingList::from_posting_list(&posting_list)?);
1296 } else if let Some((posting_offset, posting_len)) = term_info.external_info() {
1297 let start = posting_offset;
1298 let end = start + posting_len;
1299 if end > self.postings_handle.len() {
1300 continue;
1301 }
1302 let posting_bytes = self.postings_handle.read_bytes_range_sync(start..end)?;
1303 results.push(BlockPostingList::deserialize_zero_copy(posting_bytes)?);
1304 }
1305 }
1306
1307 Ok(results)
1308 }
1309
1310 pub fn get_positions_sync(
1312 &self,
1313 field: Field,
1314 term: &[u8],
1315 ) -> Result<Option<crate::structures::PositionPostingList>> {
1316 let handle = match &self.positions_handle {
1317 Some(h) => h,
1318 None => return Ok(None),
1319 };
1320
1321 let mut key = Vec::with_capacity(4 + term.len());
1323 key.extend_from_slice(&field.0.to_le_bytes());
1324 key.extend_from_slice(term);
1325
1326 let term_info = match self.term_dict.get_sync(&key)? {
1328 Some(info) => info,
1329 None => return Ok(None),
1330 };
1331
1332 let (offset, length) = match term_info.position_info() {
1333 Some((o, l)) => (o, l),
1334 None => return Ok(None),
1335 };
1336
1337 let slice = handle.slice(offset..offset + length);
1338 let data = slice.read_bytes_sync()?;
1339
1340 let pos_list = crate::structures::PositionPostingList::deserialize(data.as_slice())?;
1341 Ok(Some(pos_list))
1342 }
1343
1344 pub fn search_dense_vector_sync(
1347 &self,
1348 field: Field,
1349 query: &[f32],
1350 k: usize,
1351 nprobe: usize,
1352 rerank_factor: f32,
1353 combiner: crate::query::MultiValueCombiner,
1354 ) -> Result<Vec<VectorSearchResult>> {
1355 let ann_index = self.vector_indexes.get(&field.0);
1356 let lazy_flat = self.flat_vectors.get(&field.0);
1357
1358 if ann_index.is_none() && lazy_flat.is_none() {
1359 return Ok(Vec::new());
1360 }
1361
1362 let unit_norm = self
1363 .schema
1364 .get_field_entry(field)
1365 .and_then(|e| e.dense_vector_config.as_ref())
1366 .is_some_and(|c| c.unit_norm);
1367
1368 const BRUTE_FORCE_BATCH: usize = 4096;
1369 let fetch_k = (k as f32 * rerank_factor.max(1.0)).ceil() as usize;
1370
1371 let mut results: Vec<(u32, u16, f32)> = if let Some(index) = ann_index {
1372 match index {
1374 VectorIndex::RaBitQ(lazy) => {
1375 let rabitq = lazy.get().ok_or_else(|| {
1376 Error::Schema("RaBitQ index deserialization failed".to_string())
1377 })?;
1378 rabitq
1379 .search(query, fetch_k)
1380 .into_iter()
1381 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
1382 .collect()
1383 }
1384 VectorIndex::IVF(lazy) => {
1385 let (index, codebook) = lazy.get().ok_or_else(|| {
1386 Error::Schema("IVF index deserialization failed".to_string())
1387 })?;
1388 let centroids = self.coarse_centroids.get(&field.0).ok_or_else(|| {
1389 Error::Schema(format!(
1390 "IVF index requires coarse centroids for field {}",
1391 field.0
1392 ))
1393 })?;
1394 let effective_nprobe = if nprobe > 0 { nprobe } else { 32 };
1395 index
1396 .search(centroids, codebook, query, fetch_k, Some(effective_nprobe))
1397 .into_iter()
1398 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
1399 .collect()
1400 }
1401 VectorIndex::ScaNN(lazy) => {
1402 let (index, codebook) = lazy.get().ok_or_else(|| {
1403 Error::Schema("ScaNN index deserialization failed".to_string())
1404 })?;
1405 let centroids = self.coarse_centroids.get(&field.0).ok_or_else(|| {
1406 Error::Schema(format!(
1407 "ScaNN index requires coarse centroids for field {}",
1408 field.0
1409 ))
1410 })?;
1411 let effective_nprobe = if nprobe > 0 { nprobe } else { 32 };
1412 index
1413 .search(centroids, codebook, query, fetch_k, Some(effective_nprobe))
1414 .into_iter()
1415 .map(|(doc_id, ordinal, dist)| (doc_id, ordinal, 1.0 / (1.0 + dist)))
1416 .collect()
1417 }
1418 VectorIndex::BinaryIvf(_) => {
1419 Vec::new()
1421 }
1422 }
1423 } else if let Some(lazy_flat) = lazy_flat {
1424 let dim = lazy_flat.dim;
1426 let n = lazy_flat.num_vectors;
1427 let quant = lazy_flat.quantization;
1428 let mut collector = crate::query::ScoreCollector::new(fetch_k);
1429 let mut scores = vec![0f32; BRUTE_FORCE_BATCH];
1430
1431 for batch_start in (0..n).step_by(BRUTE_FORCE_BATCH) {
1432 let batch_count = BRUTE_FORCE_BATCH.min(n - batch_start);
1433 let batch_bytes = lazy_flat
1434 .read_vectors_batch_sync(batch_start, batch_count)
1435 .map_err(crate::Error::Io)?;
1436 let raw = batch_bytes.as_slice();
1437
1438 Self::score_quantized_batch(
1439 query,
1440 raw,
1441 quant,
1442 dim,
1443 &mut scores[..batch_count],
1444 unit_norm,
1445 );
1446
1447 for (i, &score) in scores.iter().enumerate().take(batch_count) {
1448 let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
1449 collector.insert_with_ordinal(doc_id, score, ordinal);
1450 }
1451 }
1452
1453 collector
1454 .into_sorted_results()
1455 .into_iter()
1456 .map(|(doc_id, score, ordinal)| (doc_id, ordinal, score))
1457 .collect()
1458 } else {
1459 return Ok(Vec::new());
1460 };
1461
1462 if ann_index.is_some()
1464 && !results.is_empty()
1465 && let Some(lazy_flat) = lazy_flat
1466 {
1467 let dim = lazy_flat.dim;
1468 let quant = lazy_flat.quantization;
1469 let vbs = lazy_flat.vector_byte_size();
1470
1471 let mut resolved: Vec<(usize, usize)> = Vec::new();
1472 for (ri, c) in results.iter().enumerate() {
1473 let (start, entries) = lazy_flat.flat_indexes_for_doc(c.0);
1474 for (j, &(_, ord)) in entries.iter().enumerate() {
1475 if ord == c.1 {
1476 resolved.push((ri, start + j));
1477 break;
1478 }
1479 }
1480 }
1481
1482 if !resolved.is_empty() {
1483 resolved.sort_unstable_by_key(|&(_, flat_idx)| flat_idx);
1484 let mut raw_buf = vec![0u8; resolved.len() * vbs];
1485 for (buf_idx, &(_, flat_idx)) in resolved.iter().enumerate() {
1486 let _ = lazy_flat.read_vector_raw_into_sync(
1487 flat_idx,
1488 &mut raw_buf[buf_idx * vbs..(buf_idx + 1) * vbs],
1489 );
1490 }
1491
1492 let mut scores = vec![0f32; resolved.len()];
1493 Self::score_quantized_batch(query, &raw_buf, quant, dim, &mut scores, unit_norm);
1494
1495 for (buf_idx, &(ri, _)) in resolved.iter().enumerate() {
1496 results[ri].2 = scores[buf_idx];
1497 }
1498 }
1499
1500 if results.len() > fetch_k {
1501 results.select_nth_unstable_by(fetch_k, |a, b| b.2.total_cmp(&a.2));
1502 results.truncate(fetch_k);
1503 }
1504 results.sort_unstable_by(|a, b| b.2.total_cmp(&a.2));
1505 }
1506
1507 Ok(combine_ordinal_results(results, combiner, k))
1508 }
1509
1510 #[cfg(feature = "sync")]
1515 pub fn search_binary_dense_vector_sync(
1516 &self,
1517 field: Field,
1518 query: &[u8],
1519 k: usize,
1520 combiner: crate::query::MultiValueCombiner,
1521 ) -> Result<Vec<VectorSearchResult>> {
1522 if let Some(VectorIndex::BinaryIvf(lazy)) = self.vector_indexes.get(&field.0)
1525 && let Some(ivf) = lazy.get()
1526 {
1527 let nprobe = self.binary_ivf_nprobe(field);
1528 let results = ivf.search(query, k, nprobe);
1529 return Ok(combine_ordinal_results(results, combiner, k));
1530 }
1531
1532 let lazy_flat = match self.flat_vectors.get(&field.0) {
1533 Some(f) => f,
1534 None => return Ok(Vec::new()),
1535 };
1536
1537 const BRUTE_FORCE_BATCH: usize = 8192; let dim_bits = lazy_flat.dim;
1540 let byte_len = lazy_flat.vector_byte_size();
1541 let n = lazy_flat.num_vectors;
1542
1543 if byte_len != query.len() {
1544 return Err(Error::Schema(format!(
1545 "Binary query vector byte length {} != field byte length {}",
1546 query.len(),
1547 byte_len
1548 )));
1549 }
1550
1551 let mut collector = crate::query::ScoreCollector::new(k);
1552 let mut scores = vec![0f32; BRUTE_FORCE_BATCH];
1553
1554 for batch_start in (0..n).step_by(BRUTE_FORCE_BATCH) {
1555 let batch_count = BRUTE_FORCE_BATCH.min(n - batch_start);
1556 let batch_bytes = lazy_flat
1557 .read_vectors_batch_sync(batch_start, batch_count)
1558 .map_err(crate::Error::Io)?;
1559 let raw = batch_bytes.as_slice();
1560
1561 crate::structures::simd::batch_hamming_scores(
1562 query,
1563 raw,
1564 byte_len,
1565 dim_bits,
1566 &mut scores[..batch_count],
1567 );
1568
1569 let threshold = collector.threshold();
1570 for (i, &score) in scores.iter().enumerate().take(batch_count) {
1571 if score > threshold {
1572 let (doc_id, ordinal) = lazy_flat.get_doc_id(batch_start + i);
1573 collector.insert_with_ordinal(doc_id, score, ordinal);
1574 }
1575 }
1576 }
1577
1578 let results: Vec<(u32, u16, f32)> = collector
1579 .into_sorted_results()
1580 .into_iter()
1581 .map(|(doc_id, score, ordinal)| (doc_id, ordinal, score))
1582 .collect();
1583
1584 Ok(combine_ordinal_results(results, combiner, k))
1585 }
1586}