1#[cfg(feature = "native")]
11use crate::dsl::Schema;
12#[cfg(feature = "native")]
13use crate::error::Result;
14#[cfg(feature = "sync")]
15use std::collections::HashMap;
16#[cfg(feature = "native")]
17use std::sync::Arc;
18#[cfg(feature = "native")]
19use std::sync::{OnceLock, Weak};
20
21mod searcher;
22pub use searcher::Searcher;
23
24#[cfg(feature = "native")]
25mod primary_key;
26#[cfg(feature = "native")]
27mod reader;
28#[cfg(feature = "native")]
29mod vector_builder;
30#[cfg(all(feature = "wasm", not(feature = "native")))]
31mod wasm_writer;
32#[cfg(feature = "native")]
33mod writer;
34#[cfg(feature = "native")]
35pub use primary_key::PrimaryKeyIndex;
36#[cfg(feature = "native")]
37pub use reader::IndexReader;
38#[cfg(all(feature = "wasm", not(feature = "native")))]
39pub use wasm_writer::IndexWriter as WasmIndexWriter;
40#[cfg(feature = "native")]
41pub use writer::{IndexWriter, PreparedCommit, WRITER_LOCK_FILENAME};
42
43mod metadata;
44pub use metadata::{
45 FieldVectorMeta, INDEX_META_FILENAME, IndexMetadata, SegmentMetaInfo, VectorIndexState,
46};
47
48#[cfg(feature = "native")]
49mod helpers;
50#[cfg(feature = "native")]
51pub use helpers::{
52 IndexingStats, SchemaConfig, SchemaFieldConfig, create_index_at_path, create_index_from_sdl,
53 index_documents_from_reader, index_json_document, parse_schema,
54};
55
56pub const SLICE_CACHE_FILENAME: &str = "index.slicecache";
58
59#[cfg(feature = "native")]
63pub const MAX_CONCURRENT_REORDER_PASSES: usize = 2;
64
65#[cfg(feature = "native")]
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub(crate) enum ReorderPriority {
68 Optimizer,
73 AutomaticMerge,
75 Foreground,
76}
77
78#[cfg(feature = "native")]
84#[derive(Debug)]
85pub struct ReorderConcurrencyGate {
86 permits: Arc<tokio::sync::Semaphore>,
87 automatic_merge_permits: Arc<tokio::sync::Semaphore>,
91 limit: usize,
92 foreground_lock: Arc<tokio::sync::Mutex<()>>,
93 foreground_active: std::sync::atomic::AtomicBool,
94 foreground_finished: tokio::sync::Notify,
95}
96
97#[cfg(feature = "native")]
103#[derive(Debug)]
104pub(crate) struct BmpIoGate {
105 limit: usize,
106 active: parking_lot::Mutex<usize>,
107 available: parking_lot::Condvar,
108 async_available: tokio::sync::Notify,
109}
110
111#[cfg(feature = "native")]
112impl BmpIoGate {
113 fn new(limit: usize) -> Self {
114 Self {
115 limit,
116 active: parking_lot::Mutex::new(0),
117 available: parking_lot::Condvar::new(),
118 async_available: tokio::sync::Notify::new(),
119 }
120 }
121
122 fn acquire(&self) -> BmpIoPermit<'_> {
123 let mut active = self.active.lock();
124 while *active >= self.limit {
125 self.available.wait(&mut active);
126 }
127 *active += 1;
128 BmpIoPermit { gate: self }
129 }
130
131 async fn acquire_async(&self) -> BmpIoPermit<'_> {
132 loop {
133 let notified = self.async_available.notified();
136 {
137 let mut active = self.active.lock();
138 if *active < self.limit {
139 *active += 1;
140 return BmpIoPermit { gate: self };
141 }
142 }
143 notified.await;
144 }
145 }
146}
147
148#[cfg(feature = "native")]
149struct BmpIoPermit<'a> {
150 gate: &'a BmpIoGate,
151}
152
153#[cfg(feature = "native")]
154impl Drop for BmpIoPermit<'_> {
155 fn drop(&mut self) {
156 let mut active = self.gate.active.lock();
157 *active -= 1;
158 self.gate.available.notify_one();
159 self.gate.async_available.notify_one();
160 }
161}
162
163#[cfg(feature = "native")]
164impl ReorderConcurrencyGate {
165 pub fn new(requested_limit: usize) -> Self {
166 let limit = requested_limit.clamp(1, MAX_CONCURRENT_REORDER_PASSES);
167 let automatic_merge_limit = limit.saturating_sub(1).max(1);
168 Self {
169 permits: Arc::new(tokio::sync::Semaphore::new(limit)),
170 automatic_merge_permits: Arc::new(tokio::sync::Semaphore::new(automatic_merge_limit)),
171 limit,
172 foreground_lock: Arc::new(tokio::sync::Mutex::new(())),
173 foreground_active: std::sync::atomic::AtomicBool::new(false),
174 foreground_finished: tokio::sync::Notify::new(),
175 }
176 }
177
178 pub fn limit(&self) -> usize {
179 self.limit
180 }
181
182 pub(crate) async fn acquire(
183 self: &Arc<Self>,
184 priority: ReorderPriority,
185 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
186 match priority {
187 ReorderPriority::Optimizer => self.acquire_background(None).await,
188 ReorderPriority::AutomaticMerge => {
189 let merge_permit = Arc::clone(&self.automatic_merge_permits)
190 .acquire_owned()
191 .await?;
192 self.acquire_background(Some(merge_permit)).await
193 }
194 ReorderPriority::Foreground => self.acquire_foreground().await,
195 }
196 }
197
198 async fn acquire_background(
200 self: &Arc<Self>,
201 automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
202 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
203 loop {
204 if self
205 .foreground_active
206 .load(std::sync::atomic::Ordering::Acquire)
207 {
208 let notified = self.foreground_finished.notified();
209 if self
210 .foreground_active
211 .load(std::sync::atomic::Ordering::Acquire)
212 {
213 notified.await;
214 continue;
215 }
216 }
217
218 let permit = Arc::clone(&self.permits).acquire_owned().await?;
219 if !self
220 .foreground_active
221 .load(std::sync::atomic::Ordering::Acquire)
222 {
223 return Ok(ReorderPermit {
224 _permit: permit,
225 _automatic_merge: automatic_merge,
226 });
227 }
228 drop(permit);
231 }
232 }
233
234 async fn acquire_foreground(
236 self: &Arc<Self>,
237 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
238 let permit = Arc::clone(&self.permits).acquire_owned().await?;
239 Ok(ReorderPermit {
240 _permit: permit,
241 _automatic_merge: None,
242 })
243 }
244
245 pub(crate) async fn begin_foreground(
251 self: &Arc<Self>,
252 ) -> std::result::Result<ForegroundReorderGuard, tokio::sync::AcquireError> {
253 let exclusive = Arc::clone(&self.foreground_lock).lock_owned().await;
254 self.foreground_active
255 .store(true, std::sync::atomic::Ordering::Release);
256
257 let mut guard = ForegroundReorderGuard {
261 gate: Arc::clone(self),
262 reserved: None,
263 _exclusive: exclusive,
264 };
265 if self.limit > 1 {
266 guard.reserved = Some(
267 Arc::clone(&self.permits)
268 .acquire_many_owned((self.limit - 1) as u32)
269 .await?,
270 );
271 }
272 Ok(guard)
273 }
274}
275
276#[cfg(feature = "native")]
277pub(crate) struct ReorderPermit {
278 _permit: tokio::sync::OwnedSemaphorePermit,
279 _automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
280}
281
282#[cfg(feature = "native")]
283pub(crate) struct ForegroundReorderGuard {
284 gate: Arc<ReorderConcurrencyGate>,
285 reserved: Option<tokio::sync::OwnedSemaphorePermit>,
286 _exclusive: tokio::sync::OwnedMutexGuard<()>,
287}
288
289#[cfg(feature = "native")]
290impl Drop for ForegroundReorderGuard {
291 fn drop(&mut self) {
292 drop(self.reserved.take());
294 self.gate
295 .foreground_active
296 .store(false, std::sync::atomic::Ordering::Release);
297 self.gate.foreground_finished.notify_waiters();
298 }
299}
300
301#[derive(Debug, Clone)]
303pub struct IndexConfig {
304 pub num_threads: usize,
310 pub bmp_io_concurrency: usize,
315 pub num_indexing_threads: usize,
317 pub num_compression_threads: usize,
319 pub term_cache_blocks: usize,
321 pub store_cache_budget_bytes: usize,
328 pub max_indexing_memory_bytes: usize,
330 pub vector_training_max_samples: usize,
334 pub vector_training_memory_bytes: usize,
336 pub merge_policy: Box<dyn crate::merge::MergePolicy>,
338 pub optimization: crate::structures::IndexOptimization,
340 pub reload_interval_ms: u64,
342 pub max_concurrent_merges: usize,
344 #[cfg(feature = "native")]
348 pub background_merge_permits: Arc<tokio::sync::Semaphore>,
349 pub merge_bp_time_budget: Option<std::time::Duration>,
356 pub bp_memory_budget_bytes: usize,
363 #[cfg(feature = "native")]
369 pub background_reorder_permits: Arc<ReorderConcurrencyGate>,
370 #[cfg(feature = "native")]
374 pub background_reorder_pool: Option<Arc<rayon::ThreadPool>>,
375}
376
377#[cfg(feature = "sync")]
381static SEARCH_CPU_POOLS: OnceLock<parking_lot::Mutex<HashMap<usize, Weak<rayon::ThreadPool>>>> =
382 OnceLock::new();
383
384#[cfg(feature = "native")]
389static STORE_CACHE_POOLS: OnceLock<
390 parking_lot::Mutex<std::collections::HashMap<usize, Weak<crate::segment::SharedStoreCache>>>,
391> = OnceLock::new();
392
393#[cfg(feature = "native")]
394static BMP_IO_GATES: OnceLock<
395 parking_lot::Mutex<std::collections::HashMap<usize, Weak<BmpIoGate>>>,
396> = OnceLock::new();
397
398#[cfg(feature = "native")]
399pub(crate) fn shared_bmp_io_gate(limit: usize) -> Arc<BmpIoGate> {
400 let mut gates = BMP_IO_GATES
401 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
402 .lock();
403 if let Some(gate) = gates.get(&limit).and_then(Weak::upgrade) {
404 return gate;
405 }
406 let gate = Arc::new(BmpIoGate::new(limit));
407 gates.retain(|_, gate| gate.strong_count() > 0);
408 gates.insert(limit, Arc::downgrade(&gate));
409 log::info!("[bmp] process-wide random-I/O concurrency={limit}");
410 gate
411}
412
413#[cfg(feature = "native")]
414pub(crate) fn shared_store_cache(budget_bytes: usize) -> Arc<crate::segment::SharedStoreCache> {
415 let mut caches = STORE_CACHE_POOLS
416 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
417 .lock();
418 if let Some(cache) = caches.get(&budget_bytes).and_then(Weak::upgrade) {
419 return cache;
420 }
421 let cache = Arc::new(crate::segment::SharedStoreCache::new(budget_bytes));
422 caches.retain(|_, cache| cache.strong_count() > 0);
423 caches.insert(budget_bytes, Arc::downgrade(&cache));
424 log::info!(
425 "[store_cache] process-wide budget={}",
426 crate::format_bytes(budget_bytes as u64)
427 );
428 cache
429}
430
431#[cfg(feature = "sync")]
432fn shared_search_pool(num_threads: usize) -> Result<Arc<rayon::ThreadPool>> {
433 if num_threads == 0 {
434 return Err(crate::Error::Internal(
435 "IndexConfig.num_threads must be greater than zero".into(),
436 ));
437 }
438
439 let mut pools = SEARCH_CPU_POOLS
440 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
441 .lock();
442 if let Some(pool) = pools.get(&num_threads).and_then(Weak::upgrade) {
443 return Ok(pool);
444 }
445
446 let pool = Arc::new(
450 rayon::ThreadPoolBuilder::new()
451 .num_threads(num_threads)
452 .thread_name(move |idx| format!("hermes-search-{}-{}", num_threads, idx))
453 .build()
454 .map_err(|error| {
455 crate::Error::Internal(format!(
456 "failed to create {num_threads}-thread search pool: {error}"
457 ))
458 })?,
459 );
460 pools.retain(|_, pool| pool.strong_count() > 0);
461 pools.insert(num_threads, Arc::downgrade(&pool));
462 log::info!("[search] process-wide CPU pool: {} thread(s)", num_threads);
463 Ok(pool)
464}
465
466impl Default for IndexConfig {
467 fn default() -> Self {
468 #[cfg(feature = "native")]
469 let compression_threads = crate::default_compression_threads();
470 #[cfg(not(feature = "native"))]
471 let compression_threads = 1;
472
473 #[cfg(feature = "native")]
474 let search_threads = crate::default_search_threads();
475 #[cfg(not(feature = "native"))]
476 let search_threads = 1;
477
478 Self {
479 num_threads: search_threads,
480 bmp_io_concurrency: 4,
481 num_indexing_threads: 1, num_compression_threads: compression_threads,
483 term_cache_blocks: 256,
484 #[cfg(target_pointer_width = "64")]
488 store_cache_budget_bytes: 2 * 1024 * 1024 * 1024,
489 #[cfg(not(target_pointer_width = "64"))]
490 store_cache_budget_bytes: 32 * 1024 * 1024,
491 max_indexing_memory_bytes: 256 * 1024 * 1024, vector_training_max_samples: 10_000_000,
493 #[cfg(target_pointer_width = "64")]
494 vector_training_memory_bytes: 4 * 1024 * 1024 * 1024,
495 #[cfg(not(target_pointer_width = "64"))]
496 vector_training_memory_bytes: usize::MAX,
497 merge_policy: Box::new(crate::merge::TieredMergePolicy::large_scale()),
502 optimization: crate::structures::IndexOptimization::default(),
503 reload_interval_ms: 1000, max_concurrent_merges: 4,
505 #[cfg(feature = "native")]
506 background_merge_permits: Arc::new(tokio::sync::Semaphore::new(4)),
507 merge_bp_time_budget: Some(std::time::Duration::from_secs(600)),
508 #[cfg(target_pointer_width = "64")]
517 bp_memory_budget_bytes: 24 * 1024 * 1024 * 1024,
518 #[cfg(not(target_pointer_width = "64"))]
519 bp_memory_budget_bytes: usize::MAX,
520 #[cfg(feature = "native")]
521 background_reorder_permits: Arc::new(ReorderConcurrencyGate::new(2)),
522 #[cfg(feature = "native")]
523 background_reorder_pool: None,
524 }
525 }
526}
527
528#[cfg(feature = "native")]
537pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
538 directory: Arc<D>,
539 schema: Arc<Schema>,
540 config: IndexConfig,
541 search_resources: searcher::SearcherResources,
543 segment_manager: Arc<crate::merge::SegmentManager<D>>,
545 cached_reader: tokio::sync::OnceCell<IndexReader<D>>,
547}
548
549#[cfg(feature = "native")]
550impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
551 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
553 let search_resources = searcher::SearcherResources::new(
554 config.term_cache_blocks,
555 config.store_cache_budget_bytes,
556 config.num_threads,
557 config.bmp_io_concurrency,
558 )?;
559 let directory = Arc::new(directory);
560 let schema = Arc::new(schema);
561 directory.set_index_label(schema.index_label());
563
564 if directory
568 .exists(std::path::Path::new(INDEX_META_FILENAME))
569 .await?
570 {
571 return Err(crate::Error::Internal(format!(
572 "refusing to create index: {} already exists in this directory; \
573 use Index::open to open the existing index, or delete the \
574 directory first if you really want to start over",
575 INDEX_META_FILENAME
576 )));
577 }
578
579 let metadata = IndexMetadata::new((*schema).clone());
580
581 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
582 Arc::clone(&directory),
583 Arc::clone(&schema),
584 metadata,
585 config.merge_policy.clone_box(),
586 config.term_cache_blocks,
587 config.max_concurrent_merges,
588 Arc::clone(&config.background_merge_permits),
589 config.merge_bp_time_budget,
590 config.bp_memory_budget_bytes,
591 Arc::clone(&config.background_reorder_permits),
592 config.background_reorder_pool.clone(),
593 ));
594
595 segment_manager.update_metadata(|_| {}).await?;
597
598 Ok(Self {
599 directory,
600 schema,
601 config,
602 search_resources,
603 segment_manager,
604 cached_reader: tokio::sync::OnceCell::new(),
605 })
606 }
607
608 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
610 let search_resources = searcher::SearcherResources::new(
611 config.term_cache_blocks,
612 config.store_cache_budget_bytes,
613 config.num_threads,
614 config.bmp_io_concurrency,
615 )?;
616 let directory = Arc::new(directory);
617
618 let metadata = IndexMetadata::load(directory.as_ref()).await?;
620 let schema = Arc::new(metadata.schema.clone());
621 directory.set_index_label(schema.index_label());
623
624 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
625 Arc::clone(&directory),
626 Arc::clone(&schema),
627 metadata,
628 config.merge_policy.clone_box(),
629 config.term_cache_blocks,
630 config.max_concurrent_merges,
631 Arc::clone(&config.background_merge_permits),
632 config.merge_bp_time_budget,
633 config.bp_memory_budget_bytes,
634 Arc::clone(&config.background_reorder_permits),
635 config.background_reorder_pool.clone(),
636 ));
637
638 segment_manager.try_load_and_publish_trained().await?;
640
641 Ok(Self {
642 directory,
643 schema,
644 config,
645 search_resources,
646 segment_manager,
647 cached_reader: tokio::sync::OnceCell::new(),
648 })
649 }
650
651 pub fn schema(&self) -> &Schema {
653 &self.schema
654 }
655
656 pub fn schema_arc(&self) -> &Arc<Schema> {
658 &self.schema
659 }
660
661 pub fn directory(&self) -> &D {
663 &self.directory
664 }
665
666 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
668 &self.segment_manager
669 }
670
671 pub async fn reader(&self) -> Result<&IndexReader<D>> {
676 self.cached_reader
677 .get_or_try_init(|| async {
678 IndexReader::from_segment_manager_with_resources(
679 Arc::clone(&self.schema),
680 Arc::clone(&self.segment_manager),
681 self.config.reload_interval_ms,
682 self.search_resources.clone(),
683 )
684 .await
685 })
686 .await
687 }
688
689 pub fn config(&self) -> &IndexConfig {
691 &self.config
692 }
693
694 pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
696 let reader = self.reader().await?;
697 let searcher = reader.searcher().await?;
698 Ok(searcher.segment_readers().to_vec())
699 }
700
701 pub async fn num_docs(&self) -> Result<u32> {
703 let reader = self.reader().await?;
704 let searcher = reader.searcher().await?;
705 Ok(searcher.num_docs())
706 }
707
708 pub fn default_fields(&self) -> Vec<crate::Field> {
710 if !self.schema.default_fields().is_empty() {
711 self.schema.default_fields().to_vec()
712 } else {
713 self.schema
714 .fields()
715 .filter(|(_, entry)| {
716 entry.indexed && entry.field_type == crate::dsl::FieldType::Text
717 })
718 .map(|(field, _)| field)
719 .collect()
720 }
721 }
722
723 pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
725 Arc::new(crate::tokenizer::TokenizerRegistry::default())
726 }
727
728 pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
730 let default_fields = self.default_fields();
731 let tokenizers = self.tokenizers();
732
733 let query_routers = self.schema.query_routers();
734 if !query_routers.is_empty()
735 && let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
736 {
737 return crate::dsl::QueryLanguageParser::with_router(
738 Arc::clone(&self.schema),
739 default_fields,
740 tokenizers,
741 router,
742 );
743 }
744
745 crate::dsl::QueryLanguageParser::new(Arc::clone(&self.schema), default_fields, tokenizers)
746 }
747
748 pub async fn query(
750 &self,
751 query_str: &str,
752 limit: usize,
753 ) -> Result<crate::query::SearchResponse> {
754 self.query_offset(query_str, limit, 0).await
755 }
756
757 pub async fn query_offset(
759 &self,
760 query_str: &str,
761 limit: usize,
762 offset: usize,
763 ) -> Result<crate::query::SearchResponse> {
764 let parser = self.query_parser();
765 let query = parser
766 .parse(query_str)
767 .map_err(crate::error::Error::Query)?;
768 self.search_offset(query.as_ref(), limit, offset).await
769 }
770
771 pub async fn search(
773 &self,
774 query: &dyn crate::query::Query,
775 limit: usize,
776 ) -> Result<crate::query::SearchResponse> {
777 self.search_offset(query, limit, 0).await
778 }
779
780 pub async fn search_offset(
782 &self,
783 query: &dyn crate::query::Query,
784 limit: usize,
785 offset: usize,
786 ) -> Result<crate::query::SearchResponse> {
787 let reader = self.reader().await?;
788 let searcher = reader.searcher().await?;
789
790 #[cfg(feature = "sync")]
791 let (results, total_seen) = {
792 let runtime_flavor = tokio::runtime::Handle::current().runtime_flavor();
796 if runtime_flavor == tokio::runtime::RuntimeFlavor::MultiThread {
797 tokio::task::block_in_place(|| {
798 searcher.search_with_offset_and_count_sync(query, limit, offset)
799 })?
800 } else {
801 searcher.search_with_offset_and_count_sync(query, limit, offset)?
802 }
803 };
804
805 #[cfg(not(feature = "sync"))]
806 let (results, total_seen) = {
807 searcher
808 .search_with_offset_and_count(query, limit, offset)
809 .await?
810 };
811
812 let total_hits = total_seen;
813 let hits: Vec<crate::query::SearchHit> = results
814 .into_iter()
815 .map(|result| crate::query::SearchHit {
816 address: crate::query::DocAddress::new(result.segment_id, result.doc_id),
817 score: result.score,
818 matched_fields: result.extract_ordinals(),
819 })
820 .collect();
821
822 Ok(crate::query::SearchResponse { hits, total_hits })
823 }
824
825 pub async fn get_document(
827 &self,
828 address: &crate::query::DocAddress,
829 ) -> Result<Option<crate::dsl::Document>> {
830 let reader = self.reader().await?;
831 let searcher = reader.searcher().await?;
832 searcher.get_document(address).await
833 }
834
835 pub async fn get_postings(
837 &self,
838 field: crate::Field,
839 term: &[u8],
840 ) -> Result<
841 Vec<(
842 Arc<crate::segment::SegmentReader>,
843 crate::structures::BlockPostingList,
844 )>,
845 > {
846 let segments = self.segment_readers().await?;
847 let mut results = Vec::new();
848
849 for segment in segments {
850 if let Some(postings) = segment.get_postings(field, term).await? {
851 results.push((segment, postings));
852 }
853 }
854
855 Ok(results)
856 }
857}
858
859#[cfg(feature = "native")]
861impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
862 pub fn writer(&self) -> writer::IndexWriter<D> {
864 writer::IndexWriter::from_index(self)
865 }
866}
867
868#[cfg(test)]
869mod tests;
870
871