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 Background,
69 Foreground,
70}
71
72#[cfg(feature = "native")]
78#[derive(Debug)]
79pub struct ReorderConcurrencyGate {
80 permits: Arc<tokio::sync::Semaphore>,
81 limit: usize,
82 foreground_lock: Arc<tokio::sync::Mutex<()>>,
83 foreground_active: std::sync::atomic::AtomicBool,
84 foreground_finished: tokio::sync::Notify,
85}
86
87#[cfg(feature = "native")]
93#[derive(Debug)]
94pub(crate) struct BmpIoGate {
95 limit: usize,
96 active: parking_lot::Mutex<usize>,
97 available: parking_lot::Condvar,
98 async_available: tokio::sync::Notify,
99}
100
101#[cfg(feature = "native")]
102impl BmpIoGate {
103 fn new(limit: usize) -> Self {
104 Self {
105 limit,
106 active: parking_lot::Mutex::new(0),
107 available: parking_lot::Condvar::new(),
108 async_available: tokio::sync::Notify::new(),
109 }
110 }
111
112 fn acquire(&self) -> BmpIoPermit<'_> {
113 let mut active = self.active.lock();
114 while *active >= self.limit {
115 self.available.wait(&mut active);
116 }
117 *active += 1;
118 BmpIoPermit { gate: self }
119 }
120
121 async fn acquire_async(&self) -> BmpIoPermit<'_> {
122 loop {
123 let notified = self.async_available.notified();
126 {
127 let mut active = self.active.lock();
128 if *active < self.limit {
129 *active += 1;
130 return BmpIoPermit { gate: self };
131 }
132 }
133 notified.await;
134 }
135 }
136}
137
138#[cfg(feature = "native")]
139struct BmpIoPermit<'a> {
140 gate: &'a BmpIoGate,
141}
142
143#[cfg(feature = "native")]
144impl Drop for BmpIoPermit<'_> {
145 fn drop(&mut self) {
146 let mut active = self.gate.active.lock();
147 *active -= 1;
148 self.gate.available.notify_one();
149 self.gate.async_available.notify_one();
150 }
151}
152
153#[cfg(feature = "native")]
154impl ReorderConcurrencyGate {
155 pub fn new(requested_limit: usize) -> Self {
156 let limit = requested_limit.clamp(1, MAX_CONCURRENT_REORDER_PASSES);
157 Self {
158 permits: Arc::new(tokio::sync::Semaphore::new(limit)),
159 limit,
160 foreground_lock: Arc::new(tokio::sync::Mutex::new(())),
161 foreground_active: std::sync::atomic::AtomicBool::new(false),
162 foreground_finished: tokio::sync::Notify::new(),
163 }
164 }
165
166 pub fn limit(&self) -> usize {
167 self.limit
168 }
169
170 pub(crate) async fn acquire(
171 self: &Arc<Self>,
172 priority: ReorderPriority,
173 ) -> std::result::Result<tokio::sync::OwnedSemaphorePermit, tokio::sync::AcquireError> {
174 match priority {
175 ReorderPriority::Background => self.acquire_background().await,
176 ReorderPriority::Foreground => self.acquire_foreground().await,
177 }
178 }
179
180 async fn acquire_background(
182 self: &Arc<Self>,
183 ) -> std::result::Result<tokio::sync::OwnedSemaphorePermit, tokio::sync::AcquireError> {
184 loop {
185 if self
186 .foreground_active
187 .load(std::sync::atomic::Ordering::Acquire)
188 {
189 let notified = self.foreground_finished.notified();
190 if self
191 .foreground_active
192 .load(std::sync::atomic::Ordering::Acquire)
193 {
194 notified.await;
195 continue;
196 }
197 }
198
199 let permit = Arc::clone(&self.permits).acquire_owned().await?;
200 if !self
201 .foreground_active
202 .load(std::sync::atomic::Ordering::Acquire)
203 {
204 return Ok(permit);
205 }
206 drop(permit);
209 }
210 }
211
212 async fn acquire_foreground(
214 self: &Arc<Self>,
215 ) -> std::result::Result<tokio::sync::OwnedSemaphorePermit, tokio::sync::AcquireError> {
216 Arc::clone(&self.permits).acquire_owned().await
217 }
218
219 pub(crate) async fn begin_foreground(
225 self: &Arc<Self>,
226 ) -> std::result::Result<ForegroundReorderGuard, tokio::sync::AcquireError> {
227 let exclusive = Arc::clone(&self.foreground_lock).lock_owned().await;
228 self.foreground_active
229 .store(true, std::sync::atomic::Ordering::Release);
230
231 let mut guard = ForegroundReorderGuard {
235 gate: Arc::clone(self),
236 reserved: None,
237 _exclusive: exclusive,
238 };
239 if self.limit > 1 {
240 guard.reserved = Some(
241 Arc::clone(&self.permits)
242 .acquire_many_owned((self.limit - 1) as u32)
243 .await?,
244 );
245 }
246 Ok(guard)
247 }
248}
249
250#[cfg(feature = "native")]
251pub(crate) struct ForegroundReorderGuard {
252 gate: Arc<ReorderConcurrencyGate>,
253 reserved: Option<tokio::sync::OwnedSemaphorePermit>,
254 _exclusive: tokio::sync::OwnedMutexGuard<()>,
255}
256
257#[cfg(feature = "native")]
258impl Drop for ForegroundReorderGuard {
259 fn drop(&mut self) {
260 drop(self.reserved.take());
262 self.gate
263 .foreground_active
264 .store(false, std::sync::atomic::Ordering::Release);
265 self.gate.foreground_finished.notify_waiters();
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct IndexConfig {
272 pub num_threads: usize,
278 pub bmp_io_concurrency: usize,
283 pub num_indexing_threads: usize,
285 pub num_compression_threads: usize,
287 pub term_cache_blocks: usize,
289 pub store_cache_budget_bytes: usize,
296 pub max_indexing_memory_bytes: usize,
298 pub vector_training_max_samples: usize,
302 pub vector_training_memory_bytes: usize,
304 pub merge_policy: Box<dyn crate::merge::MergePolicy>,
306 pub optimization: crate::structures::IndexOptimization,
308 pub reload_interval_ms: u64,
310 pub max_concurrent_merges: usize,
312 #[cfg(feature = "native")]
316 pub background_merge_permits: Arc<tokio::sync::Semaphore>,
317 pub merge_bp_time_budget: Option<std::time::Duration>,
324 pub bp_memory_budget_bytes: usize,
331 #[cfg(feature = "native")]
337 pub background_reorder_permits: Arc<ReorderConcurrencyGate>,
338 #[cfg(feature = "native")]
342 pub background_reorder_pool: Option<Arc<rayon::ThreadPool>>,
343}
344
345#[cfg(feature = "sync")]
349static SEARCH_CPU_POOLS: OnceLock<parking_lot::Mutex<HashMap<usize, Weak<rayon::ThreadPool>>>> =
350 OnceLock::new();
351
352#[cfg(feature = "native")]
357static STORE_CACHE_POOLS: OnceLock<
358 parking_lot::Mutex<std::collections::HashMap<usize, Weak<crate::segment::SharedStoreCache>>>,
359> = OnceLock::new();
360
361#[cfg(feature = "native")]
362static BMP_IO_GATES: OnceLock<
363 parking_lot::Mutex<std::collections::HashMap<usize, Weak<BmpIoGate>>>,
364> = OnceLock::new();
365
366#[cfg(feature = "native")]
367pub(crate) fn shared_bmp_io_gate(limit: usize) -> Arc<BmpIoGate> {
368 let mut gates = BMP_IO_GATES
369 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
370 .lock();
371 if let Some(gate) = gates.get(&limit).and_then(Weak::upgrade) {
372 return gate;
373 }
374 let gate = Arc::new(BmpIoGate::new(limit));
375 gates.retain(|_, gate| gate.strong_count() > 0);
376 gates.insert(limit, Arc::downgrade(&gate));
377 log::info!("[bmp] process-wide random-I/O concurrency={limit}");
378 gate
379}
380
381#[cfg(feature = "native")]
382pub(crate) fn shared_store_cache(budget_bytes: usize) -> Arc<crate::segment::SharedStoreCache> {
383 let mut caches = STORE_CACHE_POOLS
384 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
385 .lock();
386 if let Some(cache) = caches.get(&budget_bytes).and_then(Weak::upgrade) {
387 return cache;
388 }
389 let cache = Arc::new(crate::segment::SharedStoreCache::new(budget_bytes));
390 caches.retain(|_, cache| cache.strong_count() > 0);
391 caches.insert(budget_bytes, Arc::downgrade(&cache));
392 log::info!(
393 "[store_cache] process-wide budget={}",
394 crate::format_bytes(budget_bytes as u64)
395 );
396 cache
397}
398
399#[cfg(feature = "sync")]
400fn shared_search_pool(num_threads: usize) -> Result<Arc<rayon::ThreadPool>> {
401 if num_threads == 0 {
402 return Err(crate::Error::Internal(
403 "IndexConfig.num_threads must be greater than zero".into(),
404 ));
405 }
406
407 let mut pools = SEARCH_CPU_POOLS
408 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
409 .lock();
410 if let Some(pool) = pools.get(&num_threads).and_then(Weak::upgrade) {
411 return Ok(pool);
412 }
413
414 let pool = Arc::new(
418 rayon::ThreadPoolBuilder::new()
419 .num_threads(num_threads)
420 .thread_name(move |idx| format!("hermes-search-{}-{}", num_threads, idx))
421 .build()
422 .map_err(|error| {
423 crate::Error::Internal(format!(
424 "failed to create {num_threads}-thread search pool: {error}"
425 ))
426 })?,
427 );
428 pools.retain(|_, pool| pool.strong_count() > 0);
429 pools.insert(num_threads, Arc::downgrade(&pool));
430 log::info!("[search] process-wide CPU pool: {} thread(s)", num_threads);
431 Ok(pool)
432}
433
434impl Default for IndexConfig {
435 fn default() -> Self {
436 #[cfg(feature = "native")]
437 let compression_threads = crate::default_compression_threads();
438 #[cfg(not(feature = "native"))]
439 let compression_threads = 1;
440
441 #[cfg(feature = "native")]
442 let search_threads = crate::default_search_threads();
443 #[cfg(not(feature = "native"))]
444 let search_threads = 1;
445
446 Self {
447 num_threads: search_threads,
448 bmp_io_concurrency: 4,
449 num_indexing_threads: 1, num_compression_threads: compression_threads,
451 term_cache_blocks: 256,
452 #[cfg(target_pointer_width = "64")]
456 store_cache_budget_bytes: 2 * 1024 * 1024 * 1024,
457 #[cfg(not(target_pointer_width = "64"))]
458 store_cache_budget_bytes: 32 * 1024 * 1024,
459 max_indexing_memory_bytes: 256 * 1024 * 1024, vector_training_max_samples: 10_000_000,
461 #[cfg(target_pointer_width = "64")]
462 vector_training_memory_bytes: 4 * 1024 * 1024 * 1024,
463 #[cfg(not(target_pointer_width = "64"))]
464 vector_training_memory_bytes: usize::MAX,
465 merge_policy: Box::new(crate::merge::TieredMergePolicy::large_scale()),
470 optimization: crate::structures::IndexOptimization::default(),
471 reload_interval_ms: 1000, max_concurrent_merges: 4,
473 #[cfg(feature = "native")]
474 background_merge_permits: Arc::new(tokio::sync::Semaphore::new(4)),
475 merge_bp_time_budget: Some(std::time::Duration::from_secs(600)),
476 #[cfg(target_pointer_width = "64")]
485 bp_memory_budget_bytes: 24 * 1024 * 1024 * 1024,
486 #[cfg(not(target_pointer_width = "64"))]
487 bp_memory_budget_bytes: usize::MAX,
488 #[cfg(feature = "native")]
489 background_reorder_permits: Arc::new(ReorderConcurrencyGate::new(2)),
490 #[cfg(feature = "native")]
491 background_reorder_pool: None,
492 }
493 }
494}
495
496#[cfg(feature = "native")]
505pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
506 directory: Arc<D>,
507 schema: Arc<Schema>,
508 config: IndexConfig,
509 search_resources: searcher::SearcherResources,
511 segment_manager: Arc<crate::merge::SegmentManager<D>>,
513 cached_reader: tokio::sync::OnceCell<IndexReader<D>>,
515}
516
517#[cfg(feature = "native")]
518impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
519 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
521 let search_resources = searcher::SearcherResources::new(
522 config.term_cache_blocks,
523 config.store_cache_budget_bytes,
524 config.num_threads,
525 config.bmp_io_concurrency,
526 )?;
527 let directory = Arc::new(directory);
528 let schema = Arc::new(schema);
529 directory.set_index_label(schema.index_label());
531
532 if directory
536 .exists(std::path::Path::new(INDEX_META_FILENAME))
537 .await?
538 {
539 return Err(crate::Error::Internal(format!(
540 "refusing to create index: {} already exists in this directory; \
541 use Index::open to open the existing index, or delete the \
542 directory first if you really want to start over",
543 INDEX_META_FILENAME
544 )));
545 }
546
547 let metadata = IndexMetadata::new((*schema).clone());
548
549 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
550 Arc::clone(&directory),
551 Arc::clone(&schema),
552 metadata,
553 config.merge_policy.clone_box(),
554 config.term_cache_blocks,
555 config.max_concurrent_merges,
556 Arc::clone(&config.background_merge_permits),
557 config.merge_bp_time_budget,
558 config.bp_memory_budget_bytes,
559 Arc::clone(&config.background_reorder_permits),
560 config.background_reorder_pool.clone(),
561 ));
562
563 segment_manager.update_metadata(|_| {}).await?;
565
566 Ok(Self {
567 directory,
568 schema,
569 config,
570 search_resources,
571 segment_manager,
572 cached_reader: tokio::sync::OnceCell::new(),
573 })
574 }
575
576 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
578 let search_resources = searcher::SearcherResources::new(
579 config.term_cache_blocks,
580 config.store_cache_budget_bytes,
581 config.num_threads,
582 config.bmp_io_concurrency,
583 )?;
584 let directory = Arc::new(directory);
585
586 let metadata = IndexMetadata::load(directory.as_ref()).await?;
588 let schema = Arc::new(metadata.schema.clone());
589 directory.set_index_label(schema.index_label());
591
592 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
593 Arc::clone(&directory),
594 Arc::clone(&schema),
595 metadata,
596 config.merge_policy.clone_box(),
597 config.term_cache_blocks,
598 config.max_concurrent_merges,
599 Arc::clone(&config.background_merge_permits),
600 config.merge_bp_time_budget,
601 config.bp_memory_budget_bytes,
602 Arc::clone(&config.background_reorder_permits),
603 config.background_reorder_pool.clone(),
604 ));
605
606 segment_manager.try_load_and_publish_trained().await?;
608
609 Ok(Self {
610 directory,
611 schema,
612 config,
613 search_resources,
614 segment_manager,
615 cached_reader: tokio::sync::OnceCell::new(),
616 })
617 }
618
619 pub fn schema(&self) -> &Schema {
621 &self.schema
622 }
623
624 pub fn schema_arc(&self) -> &Arc<Schema> {
626 &self.schema
627 }
628
629 pub fn directory(&self) -> &D {
631 &self.directory
632 }
633
634 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
636 &self.segment_manager
637 }
638
639 pub async fn reader(&self) -> Result<&IndexReader<D>> {
644 self.cached_reader
645 .get_or_try_init(|| async {
646 IndexReader::from_segment_manager_with_resources(
647 Arc::clone(&self.schema),
648 Arc::clone(&self.segment_manager),
649 self.config.reload_interval_ms,
650 self.search_resources.clone(),
651 )
652 .await
653 })
654 .await
655 }
656
657 pub fn config(&self) -> &IndexConfig {
659 &self.config
660 }
661
662 pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
664 let reader = self.reader().await?;
665 let searcher = reader.searcher().await?;
666 Ok(searcher.segment_readers().to_vec())
667 }
668
669 pub async fn num_docs(&self) -> Result<u32> {
671 let reader = self.reader().await?;
672 let searcher = reader.searcher().await?;
673 Ok(searcher.num_docs())
674 }
675
676 pub fn default_fields(&self) -> Vec<crate::Field> {
678 if !self.schema.default_fields().is_empty() {
679 self.schema.default_fields().to_vec()
680 } else {
681 self.schema
682 .fields()
683 .filter(|(_, entry)| {
684 entry.indexed && entry.field_type == crate::dsl::FieldType::Text
685 })
686 .map(|(field, _)| field)
687 .collect()
688 }
689 }
690
691 pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
693 Arc::new(crate::tokenizer::TokenizerRegistry::default())
694 }
695
696 pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
698 let default_fields = self.default_fields();
699 let tokenizers = self.tokenizers();
700
701 let query_routers = self.schema.query_routers();
702 if !query_routers.is_empty()
703 && let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
704 {
705 return crate::dsl::QueryLanguageParser::with_router(
706 Arc::clone(&self.schema),
707 default_fields,
708 tokenizers,
709 router,
710 );
711 }
712
713 crate::dsl::QueryLanguageParser::new(Arc::clone(&self.schema), default_fields, tokenizers)
714 }
715
716 pub async fn query(
718 &self,
719 query_str: &str,
720 limit: usize,
721 ) -> Result<crate::query::SearchResponse> {
722 self.query_offset(query_str, limit, 0).await
723 }
724
725 pub async fn query_offset(
727 &self,
728 query_str: &str,
729 limit: usize,
730 offset: usize,
731 ) -> Result<crate::query::SearchResponse> {
732 let parser = self.query_parser();
733 let query = parser
734 .parse(query_str)
735 .map_err(crate::error::Error::Query)?;
736 self.search_offset(query.as_ref(), limit, offset).await
737 }
738
739 pub async fn search(
741 &self,
742 query: &dyn crate::query::Query,
743 limit: usize,
744 ) -> Result<crate::query::SearchResponse> {
745 self.search_offset(query, limit, 0).await
746 }
747
748 pub async fn search_offset(
750 &self,
751 query: &dyn crate::query::Query,
752 limit: usize,
753 offset: usize,
754 ) -> Result<crate::query::SearchResponse> {
755 let reader = self.reader().await?;
756 let searcher = reader.searcher().await?;
757
758 #[cfg(feature = "sync")]
759 let (results, total_seen) = {
760 let runtime_flavor = tokio::runtime::Handle::current().runtime_flavor();
764 if runtime_flavor == tokio::runtime::RuntimeFlavor::MultiThread {
765 tokio::task::block_in_place(|| {
766 searcher.search_with_offset_and_count_sync(query, limit, offset)
767 })?
768 } else {
769 searcher.search_with_offset_and_count_sync(query, limit, offset)?
770 }
771 };
772
773 #[cfg(not(feature = "sync"))]
774 let (results, total_seen) = {
775 searcher
776 .search_with_offset_and_count(query, limit, offset)
777 .await?
778 };
779
780 let total_hits = total_seen;
781 let hits: Vec<crate::query::SearchHit> = results
782 .into_iter()
783 .map(|result| crate::query::SearchHit {
784 address: crate::query::DocAddress::new(result.segment_id, result.doc_id),
785 score: result.score,
786 matched_fields: result.extract_ordinals(),
787 })
788 .collect();
789
790 Ok(crate::query::SearchResponse { hits, total_hits })
791 }
792
793 pub async fn get_document(
795 &self,
796 address: &crate::query::DocAddress,
797 ) -> Result<Option<crate::dsl::Document>> {
798 let reader = self.reader().await?;
799 let searcher = reader.searcher().await?;
800 searcher.get_document(address).await
801 }
802
803 pub async fn get_postings(
805 &self,
806 field: crate::Field,
807 term: &[u8],
808 ) -> Result<
809 Vec<(
810 Arc<crate::segment::SegmentReader>,
811 crate::structures::BlockPostingList,
812 )>,
813 > {
814 let segments = self.segment_readers().await?;
815 let mut results = Vec::new();
816
817 for segment in segments {
818 if let Some(postings) = segment.get_postings(field, term).await? {
819 results.push((segment, postings));
820 }
821 }
822
823 Ok(results)
824 }
825}
826
827#[cfg(feature = "native")]
829impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
830 pub fn writer(&self) -> writer::IndexWriter<D> {
832 writer::IndexWriter::from_index(self)
833 }
834}
835
836#[cfg(test)]
837mod tests;
838
839