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 optimizer_permits: Arc<tokio::sync::Semaphore>,
92 automatic_merge_permits: Arc<tokio::sync::Semaphore>,
96 limit: usize,
97 foreground_lock: Arc<tokio::sync::Mutex<()>>,
98 foreground_active: std::sync::atomic::AtomicBool,
99 foreground_finished: tokio::sync::Notify,
100}
101
102#[cfg(feature = "native")]
108#[derive(Debug)]
109pub(crate) struct BmpIoGate {
110 limit: usize,
111 active: parking_lot::Mutex<usize>,
112 available: parking_lot::Condvar,
113 async_available: tokio::sync::Notify,
114}
115
116#[cfg(feature = "native")]
117impl BmpIoGate {
118 fn new(limit: usize) -> Self {
119 Self {
120 limit,
121 active: parking_lot::Mutex::new(0),
122 available: parking_lot::Condvar::new(),
123 async_available: tokio::sync::Notify::new(),
124 }
125 }
126
127 fn acquire(&self) -> BmpIoPermit<'_> {
128 let mut active = self.active.lock();
129 while *active >= self.limit {
130 self.available.wait(&mut active);
131 }
132 *active += 1;
133 BmpIoPermit { gate: self }
134 }
135
136 async fn acquire_async(&self) -> BmpIoPermit<'_> {
137 loop {
138 let notified = self.async_available.notified();
141 {
142 let mut active = self.active.lock();
143 if *active < self.limit {
144 *active += 1;
145 return BmpIoPermit { gate: self };
146 }
147 }
148 notified.await;
149 }
150 }
151}
152
153#[cfg(feature = "native")]
154struct BmpIoPermit<'a> {
155 gate: &'a BmpIoGate,
156}
157
158#[cfg(feature = "native")]
159impl Drop for BmpIoPermit<'_> {
160 fn drop(&mut self) {
161 let mut active = self.gate.active.lock();
162 *active -= 1;
163 self.gate.available.notify_one();
164 self.gate.async_available.notify_one();
165 }
166}
167
168#[cfg(feature = "native")]
169impl ReorderConcurrencyGate {
170 pub fn new(requested_limit: usize) -> Self {
171 let limit = requested_limit.clamp(1, MAX_CONCURRENT_REORDER_PASSES);
172 let automatic_merge_limit = limit.saturating_sub(1).max(1);
173 Self {
174 permits: Arc::new(tokio::sync::Semaphore::new(limit)),
175 optimizer_permits: Arc::new(tokio::sync::Semaphore::new(1)),
176 automatic_merge_permits: Arc::new(tokio::sync::Semaphore::new(automatic_merge_limit)),
177 limit,
178 foreground_lock: Arc::new(tokio::sync::Mutex::new(())),
179 foreground_active: std::sync::atomic::AtomicBool::new(false),
180 foreground_finished: tokio::sync::Notify::new(),
181 }
182 }
183
184 pub fn limit(&self) -> usize {
185 self.limit
186 }
187
188 pub(crate) async fn acquire(
189 self: &Arc<Self>,
190 priority: ReorderPriority,
191 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
192 match priority {
193 ReorderPriority::Optimizer => {
194 let optimizer_permit = Arc::clone(&self.optimizer_permits).acquire_owned().await?;
195 self.acquire_background(Some(optimizer_permit), None).await
196 }
197 ReorderPriority::AutomaticMerge => {
198 let merge_permit = Arc::clone(&self.automatic_merge_permits)
199 .acquire_owned()
200 .await?;
201 self.acquire_background(None, Some(merge_permit)).await
202 }
203 ReorderPriority::Foreground => self.acquire_foreground().await,
204 }
205 }
206
207 async fn acquire_background(
209 self: &Arc<Self>,
210 optimizer: Option<tokio::sync::OwnedSemaphorePermit>,
211 automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
212 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
213 loop {
214 if self
215 .foreground_active
216 .load(std::sync::atomic::Ordering::Acquire)
217 {
218 let notified = self.foreground_finished.notified();
219 if self
220 .foreground_active
221 .load(std::sync::atomic::Ordering::Acquire)
222 {
223 notified.await;
224 continue;
225 }
226 }
227
228 let permit = Arc::clone(&self.permits).acquire_owned().await?;
229 if !self
230 .foreground_active
231 .load(std::sync::atomic::Ordering::Acquire)
232 {
233 return Ok(ReorderPermit {
234 _permit: permit,
235 _optimizer: optimizer,
236 _automatic_merge: automatic_merge,
237 });
238 }
239 drop(permit);
242 }
243 }
244
245 async fn acquire_foreground(
247 self: &Arc<Self>,
248 ) -> std::result::Result<ReorderPermit, tokio::sync::AcquireError> {
249 let permit = Arc::clone(&self.permits).acquire_owned().await?;
250 Ok(ReorderPermit {
251 _permit: permit,
252 _optimizer: None,
253 _automatic_merge: None,
254 })
255 }
256
257 pub(crate) async fn begin_foreground(
263 self: &Arc<Self>,
264 ) -> std::result::Result<ForegroundReorderGuard, tokio::sync::AcquireError> {
265 let exclusive = Arc::clone(&self.foreground_lock).lock_owned().await;
266 self.foreground_active
267 .store(true, std::sync::atomic::Ordering::Release);
268
269 let mut guard = ForegroundReorderGuard {
273 gate: Arc::clone(self),
274 reserved: None,
275 _exclusive: exclusive,
276 };
277 if self.limit > 1 {
278 guard.reserved = Some(
279 Arc::clone(&self.permits)
280 .acquire_many_owned((self.limit - 1) as u32)
281 .await?,
282 );
283 }
284 Ok(guard)
285 }
286}
287
288#[cfg(feature = "native")]
289pub(crate) struct ReorderPermit {
290 _permit: tokio::sync::OwnedSemaphorePermit,
291 _optimizer: Option<tokio::sync::OwnedSemaphorePermit>,
292 _automatic_merge: Option<tokio::sync::OwnedSemaphorePermit>,
293}
294
295#[cfg(feature = "native")]
296pub(crate) struct ForegroundReorderGuard {
297 gate: Arc<ReorderConcurrencyGate>,
298 reserved: Option<tokio::sync::OwnedSemaphorePermit>,
299 _exclusive: tokio::sync::OwnedMutexGuard<()>,
300}
301
302#[cfg(feature = "native")]
303impl Drop for ForegroundReorderGuard {
304 fn drop(&mut self) {
305 drop(self.reserved.take());
307 self.gate
308 .foreground_active
309 .store(false, std::sync::atomic::Ordering::Release);
310 self.gate.foreground_finished.notify_waiters();
311 }
312}
313
314#[derive(Debug, Clone)]
316pub struct IndexConfig {
317 pub num_threads: usize,
323 pub bmp_io_concurrency: usize,
328 pub num_indexing_threads: usize,
330 pub num_compression_threads: usize,
334 pub term_cache_blocks: usize,
336 pub store_cache_budget_bytes: usize,
343 pub max_indexing_memory_bytes: usize,
345 pub vector_training_max_samples: usize,
349 pub vector_training_memory_bytes: usize,
351 pub merge_policy: Box<dyn crate::merge::MergePolicy>,
353 pub optimization: crate::structures::IndexOptimization,
355 pub reload_interval_ms: u64,
357 pub max_concurrent_merges: usize,
359 #[cfg(feature = "native")]
363 pub background_merge_permits: Arc<tokio::sync::Semaphore>,
364 pub merge_bp_time_budget: Option<std::time::Duration>,
371 pub bp_memory_budget_bytes: usize,
378 #[cfg(feature = "native")]
384 pub background_reorder_permits: Arc<ReorderConcurrencyGate>,
385 #[cfg(feature = "native")]
389 pub background_reorder_pool: Option<Arc<rayon::ThreadPool>>,
390}
391
392#[cfg(feature = "sync")]
396static SEARCH_CPU_POOLS: OnceLock<parking_lot::Mutex<HashMap<usize, Weak<rayon::ThreadPool>>>> =
397 OnceLock::new();
398
399#[cfg(feature = "native")]
404static STORE_CACHE_POOLS: OnceLock<
405 parking_lot::Mutex<std::collections::HashMap<usize, Weak<crate::segment::SharedStoreCache>>>,
406> = OnceLock::new();
407
408#[cfg(feature = "native")]
409static BMP_IO_GATES: OnceLock<
410 parking_lot::Mutex<std::collections::HashMap<usize, Weak<BmpIoGate>>>,
411> = OnceLock::new();
412
413#[cfg(feature = "native")]
414pub(crate) fn shared_bmp_io_gate(limit: usize) -> Arc<BmpIoGate> {
415 let mut gates = BMP_IO_GATES
416 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
417 .lock();
418 if let Some(gate) = gates.get(&limit).and_then(Weak::upgrade) {
419 return gate;
420 }
421 let gate = Arc::new(BmpIoGate::new(limit));
422 gates.retain(|_, gate| gate.strong_count() > 0);
423 gates.insert(limit, Arc::downgrade(&gate));
424 log::info!("[bmp] process-wide random-I/O concurrency={limit}");
425 gate
426}
427
428#[cfg(feature = "native")]
429pub(crate) fn shared_store_cache(budget_bytes: usize) -> Arc<crate::segment::SharedStoreCache> {
430 let mut caches = STORE_CACHE_POOLS
431 .get_or_init(|| parking_lot::Mutex::new(std::collections::HashMap::new()))
432 .lock();
433 if let Some(cache) = caches.get(&budget_bytes).and_then(Weak::upgrade) {
434 return cache;
435 }
436 let cache = Arc::new(crate::segment::SharedStoreCache::new(budget_bytes));
437 caches.retain(|_, cache| cache.strong_count() > 0);
438 caches.insert(budget_bytes, Arc::downgrade(&cache));
439 log::info!(
440 "[store_cache] process-wide budget={}",
441 crate::format_bytes(budget_bytes as u64)
442 );
443 cache
444}
445
446#[cfg(feature = "sync")]
447fn shared_search_pool(num_threads: usize) -> Result<Arc<rayon::ThreadPool>> {
448 if num_threads == 0 {
449 return Err(crate::Error::Internal(
450 "IndexConfig.num_threads must be greater than zero".into(),
451 ));
452 }
453
454 let mut pools = SEARCH_CPU_POOLS
455 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
456 .lock();
457 if let Some(pool) = pools.get(&num_threads).and_then(Weak::upgrade) {
458 return Ok(pool);
459 }
460
461 let pool = Arc::new(
465 rayon::ThreadPoolBuilder::new()
466 .num_threads(num_threads)
467 .thread_name(move |idx| format!("hermes-search-{}-{}", num_threads, idx))
468 .build()
469 .map_err(|error| {
470 crate::Error::Internal(format!(
471 "failed to create {num_threads}-thread search pool: {error}"
472 ))
473 })?,
474 );
475 pools.retain(|_, pool| pool.strong_count() > 0);
476 pools.insert(num_threads, Arc::downgrade(&pool));
477 log::info!("[search] process-wide CPU pool: {} thread(s)", num_threads);
478 Ok(pool)
479}
480
481impl Default for IndexConfig {
482 fn default() -> Self {
483 #[cfg(feature = "native")]
484 let compression_threads = crate::default_compression_threads();
485 #[cfg(not(feature = "native"))]
486 let compression_threads = 1;
487
488 #[cfg(feature = "native")]
489 let search_threads = crate::default_search_threads();
490 #[cfg(not(feature = "native"))]
491 let search_threads = 1;
492
493 Self {
494 num_threads: search_threads,
495 bmp_io_concurrency: 4,
496 num_indexing_threads: 1, num_compression_threads: compression_threads,
498 term_cache_blocks: 256,
499 #[cfg(target_pointer_width = "64")]
503 store_cache_budget_bytes: 2 * 1024 * 1024 * 1024,
504 #[cfg(not(target_pointer_width = "64"))]
505 store_cache_budget_bytes: 32 * 1024 * 1024,
506 max_indexing_memory_bytes: 256 * 1024 * 1024, vector_training_max_samples: 10_000_000,
508 #[cfg(target_pointer_width = "64")]
509 vector_training_memory_bytes: 4 * 1024 * 1024 * 1024,
510 #[cfg(not(target_pointer_width = "64"))]
511 vector_training_memory_bytes: usize::MAX,
512 merge_policy: Box::new(crate::merge::TieredMergePolicy::large_scale()),
517 optimization: crate::structures::IndexOptimization::default(),
518 reload_interval_ms: 1000, max_concurrent_merges: 4,
520 #[cfg(feature = "native")]
521 background_merge_permits: Arc::new(tokio::sync::Semaphore::new(4)),
522 merge_bp_time_budget: Some(std::time::Duration::from_secs(600)),
523 #[cfg(target_pointer_width = "64")]
532 bp_memory_budget_bytes: 24 * 1024 * 1024 * 1024,
533 #[cfg(not(target_pointer_width = "64"))]
534 bp_memory_budget_bytes: usize::MAX,
535 #[cfg(feature = "native")]
536 background_reorder_permits: Arc::new(ReorderConcurrencyGate::new(2)),
537 #[cfg(feature = "native")]
538 background_reorder_pool: None,
539 }
540 }
541}
542
543#[cfg(feature = "native")]
552pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
553 directory: Arc<D>,
554 schema: Arc<Schema>,
555 config: IndexConfig,
556 search_resources: searcher::SearcherResources,
558 segment_manager: Arc<crate::merge::SegmentManager<D>>,
560 cached_reader: tokio::sync::OnceCell<IndexReader<D>>,
562}
563
564#[cfg(feature = "native")]
565impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
566 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
568 let search_resources = searcher::SearcherResources::new(
569 config.term_cache_blocks,
570 config.store_cache_budget_bytes,
571 config.num_threads,
572 config.bmp_io_concurrency,
573 )?;
574 let directory = Arc::new(directory);
575 let schema = Arc::new(schema);
576 directory.set_index_label(schema.index_label());
578
579 if directory
583 .exists(std::path::Path::new(INDEX_META_FILENAME))
584 .await?
585 {
586 return Err(crate::Error::Internal(format!(
587 "refusing to create index: {} already exists in this directory; \
588 use Index::open to open the existing index, or delete the \
589 directory first if you really want to start over",
590 INDEX_META_FILENAME
591 )));
592 }
593
594 let metadata = IndexMetadata::new((*schema).clone());
595
596 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
597 Arc::clone(&directory),
598 Arc::clone(&schema),
599 metadata,
600 config.merge_policy.clone_box(),
601 config.term_cache_blocks,
602 config.max_concurrent_merges,
603 Arc::clone(&config.background_merge_permits),
604 config.merge_bp_time_budget,
605 config.bp_memory_budget_bytes,
606 Arc::clone(&config.background_reorder_permits),
607 config.background_reorder_pool.clone(),
608 ));
609
610 segment_manager.update_metadata(|_| {}).await?;
612
613 Ok(Self {
614 directory,
615 schema,
616 config,
617 search_resources,
618 segment_manager,
619 cached_reader: tokio::sync::OnceCell::new(),
620 })
621 }
622
623 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
625 let search_resources = searcher::SearcherResources::new(
626 config.term_cache_blocks,
627 config.store_cache_budget_bytes,
628 config.num_threads,
629 config.bmp_io_concurrency,
630 )?;
631 let directory = Arc::new(directory);
632
633 let metadata = IndexMetadata::load(directory.as_ref()).await?;
635 let schema = Arc::new(metadata.schema.clone());
636 directory.set_index_label(schema.index_label());
638
639 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
640 Arc::clone(&directory),
641 Arc::clone(&schema),
642 metadata,
643 config.merge_policy.clone_box(),
644 config.term_cache_blocks,
645 config.max_concurrent_merges,
646 Arc::clone(&config.background_merge_permits),
647 config.merge_bp_time_budget,
648 config.bp_memory_budget_bytes,
649 Arc::clone(&config.background_reorder_permits),
650 config.background_reorder_pool.clone(),
651 ));
652
653 segment_manager.try_load_and_publish_trained().await?;
655
656 Ok(Self {
657 directory,
658 schema,
659 config,
660 search_resources,
661 segment_manager,
662 cached_reader: tokio::sync::OnceCell::new(),
663 })
664 }
665
666 pub fn schema(&self) -> &Schema {
668 &self.schema
669 }
670
671 pub fn schema_arc(&self) -> &Arc<Schema> {
673 &self.schema
674 }
675
676 pub fn directory(&self) -> &D {
678 &self.directory
679 }
680
681 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
683 &self.segment_manager
684 }
685
686 pub async fn reader(&self) -> Result<&IndexReader<D>> {
691 self.cached_reader
692 .get_or_try_init(|| async {
693 IndexReader::from_segment_manager_with_resources(
694 Arc::clone(&self.schema),
695 Arc::clone(&self.segment_manager),
696 self.config.reload_interval_ms,
697 self.search_resources.clone(),
698 )
699 .await
700 })
701 .await
702 }
703
704 pub fn config(&self) -> &IndexConfig {
706 &self.config
707 }
708
709 pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
711 let reader = self.reader().await?;
712 let searcher = reader.searcher().await?;
713 Ok(searcher.segment_readers().to_vec())
714 }
715
716 pub async fn num_docs(&self) -> Result<u32> {
718 let reader = self.reader().await?;
719 let searcher = reader.searcher().await?;
720 Ok(searcher.num_docs())
721 }
722
723 pub fn default_fields(&self) -> Vec<crate::Field> {
725 if !self.schema.default_fields().is_empty() {
726 self.schema.default_fields().to_vec()
727 } else {
728 self.schema
729 .fields()
730 .filter(|(_, entry)| {
731 entry.indexed && entry.field_type == crate::dsl::FieldType::Text
732 })
733 .map(|(field, _)| field)
734 .collect()
735 }
736 }
737
738 pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
740 Arc::new(crate::tokenizer::TokenizerRegistry::default())
741 }
742
743 pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
745 let default_fields = self.default_fields();
746 let tokenizers = self.tokenizers();
747
748 let query_routers = self.schema.query_routers();
749 if !query_routers.is_empty()
750 && let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
751 {
752 return crate::dsl::QueryLanguageParser::with_router(
753 Arc::clone(&self.schema),
754 default_fields,
755 tokenizers,
756 router,
757 );
758 }
759
760 crate::dsl::QueryLanguageParser::new(Arc::clone(&self.schema), default_fields, tokenizers)
761 }
762
763 pub async fn query(
765 &self,
766 query_str: &str,
767 limit: usize,
768 ) -> Result<crate::query::SearchResponse> {
769 self.query_offset(query_str, limit, 0).await
770 }
771
772 pub async fn query_offset(
774 &self,
775 query_str: &str,
776 limit: usize,
777 offset: usize,
778 ) -> Result<crate::query::SearchResponse> {
779 let parser = self.query_parser();
780 let query = parser
781 .parse(query_str)
782 .map_err(crate::error::Error::Query)?;
783 self.search_offset(query.as_ref(), limit, offset).await
784 }
785
786 pub async fn search(
788 &self,
789 query: &dyn crate::query::Query,
790 limit: usize,
791 ) -> Result<crate::query::SearchResponse> {
792 self.search_offset(query, limit, 0).await
793 }
794
795 pub async fn search_offset(
797 &self,
798 query: &dyn crate::query::Query,
799 limit: usize,
800 offset: usize,
801 ) -> Result<crate::query::SearchResponse> {
802 let reader = self.reader().await?;
803 let searcher = reader.searcher().await?;
804
805 #[cfg(feature = "sync")]
806 let (results, total_seen) = {
807 let runtime_flavor = tokio::runtime::Handle::current().runtime_flavor();
811 if runtime_flavor == tokio::runtime::RuntimeFlavor::MultiThread {
812 tokio::task::block_in_place(|| {
813 searcher.search_with_offset_and_count_sync(query, limit, offset)
814 })?
815 } else {
816 searcher.search_with_offset_and_count_sync(query, limit, offset)?
817 }
818 };
819
820 #[cfg(not(feature = "sync"))]
821 let (results, total_seen) = {
822 searcher
823 .search_with_offset_and_count(query, limit, offset)
824 .await?
825 };
826
827 let total_hits = total_seen;
828 let hits: Vec<crate::query::SearchHit> = results
829 .into_iter()
830 .map(|result| crate::query::SearchHit {
831 address: crate::query::DocAddress::new(result.segment_id, result.doc_id),
832 score: result.score,
833 matched_fields: result.extract_ordinals(),
834 })
835 .collect();
836
837 Ok(crate::query::SearchResponse { hits, total_hits })
838 }
839
840 pub async fn get_document(
842 &self,
843 address: &crate::query::DocAddress,
844 ) -> Result<Option<crate::dsl::Document>> {
845 let reader = self.reader().await?;
846 let searcher = reader.searcher().await?;
847 searcher.get_document(address).await
848 }
849
850 pub async fn get_postings(
852 &self,
853 field: crate::Field,
854 term: &[u8],
855 ) -> Result<
856 Vec<(
857 Arc<crate::segment::SegmentReader>,
858 crate::structures::BlockPostingList,
859 )>,
860 > {
861 let segments = self.segment_readers().await?;
862 let mut results = Vec::new();
863
864 for segment in segments {
865 if let Some(postings) = segment.get_postings(field, term).await? {
866 results.push((segment, postings));
867 }
868 }
869
870 Ok(results)
871 }
872}
873
874#[cfg(feature = "native")]
876impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
877 pub fn writer(&self) -> writer::IndexWriter<D> {
879 writer::IndexWriter::from_index(self)
880 }
881}
882
883#[cfg(test)]
884mod tests;
885
886