use crate::{Encoder, Error, ModelInfo, model_info};
const DIMENSIONS: usize = 384;
const MAX_VECTORS: usize = 4096;
const MAGIC: &[u8; 8] = b"PKTIDX01";
const HEADER_BYTES: usize = 16;
const MAX_MODEL_BYTES: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Neighbor {
pub index: usize,
pub score: f32,
}
#[derive(Clone, Debug)]
pub struct Index {
model: ModelInfo,
vectors: Vec<f32>,
}
impl Index {
pub fn new(texts: &[impl AsRef<str>]) -> Result<Self, Error> {
if texts.len() > MAX_VECTORS {
return Err(Error::Input("index exceeds 4096 vectors"));
}
let embeddings = if texts.is_empty() {
Vec::new()
} else {
Encoder::new()?.embed(texts)?
};
Self::from_embeddings(model_info(), embeddings)
}
pub fn from_embeddings(model: ModelInfo, embeddings: Vec<Vec<f32>>) -> Result<Self, Error> {
validate_model(&model)?;
if embeddings.len() > MAX_VECTORS {
return Err(Error::Input("index exceeds 4096 vectors"));
}
let mut vectors = Vec::with_capacity(embeddings.len() * DIMENSIONS);
for row in embeddings {
validate_vector(&row)?;
vectors.extend(row);
}
Ok(Self { model, vectors })
}
pub fn model(&self) -> &ModelInfo {
&self.model
}
pub fn len(&self) -> usize {
self.vectors.len() / DIMENSIONS
}
pub fn is_empty(&self) -> bool {
self.vectors.is_empty()
}
pub fn search(&self, query: &str, limit: usize) -> Result<Vec<Neighbor>, Error> {
if query.trim().is_empty() {
return Err(Error::Input("text must not be empty"));
}
if query.len() > 65536 {
return Err(Error::Input("text exceeds 64 KiB"));
}
if limit == 0 || self.is_empty() {
return Ok(Vec::new());
}
let vector = Encoder::new()?.embed(&[query])?.remove(0);
Ok(self.nearest(&vector, limit))
}
pub fn search_embedding(
&self,
model: &ModelInfo,
query: &[f32],
limit: usize,
) -> Result<Vec<Neighbor>, Error> {
if model != &self.model {
return Err(Error::Input("index encoder fingerprint mismatch"));
}
validate_vector(query)?;
Ok(self.nearest(query, limit))
}
fn nearest(&self, query: &[f32], limit: usize) -> Vec<Neighbor> {
let limit = limit.min(self.len());
if limit == 0 {
return Vec::new();
}
let mut hits = self
.vectors
.as_chunks::<DIMENSIONS>()
.0
.iter()
.enumerate()
.map(|(index, row)| Neighbor {
index,
score: row
.iter()
.zip(query)
.map(|(a, b)| a * b)
.sum::<f32>()
.clamp(-1.0, 1.0),
})
.collect::<Vec<_>>();
let order =
|a: &Neighbor, b: &Neighbor| b.score.total_cmp(&a.score).then(a.index.cmp(&b.index));
if limit < hits.len() {
hits.select_nth_unstable_by(limit, order);
hits.truncate(limit);
}
hits.sort_unstable_by(order);
hits
}
pub fn to_bytes(&self) -> Vec<u8> {
let model = serde_json::to_vec(&self.model).expect("serializable model metadata");
let mut bytes = Vec::with_capacity(HEADER_BYTES + model.len() + self.vectors.len() * 4);
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&(model.len() as u32).to_le_bytes());
bytes.extend_from_slice(&(self.len() as u32).to_le_bytes());
bytes.extend(model);
for value in &self.vectors {
bytes.extend_from_slice(&value.to_le_bytes());
}
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() < HEADER_BYTES || &bytes[..8] != MAGIC {
return Err(Error::Input("invalid index header or version"));
}
let model_len = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
let count = u32::from_le_bytes(bytes[12..16].try_into().unwrap()) as usize;
if model_len > MAX_MODEL_BYTES || count > MAX_VECTORS {
return Err(Error::Input("index metadata or row count exceeds bounds"));
}
let start = HEADER_BYTES + model_len;
if bytes.len() != start + count * DIMENSIONS * 4 {
return Err(Error::Input("index byte length mismatch"));
}
let model = serde_json::from_slice(&bytes[HEADER_BYTES..start])
.map_err(|_| Error::Input("invalid index model metadata"))?;
validate_model(&model)?;
let vectors = bytes[start..]
.as_chunks::<4>()
.0
.iter()
.map(|value| f32::from_le_bytes(*value))
.collect::<Vec<_>>();
for row in vectors.as_chunks::<DIMENSIONS>().0 {
validate_vector(row)?;
}
Ok(Self { model, vectors })
}
}
fn validate_model(model: &ModelInfo) -> Result<(), Error> {
if model != &model_info() {
return Err(Error::Input("index encoder fingerprint mismatch"));
}
Ok(())
}
fn validate_vector(vector: &[f32]) -> Result<(), Error> {
let valid = vector.len() == DIMENSIONS && {
let norm = vector.iter().map(|v| (*v as f64).powi(2)).sum::<f64>();
norm.is_finite() && (norm - 1.0).abs() <= 0.002
};
if !valid {
return Err(Error::Input(
"index vector must be finite, normalized and 384-dimensional",
));
}
Ok(())
}