use crate::{HawDBError, PropertyId, Result, TableId, VectorDimension, VectorDistance};
mod codec;
pub const MAX_VECTOR_INDEX_DESCRIPTOR_BYTES: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VectorIndexReadIdentity {
pub database: crate::Uuid,
pub branch: crate::Uuid,
pub consumer_id_hash: [u8; 32],
pub target_locator_hash: [u8; 32],
pub registration: crate::Uuid,
pub projection: crate::Uuid,
pub checkpoint: crate::Uuid,
pub target_incarnation: std::num::NonZeroU64,
pub generation: std::num::NonZeroU64,
pub operation: std::num::NonZeroU64,
pub source_epoch: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum VectorIndexQueryMode {
#[default]
AnnWindow,
Exact,
FilteredAnn,
}
impl VectorIndexQueryMode {
pub fn parse(value: &str) -> Result<Self> {
match value {
"ann_window" => Ok(Self::AnnWindow),
"exact" => Ok(Self::Exact),
"filtered_ann" => Ok(Self::FilteredAnn),
_ => Err(invalid(
"query mode must be 'ann_window', 'exact' or 'filtered_ann'",
)),
}
}
pub const fn as_str(self) -> &'static str {
match self {
Self::AnnWindow => "ann_window",
Self::Exact => "exact",
Self::FilteredAnn => "filtered_ann",
}
}
pub const fn requires_eligible_domain(self) -> bool {
matches!(self, Self::Exact | Self::FilteredAnn)
}
pub const fn filter_position(self) -> &'static str {
match self {
Self::AnnWindow => "after_window",
Self::Exact => "before_ranking",
Self::FilteredAnn => "before_candidates",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VectorIndexTarget {
Relational {
table: String,
column: String,
column_ordinal: u32,
},
NodeProperty {
table: String,
property: String,
table_id: TableId,
property_id: PropertyId,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VectorIndexBackend {
RaBitQ {
bit_width: u8,
segment_rows: u32,
transform_seed: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VectorIndexDescriptor {
name: String,
target: VectorIndexTarget,
dimension: VectorDimension,
distance: VectorDistance,
backend: VectorIndexBackend,
schema_digest: [u8; 32],
}
impl VectorIndexDescriptor {
pub fn new(
name: String,
target: VectorIndexTarget,
dimension: VectorDimension,
distance: VectorDistance,
backend: VectorIndexBackend,
schema_digest: [u8; 32],
) -> Result<Self> {
if name.is_empty()
|| name.len() > 128
|| !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
{
return Err(invalid(
"name must be 1..128 ASCII letters, digits, '.', '_' or '-'",
));
}
let (table, field) = match &target {
VectorIndexTarget::Relational { table, column, .. } => (table, column),
VectorIndexTarget::NodeProperty {
table, property, ..
} => (table, property),
};
for name in [table, field] {
if name.is_empty() || name.len() > 1024 || name.contains('\0') {
return Err(invalid(
"target names must contain 1..1024 non-NUL UTF-8 bytes",
));
}
}
let VectorIndexBackend::RaBitQ {
bit_width,
segment_rows,
..
} = backend;
if !matches!(bit_width, 1 | 4) || segment_rows == 0 {
return Err(invalid(
"RaBitQ requires 1 or 4 bits and a positive segment size",
));
}
if distance != VectorDistance::Cosine {
return Err(invalid(
"RaBitQ indexes currently support cosine distance only",
));
}
Ok(Self {
name,
target,
dimension,
distance,
backend,
schema_digest,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn target(&self) -> &VectorIndexTarget {
&self.target
}
pub const fn dimension(&self) -> VectorDimension {
self.dimension
}
pub const fn distance(&self) -> VectorDistance {
self.distance
}
pub const fn backend(&self) -> VectorIndexBackend {
self.backend
}
pub const fn schema_digest(&self) -> &[u8; 32] {
&self.schema_digest
}
pub fn retained_bytes(&self) -> usize {
let (table, field) = match &self.target {
VectorIndexTarget::Relational { table, column, .. } => (table, column),
VectorIndexTarget::NodeProperty {
table, property, ..
} => (table, property),
};
std::mem::size_of::<Self>() + self.name.capacity() + table.capacity() + field.capacity()
}
}
fn invalid(reason: &str) -> HawDBError {
HawDBError::Semantic(format!("invalid vector index: {reason}"))
}