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 = "sync")]
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#[derive(Debug, Clone)]
61pub struct IndexConfig {
62 pub num_threads: usize,
68 pub num_indexing_threads: usize,
70 pub num_compression_threads: usize,
72 pub term_cache_blocks: usize,
74 pub store_cache_blocks: usize,
76 pub max_indexing_memory_bytes: usize,
78 pub merge_policy: Box<dyn crate::merge::MergePolicy>,
80 pub optimization: crate::structures::IndexOptimization,
82 pub reload_interval_ms: u64,
84 pub max_concurrent_merges: usize,
86 #[cfg(feature = "native")]
90 pub background_merge_permits: Arc<tokio::sync::Semaphore>,
91 pub merge_bp_time_budget: Option<std::time::Duration>,
98 pub bp_memory_budget_bytes: usize,
105 #[cfg(feature = "native")]
111 pub background_reorder_permits: Arc<tokio::sync::Semaphore>,
112 #[cfg(feature = "native")]
116 pub background_reorder_pool: Option<Arc<rayon::ThreadPool>>,
117}
118
119#[cfg(feature = "sync")]
123static SEARCH_CPU_POOLS: OnceLock<parking_lot::Mutex<HashMap<usize, Weak<rayon::ThreadPool>>>> =
124 OnceLock::new();
125
126#[cfg(feature = "sync")]
127fn shared_search_pool(num_threads: usize) -> Result<Arc<rayon::ThreadPool>> {
128 if num_threads == 0 {
129 return Err(crate::Error::Internal(
130 "IndexConfig.num_threads must be greater than zero".into(),
131 ));
132 }
133
134 let mut pools = SEARCH_CPU_POOLS
135 .get_or_init(|| parking_lot::Mutex::new(HashMap::new()))
136 .lock();
137 if let Some(pool) = pools.get(&num_threads).and_then(Weak::upgrade) {
138 return Ok(pool);
139 }
140
141 let pool = Arc::new(
145 rayon::ThreadPoolBuilder::new()
146 .num_threads(num_threads)
147 .thread_name(move |idx| format!("hermes-search-{}-{}", num_threads, idx))
148 .build()
149 .map_err(|error| {
150 crate::Error::Internal(format!(
151 "failed to create {num_threads}-thread search pool: {error}"
152 ))
153 })?,
154 );
155 pools.retain(|_, pool| pool.strong_count() > 0);
156 pools.insert(num_threads, Arc::downgrade(&pool));
157 log::info!("[search] process-wide CPU pool: {} thread(s)", num_threads);
158 Ok(pool)
159}
160
161impl Default for IndexConfig {
162 fn default() -> Self {
163 #[cfg(feature = "native")]
164 let compression_threads = crate::default_compression_threads();
165 #[cfg(not(feature = "native"))]
166 let compression_threads = 1;
167
168 #[cfg(feature = "native")]
169 let search_threads = crate::default_search_threads();
170 #[cfg(not(feature = "native"))]
171 let search_threads = 1;
172
173 Self {
174 num_threads: search_threads,
175 num_indexing_threads: 1, num_compression_threads: compression_threads,
177 term_cache_blocks: 256,
178 store_cache_blocks: 32,
179 max_indexing_memory_bytes: 256 * 1024 * 1024, merge_policy: Box::new(crate::merge::TieredMergePolicy::large_scale()),
185 optimization: crate::structures::IndexOptimization::default(),
186 reload_interval_ms: 1000, max_concurrent_merges: 4,
188 #[cfg(feature = "native")]
189 background_merge_permits: Arc::new(tokio::sync::Semaphore::new(4)),
190 merge_bp_time_budget: Some(std::time::Duration::from_secs(600)),
191 #[cfg(target_pointer_width = "64")]
200 bp_memory_budget_bytes: 24 * 1024 * 1024 * 1024,
201 #[cfg(not(target_pointer_width = "64"))]
202 bp_memory_budget_bytes: usize::MAX,
203 #[cfg(feature = "native")]
204 background_reorder_permits: Arc::new(tokio::sync::Semaphore::new(2)),
205 #[cfg(feature = "native")]
206 background_reorder_pool: None,
207 }
208 }
209}
210
211#[cfg(feature = "native")]
220pub struct Index<D: crate::directories::DirectoryWriter + 'static> {
221 directory: Arc<D>,
222 schema: Arc<Schema>,
223 config: IndexConfig,
224 search_resources: searcher::SearcherResources,
226 segment_manager: Arc<crate::merge::SegmentManager<D>>,
228 cached_reader: tokio::sync::OnceCell<IndexReader<D>>,
230}
231
232#[cfg(feature = "native")]
233impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
234 pub async fn create(directory: D, schema: Schema, config: IndexConfig) -> Result<Self> {
236 let search_resources = searcher::SearcherResources::new(
237 config.term_cache_blocks,
238 config.store_cache_blocks,
239 config.num_threads,
240 )?;
241 let directory = Arc::new(directory);
242 let schema = Arc::new(schema);
243 directory.set_index_label(schema.index_label());
245
246 if directory
250 .exists(std::path::Path::new(INDEX_META_FILENAME))
251 .await?
252 {
253 return Err(crate::Error::Internal(format!(
254 "refusing to create index: {} already exists in this directory; \
255 use Index::open to open the existing index, or delete the \
256 directory first if you really want to start over",
257 INDEX_META_FILENAME
258 )));
259 }
260
261 let metadata = IndexMetadata::new((*schema).clone());
262
263 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
264 Arc::clone(&directory),
265 Arc::clone(&schema),
266 metadata,
267 config.merge_policy.clone_box(),
268 config.term_cache_blocks,
269 config.max_concurrent_merges,
270 Arc::clone(&config.background_merge_permits),
271 config.merge_bp_time_budget,
272 config.bp_memory_budget_bytes,
273 Arc::clone(&config.background_reorder_permits),
274 config.background_reorder_pool.clone(),
275 ));
276
277 segment_manager.update_metadata(|_| {}).await?;
279
280 Ok(Self {
281 directory,
282 schema,
283 config,
284 search_resources,
285 segment_manager,
286 cached_reader: tokio::sync::OnceCell::new(),
287 })
288 }
289
290 pub async fn open(directory: D, config: IndexConfig) -> Result<Self> {
292 let search_resources = searcher::SearcherResources::new(
293 config.term_cache_blocks,
294 config.store_cache_blocks,
295 config.num_threads,
296 )?;
297 let directory = Arc::new(directory);
298
299 let metadata = IndexMetadata::load(directory.as_ref()).await?;
301 let schema = Arc::new(metadata.schema.clone());
302 directory.set_index_label(schema.index_label());
304
305 let segment_manager = Arc::new(crate::merge::SegmentManager::new(
306 Arc::clone(&directory),
307 Arc::clone(&schema),
308 metadata,
309 config.merge_policy.clone_box(),
310 config.term_cache_blocks,
311 config.max_concurrent_merges,
312 Arc::clone(&config.background_merge_permits),
313 config.merge_bp_time_budget,
314 config.bp_memory_budget_bytes,
315 Arc::clone(&config.background_reorder_permits),
316 config.background_reorder_pool.clone(),
317 ));
318
319 segment_manager.try_load_and_publish_trained().await?;
321
322 Ok(Self {
323 directory,
324 schema,
325 config,
326 search_resources,
327 segment_manager,
328 cached_reader: tokio::sync::OnceCell::new(),
329 })
330 }
331
332 pub fn schema(&self) -> &Schema {
334 &self.schema
335 }
336
337 pub fn schema_arc(&self) -> &Arc<Schema> {
339 &self.schema
340 }
341
342 pub fn directory(&self) -> &D {
344 &self.directory
345 }
346
347 pub fn segment_manager(&self) -> &Arc<crate::merge::SegmentManager<D>> {
349 &self.segment_manager
350 }
351
352 pub async fn reader(&self) -> Result<&IndexReader<D>> {
357 self.cached_reader
358 .get_or_try_init(|| async {
359 IndexReader::from_segment_manager_with_resources(
360 Arc::clone(&self.schema),
361 Arc::clone(&self.segment_manager),
362 self.config.reload_interval_ms,
363 self.search_resources.clone(),
364 )
365 .await
366 })
367 .await
368 }
369
370 pub fn config(&self) -> &IndexConfig {
372 &self.config
373 }
374
375 pub async fn segment_readers(&self) -> Result<Vec<Arc<crate::segment::SegmentReader>>> {
377 let reader = self.reader().await?;
378 let searcher = reader.searcher().await?;
379 Ok(searcher.segment_readers().to_vec())
380 }
381
382 pub async fn num_docs(&self) -> Result<u32> {
384 let reader = self.reader().await?;
385 let searcher = reader.searcher().await?;
386 Ok(searcher.num_docs())
387 }
388
389 pub fn default_fields(&self) -> Vec<crate::Field> {
391 if !self.schema.default_fields().is_empty() {
392 self.schema.default_fields().to_vec()
393 } else {
394 self.schema
395 .fields()
396 .filter(|(_, entry)| {
397 entry.indexed && entry.field_type == crate::dsl::FieldType::Text
398 })
399 .map(|(field, _)| field)
400 .collect()
401 }
402 }
403
404 pub fn tokenizers(&self) -> Arc<crate::tokenizer::TokenizerRegistry> {
406 Arc::new(crate::tokenizer::TokenizerRegistry::default())
407 }
408
409 pub fn query_parser(&self) -> crate::dsl::QueryLanguageParser {
411 let default_fields = self.default_fields();
412 let tokenizers = self.tokenizers();
413
414 let query_routers = self.schema.query_routers();
415 if !query_routers.is_empty()
416 && let Ok(router) = crate::dsl::QueryFieldRouter::from_rules(query_routers)
417 {
418 return crate::dsl::QueryLanguageParser::with_router(
419 Arc::clone(&self.schema),
420 default_fields,
421 tokenizers,
422 router,
423 );
424 }
425
426 crate::dsl::QueryLanguageParser::new(Arc::clone(&self.schema), default_fields, tokenizers)
427 }
428
429 pub async fn query(
431 &self,
432 query_str: &str,
433 limit: usize,
434 ) -> Result<crate::query::SearchResponse> {
435 self.query_offset(query_str, limit, 0).await
436 }
437
438 pub async fn query_offset(
440 &self,
441 query_str: &str,
442 limit: usize,
443 offset: usize,
444 ) -> Result<crate::query::SearchResponse> {
445 let parser = self.query_parser();
446 let query = parser
447 .parse(query_str)
448 .map_err(crate::error::Error::Query)?;
449 self.search_offset(query.as_ref(), limit, offset).await
450 }
451
452 pub async fn search(
454 &self,
455 query: &dyn crate::query::Query,
456 limit: usize,
457 ) -> Result<crate::query::SearchResponse> {
458 self.search_offset(query, limit, 0).await
459 }
460
461 pub async fn search_offset(
463 &self,
464 query: &dyn crate::query::Query,
465 limit: usize,
466 offset: usize,
467 ) -> Result<crate::query::SearchResponse> {
468 let reader = self.reader().await?;
469 let searcher = reader.searcher().await?;
470
471 #[cfg(feature = "sync")]
472 let (results, total_seen) = {
473 let runtime_flavor = tokio::runtime::Handle::current().runtime_flavor();
477 if runtime_flavor == tokio::runtime::RuntimeFlavor::MultiThread {
478 tokio::task::block_in_place(|| {
479 searcher.search_with_offset_and_count_sync(query, limit, offset)
480 })?
481 } else {
482 searcher.search_with_offset_and_count_sync(query, limit, offset)?
483 }
484 };
485
486 #[cfg(not(feature = "sync"))]
487 let (results, total_seen) = {
488 searcher
489 .search_with_offset_and_count(query, limit, offset)
490 .await?
491 };
492
493 let total_hits = total_seen;
494 let hits: Vec<crate::query::SearchHit> = results
495 .into_iter()
496 .map(|result| crate::query::SearchHit {
497 address: crate::query::DocAddress::new(result.segment_id, result.doc_id),
498 score: result.score,
499 matched_fields: result.extract_ordinals(),
500 })
501 .collect();
502
503 Ok(crate::query::SearchResponse { hits, total_hits })
504 }
505
506 pub async fn get_document(
508 &self,
509 address: &crate::query::DocAddress,
510 ) -> Result<Option<crate::dsl::Document>> {
511 let reader = self.reader().await?;
512 let searcher = reader.searcher().await?;
513 searcher.get_document(address).await
514 }
515
516 pub async fn get_postings(
518 &self,
519 field: crate::Field,
520 term: &[u8],
521 ) -> Result<
522 Vec<(
523 Arc<crate::segment::SegmentReader>,
524 crate::structures::BlockPostingList,
525 )>,
526 > {
527 let segments = self.segment_readers().await?;
528 let mut results = Vec::new();
529
530 for segment in segments {
531 if let Some(postings) = segment.get_postings(field, term).await? {
532 results.push((segment, postings));
533 }
534 }
535
536 Ok(results)
537 }
538}
539
540#[cfg(feature = "native")]
542impl<D: crate::directories::DirectoryWriter + 'static> Index<D> {
543 pub fn writer(&self) -> writer::IndexWriter<D> {
545 writer::IndexWriter::from_index(self)
546 }
547}
548
549#[cfg(test)]
550mod tests;
551
552