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 type IndexPage = (Vec<(Vec<u8>, IndexValue)>, Option<Cursor>);
27
28#[derive(Default)]
31pub(crate) struct IndexReg {
32 pub(crate) catalog: RwLock<(u64, Catalog)>,
33}
34
35#[derive(Default)]
38pub(crate) struct ShardSegs {
39 pub(crate) version: u64,
40 pub(crate) segs: Vec<(IndexSpec, Segment)>,
41 pub(crate) text: Vec<(IndexSpec, kevy_text::TextSegment)>,
44 pub(crate) ann: Vec<(IndexSpec, kevy_vector::Hnsw)>,
46}
47
48fn new_graph(spec: &IndexSpec) -> kevy_vector::Hnsw {
49 let a = spec.ann.as_ref().expect("ann spec");
50 kevy_vector::Hnsw::new(
51 a.dim as usize,
52 kevy_vector::HnswParams {
53 m: a.m as usize,
54 ef_construction: a.ef as usize,
55 distance: match a.distance {
56 1 => kevy_vector::Distance::L2,
57 2 => kevy_vector::Distance::Ip,
58 _ => kevy_vector::Distance::Cosine,
59 },
60 },
61 )
62}
63
64const SIDECAR: &str = "index-catalog.meta";
65
66impl Store {
67 pub fn idx_create(
70 &self,
71 name: &[u8],
72 prefix: &[u8],
73 field: &[u8],
74 ty: ValType,
75 kind: IndexKind,
76 ) -> io::Result<()> {
77 if prefix.is_empty() {
78 return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty prefix"));
79 }
80 let spec = IndexSpec {
81 name: name.to_vec(),
82 prefix: prefix.to_vec(),
83 field: field.to_vec(),
84 ty,
85 kind,
86 max_bytes: 0,
87 ann: None,
88 };
89 self.register_spec(spec)
90 }
91
92 fn register_spec(&self, spec: IndexSpec) -> io::Result<()> {
93 {
94 let mut g = self
95 .indexes
96 .catalog
97 .write()
98 .unwrap_or_else(std::sync::PoisonError::into_inner);
99 let (ver, cat) = &mut *g;
100 cat.create(spec)
101 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
102 *ver += 1;
103 }
104 self.persist_index_sidecar();
105 for shard in self.shards.iter() {
107 let mut g = lock_write(shard);
108 let inner = &mut *g;
109 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
110 }
111 Ok(())
112 }
113
114 pub fn idx_create_ann(
118 &self,
119 name: &[u8],
120 prefix: &[u8],
121 field: &[u8],
122 params: kevy_index::AnnSpec,
123 ) -> io::Result<()> {
124 if params.dim == 0 || params.distance > 2 {
125 return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad ann parameters"));
126 }
127 let spec = IndexSpec {
128 name: name.to_vec(),
129 prefix: prefix.to_vec(),
130 field: field.to_vec(),
131 ty: ValType::Vector,
132 kind: IndexKind::Ann,
133 max_bytes: 0,
134 ann: Some(kevy_index::AnnSpec {
135 m: if params.m == 0 { 16 } else { params.m },
136 ef: if params.ef == 0 { 200 } else { params.ef },
137 ..params
138 }),
139 };
140 self.register_spec(spec)
141 }
142
143 pub fn idx_drop(&self, name: &[u8]) -> bool {
144 let hit = {
145 let mut g = self
146 .indexes
147 .catalog
148 .write()
149 .unwrap_or_else(std::sync::PoisonError::into_inner);
150 let (ver, cat) = &mut *g;
151 let hit = cat.drop_index(name);
152 if hit {
153 *ver += 1;
154 }
155 hit
156 };
157 if hit {
158 self.persist_index_sidecar();
159 }
160 hit
161 }
162
163 pub fn idx_query(
167 &self,
168 name: &[u8],
169 min: &IndexValue,
170 max: &IndexValue,
171 cursor: Option<&Cursor>,
172 limit: usize,
173 ) -> io::Result<IndexPage> {
174 let limit = limit.clamp(1, 100_000);
175 let mut all: Vec<(IndexValue, Vec<u8>)> = Vec::new();
176 self.for_each_segment(name, |seg| {
177 let (hits, _) = seg.range(min, max, cursor, limit);
178 all.extend(hits.into_iter().map(|(k, v)| (v, k)));
179 })?;
180 all.sort();
181 all.truncate(limit);
182 let next = if all.len() == limit {
183 all.last().map(|(v, k)| Cursor { value: v.clone(), key: k.clone() })
184 } else {
185 None
186 };
187 Ok((all.into_iter().map(|(v, k)| (k, v)).collect(), next))
188 }
189
190 pub fn idx_count(&self, name: &[u8], min: &IndexValue, max: &IndexValue) -> io::Result<u64> {
192 let mut total = 0u64;
193 self.for_each_segment(name, |seg| total += seg.count(min, max))?;
194 Ok(total)
195 }
196
197 pub fn idx_stats(&self, name: &[u8]) -> io::Result<SegmentStats> {
200 let mut sum = SegmentStats::default();
201 self.for_each_segment(name, |seg| {
202 let s = seg.stats();
203 sum.entries += s.entries;
204 sum.approx_bytes += s.approx_bytes;
205 sum.coerce_failures += s.coerce_failures;
206 sum.duplicates += s.duplicates;
207 })?;
208 Ok(sum)
209 }
210
211 pub fn idx_list(&self) -> Vec<(Vec<u8>, Vec<u8>, IndexKind)> {
213 let g = self
214 .indexes
215 .catalog
216 .read()
217 .unwrap_or_else(std::sync::PoisonError::into_inner);
218 g.1.iter()
219 .map(|(s, _)| (s.name.clone(), s.prefix.clone(), s.kind))
220 .collect()
221 }
222
223 pub fn idx_match(
226 &self,
227 name: &[u8],
228 query: &[u8],
229 limit: usize,
230 ) -> io::Result<Vec<(Vec<u8>, f64)>> {
231 let limit = limit.clamp(1, 1000);
232 let mut all: Vec<kevy_text::TextMatch> = Vec::new();
233 let mut found = false;
234 for shard in self.shards.iter() {
235 let mut g = lock_write(shard);
236 let inner = &mut *g;
237 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
238 if let Some((_, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
239 found = true;
240 all.extend(ts.matches(query, limit));
241 }
242 }
243 if !found {
244 return Err(io::Error::new(io::ErrorKind::NotFound, "no such text index"));
245 }
246 all.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
247 all.truncate(limit);
248 Ok(all.into_iter().map(|m| (m.key, m.score)).collect())
249 }
250
251 pub fn idx_knn(
254 &self,
255 name: &[u8],
256 query: &[f32],
257 k: usize,
258 ef: usize,
259 ) -> io::Result<Vec<(Vec<u8>, f32)>> {
260 let k = k.clamp(1, 1000);
261 let mut all: Vec<(Vec<u8>, f32)> = Vec::new();
262 let mut found = false;
263 for shard in self.shards.iter() {
264 let mut g = lock_write(shard);
265 let inner = &mut *g;
266 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
267 if let Some((_, graph)) = inner.idx_segs.ann.iter().find(|(s, _)| s.name == name) {
268 found = true;
269 all.extend(graph.knn(query, k, ef));
270 }
271 }
272 if !found {
273 return Err(io::Error::new(io::ErrorKind::NotFound, "no such vector index"));
274 }
275 all.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
276 all.truncate(k);
277 Ok(all)
278 }
279
280 fn for_each_segment(
281 &self,
282 name: &[u8],
283 mut f: impl FnMut(&Segment),
284 ) -> io::Result<()> {
285 let mut found = false;
286 for shard in self.shards.iter() {
287 let mut g = lock_write(shard);
288 let inner = &mut *g;
289 sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
290 if let Some((_, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == name) {
291 found = true;
292 f(seg);
293 }
294 }
295 if found {
296 Ok(())
297 } else {
298 Err(io::Error::new(io::ErrorKind::NotFound, "no such index"))
299 }
300 }
301
302 fn persist_index_sidecar(&self) {
303 let Some(dir) = &self.config.data_dir else { return };
304 let g = self
305 .indexes
306 .catalog
307 .read()
308 .unwrap_or_else(std::sync::PoisonError::into_inner);
309 let tmp = dir.join("index-catalog.meta.tmp");
310 if std::fs::write(&tmp, g.1.to_sidecar()).is_ok() {
311 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
312 }
313 }
314
315 pub(crate) fn idx_boot(&self) {
318 let Some(dir) = &self.config.data_dir else { return };
319 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
320 && let Some(cat) = Catalog::from_sidecar(&text)
321 && !cat.is_empty()
322 {
323 let mut g = self
324 .indexes
325 .catalog
326 .write()
327 .unwrap_or_else(std::sync::PoisonError::into_inner);
328 *g = (g.0 + 1, cat);
329 }
330 }
331}
332
333pub(crate) fn sync_segs(
337 reg: &IndexReg,
338 shard_segs: &mut ShardSegs,
339 store: &mut kevy_store::Store,
340) {
341 let g = reg
342 .catalog
343 .read()
344 .unwrap_or_else(std::sync::PoisonError::into_inner);
345 let (ver, cat) = &*g;
346 if shard_segs.version == *ver {
347 return;
348 }
349 let mut next: Vec<(IndexSpec, Segment)> = Vec::new();
350 let mut next_text: Vec<(IndexSpec, kevy_text::TextSegment)> = Vec::new();
351 let mut next_ann: Vec<(IndexSpec, kevy_vector::Hnsw)> = Vec::new();
352 for (spec, _) in cat.iter() {
353 if spec.kind == kevy_index::IndexKind::Ann {
354 match shard_segs.ann.iter().position(|(s, _)| s == spec) {
355 Some(i) => next_ann.push(shard_segs.ann.swap_remove(i)),
356 None => {
357 let mut g = new_graph(spec);
358 let mut pat = spec.prefix.clone();
359 pat.push(b'*');
360 for key in store.collect_keys(Some(&pat), None) {
361 apply_ann_key(store, spec, &mut g, &key);
362 }
363 next_ann.push((spec.clone(), g));
364 }
365 }
366 continue;
367 }
368 if spec.kind == kevy_index::IndexKind::Text {
369 match shard_segs.text.iter().position(|(s, _)| s == spec) {
370 Some(i) => next_text.push(shard_segs.text.swap_remove(i)),
371 None => {
372 let mut ts = kevy_text::TextSegment::new();
373 let mut pat = spec.prefix.clone();
374 pat.push(b'*');
375 for key in store.collect_keys(Some(&pat), None) {
376 apply_text_key(store, spec, &mut ts, &key);
377 }
378 next_text.push((spec.clone(), ts));
379 }
380 }
381 continue;
382 }
383 match shard_segs.segs.iter().position(|(s, _)| s == spec) {
384 Some(i) => next.push(shard_segs.segs.swap_remove(i)),
385 None => {
386 let mut seg = Segment::new();
387 let mut pat = spec.prefix.clone();
388 pat.push(b'*');
389 for key in store.collect_keys(Some(&pat), None) {
390 apply_key(store, spec, &mut seg, &key);
391 }
392 next.push((spec.clone(), seg));
393 }
394 }
395 }
396 shard_segs.segs = next;
397 shard_segs.text = next_text;
398 shard_segs.ann = next_ann;
399 shard_segs.version = *ver;
400}
401
402fn apply_ann_key(
403 store: &mut kevy_store::Store,
404 spec: &IndexSpec,
405 g: &mut kevy_vector::Hnsw,
406 key: &[u8],
407) {
408 let v = match store.hget(key, &spec.field) {
409 Ok(Some(raw)) => {
410 let raw = raw.to_vec();
411 kevy_vector::parse_vector(&raw, g.dim())
412 }
413 _ => None,
414 };
415 g.apply(key, v);
416}
417
418fn apply_text_key(
419 store: &mut kevy_store::Store,
420 spec: &IndexSpec,
421 ts: &mut kevy_text::TextSegment,
422 key: &[u8],
423) {
424 match store.hget(key, &spec.field) {
425 Ok(Some(raw)) => {
426 let raw = raw.to_vec();
427 ts.apply(key, Some(&raw));
428 }
429 _ => ts.apply(key, None),
430 }
431}
432
433pub(crate) fn on_commit(
437 reg: &IndexReg,
438 shard_segs: &mut ShardSegs,
439 store: &mut kevy_store::Store,
440 parts: &[&[u8]],
441) {
442 {
443 let g = reg
445 .catalog
446 .read()
447 .unwrap_or_else(std::sync::PoisonError::into_inner);
448 if g.1.is_empty() {
449 return;
450 }
451 }
452 sync_segs(reg, shard_segs, store);
453 let verb = parts.first().copied().unwrap_or(b"");
454 if verb.eq_ignore_ascii_case(b"FLUSHALL") || verb.eq_ignore_ascii_case(b"FLUSHDB") {
455 for (_, seg) in &mut shard_segs.segs {
456 *seg = Segment::new();
457 }
458 for (_, ts) in &mut shard_segs.text {
459 *ts = kevy_text::TextSegment::new();
460 }
461 for (spec, g) in &mut shard_segs.ann {
462 *g = new_graph(spec);
463 }
464 return;
465 }
466 each_written_key(verb, parts, |key| {
467 for (spec, seg) in &mut shard_segs.segs {
468 if key.starts_with(&spec.prefix) {
469 apply_key(store, spec, seg, key);
470 }
471 }
472 for (spec, ts) in &mut shard_segs.text {
473 if key.starts_with(&spec.prefix) {
474 apply_text_key(store, spec, ts, key);
475 }
476 }
477 for (spec, g) in &mut shard_segs.ann {
478 if key.starts_with(&spec.prefix) {
479 apply_ann_key(store, spec, g, key);
480 }
481 }
482 });
483}
484
485pub(crate) fn each_written_key_pub(verb: &[u8], parts: &[&[u8]], f: impl FnMut(&[u8])) {
489 each_written_key(verb, parts, f);
490}
491
492fn each_written_key(verb: &[u8], parts: &[&[u8]], mut f: impl FnMut(&[u8])) {
493 let up = |v: &[u8], t: &[u8]| v.eq_ignore_ascii_case(t);
494 if up(verb, b"DEL") || up(verb, b"UNLINK") {
495 for k in &parts[1..] {
496 f(k);
497 }
498 } else if up(verb, b"MSET") {
499 let mut i = 1;
500 while i + 1 < parts.len() {
501 f(parts[i]);
502 i += 2;
503 }
504 } else if up(verb, b"COPY") || up(verb, b"RENAME") || up(verb, b"RENAMENX") {
505 if let Some(k) = parts.get(1) {
506 f(k);
507 }
508 if let Some(k) = parts.get(2) {
509 f(k);
510 }
511 } else if let Some(k) = parts.get(1) {
512 f(k);
513 }
514}
515
516fn apply_key(store: &mut kevy_store::Store, spec: &IndexSpec, seg: &mut Segment, key: &[u8]) {
517 match store.hget(key, &spec.field) {
518 Ok(Some(raw)) => {
519 let raw = raw.to_vec();
520 match IndexValue::coerce(spec.ty, &raw) {
521 Some(v) => seg.apply(key, Some(v)),
522 None => seg.apply(key, None),
523 }
524 }
525 Ok(None) => {
526 if store.exists(&[key.to_vec()]) == 0 {
527 seg.remove(key);
528 } else {
529 seg.apply(key, None);
530 }
531 }
532 Err(_) => seg.remove(key),
533 }
534}