Skip to main content

a3s_vec/collection/
index_api.rs

1//! Runtime in-memory index lifecycle and generation publication.
2
3use super::{
4    ensure_writable, finish_schema_commit, persist_index_cache, Collection,
5    CollectionResourceLimits,
6};
7use crate::error::{Error, Result};
8use crate::index::IndexRegistry;
9use crate::schema::{CollectionSchema, IndexParams};
10use crate::types::IndexType;
11
12impl Collection {
13    pub fn create_index(&self, field_name: &str, params: &IndexParams) -> Result<()> {
14        self.ensure_open()?;
15        let _writer = self
16            .inner
17            .writer
18            .lock()
19            .map_err(|_| Error::internal("writer lock poisoned"))?;
20        let current = self
21            .inner
22            .state
23            .read()
24            .map_err(|_| Error::internal("collection state lock poisoned"))?
25            .clone();
26        ensure_writable(&current.options)?;
27        current
28            .schema
29            .check_index_configuration(field_name, params)?;
30        if !matches!(
31            params.index_type,
32            IndexType::Flat
33                | IndexType::Hnsw
34                | IndexType::HnswRabitq
35                | IndexType::Ivf
36                | IndexType::IvfRabitq
37                | IndexType::Diskann
38                | IndexType::Vamana
39                | IndexType::Invert
40                | IndexType::Fts
41        ) {
42            return Err(Error::not_supported(format!(
43                "{:?} physical index creation is not implemented",
44                params.index_type
45            )));
46        }
47        let mut next = current.clone();
48        next.schema.add_index(field_name, params)?;
49        let config = current.config.clone();
50        finish_schema_commit(
51            self,
52            &current.docs,
53            current.revision,
54            &current.schema,
55            next,
56            &config,
57        )
58    }
59
60    pub fn drop_index(&self, field_name: &str) -> Result<()> {
61        self.ensure_open()?;
62        let _writer = self
63            .inner
64            .writer
65            .lock()
66            .map_err(|_| Error::internal("writer lock poisoned"))?;
67        let current = self
68            .inner
69            .state
70            .read()
71            .map_err(|_| Error::internal("collection state lock poisoned"))?
72            .clone();
73        ensure_writable(&current.options)?;
74        let mut next = current.clone();
75        next.schema.drop_index(field_name)?;
76        let config = current.config.clone();
77        finish_schema_commit(
78            self,
79            &current.docs,
80            current.revision,
81            &current.schema,
82            next,
83            &config,
84        )
85    }
86
87    pub fn optimize(&self) -> Result<()> {
88        self.rebuild_index_generation(None)
89    }
90
91    /// Rebuilds one configured in-memory index generation.
92    pub fn rebuild_index(&self, field_name: &str) -> Result<()> {
93        self.rebuild_index_generation(Some(field_name))
94    }
95
96    fn rebuild_index_generation(&self, field_name: Option<&str>) -> Result<()> {
97        self.ensure_open()?;
98        let _writer = self
99            .inner
100            .writer
101            .lock()
102            .map_err(|_| Error::internal("writer lock poisoned"))?;
103        let snapshot = {
104            let state = self
105                .inner
106                .state
107                .read()
108                .map_err(|_| Error::internal("collection state lock poisoned"))?;
109            ensure_writable(&state.options)?;
110            (
111                state.schema.clone(),
112                state.docs.clone(),
113                state.revision,
114                state.indexes.clone(),
115                state.options.resource_limits,
116                state.stats.clone(),
117            )
118        };
119
120        // The old immutable generation remains visible to readers throughout
121        // construction. Publication is one state-lock assignment below.
122        let indexes = if let Some(field_name) = field_name {
123            snapshot
124                .3
125                .rebuild_field(&snapshot.0, &snapshot.1, snapshot.2, field_name)?
126        } else {
127            IndexRegistry::build(&snapshot.0, &snapshot.1, snapshot.2)?
128        };
129        let resource_usage =
130            enforce_rebuild_resources(snapshot.4, &snapshot.0, &snapshot.1, &indexes, &snapshot.5)?;
131        let refresh_cache = should_rewrite_index_cache(&snapshot.0, field_name);
132        let mut state = self
133            .inner
134            .state
135            .write()
136            .map_err(|_| Error::internal("collection state lock poisoned"))?;
137        if state.revision != snapshot.2 || state.schema != snapshot.0 {
138            return Err(Error::failed_precondition(
139                "collection changed while indexes were rebuilding",
140            ));
141        }
142        let indexes = std::sync::Arc::new(indexes);
143        state.indexes = std::sync::Arc::clone(&indexes);
144        state.resource_usage = resource_usage;
145        let schema = state.schema.clone();
146        let revision = state.revision;
147        drop(state);
148        if refresh_cache {
149            let storage = self
150                .inner
151                .storage
152                .lock()
153                .map_err(|_| Error::internal("storage lock poisoned"))?;
154            persist_index_cache(&storage, &schema, &indexes, revision, true);
155        }
156        Ok(())
157    }
158}
159
160fn enforce_rebuild_resources(
161    limits: CollectionResourceLimits,
162    schema: &CollectionSchema,
163    docs: &crate::doc::DocumentMap,
164    indexes: &IndexRegistry,
165    stats: &crate::stats::StatsRegistry,
166) -> Result<super::resource::ResourceUsage> {
167    match limits.enforce_state(schema, docs, indexes) {
168        Ok(usage) => Ok(usage),
169        Err(error) => {
170            stats.record_resource_limit_rejection();
171            Err(error)
172        }
173    }
174}
175
176// Exact scalar/FTS rebuilds preserve the cache's logical generation, whereas
177// an ANN rebuild can replace graph/centroid structure and bounded overlays.
178fn should_rewrite_index_cache(schema: &CollectionSchema, field_name: Option<&str>) -> bool {
179    field_name.is_none()
180        || field_name.is_some_and(|field_name| {
181            schema.vectors.iter().any(|field| {
182                field.name == field_name
183                    && field.index_params.as_ref().is_some_and(|params| {
184                        matches!(
185                            params.index_type,
186                            IndexType::Hnsw
187                                | IndexType::HnswRabitq
188                                | IndexType::Ivf
189                                | IndexType::IvfRabitq
190                                | IndexType::Diskann
191                                | IndexType::Vamana
192                        )
193                    })
194            })
195        })
196}
197
198#[cfg(test)]
199mod tests {
200    use super::should_rewrite_index_cache;
201    use crate::{CollectionSchema, DataType, FieldSchema, IndexParams, MetricType};
202
203    #[test]
204    fn only_ann_and_full_rebuilds_rewrite_the_derived_cache() {
205        let mut embedding = FieldSchema::new("embedding", DataType::VectorFp32, false, 2)
206            .expect("field must be valid");
207        embedding
208            .set_index_params(
209                &IndexParams::hnsw(MetricType::L2, 4, 16).expect("HNSW params must be valid"),
210            )
211            .expect("HNSW index must be valid");
212        let mut language =
213            FieldSchema::new("language", DataType::String, false, 0).expect("field must be valid");
214        language
215            .set_index_params(
216                &IndexParams::invert(false, false).expect("scalar params must be valid"),
217            )
218            .expect("scalar index must be valid");
219        let mut body =
220            FieldSchema::new("body", DataType::String, false, 0).expect("field must be valid");
221        body.set_index_params(
222            &IndexParams::fts(Some("standard"), None, None).expect("FTS params must be valid"),
223        )
224        .expect("FTS index must be valid");
225        let schema = CollectionSchema::builder("cache-refresh")
226            .add_field(embedding)
227            .add_field(language)
228            .add_field(body)
229            .build()
230            .expect("schema must be valid");
231
232        assert!(should_rewrite_index_cache(&schema, None));
233        assert!(should_rewrite_index_cache(&schema, Some("embedding")));
234        assert!(!should_rewrite_index_cache(&schema, Some("language")));
235        assert!(!should_rewrite_index_cache(&schema, Some("body")));
236    }
237}