///|
/// Validate a corpus before it is handed to an index.
///
/// The check is deliberately strict: vector databases should reject malformed
/// data at ingestion time instead of producing incomplete or misleading search
/// results later.
pub fn validate_documents(docs : Array[Document]) -> Unit raise VectorError {
if docs.length() == 0 {
raise IndexError("Document corpus must not be empty")
}
let expected_dim = docs[0].vector.length()
if expected_dim == 0 {
raise EmptyVector
}
let ids = Map([])
for doc in docs {
if doc.id.trim().length() == 0 {
raise IndexError("Document id must not be empty")
}
if doc.vector.length() != expected_dim {
raise DimensionMismatch(
"Corpus contains dimensions " +
expected_dim.to_string() +
" and " +
doc.vector.length().to_string(),
)
}
if ids.contains(doc.id) {
raise IndexError("Duplicate document id: " + doc.id)
}
ids.set(doc.id, true)
}
}
///|
/// Return a copy of `vector` with unit L2 norm.
pub fn normalize_l2(vector : Array[Double]) -> Array[Double] raise VectorError {
if vector.length() == 0 {
raise EmptyVector
}
let mut squared = 0.0
for value in vector {
squared = squared + value * value
}
if squared == 0.0 {
raise IndexError("Cannot normalize a zero vector")
}
let norm = squared.sqrt()
let normalized = Array::make(vector.length(), 0.0)
for i = 0; i < vector.length(); i = i + 1 {
normalized[i] = vector[i] / norm
}
normalized
}
///|
/// Normalize every document while preserving ids and metadata.
pub fn normalize_documents(
docs : Array[Document],
) -> Array[Document] raise VectorError {
validate_documents(docs)
let normalized = []
for doc in docs {
normalized.push(
Document::new(doc.id, normalize_l2(doc.vector), doc.metadata),
)
}
normalized
}
///|
/// Return the number of distinct ids in a corpus.
pub fn distinct_document_count(docs : Array[Document]) -> Int {
let ids = Map([])
for doc in docs {
ids.set(doc.id, true)
}
ids.length()
}