mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-23 21:25:02 +00:00
perf(postgres): Zero-copy HNSW insert path optimization
- Eliminate vector clone in insert() by searching first, then inserting - Remove unused hybrid-search and filtered-search feature flags - Bump versions: ruvector-postgres 0.2.2, @ruvector/postgres-cli 0.1.2 Performance: Insert operations now require zero vector copies for the common case (non-empty index), reducing memory allocations in hot path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b36ec5a690
commit
37ff1eb3c2
3 changed files with 41 additions and 31 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ruvector-postgres"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "High-performance PostgreSQL vector database extension - pgvector drop-in replacement with 53+ SQL functions, SIMD acceleration, hyperbolic embeddings, GNN layers, and self-learning capabilities"
|
||||
|
|
@ -44,8 +44,7 @@ quantization-all = ["quantization-scalar", "quantization-product", "quantization
|
|||
quant-all = ["quantization-all"] # Alias for convenience
|
||||
|
||||
# Optional features
|
||||
hybrid-search = []
|
||||
filtered-search = []
|
||||
# Note: hybrid-search and filtered-search are planned for future releases
|
||||
neon-compat = [] # Neon-specific optimizations
|
||||
|
||||
# Advanced AI features (opt-in)
|
||||
|
|
|
|||
|
|
@ -196,23 +196,8 @@ impl HnswIndex {
|
|||
return id;
|
||||
}
|
||||
|
||||
// For non-empty index, we need to search with the vector, then store it
|
||||
// Clone once for search operations (required since we need both search and store)
|
||||
let query_vec = vector.clone();
|
||||
|
||||
// Create and insert node with the original vector
|
||||
let mut neighbors_vec = Vec::with_capacity(level + 1);
|
||||
for _ in 0..=level {
|
||||
neighbors_vec.push(RwLock::new(Vec::new()));
|
||||
}
|
||||
|
||||
let node = HnswNode {
|
||||
vector, // Move original into node
|
||||
neighbors: neighbors_vec,
|
||||
max_layer: level,
|
||||
};
|
||||
self.nodes.insert(id, node);
|
||||
|
||||
// For non-empty index: search FIRST with borrowed vector, then insert
|
||||
// This avoids cloning the vector entirely - zero-copy insert path
|
||||
let entry_point_id = current_entry.unwrap();
|
||||
let current_max_layer = self.max_layer.load(AtomicOrdering::Relaxed);
|
||||
|
||||
|
|
@ -221,13 +206,16 @@ impl HnswIndex {
|
|||
|
||||
// Descend through layers above the new node's max layer
|
||||
for layer in (level + 1..=current_max_layer).rev() {
|
||||
curr_id = self.search_layer_single(&query_vec, curr_id, layer);
|
||||
curr_id = self.search_layer_single(&vector, curr_id, layer);
|
||||
}
|
||||
|
||||
// Insert at each layer from the node's max layer down to 0
|
||||
// Collect all neighbor selections before inserting the node
|
||||
// This allows us to search with borrowed vector, then move it
|
||||
let mut layer_neighbors: Vec<Vec<NodeId>> =
|
||||
Vec::with_capacity(level.min(current_max_layer) + 1);
|
||||
|
||||
for layer in (0..=level.min(current_max_layer)).rev() {
|
||||
let neighbors =
|
||||
self.search_layer(&query_vec, curr_id, self.config.ef_construction, layer);
|
||||
let neighbors = self.search_layer(&vector, curr_id, self.config.ef_construction, layer);
|
||||
|
||||
// Select best neighbors
|
||||
let max_connections = if layer == 0 {
|
||||
|
|
@ -241,6 +229,34 @@ impl HnswIndex {
|
|||
.map(|n| n.id)
|
||||
.collect();
|
||||
|
||||
// Update curr_id for next layer
|
||||
if !selected.is_empty() {
|
||||
curr_id = selected[0];
|
||||
}
|
||||
|
||||
layer_neighbors.push(selected);
|
||||
}
|
||||
|
||||
// Reverse since we collected in reverse order
|
||||
layer_neighbors.reverse();
|
||||
|
||||
// NOW create and insert the node (moving the vector - no clone needed)
|
||||
let mut neighbors_vec = Vec::with_capacity(level + 1);
|
||||
for _ in 0..=level {
|
||||
neighbors_vec.push(RwLock::new(Vec::new()));
|
||||
}
|
||||
|
||||
let node = HnswNode {
|
||||
vector, // Move original into node - zero copy!
|
||||
neighbors: neighbors_vec,
|
||||
max_layer: level,
|
||||
};
|
||||
self.nodes.insert(id, node);
|
||||
|
||||
// Apply the pre-computed neighbor connections
|
||||
for (layer_idx, selected) in layer_neighbors.iter().enumerate() {
|
||||
let layer = layer_idx;
|
||||
|
||||
// Set neighbors for new node
|
||||
if let Some(node) = self.nodes.get(&id) {
|
||||
if layer < node.neighbors.len() {
|
||||
|
|
@ -249,14 +265,9 @@ impl HnswIndex {
|
|||
}
|
||||
|
||||
// Add bidirectional connections
|
||||
for &neighbor_id in &selected {
|
||||
for &neighbor_id in selected {
|
||||
self.connect(neighbor_id, id, layer);
|
||||
}
|
||||
|
||||
// Update curr_id for next layer
|
||||
if !selected.is_empty() {
|
||||
curr_id = selected[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Update entry point if necessary
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@ruvector/postgres-cli",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "Advanced AI vector database CLI for PostgreSQL - pgvector drop-in replacement with 53+ SQL functions, 39 attention mechanisms, GNN layers, hyperbolic embeddings, and self-learning capabilities",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue