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(any(feature = "native", feature = "wasm"))]
25mod content_hash;
26#[cfg(any(feature = "native", feature = "wasm"))]
27mod primary_key;
28#[cfg(feature = "native")]
29mod reader;
30#[cfg(any(feature = "native", feature = "wasm"))]
31pub(crate) mod staged_row;
32#[cfg(feature = "native")]
33mod vector_builder;
34#[cfg(all(feature = "wasm", not(feature = "native")))]
35mod wasm_writer;
36#[cfg(feature = "native")]
37mod writer;
38#[cfg(any(feature = "native", feature = "wasm"))]
39pub use primary_key::PrimaryKeyIndex;
40#[cfg(feature = "native")]
41pub use reader::IndexReader;
42#[cfg(feature = "native")]
43pub use vector_builder::{AlterVectorIndexOutcome, AlterVectorIndexState};
44#[cfg(all(feature = "wasm", not(feature = "native")))]
45pub use wasm_writer::IndexWriter as WasmIndexWriter;
46#[cfg(feature = "native")]
47pub use writer::{IndexWriter, PreparedCommit, WRITER_LOCK_FILENAME};
48
49mod metadata;
50pub use metadata::{
51 FieldVectorMeta, INDEX_META_FILENAME, IndexMetadata, SegmentMetaInfo, VectorIndexState,
52};
53
54#[cfg(feature = "native")]
55mod helpers;
56#[cfg(feature = "native")]
57pub use helpers::{
58 IndexingStats, SchemaConfig, SchemaFieldConfig, create_index_at_path, create_index_from_sdl,
59 index_documents_from_reader, index_json_document, parse_schema,
60};
61
62pub const SLICE_CACHE_FILENAME: &str = "index.slicecache";
64
65#[cfg(feature = "native")]
69pub const MAX_CONCURRENT_REORDER_PASSES: usize = 2;
70
71#[cfg(feature = "native")]
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73pub(crate) enum ReorderPriority {
74 Optimizer,
79 AutomaticMerge,
81 Foreground,
82}
83
84#[cfg(feature = "native")]
90#[derive(Debug)]
91pub struct ReorderConcurrencyGate {
92 permits: Arc<tokio::sync::Semaphore>,
93 optimizer_permits: Arc<tokio::sync::Semaphore>,
98 automatic_merge_permits: Arc<tokio::sync::Semaphore>,
102 limit: usize,
103 foreground_lock: Arc<tokio::sync::Mutex<()>>,
104 foreground_active: std::sync::atomic::AtomicBool,
105 foreground_finished: tokio::sync::Notify,
106}
107
108#[cfg(feature = "native")]
114#[derive(Debug)]
115pub(crate) struct SparseIoGate {
116 limit: usize,
117 active: parking_lot::Mutex<usize>,
118 available: parking_lot::Condvar,
119 async_available: tokio::sync::Notify,
120}
121
122#[cfg(feature = "native")]
123impl SparseIoGate {
124 fn new(limit: usize) -> Self {
125 Self {
126 limit,
127 active: parking_lot::Mutex::new(0),
128 available: parking_lot::Condvar::new(),
129 async_available: tokio::sync::Notify::new(),
130 }
131 }
132
133 #[cfg(feature = "sync")]
134 fn acquire(&self) -> SparseIoPermit<'_> {
135 let mut active = self.active.lock();
136 while *active >= self.limit {
137 self.available.wait(&mut active);
138 }
139 *active += 1;
140 SparseIoPermit { gate: self }
141 }
142
143 async fn acquire_async(&self) -> SparseIoPermit<'_> {
144 loop {
145 let notified = self.async_available.notified();
148 {
149 let mut active = self.active.lock();
150 if *active < self.limit {
151 *active += 1;
152 return SparseIoPermit { gate: self };
153 }
154 }
155 notified.await;
156 }
157 }
158}
159
160#[cfg(feature = "native")]
161struct SparseIoPermit<'a> {
162 gate: &'a SparseIoGate,
163}
164
165#[cfg(feature = "native")]
166impl Drop for SparseIoPermit<'_> {
167 fn drop(&mut self) {
168 let mut active = self.gate.active.lock();
169 *active -= 1;
170 self.gate.available.notify_one();
171 self.gate.async_available.notify_one();
172 }
173}
174
175#[cfg(feature = "native")]
176impl ReorderConcurrencyGate {
177 pub fn new(requested_limit: usize) -> Self {
178 let limit = requested_limit.clamp(1, MAX_CONCURRENT_REORDER_PASSES);
179 let automatic_merge_limit = limit.saturating_sub(1).max(1);
180 Self {
181 permits: Arc::new(tokio::sync::Semaphore::new(limit)),
182 optimizer_permits: Arc::new(tokio::sync::Semaphore::new(1)),
183 automatic_merge_permits: Arc::new(tokio::sync::Semaphore::new(automatic_merge_limit)),
184 limit,
185 foreground_lock: Arc::new(tokio::sync::Mutex::new(())),
186 foreground_active: std::sync::atomic::AtomicBool::new(false),
187 foreground_finished: tokio::sync::Notify::new(),
188 }
189 }
190
191 pub fn limit(&self) -> usize {
192 self.limit
193 }
194
195 pub(crate) fn try_acquire_optimizer(
198 self: &Arc<Self>,
199 ) -> std::result::Result<ReorderPermit, tokio::sync::TryAcquireError> {
200 use std::sync::atomic::Ordering;
201 use tokio::sync::TryAcquireError;
202 if self.foreground_active.load(Ordering::Acquire) {
203 return Err(TryAcquireError::NoPermits);
204 }
205 let optimizer = Arc::clone(&self.optimizer_permits).try_acquire_owned()?;
206 let permit = Arc::clone(&self.permits).try_acquire_owned()?;
207 if self.foreground_active.load(Ordering::Acquire) {
208 return Err(TryAcquireError::NoPermits);
209 }
210 Ok(ReorderPermit {
211 _permit: permit,
212 _optimizer: Some(optimizer),
213 _automatic_merge: None,
214 })
215 }
216
217 pub(crate) async fn acquire(
218 self: &Arc<Self>,
219 priority: ReorderPriority,
220 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
221 match priority {
222 ReorderPriority::Optimizer => {
223 let optimizer_permit = Arc::clone(&self.optimizer_permits).acquire_owned().await?;
224 self.acquire_background(Some(optimizer_permit), None).await
225 }
226 ReorderPriority::AutomaticMerge => {
227 let merge_permit = Arc::clone(&self.automatic_merge_permits)
228 .acquire_owned()
229 .await?;
230 self.acquire_background(None, Some(merge_permit)).await
231 }
232 ReorderPriority::Foreground => self.acquire_foreground().await,
233 }
234 }
235
236 async fn acquire_background(
238 self: &Arc<Self>,
239 optimizer: Option<tokio::sync::OwnedSemaphorePermit>,
240 automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
241 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
242 loop {
243 if self
244 .foreground_active
245 .load(std::sync::atomic::Ordering::Acquire)
246 {
247 let notified = self.foreground_finished.notified();
248 if self
249 .foreground_active
250 .load(std::sync::atomic::Ordering::Acquire)
251 {
252 notified.await;
253 continue;
254 }
255 }
256
257 let permit = Arc::clone(&self.permits).acquire_owned().await?;
258 if !self
259 .foreground_active
260 .load(std::sync::atomic::Ordering::Acquire)
261 {
262 return Ok(ReorderPermit {
263 _permit: permit,
264 _optimizer: optimizer,
265 _automatic_merge: automatic_merge,
266 });
267 }
268 drop(permit);
271 }
272 }
273
274 async fn acquire_foreground(
276 self: &Arc<Self>,
277 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
278 let permit = Arc::clone(&self.permits).acquire_owned().await?;
279 Ok(ReorderPermit {
280 _permit: permit,
281 _optimizer: None,
282 _automatic_merge: None,
283 })
284 }
285
286 pub(crate) async fn begin_foreground(
292 self: &Arc<Self>,
293 ) -> std::result::Result<ForegroundReorderGuard, tokio::sync::AcquireError> {
294 let exclusive = Arc::clone(&self.foreground_lock).lock_owned().await;
295 self.foreground_active
296 .store(true, std::sync::atomic::Ordering::Release);
297
298 let mut guard = ForegroundReorderGuard {
302 gate: Arc::clone(self),
303 reserved: None,
304 _exclusive: exclusive,
305 };
306 if self.limit > 1 {
307 guard.reserved = Some(
308 Arc::clone(&self.permits)
309 .acquire_many_owned((self.limit - 1) as u32)
310 .await?,
311 );
312 }
313 Ok(guard)
314 }
315}
316
317#[cfg(feature = "native")]
318pub(crate) struct ReorderPermit {
319 _permit: tokio::sync::OwnedSemaphorePermit,
320 _optimizer: Option<tokio::sync::OwnedSemaphorePermit>,
321 _automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
322}
323
324#[cfg(feature = "native")]
325pub(crate) struct ForegroundReorderGuard {
326 gate: Arc<ReorderConcurrencyGate>,
327 reserved: Option<tokio::sync::OwnedSemaphorePermit>,
328 _exclusive: tokio::sync::OwnedMutexGuard<()>,
329}
330
331#[cfg(feature = "native")]
332impl Drop for ForegroundReorderGuard {
333 fn drop(&mut self) {
334 drop(self.reserved.take());
336 self.gate
337 .foreground_active
338 .store(false, std::sync::atomic::Ordering::Release);
339 self.gate.foreground_finished.notify_waiters();
340 }
341}
342
343#[derive(Debug, Clone)]
345pub struct IndexConfig {
346 pub num_threads: usize,
352 pub sparse_io_concurrency: usize,
357 pub num_indexing_threads: usize,
359 pub num_compression_threads: usize,
363 pub term_cache_blocks: usize,
365 pub term_cache_budget_bytes: Option<usize>,
368 pub term_dict_block_size: crate::structures::SSTableBlockSize,
370 pub store_cache_budget_bytes: usize,
377 pub max_indexing_memory_bytes: usize,
379 pub vector_training_max_samples: usize,
383 pub vector_training_memory_bytes: usize,
385 pub merge_policy: Box<dyn crate::merge::MergePolicy>,
387 pub optimization: crate::structures::IndexOptimization,
392 pub posting_codec: Option<crate::structures::PostingCodec>,
395 pub quantized_norms: bool,
397 pub compact_text: bool,
399 pub posting_ratio_bounds: bool,
407 pub posting_impact_bounds: bool,
411 pub reload_interval_ms: u64,
413 pub max_concurrent_merges: usize,
415 #[cfg(feature = "native")]
419 pub background_merge_permits: Arc<tokio::sync::Semaphore>,
420 pub merge_bp_time_budget: Option<std::time::Duration>,
427 pub bp_memory_budget_bytes: usize,
434 pub compaction_memory_budget_bytes: usize,
436 #[cfg(feature = "native")]
442 pub background_reorder_permits: Arc<ReorderConcurrencyGate>,
443 #[cfg(feature = "native")]
447 pub background_reorder_pool: Option<Arc<rayon::ThreadPool>>,
448}
449
450#[cfg(feature = "sync")]
454static SEARCH_CPU_POOLS: OnceLock<parking_lot::Mutex<HashMap<usize, Weak<rayon::ThreadPool>>>> =
455 OnceLock::new();
456
457#[cfg(feature = "native")]
462static STORE_CACHE_POOLS: OnceLock<
463 parking_lot::Mutex<std::collections::HashMap<usize, Weak<crate::segment::SharedStoreCache>>>,
464> = OnceLock::new();
465
466#[cfg(feature = "native")]
467static SPARSE_IO_GATES: OnceLock<
468 parking_lot::Mutex<std::collections::HashMap<usize, Weak<SparseIoGate>>>,
469> = OnceLock::new();
470
471#[cfg(feature = "native")]
476fn shared_resource_log_level(announced: &OnceLock<()>) -> log::Level {
477 if announced.set(()).is_ok() {
478 log::Level::Info
479 } else {
480 log::Level::Debug
481 }
482}
483
484#[cfg(feature = "native")]
485pub(crate) fn shared_sparse_io_gate(limit: usize) -> Arc<SparseIoGate> {
486 let mut gates = SPARSE_IO_GATES
487 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
488 .lock();
489 if let Some(gate) = gates.get(&limit).and_then(Weak::upgrade) {
490 return gate;
491 }
492 let gate = Arc::new(SparseIoGate::new(limit));
493 gates.retain(|_, gate| gate.strong_count() > 0);
494 gates.insert(limit, Arc::downgrade(&gate));
495 static ANNOUNCED: OnceLock<()> = OnceLock::new();
496 log::log!(
497 shared_resource_log_level(&ANNOUNCED),
498 "[sparse] process-wide random-I/O concurrency={limit}"
499 );
500 gate
501}
502
503#[cfg(feature = "native")]
504pub(crate) fn shared_store_cache(budget_bytes: usize) -> Arc<crate::segment::SharedStoreCache> {
505 let mut caches = STORE_CACHE_POOLS
506 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
507 .lock();
508 if let Some(cache) = caches.get(&budget_bytes).and_then(Weak::upgrade) {
509 return cache;
510 }
511 let cache = Arc::new(crate::segment::SharedStoreCache::new(budget_bytes));
512 caches.retain(|_, cache| cache.strong_count() > 0);
513 caches.insert(budget_bytes, Arc::downgrade(&cache));
514 static ANNOUNCED: OnceLock<()> = OnceLock::new();
515 log::log!(
516 shared_resource_log_level(&ANNOUNCED),
517 "[store_cache] process-wide budget={}",
518 crate::format_bytes(budget_bytes as u64)
519 );
520 cache
521}
522
523#[cfg(feature = "sync")]
524fn shared_search_pool(num_threads: usize) -> Result<Arc<rayon::ThreadPool>> {
525 if num_threads == 0 {
526 return Err(crate::Error::Internal(
527 "IndexConfig.num_threads must be greater than zero".into(),
528 ));
529 }
530
531 let mut pools = SEARCH_CPU_POOLS
532 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
533 .lock();
534 if let Some(pool) = pools.get(&num_threads).and_then(Weak::upgrade) {
535 return Ok(pool);
536 }
537
538 let pool = Arc::new(
542 rayon::ThreadPoolBuilder::new()
543 .num_threads(num_threads)
544 .thread_name(move |idx| format!("summa-search-{}-{}", num_threads, idx))
545 .build()
546 .map_err(|error| {
547 crate::Error::Internal(format!(
548 "failed to create {num_threads}-thread search pool: {error}"
549 ))
550 })?,
551 );
552 pools.retain(|_, pool| pool.strong_count() > 0);
553 pools.insert(num_threads, Arc::downgrade(&pool));
554 static ANNOUNCED: OnceLock<()> = OnceLock::new();
555 log::log!(
556 shared_resource_log_level(&ANNOUNCED),
557 "[search] process-wide CPU pool: {} thread(s)",
558 num_threads
559 );
560 Ok(pool)
561}
562
563impl Default for IndexConfig {
564 fn default() -> Self {
565 #[cfg(feature = "native")]
566 let compression_threads = crate::default_compression_threads();
567 #[cfg(not(feature = "native"))]
568 let compression_threads = 1;
569
570 #[cfg(feature = "native")]
571 let search_threads = crate::default_search_threads();
572 #[cfg(not(feature = "native"))]
573 let search_threads = 1;
574
575 Self {
576 num_threads: search_threads,
577 sparse_io_concurrency: 4,
578 num_indexing_threads: 1, num_compression_threads: compression_threads,
580 term_cache_blocks: 256,
581 term_cache_budget_bytes: None,
582 term_dict_block_size: crate::structures::SSTableBlockSize::default(),
583 #[cfg(target_pointer_width = "64")]
587 store_cache_budget_bytes: 2 * 1024 * 1024 * 1024,
588 #[cfg(not(target_pointer_width = "64"))]
589 store_cache_budget_bytes: 32 * 1024 * 1024,
590 max_indexing_memory_bytes: 256 * 1024 * 1024, vector_training_max_samples: 10_000_000,
592 #[cfg(target_pointer_width = "64")]
593 vector_training_memory_bytes: 4 * 1024 * 1024 * 1024,
594 #[cfg(not(target_pointer_width = "64"))]
595 vector_training_memory_bytes: usize::MAX,
596 merge_policy: Box::new(crate::merge::TieredMergePolicy::large_scale()),
601 optimization: crate::structures::IndexOptimization::default(),
602 posting_codec: None,
603 quantized_norms: false,
604 compact_text: false,
605 posting_ratio_bounds: false,
606 posting_impact_bounds: false,
607 reload_interval_ms: 1000, max_concurrent_merges: 4,
609 #[cfg(feature = "native")]
610 background_merge_permits: Arc::new(tokio::sync::Semaphore::new(4)),
611 merge_bp_time_budget: Some(std::time::Duration::from_secs(600)),
612 #[cfg(target_pointer_width = "64")]
621 bp_memory_budget_bytes: 24 * 1024 * 1024 * 1024,
622 #[cfg(not(target_pointer_width = "64"))]
623 bp_memory_budget_bytes: usize::MAX,
624 compaction_memory_budget_bytes: 256 * 1024 * 1024,
625 #[cfg(feature = "native")]
626 background_reorder_permits: Arc::new(ReorderConcurrencyGate::new(2)),
627 #[cfg(feature = "native")]
628 background_reorder_pool: None,
629 }
630 }
631}
632
633pub const MAX_TERM_CACHE_BLOCKS: usize = 65_536;
637
638#[cfg(feature = "native")]
640pub(crate) fn validate_term_cache_blocks(blocks: usize) -> crate::Result<()> {
641 if blocks > MAX_TERM_CACHE_BLOCKS {
642 return Err(crate::Error::Internal(format!(
643 "IndexConfig.term_cache_blocks must be at most {MAX_TERM_CACHE_BLOCKS} (got {blocks})"
644 )));
645 }
646 Ok(())
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
651pub struct PostingBounds {
652 pub ratio: bool,
654 pub impact: bool,
656}
657
658impl PostingBounds {
659 pub(crate) fn new(ratio: bool, impact: bool) -> Self {
660 Self {
661 ratio: ratio || impact,
662 impact,
663 }
664 }
665}
666
667impl IndexConfig {
668 pub fn effective_posting_codec(&self) -> crate::structures::PostingCodec {
670 self.posting_codec
671 .unwrap_or_else(|| self.optimization.default_posting_codec())
672 }
673
674 pub fn effective_posting_bounds(&self) -> PostingBounds {
677 PostingBounds::new(self.posting_ratio_bounds, self.posting_impact_bounds)
678 }
679}
680
681#[cfg(feature = "native")]
684const POSTING_BOUNDS_PROBE_SEGMENTS: usize = 32;
685#[cfg(feature = "native")]
686const POSTING_BOUNDS_PROBE_TERMS: usize = 4096;
687
688#[cfg(feature = "native")]
698pub(crate) async fn segments_missing_posting_bounds<D: crate::directories::Directory>(
699 directory: &D,
700 metadata: &IndexMetadata,
701 config: &IndexConfig,
702) -> Result<(Vec<String>, usize)> {
703 use crate::segment::{SegmentFiles, SegmentId};
704 use crate::structures::{AsyncSSTableReader, BlockPostingList, TermInfo};
705
706 let mut missing = Vec::new();
707 let mut probed = 0usize;
708 if !config.effective_posting_bounds().ratio {
709 return Ok((missing, probed));
710 }
711 for id in metadata
712 .segment_ids()
713 .into_iter()
714 .take(POSTING_BOUNDS_PROBE_SEGMENTS)
715 {
716 let Some(segment_id) = SegmentId::from_hex(&id) else {
717 continue;
718 };
719 let files = SegmentFiles::new(segment_id.0);
720 if !directory.exists(&files.term_dict).await? {
721 continue;
722 }
723 let term_dict = AsyncSSTableReader::<TermInfo>::open_with_cache_budget(
724 directory.open_lazy(&files.term_dict).await?,
725 1,
726 None,
727 )
728 .await?;
729 let mut terms = term_dict.iter();
730 let mut external = None;
731 for _ in 0..POSTING_BOUNDS_PROBE_TERMS {
732 match terms.next().await? {
733 Some((_, info)) => {
734 if let Some(range) = info.external_info() {
735 external = Some(range);
736 break;
737 }
738 }
739 None => break,
740 }
741 }
742 let Some((offset, len)) = external else {
743 continue;
744 };
745 let postings = directory.open_lazy(&files.postings).await?;
746 let end = offset.checked_add(len).ok_or_else(|| {
747 crate::Error::Corruption("posting range overflow while probing bounds".into())
748 })?;
749 let list =
750 BlockPostingList::deserialize_zero_copy(postings.read_bytes_range(offset..end).await?)?;
751 probed += 1;
752 if !list.has_ratio_bounds() {
753 missing.push(id);
754 }
755 }
756 Ok((missing, probed))
757}
758
759#[cfg(feature = "native")]
763async fn log_posting_bounds_policy<D: crate::directories::Directory>(
764 directory: &D,
765 metadata: &IndexMetadata,
766 config: &IndexConfig,
767) {
768 match segments_missing_posting_bounds(directory, metadata, config).await {
769 Ok((missing, probed)) if !missing.is_empty() => log::info!(
770 "[index] {}: posting_ratio_bounds/posting_impact_bounds are enabled but {} of {} \
771 probed existing segments carry no block-bound metadata (e.g. {}). Bounds apply \
772 to new segments only; merges and compaction keep existing block layouts. \
773 Re-index to add bounds to old data.",
774 metadata.schema.index_label(),
775 missing.len(),
776 probed,
777 missing[0]
778 ),
779 Ok(_) => {}
780 Err(error) => log::warn!(
781 "[index] {}: could not probe existing segments for posting bounds: {}",
782 metadata.schema.index_label(),
783 error
784 ),
785 }
786}
787
788#[cfg(feature = "native")]
794fn segment_manager_from_config<D: crate::directories::DirectoryWriter + 'static>(
795 directory: &Arc<D>,
796 schema: &Arc<Schema>,
797 metadata: IndexMetadata,
798 config: &IndexConfig,
799) -> Result<Arc<crate::merge::SegmentManager<D>>> {
800 validate_term_cache_blocks(config.term_cache_blocks)?;
803 Ok(Arc::new(
804 crate::merge::SegmentManager::new(
805 Arc::clone(directory),
806 Arc::clone(schema),
807 metadata,
808 config.merge_policy.clone_box(),
809 config.term_cache_blocks,
810 config.max_concurrent_merges,
811 Arc::clone(&config.background_merge_permits),
812 config.merge_bp_time_budget,
813 config.bp_memory_budget_bytes,
814 Arc::clone(&config.background_reorder_permits),
815 config.background_reorder_pool.clone(),
816 )
817 .with_posting_config(config.optimization, config.effective_posting_codec())
818 .with_term_dict_block_size(config.term_dict_block_size)
819 .with_term_cache_budget(config.term_cache_budget_bytes),
820 ))
821}
822
823#[cfg(feature = "native")]
832pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
833 directory: Arc<D>,
834 config: IndexConfig,
835 search_resources: searcher::SearcherResources,
837 segment_manager: Arc<crate::merge::SegmentManager<D>>,
839 cached_reader: tokio::sync::OnceCell<IndexReader<D>>,
841}
842
843#[cfg(feature = "native")]
844impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
845 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
847 schema.validate()?;
848 let search_resources = searcher::SearcherResources::from_config(&config)?;
849 let directory = Arc::new(directory);
850 let schema = Arc::new(schema);
851 directory.set_index_label(schema.index_label());
853
854 if directory
858 .exists(std::path::Path::new(INDEX_META_FILENAME))
859 .await?
860 {
861 return Err(crate::Error::Internal(format!(
862 "refusing to create index: {} already exists in this directory; \
863 use Index::open to open the existing index, or delete the \
864 directory first if you really want to start over",
865 INDEX_META_FILENAME
866 )));
867 }
868
869 let metadata = IndexMetadata::new((*schema).clone());
870
871 let segment_manager = segment_manager_from_config(&directory, &schema, metadata, &config)?;
872
873 segment_manager.update_metadata(|_| {}).await?;
875
876 Ok(Self {
877 directory,
878 config,
879 search_resources,
880 segment_manager,
881 cached_reader: tokio::sync::OnceCell::new(),
882 })
883 }
884
885 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
887 let search_resources = searcher::SearcherResources::from_config(&config)?;
888 let directory = Arc::new(directory);
889
890 let metadata = IndexMetadata::load(directory.as_ref()).await?;
892 let schema = Arc::new(metadata.schema.clone());
893 directory.set_index_label(schema.index_label());
895 log_posting_bounds_policy(directory.as_ref(), &metadata, &config).await;
896
897 let segment_manager = segment_manager_from_config(&directory, &schema, metadata, &config)?;
898
899 segment_manager.try_load_and_publish_trained().await?;
901
902 Ok(Self {
903 directory,
904 config,
905 search_resources,
906 segment_manager,
907 cached_reader: tokio::sync::OnceCell::new(),
908 })
909 }
910
911 pub async fn open_with_writer(
916 directory: D,
917 config: IndexConfig,
918 ) -> Result<(Self, IndexWriter<D>)> {
919 let search_resources = searcher::SearcherResources::from_config(&config)?;
920 let writer = IndexWriter::open(directory, config.clone()).await?;
921 let index = Self {
922 directory: Arc::clone(&writer.directory),
923 config,
924 search_resources,
925 segment_manager: Arc::clone(writer.segment_manager()),
926 cached_reader: tokio::sync::OnceCell::new(),
927 };
928 Ok((index, writer))
929 }
930
931 pub fn schema(&self) -> Arc<Schema> {
933 self.schema_arc()
934 }
935
936 pub fn schema_arc(&self) -> Arc<Schema> {
938 self.segment_manager.published_generation().schema.clone()
939 }
940
941 pub fn directory(&self) -> &D {
943 &self.directory
944 }
945
946 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
948 &self.segment_manager
949 }
950
951 pub async fn reader(&self) -> Result<&IndexReader<D>> {
956 self.cached_reader
957 .get_or_try_init(|| async {
958 IndexReader::from_segment_manager_with_resources(
959 self.schema_arc(),
960 Arc::clone(&self.segment_manager),
961 self.config.reload_interval_ms,
962 self.search_resources.clone(),
963 )
964 .await
965 })
966 .await
967 }
968
969 pub fn config(&self) -> &IndexConfig {
971 &self.config
972 }
973
974 pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
976 let reader = self.reader().await?;
977 let searcher = reader.searcher().await?;
978 Ok(searcher.segment_readers().to_vec())
979 }
980
981 pub async fn num_docs(&self) -> Result<u32> {
983 let reader = self.reader().await?;
984 let searcher = reader.searcher().await?;
985 Ok(searcher.num_docs())
986 }
987
988 pub fn default_fields(&self) -> Vec<crate::Field> {
990 let schema = self.schema_arc();
991 if !schema.default_fields().is_empty() {
992 schema.default_fields().to_vec()
993 } else {
994 schema
995 .fields()
996 .filter(|(_, entry)| {
997 entry.indexed && entry.field_type == crate::dsl::FieldType::Text
998 })
999 .map(|(field, _)| field)
1000 .collect()
1001 }
1002 }
1003
1004 pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
1006 Arc::new(crate::tokenizer::TokenizerRegistry::default())
1007 }
1008
1009 pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
1011 let default_fields = self.default_fields();
1012 let tokenizers = self.tokenizers();
1013 let schema = self.schema_arc();
1014
1015 let query_routers = schema.query_routers();
1016 if !query_routers.is_empty()
1017 && let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
1018 {
1019 return crate::dsl::QueryLanguageParser::with_router(
1020 Arc::clone(&schema),
1021 default_fields,
1022 tokenizers,
1023 router,
1024 );
1025 }
1026
1027 crate::dsl::QueryLanguageParser::new(schema, default_fields, tokenizers)
1028 }
1029
1030 pub async fn query(
1032 &self,
1033 query_str: &str,
1034 limit: usize,
1035 ) -> Result<crate::query::SearchResponse> {
1036 self.query_offset(query_str, limit, 0).await
1037 }
1038
1039 pub async fn query_offset(
1041 &self,
1042 query_str: &str,
1043 limit: usize,
1044 offset: usize,
1045 ) -> Result<crate::query::SearchResponse> {
1046 let parser = self.query_parser();
1047 let query = parser
1048 .parse(query_str)
1049 .map_err(crate::error::Error::Query)?;
1050 self.search_offset(query.as_ref(), limit, offset).await
1051 }
1052
1053 pub async fn search(
1055 &self,
1056 query: &dyn crate::query::Query,
1057 limit: usize,
1058 ) -> Result<crate::query::SearchResponse> {
1059 self.search_offset(query, limit, 0).await
1060 }
1061
1062 pub async fn search_offset(
1064 &self,
1065 query: &dyn crate::query::Query,
1066 limit: usize,
1067 offset: usize,
1068 ) -> Result<crate::query::SearchResponse> {
1069 let reader = self.reader().await?;
1070 let searcher = reader.searcher().await?;
1071
1072 #[cfg(feature = "sync")]
1073 let (results, total_seen) = {
1074 let runtime_flavor = tokio::runtime::Handle::current().runtime_flavor();
1078 if runtime_flavor == tokio::runtime::RuntimeFlavor::MultiThread {
1079 tokio::task::block_in_place(|| {
1080 searcher.search_with_offset_and_count_sync(query, limit, offset)
1081 })?
1082 } else {
1083 searcher.search_with_offset_and_count_sync(query, limit, offset)?
1084 }
1085 };
1086
1087 #[cfg(not(feature = "sync"))]
1088 let (results, total_seen) = {
1089 searcher
1090 .search_with_offset_and_count(query, limit, offset)
1091 .await?
1092 };
1093
1094 let total_hits = total_seen;
1095 let hits: Vec<crate::query::SearchHit> = results
1096 .into_iter()
1097 .map(|result| crate::query::SearchHit {
1098 address: crate::query::DocAddress::new(result.segment_id, result.doc_id),
1099 score: result.score,
1100 matched_fields: result.extract_ordinals(),
1101 })
1102 .collect();
1103
1104 Ok(crate::query::SearchResponse { hits, total_hits })
1105 }
1106
1107 pub async fn get_document(
1109 &self,
1110 address: &crate::query::DocAddress,
1111 ) -> Result<Option<crate::dsl::Document>> {
1112 let reader = self.reader().await?;
1113 let searcher = reader.searcher().await?;
1114 searcher.get_document(address).await
1115 }
1116
1117 pub async fn get_postings(
1119 &self,
1120 field: crate::Field,
1121 term: &[u8],
1122 ) -> Result<
1123 Vec<(
1124 Arc<crate::segment::SegmentReader>,
1125 crate::structures::BlockPostingList,
1126 )>,
1127 > {
1128 let segments = self.segment_readers().await?;
1129 let mut results = Vec::new();
1130
1131 for segment in segments {
1132 if let Some(postings) = segment.get_postings(field, term).await? {
1133 results.push((segment, postings));
1134 }
1135 }
1136
1137 Ok(results)
1138 }
1139}
1140
1141#[cfg(feature = "native")]
1143impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
1144 pub fn writer(&self) -> writer::IndexWriter<D> {
1146 writer::IndexWriter::from_index(self)
1147 }
1148}
1149
1150#[cfg(test)]
1151mod tests;
1152
1153