a3s_vec/collection/
index_api.rs1use 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(¤t.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 ¤t.docs,
53 current.revision,
54 ¤t.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(¤t.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 ¤t.docs,
80 current.revision,
81 ¤t.schema,
82 next,
83 &config,
84 )
85 }
86
87 pub fn optimize(&self) -> Result<()> {
88 self.rebuild_index_generation(None)
89 }
90
91 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 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
176fn 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}