use std::io;
use std::sync::RwLock;
use kevy_index::{Catalog, Cursor, IndexKind, IndexSpec, IndexValue, Segment, SegmentStats, ValType};
use crate::store::{Store, lock_write};
pub(crate) use crate::ops_index_sync::{each_written_key_pub, on_commit, sync_segs};
pub type IndexPage = (Vec<(Vec<u8>, IndexValue)>, Option<Cursor>);
#[derive(Default)]
pub(crate) struct IndexReg {
pub(crate) catalog: RwLock<(u64, Catalog)>,
}
#[derive(Default)]
pub(crate) struct ShardSegs {
pub(crate) version: u64,
pub(crate) segs: Vec<(IndexSpec, Segment)>,
pub(crate) text: Vec<(IndexSpec, kevy_text::TextSegment)>,
pub(crate) ann: Vec<(IndexSpec, kevy_vector::Hnsw)>,
pub(crate) agg: Vec<(IndexSpec, kevy_index::AggSegment)>,
}
const SIDECAR: &str = "index-catalog.meta";
impl Store {
pub fn idx_create(
&self,
name: &[u8],
prefix: &[u8],
field: &[u8],
ty: ValType,
kind: IndexKind,
) -> io::Result<()> {
if prefix.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty prefix"));
}
let spec = IndexSpec {
name: name.to_vec(),
prefix: prefix.to_vec(),
field: field.to_vec(),
ty,
kind,
max_bytes: 0,
ann: None,
group_by: None,
};
self.register_spec(spec)
}
fn register_spec(&self, spec: IndexSpec) -> io::Result<()> {
{
let mut g = self
.indexes
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (ver, cat) = &mut *g;
cat.create(spec)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
*ver += 1;
}
self.persist_index_sidecar();
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
}
Ok(())
}
pub fn idx_create_ann(
&self,
name: &[u8],
prefix: &[u8],
field: &[u8],
params: kevy_index::AnnSpec,
) -> io::Result<()> {
if params.dim == 0 || params.distance > 2 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad ann parameters"));
}
let spec = IndexSpec {
name: name.to_vec(),
prefix: prefix.to_vec(),
field: field.to_vec(),
ty: ValType::Vector,
kind: IndexKind::Ann,
max_bytes: 0,
ann: Some(kevy_index::AnnSpec {
m: if params.m == 0 { 16 } else { params.m },
ef: if params.ef == 0 { 200 } else { params.ef },
..params
}),
group_by: None,
};
self.register_spec(spec)
}
pub fn idx_drop(&self, name: &[u8]) -> bool {
let hit = {
let mut g = self
.indexes
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (ver, cat) = &mut *g;
let hit = cat.drop_index(name);
if hit {
*ver += 1;
}
hit
};
if hit {
self.persist_index_sidecar();
}
hit
}
pub fn idx_query(
&self,
name: &[u8],
min: &IndexValue,
max: &IndexValue,
cursor: Option<&Cursor>,
limit: usize,
) -> io::Result<IndexPage> {
let limit = limit.clamp(1, 100_000);
let mut all: Vec<(IndexValue, Vec<u8>)> = Vec::new();
self.for_each_segment(name, |seg| {
let (hits, _) = seg.range(min, max, cursor, limit);
all.extend(hits.into_iter().map(|(k, v)| (v, k)));
})?;
all.sort();
all.truncate(limit);
let next = if all.len() == limit {
all.last().map(|(v, k)| Cursor { value: v.clone(), key: k.clone() })
} else {
None
};
Ok((all.into_iter().map(|(v, k)| (k, v)).collect(), next))
}
pub fn idx_count(&self, name: &[u8], min: &IndexValue, max: &IndexValue) -> io::Result<u64> {
let mut total = 0u64;
self.for_each_segment(name, |seg| total += seg.count(min, max))?;
Ok(total)
}
pub fn idx_stats(&self, name: &[u8]) -> io::Result<SegmentStats> {
let mut sum = SegmentStats::default();
self.for_each_segment(name, |seg| {
let s = seg.stats();
sum.entries += s.entries;
sum.approx_bytes += s.approx_bytes;
sum.coerce_failures += s.coerce_failures;
sum.duplicates += s.duplicates;
})?;
Ok(sum)
}
pub fn idx_list(&self) -> Vec<(Vec<u8>, Vec<u8>, IndexKind)> {
let g = self
.indexes
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.1.iter()
.map(|(s, _)| (s.name.clone(), s.prefix.clone(), s.kind))
.collect()
}
pub fn idx_match(
&self,
name: &[u8],
query: &[u8],
limit: usize,
) -> io::Result<Vec<(Vec<u8>, f64)>> {
let limit = limit.clamp(1, 1000);
let mut all: Vec<kevy_text::TextMatch> = Vec::new();
let mut found = false;
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
if let Some((_, ts)) = inner.idx_segs.text.iter().find(|(s, _)| s.name == name) {
found = true;
all.extend(ts.matches(query, limit));
}
}
if !found {
return Err(io::Error::new(io::ErrorKind::NotFound, "no such text index"));
}
all.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.key.cmp(&b.key)));
all.truncate(limit);
Ok(all.into_iter().map(|m| (m.key, m.score)).collect())
}
pub fn idx_create_agg(
&self,
name: &[u8],
prefix: &[u8],
field: &[u8],
ty: ValType,
group_by: &[u8],
) -> io::Result<()> {
if !matches!(ty, ValType::I64 | ValType::F64) || group_by.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "agg requires numeric type + group field"));
}
let spec = IndexSpec {
name: name.to_vec(),
prefix: prefix.to_vec(),
field: field.to_vec(),
ty,
kind: IndexKind::Agg,
max_bytes: 0,
ann: None,
group_by: Some(group_by.to_vec()),
};
self.register_spec(spec)
}
pub fn idx_group(&self, name: &[u8], group: &[u8]) -> io::Result<kevy_index::GroupStats> {
let mut merged = kevy_index::GroupStats { count: 0, sum: 0.0, min: None, max: None };
let mut found = false;
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
if let Some((_, a)) = inner.idx_segs.agg.iter().find(|(s, _)| s.name == name) {
found = true;
kevy_index::merge_group(&mut merged, &a.group(group));
}
}
if !found {
return Err(io::Error::new(io::ErrorKind::NotFound, "no such aggregate index"));
}
Ok(merged)
}
pub fn idx_groups(
&self,
name: &[u8],
by: kevy_index::AggBy,
limit: usize,
) -> io::Result<Vec<(Vec<u8>, kevy_index::GroupStats)>> {
let limit = limit.clamp(1, 1000);
let mut merged: std::collections::HashMap<Vec<u8>, kevy_index::GroupStats> =
std::collections::HashMap::new();
let mut found = false;
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
if let Some((_, a)) = inner.idx_segs.agg.iter().find(|(s, _)| s.name == name) {
found = true;
for (gk, st) in a.all_groups() {
match merged.get_mut(&gk) {
Some(m) => kevy_index::merge_group(m, &st),
None => {
merged.insert(gk, st);
}
}
}
}
}
if !found {
return Err(io::Error::new(io::ErrorKind::NotFound, "no such aggregate index"));
}
let mut ranked: Vec<(Vec<u8>, kevy_index::GroupStats)> = merged.into_iter().collect();
kevy_index::sort_groups(&mut ranked, by);
ranked.truncate(limit);
Ok(ranked)
}
pub fn idx_knn(
&self,
name: &[u8],
query: &[f32],
k: usize,
ef: usize,
) -> io::Result<Vec<(Vec<u8>, f32)>> {
let k = k.clamp(1, 1000);
let mut all: Vec<(Vec<u8>, f32)> = Vec::new();
let mut found = false;
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
if let Some((_, graph)) = inner.idx_segs.ann.iter().find(|(s, _)| s.name == name) {
found = true;
all.extend(graph.knn(query, k, ef));
}
}
if !found {
return Err(io::Error::new(io::ErrorKind::NotFound, "no such vector index"));
}
all.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
all.truncate(k);
Ok(all)
}
fn for_each_segment(
&self,
name: &[u8],
mut f: impl FnMut(&Segment),
) -> io::Result<()> {
let mut found = false;
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
if let Some((_, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == name) {
found = true;
f(seg);
}
}
if found {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "no such index"))
}
}
fn persist_index_sidecar(&self) {
let Some(dir) = &self.config.data_dir else { return };
let g = self
.indexes
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = dir.join("index-catalog.meta.tmp");
if std::fs::write(&tmp, g.1.to_sidecar()).is_ok() {
let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
}
}
pub(crate) fn idx_boot(&self) {
let Some(dir) = &self.config.data_dir else { return };
if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
&& let Some(cat) = Catalog::from_sidecar(&text)
&& !cat.is_empty()
{
let mut g = self
.indexes
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*g = (g.0 + 1, cat);
}
}
}