Skip to main content

hermes_core/index/
reader.rs

1//! IndexReader - manages Searcher with reload policy (native only)
2//!
3//! The IndexReader periodically reloads its Searcher to pick up new segments.
4//! Uses SegmentManager as authoritative source for segment state.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8
9use arc_swap::ArcSwap;
10use parking_lot::RwLock;
11
12use crate::directories::DirectoryWriter;
13use crate::dsl::Schema;
14use crate::error::Result;
15
16use super::Searcher;
17use super::searcher::SearcherResources;
18
19/// IndexReader - manages Searcher with reload policy
20///
21/// The IndexReader periodically reloads its Searcher to pick up new segments.
22/// Uses SegmentManager as authoritative source for segment state (avoids race conditions).
23/// Combined searcher + segment IDs, swapped atomically via ArcSwap (wait-free reads).
24struct SearcherState<D: DirectoryWriter + 'static> {
25    searcher: Arc<Searcher<D>>,
26    segment_ids: Vec<String>,
27    publication_id: u64,
28}
29
30/// Cancellation-safe ownership of the reload flag. Async reload checks may be
31/// dropped at any await point; resetting manually only on normal return leaves
32/// every future reload disabled after request cancellation or panic.
33struct ReloadGuard<'a>(&'a AtomicBool);
34
35impl Drop for ReloadGuard<'_> {
36    fn drop(&mut self) {
37        self.0.store(false, Ordering::Release);
38    }
39}
40
41pub struct IndexReader<D: DirectoryWriter + 'static> {
42    /// Schema
43    schema: Arc<Schema>,
44    /// Segment manager - authoritative source for segments
45    segment_manager: Arc<crate::merge::SegmentManager<D>>,
46    /// Current searcher + segment IDs (ArcSwap for wait-free reads)
47    state: ArcSwap<SearcherState<D>>,
48    /// Cache and CPU policy preserved across every searcher reload.
49    resources: SearcherResources,
50    /// Last reload check time
51    last_reload_check: RwLock<std::time::Instant>,
52    /// Reload check interval (default 1 second)
53    reload_check_interval: std::time::Duration,
54    /// Guard against concurrent reloads
55    reloading: AtomicBool,
56}
57
58impl<D: DirectoryWriter + 'static> IndexReader<D> {
59    /// Create a new IndexReader from a segment manager
60    ///
61    /// Centroids are loaded dynamically from metadata on each reload,
62    /// so the reader always picks up centroids trained after Index::create().
63    pub async fn from_segment_manager(
64        schema: Arc<Schema>,
65        segment_manager: Arc<crate::merge::SegmentManager<D>>,
66        term_cache_blocks: usize,
67        reload_interval_ms: u64,
68    ) -> Result<Self> {
69        const STANDALONE_STORE_CACHE_BYTES: usize = 32 * 1024 * 1024;
70        let resources = SearcherResources::new(
71            term_cache_blocks,
72            STANDALONE_STORE_CACHE_BYTES,
73            crate::default_search_threads(),
74            4,
75        )?;
76        Self::from_segment_manager_with_resources(
77            schema,
78            segment_manager,
79            reload_interval_ms,
80            resources,
81        )
82        .await
83    }
84
85    /// Internal constructor used by `Index` to preserve its configured cache
86    /// and search CPU policy across reader reloads.
87    pub(crate) async fn from_segment_manager_with_resources(
88        schema: Arc<Schema>,
89        segment_manager: Arc<crate::merge::SegmentManager<D>>,
90        reload_interval_ms: u64,
91        resources: SearcherResources,
92    ) -> Result<Self> {
93        // Get initial segment IDs
94        let initial_segment_ids = segment_manager.get_segment_ids().await;
95
96        let (reader, publication_id) =
97            Self::create_reader(&schema, &segment_manager, resources.clone()).await?;
98
99        Ok(Self {
100            schema,
101            segment_manager,
102            state: ArcSwap::from_pointee(SearcherState {
103                searcher: Arc::new(reader),
104                segment_ids: initial_segment_ids,
105                publication_id,
106            }),
107            resources,
108            last_reload_check: RwLock::new(std::time::Instant::now()),
109            reload_check_interval: std::time::Duration::from_millis(reload_interval_ms),
110            reloading: AtomicBool::new(false),
111        })
112    }
113
114    /// Create a new reader with fresh snapshot from segment manager
115    ///
116    /// Captures segment IDs and their trained vector generation together.
117    async fn create_reader(
118        schema: &Arc<Schema>,
119        segment_manager: &Arc<crate::merge::SegmentManager<D>>,
120        resources: SearcherResources,
121    ) -> Result<(Searcher<D>, u64)> {
122        let snapshot = segment_manager.acquire_snapshot().await;
123        let generation = snapshot.published_generation();
124        let snapshot_schema = generation
125            .as_ref()
126            .map(|generation| Arc::clone(&generation.schema))
127            .unwrap_or_else(|| Arc::clone(schema));
128        let trained = generation
129            .as_ref()
130            .and_then(|generation| generation.trained_vectors.clone())
131            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
132        let publication_id = generation
133            .as_ref()
134            .map_or(0, |generation| generation.publication_id);
135
136        let searcher = Searcher::from_snapshot(
137            segment_manager.directory(),
138            snapshot_schema,
139            snapshot,
140            trained,
141            resources,
142        )
143        .await?;
144        Ok((searcher, publication_id))
145    }
146
147    /// Set reload check interval
148    pub fn set_reload_interval(&mut self, interval: std::time::Duration) {
149        self.reload_check_interval = interval;
150    }
151
152    /// Get current searcher (reloads only if segments changed)
153    ///
154    /// Wait-free read path via ArcSwap::load(). Reload checks are guarded
155    /// by an AtomicBool to prevent concurrent reloads.
156    pub async fn searcher(&self) -> Result<Arc<Searcher<D>>> {
157        // Check if we should check for segment changes
158        let should_check = {
159            let last = self.last_reload_check.read();
160            last.elapsed() >= self.reload_check_interval
161        };
162
163        if should_check {
164            // Try to acquire the reload guard (non-blocking)
165            if self
166                .reloading
167                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
168                .is_ok()
169            {
170                let _reload_guard = ReloadGuard(&self.reloading);
171                // We won the race — do the reload check
172                self.do_reload_check().await?;
173            }
174            // Otherwise another reload is in progress — just return current searcher
175        }
176
177        // Wait-free load (no lock contention with reloads)
178        Ok(Arc::clone(&self.state.load().searcher))
179    }
180
181    /// Actual reload check (called under the `reloading` guard)
182    async fn do_reload_check(&self) -> Result<()> {
183        *self.last_reload_check.write() = std::time::Instant::now();
184
185        // Get current segment IDs from segment manager
186        let new_segment_ids = self.segment_manager.get_segment_ids().await;
187
188        // Check if segments actually changed (wait-free read)
189        let publication_id = self.segment_manager.publication_id();
190        let generation_changed = {
191            let state = self.state.load();
192            state.segment_ids != new_segment_ids || state.publication_id != publication_id
193        };
194
195        if generation_changed {
196            let old_count = self.state.load().segment_ids.len();
197            let new_count = new_segment_ids.len();
198            log::info!(
199                "[index_reload] index={} old_count={} new_count={}",
200                self.schema.index_label(),
201                old_count,
202                new_count
203            );
204            self.reload_with_segments(new_segment_ids).await?;
205        }
206        Ok(())
207    }
208
209    /// Force reload reader with fresh snapshot.
210    ///
211    /// Waits for any in-progress reload (from `searcher()`) to finish, then
212    /// performs its own reload with the latest segment IDs. This guarantees
213    /// the reload actually happens — unlike `searcher()` which silently skips
214    /// if another reload is in progress.
215    pub async fn reload(&self) -> Result<()> {
216        // Wait for any in-progress reload to finish, then acquire the guard.
217        // This is critical: a concurrent do_reload_check() may have started
218        // before a commit, so its reload won't see the new segments.
219        loop {
220            if self
221                .reloading
222                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
223                .is_ok()
224            {
225                break;
226            }
227            tokio::task::yield_now().await;
228        }
229        let _reload_guard = ReloadGuard(&self.reloading);
230        let new_segment_ids = self.segment_manager.get_segment_ids().await;
231
232        // Fast path: skip reload if segments haven't changed
233        let publication_id = self.segment_manager.publication_id();
234        let generation_changed = {
235            let state = self.state.load();
236            state.segment_ids != new_segment_ids || state.publication_id != publication_id
237        };
238
239        if generation_changed {
240            self.reload_with_segments(new_segment_ids).await
241        } else {
242            log::debug!(
243                "[reload] index={} segments unchanged, skipping",
244                self.schema.index_label()
245            );
246            Ok(())
247        }
248    }
249
250    /// Internal reload with specific segment IDs.
251    /// Reuses existing segment readers for unchanged segments (avoids re-opening
252    /// mmaps, fast fields, sparse indexes, etc.).
253    /// Atomic swap via ArcSwap::store (wait-free for readers).
254    async fn reload_with_segments(&self, new_segment_ids: Vec<String>) -> Result<()> {
255        // Collect existing segment readers for reuse
256        let existing_segments: Vec<Arc<crate::segment::SegmentReader>> =
257            self.state.load().searcher.segment_readers().to_vec();
258
259        let snapshot = self.segment_manager.acquire_snapshot().await;
260        let generation = snapshot.published_generation();
261        let schema = generation
262            .as_ref()
263            .map(|generation| Arc::clone(&generation.schema))
264            .unwrap_or_else(|| Arc::clone(&self.schema));
265        let trained = generation
266            .as_ref()
267            .and_then(|generation| generation.trained_vectors.clone())
268            .unwrap_or_else(|| Arc::new(crate::segment::TrainedVectorStructures::default()));
269        let publication_id = generation
270            .as_ref()
271            .map_or(0, |generation| generation.publication_id);
272
273        let new_reader = Searcher::from_snapshot_reuse(
274            self.segment_manager.directory(),
275            schema,
276            snapshot,
277            trained,
278            self.resources.clone(),
279            &existing_segments,
280        )
281        .await?;
282
283        // Atomic swap — readers see old or new state, never a torn read
284        self.state.store(Arc::new(SearcherState {
285            searcher: Arc::new(new_reader),
286            segment_ids: new_segment_ids,
287            publication_id,
288        }));
289
290        Ok(())
291    }
292
293    /// Get schema
294    pub fn schema(&self) -> Arc<Schema> {
295        self.schema_arc()
296    }
297
298    /// Schema of the currently published search generation.
299    pub fn schema_arc(&self) -> Arc<Schema> {
300        self.state.load().searcher.schema_arc()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn reload_guard_releases_flag_on_unwind() {
310        let reloading = AtomicBool::new(true);
311        let result = std::panic::catch_unwind(|| {
312            let _guard = ReloadGuard(&reloading);
313            panic!("cancel reload");
314        });
315        assert!(result.is_err());
316        assert!(!reloading.load(Ordering::Acquire));
317    }
318
319    #[tokio::test]
320    async fn schema_publication_is_atomic_for_old_and_new_searchers() {
321        use crate::directories::RamDirectory;
322        use crate::dsl::{DenseVectorConfig, SchemaBuilder, VectorIndexAlter, VectorIndexType};
323
324        let mut builder = SchemaBuilder::default();
325        let field = builder.add_dense_vector_field_with_config(
326            "embedding",
327            true,
328            true,
329            DenseVectorConfig::ivf_tq(4, Some(2), 1),
330        );
331        let directory = RamDirectory::new();
332        let index = crate::Index::create(
333            directory.clone(),
334            builder.build(),
335            crate::IndexConfig::default(),
336        )
337        .await
338        .unwrap();
339        let reader = index.reader().await.unwrap();
340        let old_searcher = reader.searcher().await.unwrap();
341
342        let mut target = DenseVectorConfig::ivf_tq(4, Some(2), 1);
343        target.index_type = VectorIndexType::Scann;
344        target.tree_levels = Some(1);
345        target.soar = None;
346        let next_schema = Arc::new(
347            index
348                .schema_arc()
349                .with_vector_index_alter(field, VectorIndexAlter::Dense(target))
350                .unwrap(),
351        );
352        let update = index
353            .segment_manager()
354            .begin_vector_artifact_update()
355            .await
356            .unwrap();
357        index
358            .segment_manager()
359            .publish_vector_schema_only(&update, next_schema)
360            .await
361            .unwrap();
362        drop(update);
363
364        assert_eq!(
365            old_searcher
366                .schema()
367                .get_field_entry(field)
368                .unwrap()
369                .dense_vector_config
370                .as_ref()
371                .unwrap()
372                .index_type,
373            VectorIndexType::IvfTq
374        );
375        reader.reload().await.unwrap();
376        let new_searcher = reader.searcher().await.unwrap();
377        assert_eq!(
378            new_searcher
379                .schema()
380                .get_field_entry(field)
381                .unwrap()
382                .dense_vector_config
383                .as_ref()
384                .unwrap()
385                .index_type,
386            VectorIndexType::Scann
387        );
388
389        let reopened = crate::Index::open(directory, crate::IndexConfig::default())
390            .await
391            .unwrap();
392        assert_eq!(
393            reopened
394                .schema_arc()
395                .get_field_entry(field)
396                .unwrap()
397                .dense_vector_config
398                .as_ref()
399                .unwrap()
400                .index_type,
401            VectorIndexType::Scann
402        );
403    }
404}