1use std::io;
19use std::sync::RwLock;
20
21use kevy_index::{Catalog, Cursor, IndexKind, IndexSpec, IndexValue, Segment, SegmentStats, ValType};
22
23use crate::store::{Store, lock_write};
24
25pub(crate) use crate::ops_index_sync::{each_written_key_pub, on_commit, sync_segs};
26
27pub type IndexPage = (Vec<(Vec<u8>, IndexValue)>, Option<Cursor>);
29
30#[derive(Default)]
33pub(crate) struct IndexReg {
34 pub(crate) catalog: RwLock<(u64, Catalog)>,
35}
36
37#[derive(Default)]
40pub(crate) struct ShardSegs {
41 pub(crate) version: u64,
42 pub(crate) segs: Vec<(IndexSpec, Segment)>,
43 pub(crate) text: Vec<(IndexSpec, kevy_text::TextSegment)>,
46 pub(crate) ann: Vec<(IndexSpec, kevy_vector::Hnsw)>,
48 pub(crate) agg: Vec<(IndexSpec, kevy_index::AggSegment)>,
50}
51
52const SIDECAR: &str = "index-catalog.meta";
53
54impl Store {
55 pub fn idx_create(
58 &self,
59 name: &[u8],
60 prefix: &[u8],
61 field: &[u8],
62 ty: ValType,
63 kind: IndexKind,
64 ) -> io::Result<()> {
65 if prefix.is_empty() {
66 return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty prefix"));
67 }
68 let spec = IndexSpec {
69 name: name.to_vec(),
70 prefix: prefix.to_vec(),
71 field: field.to_vec(),
72 ty,
73 kind,
74 max_bytes: 0,
75 ann: None,
76 group_by: None,
77 };
78 self.register_spec(spec)
79 }
80
81 fn register_spec(&self, spec: IndexSpec) -> io::Result<()> {
82 {
83 let mut g = self
84 .indexes
85 .catalog
86 .write()
87 .unwrap_or_else(std::sync::PoisonError::into_inner);
88 let (ver, cat) = &mut *g;
89 cat.create(spec)
90 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
91 *ver += 1;
92 }
93 self.persist_index_sidecar();
94 for shard in self.shards.iter() {
96 let mut g = lock_write(shard);
97 let inner = &mut *g;
98 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
99 }
100 Ok(())
101 }
102
103 pub fn idx_create_ann(
106 &self,
107 name: &[u8],
108 prefix: &[u8],
109 field: &[u8],
110 params: kevy_index::AnnSpec,
111 ) -> io::Result<()> {
112 if params.dim == 0 || params.distance > 2 {
113 return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad ann parameters"));
114 }
115 let spec = IndexSpec {
116 name: name.to_vec(),
117 prefix: prefix.to_vec(),
118 field: field.to_vec(),
119 ty: ValType::Vector,
120 kind: IndexKind::Ann,
121 max_bytes: 0,
122 ann: Some(kevy_index::AnnSpec {
123 m: if params.m == 0 { 16 } else { params.m },
124 ef: if params.ef == 0 { 200 } else { params.ef },
125 ..params
126 }),
127 group_by: None,
128 };
129 self.register_spec(spec)
130 }
131
132 pub fn idx_drop(&self, name: &[u8]) -> bool {
135 let hit = {
136 let mut g = self
137 .indexes
138 .catalog
139 .write()
140 .unwrap_or_else(std::sync::PoisonError::into_inner);
141 let (ver, cat) = &mut *g;
142 let hit = cat.drop_index(name);
143 if hit {
144 *ver += 1;
145 }
146 hit
147 };
148 if hit {
149 self.persist_index_sidecar();
150 }
151 hit
152 }
153
154 pub fn idx_query(
158 &self,
159 name: &[u8],
160 min: &IndexValue,
161 max: &IndexValue,
162 cursor: Option<&Cursor>,
163 limit: usize,
164 ) -> io::Result<IndexPage> {
165 let limit = limit.clamp(1, 100_000);
166 let mut all: Vec<(IndexValue, Vec<u8>)> = Vec::new();
167 self.for_each_segment(name, |seg| {
168 let (hits, _) = seg.range(min, max, cursor, limit);
169 all.extend(hits.into_iter().map(|(k, v)| (v, k)));
170 })?;
171 all.sort();
172 all.truncate(limit);
173 let next = if all.len() == limit {
174 all.last().map(|(v, k)| Cursor { value: v.clone(), key: k.clone() })
175 } else {
176 None
177 };
178 Ok((all.into_iter().map(|(v, k)| (k, v)).collect(), next))
179 }
180
181 pub fn idx_count(&self, name: &[u8], min: &IndexValue, max: &IndexValue) -> io::Result<u64> {
183 let mut total = 0u64;
184 self.for_each_segment(name, |seg| total += seg.count(min, max))?;
185 Ok(total)
186 }
187
188 pub fn idx_stats(&self, name: &[u8]) -> io::Result<SegmentStats> {
191 let mut sum = SegmentStats::default();
192 self.for_each_segment(name, |seg| {
193 let s = seg.stats();
194 sum.entries += s.entries;
195 sum.approx_bytes += s.approx_bytes;
196 sum.coerce_failures += s.coerce_failures;
197 sum.duplicates += s.duplicates;
198 })?;
199 Ok(sum)
200 }
201
202 pub fn idx_list(&self) -> Vec<(Vec<u8>, Vec<u8>, IndexKind)> {
204 let g = self
205 .indexes
206 .catalog
207 .read()
208 .unwrap_or_else(std::sync::PoisonError::into_inner);
209 g.1.iter()
210 .map(|(s, _)| (s.name.clone(), s.prefix.clone(), s.kind))
211 .collect()
212 }
213
214 pub fn idx_match(
217 &self,
218 name: &[u8],
219 query: &[u8],
220 limit: usize,
221 ) -> io::Result<Vec<(Vec<u8>, f64)>> {
222 let limit = limit.clamp(1, 1000);
223 let mut all: Vec<kevy_text::TextMatch> = Vec::new();
224 let mut found = false;
225 for shard in self.shards.iter() {
226 let mut g = lock_write(shard);
227 let inner = &mut *g;
228 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
229 if let Some((_, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
230 found = true;
231 all.extend(ts.matches(query, limit));
232 }
233 }
234 if !found {
235 return Err(io::Error::new(io::ErrorKind::NotFound, "no such text index"));
236 }
237 all.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
238 all.truncate(limit);
239 Ok(all.into_iter().map(|m| (m.key, m.score)).collect())
240 }
241
242 pub fn idx_create_agg(
245 &self,
246 name: &[u8],
247 prefix: &[u8],
248 field: &[u8],
249 ty: ValType,
250 group_by: &[u8],
251 ) -> io::Result<()> {
252 if !matches!(ty, ValType::I64 | ValType::F64) || group_by.is_empty() {
253 return Err(io::Error::new(io::ErrorKind::InvalidInput, "agg requires numeric type + group field"));
254 }
255 let spec = IndexSpec {
256 name: name.to_vec(),
257 prefix: prefix.to_vec(),
258 field: field.to_vec(),
259 ty,
260 kind: IndexKind::Agg,
261 max_bytes: 0,
262 ann: None,
263 group_by: Some(group_by.to_vec()),
264 };
265 self.register_spec(spec)
266 }
267
268 pub fn idx_group(&self, name: &[u8], group: &[u8]) -> io::Result<kevy_index::GroupStats> {
270 let mut merged = kevy_index::GroupStats { count: 0, sum: 0.0, min: None, max: None };
271 let mut found = false;
272 for shard in self.shards.iter() {
273 let mut g = lock_write(shard);
274 let inner = &mut *g;
275 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
276 if let Some((_, a)) = inner.idx_segs.agg.iter().find(|(s, _)| s.name == name) {
277 found = true;
278 kevy_index::merge_group(&mut merged, &a.group(group));
279 }
280 }
281 if !found {
282 return Err(io::Error::new(io::ErrorKind::NotFound, "no such aggregate index"));
283 }
284 Ok(merged)
285 }
286
287 pub fn idx_groups(
289 &self,
290 name: &[u8],
291 by: kevy_index::AggBy,
292 limit: usize,
293 ) -> io::Result<Vec<(Vec<u8>, kevy_index::GroupStats)>> {
294 let limit = limit.clamp(1, 1000);
295 let mut merged: std::collections::HashMap<Vec<u8>, kevy_index::GroupStats> =
298 std::collections::HashMap::new();
299 let mut found = false;
300 for shard in self.shards.iter() {
301 let mut g = lock_write(shard);
302 let inner = &mut *g;
303 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
304 if let Some((_, a)) = inner.idx_segs.agg.iter().find(|(s, _)| s.name == name) {
305 found = true;
306 for (gk, st) in a.all_groups() {
307 match merged.get_mut(&gk) {
308 Some(m) => kevy_index::merge_group(m, &st),
309 None => {
310 merged.insert(gk, st);
311 }
312 }
313 }
314 }
315 }
316 if !found {
317 return Err(io::Error::new(io::ErrorKind::NotFound, "no such aggregate index"));
318 }
319 let mut ranked: Vec<(Vec<u8>, kevy_index::GroupStats)> = merged.into_iter().collect();
320 kevy_index::sort_groups(&mut ranked, by);
321 ranked.truncate(limit);
322 Ok(ranked)
323 }
324
325 pub fn idx_knn(
328 &self,
329 name: &[u8],
330 query: &[f32],
331 k: usize,
332 ef: usize,
333 ) -> io::Result<Vec<(Vec<u8>, f32)>> {
334 let k = k.clamp(1, 1000);
335 let mut all: Vec<(Vec<u8>, f32)> = Vec::new();
336 let mut found = false;
337 for shard in self.shards.iter() {
338 let mut g = lock_write(shard);
339 let inner = &mut *g;
340 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
341 if let Some((_, graph)) = inner.idx_segs.ann.iter().find(|(s, _)| s.name == name) {
342 found = true;
343 all.extend(graph.knn(query, k, ef));
344 }
345 }
346 if !found {
347 return Err(io::Error::new(io::ErrorKind::NotFound, "no such vector index"));
348 }
349 all.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
350 all.truncate(k);
351 Ok(all)
352 }
353
354 fn for_each_segment(
355 &self,
356 name: &[u8],
357 mut f: impl FnMut(&Segment),
358 ) -> io::Result<()> {
359 let mut found = false;
360 for shard in self.shards.iter() {
361 let mut g = lock_write(shard);
362 let inner = &mut *g;
363 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
364 if let Some((_, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == name) {
365 found = true;
366 f(seg);
367 }
368 }
369 if found {
370 Ok(())
371 } else {
372 Err(io::Error::new(io::ErrorKind::NotFound, "no such index"))
373 }
374 }
375
376 fn persist_index_sidecar(&self) {
377 let Some(dir) = &self.config.data_dir else { return };
378 let g = self
379 .indexes
380 .catalog
381 .read()
382 .unwrap_or_else(std::sync::PoisonError::into_inner);
383 let tmp = dir.join("index-catalog.meta.tmp");
384 if std::fs::write(&tmp, g.1.to_sidecar()).is_ok() {
385 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
386 }
387 }
388
389 pub(crate) fn idx_boot(&self) {
392 let Some(dir) = &self.config.data_dir else { return };
393 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
394 && let Some(cat) = Catalog::from_sidecar(&text)
395 && !cat.is_empty()
396 {
397 let mut g = self
398 .indexes
399 .catalog
400 .write()
401 .unwrap_or_else(std::sync::PoisonError::into_inner);
402 *g = (g.0 + 1, cat);
403 }
404 }
405}