use super::{bincode_deser, bincode_options, bincode_ser};
use crate::datatypes::values::Value;
use crate::graph::algorithms::hnsw::HnswIndex;
use crate::graph::schema::DirGraph;
use crate::graph::storage::GraphRead;
use bincode::Options;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
const VECTOR_INDEX_MAGIC: &[u8; 8] = b"KGLVIDX1";
const VECTOR_INDEX_FORMAT_VERSION: u32 = 1;
pub(super) fn encode_vector_indexes(graph: &DirGraph) -> io::Result<Option<Vec<u8>>> {
let entries: Vec<(&String, &String, &HnswIndex)> = graph
.embeddings
.iter()
.filter_map(|((nt, prop), s)| s.index.as_ref().map(|idx| (nt, prop, idx)))
.collect();
if entries.is_empty() {
return Ok(None);
}
let body = bincode_ser(&entries)?;
let mut payload = Vec::with_capacity(12 + body.len());
payload.extend_from_slice(VECTOR_INDEX_MAGIC);
payload.extend_from_slice(&VECTOR_INDEX_FORMAT_VERSION.to_le_bytes());
payload.extend_from_slice(&body);
Ok(Some(payload))
}
pub(super) fn decode_vector_indexes(payload: &[u8], graph: &mut DirGraph) {
if payload.len() < 12 || &payload[..8] != VECTOR_INDEX_MAGIC {
return;
}
let ver = u32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]);
if ver != VECTOR_INDEX_FORMAT_VERSION {
return; }
let entries: Vec<(String, String, HnswIndex)> = match bincode_deser(&payload[12..]) {
Ok(e) => e,
Err(_) => return,
};
for (node_type, prop, idx) in entries {
if let Some(store) = graph.embeddings.get_mut(&(node_type, prop)) {
if idx.dim() == store.dimension && idx.len() == store.len() {
store.index = Some(idx);
}
}
}
}
const KGLE_MAGIC: [u8; 4] = *b"KGLE";
const KGLE_VERSION: u32 = 2;
#[derive(Deserialize)]
struct ExportedEmbeddingStoreV1 {
node_type: String,
text_column: String,
dimension: usize,
entries: Vec<(Value, Vec<f32>)>,
}
#[derive(Serialize, Deserialize)]
struct ExportedEmbeddingStore {
node_type: String,
text_column: String, dimension: usize,
metric: Option<String>,
model_id: Option<String>,
entries: Vec<(Value, Vec<f32>, Option<u64>)>,
}
pub enum EmbeddingExportFilter {
Types(Vec<String>),
TypeProperties(HashMap<String, Vec<String>>),
}
pub struct ExportStats {
pub stores: usize,
pub embeddings: usize,
}
pub struct ImportStats {
pub stores: usize,
pub imported: usize,
pub skipped: usize,
pub dropped_stores: usize,
}
pub fn export_embeddings_to_file(
graph: &DirGraph,
path: &str,
filter: Option<&EmbeddingExportFilter>,
) -> io::Result<ExportStats> {
let _arena_guard = graph.graph.begin_query();
let mut exported_stores: Vec<ExportedEmbeddingStore> = Vec::new();
let mut total_embeddings = 0usize;
for ((node_type, store_name), store) in &graph.embeddings {
let text_column = store_name
.strip_suffix("_emb")
.unwrap_or(store_name.as_str());
if let Some(f) = filter {
match f {
EmbeddingExportFilter::Types(types) => {
if !types.iter().any(|t| t == node_type) {
continue;
}
}
EmbeddingExportFilter::TypeProperties(map) => {
match map.get(node_type) {
None => continue, Some(props) if !props.is_empty() => {
if !props.iter().any(|p| p == text_column) {
continue;
}
}
Some(_) => {} }
}
}
}
let mut entries: Vec<(Value, Vec<f32>, Option<u64>)> = Vec::with_capacity(store.len());
for &node_index in &store.slot_to_node {
if let Some(node) = graph
.graph
.node_weight(petgraph::graph::NodeIndex::new(node_index))
{
if let Some(embedding) = store.get_embedding(node_index) {
let hash = store.text_hashes.get(&node_index).copied();
entries.push((node.id().into_owned(), embedding.to_vec(), hash));
}
}
}
total_embeddings += entries.len();
exported_stores.push(ExportedEmbeddingStore {
node_type: node_type.clone(),
text_column: text_column.to_string(),
dimension: store.dimension,
metric: store.metric.clone(),
model_id: store.model_id.clone(),
entries,
});
}
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
writer.write_all(&KGLE_MAGIC)?;
writer.write_all(&KGLE_VERSION.to_le_bytes())?;
let gz = GzEncoder::new(&mut writer, Compression::new(3));
bincode_options()
.serialize_into(gz, &exported_stores)
.map_err(|e| io::Error::other(format!("Failed to serialize embeddings: {}", e)))?;
writer.flush()?;
Ok(ExportStats {
stores: exported_stores.len(),
embeddings: total_embeddings,
})
}
pub fn import_embeddings_from_file(graph: &mut DirGraph, path: &str) -> io::Result<ImportStats> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
if buf.len() < 8 {
return Err(io::Error::other(
"File is too small to be a valid .kgle file.",
));
}
if buf[..4] != KGLE_MAGIC {
return Err(io::Error::other(
"Not a valid .kgle file (bad magic bytes).",
));
}
let version = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
if version > KGLE_VERSION {
return Err(io::Error::other(format!(
"Embedding file version {} is newer than supported version {}. Please upgrade kglite.",
version, KGLE_VERSION,
)));
}
let gz = GzDecoder::new(&buf[8..]);
let exported_stores: Vec<ExportedEmbeddingStore> = if version >= 2 {
bincode_options()
.deserialize_from(gz)
.map_err(|e| io::Error::other(format!("Failed to deserialize embedding data: {}", e)))?
} else {
let v1: Vec<ExportedEmbeddingStoreV1> =
bincode_options().deserialize_from(gz).map_err(|e| {
io::Error::other(format!("Failed to deserialize embedding data: {}", e))
})?;
v1.into_iter()
.map(|s| ExportedEmbeddingStore {
node_type: s.node_type,
text_column: s.text_column,
dimension: s.dimension,
metric: None,
model_id: None,
entries: s.entries.into_iter().map(|(id, v)| (id, v, None)).collect(),
})
.collect()
};
let mut total_imported = 0usize;
let mut total_skipped = 0usize;
let mut stores_count = 0usize;
let mut dropped_stores = 0usize;
for exported in exported_stores {
graph.build_id_index(&exported.node_type);
let mut store = crate::graph::schema::EmbeddingStore::new(exported.dimension);
store.metric = exported.metric.clone();
store.model_id = exported.model_id.clone();
store
.data
.reserve(exported.entries.len() * exported.dimension);
let mut imported = 0usize;
let mut skipped = 0usize;
for (id, vec, hash) in &exported.entries {
match graph.lookup_by_id(&exported.node_type, id) {
Some(node_idx) => {
store.set_embedding(node_idx.index(), vec);
if let Some(h) = hash {
store.set_text_hash(node_idx.index(), *h);
}
imported += 1;
}
None => {
skipped += 1;
}
}
}
if imported > 0 {
let key = (exported.node_type, format!("{}_emb", exported.text_column));
graph.embeddings.insert(key, store);
stores_count += 1;
} else if !exported.entries.is_empty() {
dropped_stores += 1;
}
total_imported += imported;
total_skipped += skipped;
}
Ok(ImportStats {
stores: stores_count,
imported: total_imported,
skipped: total_skipped,
dropped_stores,
})
}