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:
rUv 2025-12-03 06:46:03 +00:00
parent b36ec5a690
commit 37ff1eb3c2
3 changed files with 41 additions and 31 deletions

View file

@ -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)

View file

@ -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

View file

@ -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",