Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions memory/lightweight_vector_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,31 +75,105 @@ def __init__(self, dimension: int = 384, max_m: int = 16):
self.graph = {} # Simplified graph structure
self.data = {} # Vector data
self.metadata = {} # Associated metadata

# Vectorized cache
self.vectors_matrix = None
self.ids_list = []
self.dirty = False

def add(self, idx: int, vector: np.ndarray, metadata: Dict = None):
"""Add vector to index"""
self.graph[idx] = []
self.data[idx] = vector
if metadata:
self.metadata[idx] = metadata
self.dirty = True

def _rebuild_index(self):
"""Rebuild the vectorized index"""
if not self.data:
self.vectors_matrix = None
self.ids_list = []
return

self.ids_list = []
vectors = []
for k, v in self.data.items():
self.ids_list.append(k)
vectors.append(v if isinstance(v, np.ndarray) else np.array(v))

try:
if not vectors:
return

matrix = np.stack(vectors)

# Normalize matrix rows
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
# Avoid division by zero
norms[norms == 0] = 1.0
self.vectors_matrix = matrix / norms

self.dirty = False
except Exception as e:
print(f"Error rebuilding index: {e}")
self.vectors_matrix = None
self.ids_list = []

def search(self, query_vector: np.ndarray, k: int = 5) -> List[Tuple[int, float]]:
"""Search for k nearest neighbors"""
if not self.data:
return []

# Compute distances to all vectors
distances = []
for idx, vector in self.data.items():
# Cosine distance
dist = 1 - np.dot(query_vector, vector) / (
np.linalg.norm(query_vector) * np.linalg.norm(vector) + 1e-8
)
distances.append((idx, dist))

# Sort by distance and return top k
distances.sort(key=lambda x: x[1])
return distances[:k]

if self.dirty or self.vectors_matrix is None:
self._rebuild_index()

if self.vectors_matrix is None:
# Fallback to empty if rebuild failed
return []

# Normalize query vector
query_norm = np.linalg.norm(query_vector)
if query_norm > 0:
query_vector = query_vector / query_norm

# Compute cosine similarity (dot product of normalized vectors)
# Result shape: (num_vectors,)
try:
scores = self.vectors_matrix @ query_vector

# Convert to distances (1 - similarity)
distances = 1 - scores

# Get top k indices
num_vectors = len(distances)
k = min(k, num_vectors)

if k == 0:
return []

if k >= num_vectors:
# If we need all or more, just sort all
top_k_indices = np.argsort(distances)
else:
# argpartition is faster for top k
# It puts the smallest k elements in the first k positions (unsorted)
partitioned_indices = np.argpartition(distances, k)[:k]
# Now sort these indices based on their distance
# We want indices that sort the *subset* of distances
subset_indices = np.argsort(distances[partitioned_indices])
top_k_indices = partitioned_indices[subset_indices]

results = []
for i in top_k_indices:
idx = self.ids_list[i]
dist = float(distances[i])
results.append((idx, dist))

return results
except Exception as e:
print(f"Error in vectorized search: {e}")
return []


class LightweightVectorDB:
Expand Down
53 changes: 53 additions & 0 deletions tests/benchmark_vector_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

import time
import numpy as np
import sys
import os

# Add the project root to sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from memory.lightweight_vector_db import HNSWIndex, EmbeddingModel

def benchmark_search():
print("🚀 Benchmarking HNSWIndex Search...")

# Initialize index
dimension = 384
index = HNSWIndex(dimension=dimension)

# Create fake data
num_vectors = 10000
print(f"Generating {num_vectors} random vectors of dimension {dimension}...")

# Pre-generate random normalized vectors
vectors = np.random.randn(num_vectors, dimension).astype(np.float32)
vectors /= np.linalg.norm(vectors, axis=1)[:, np.newaxis]

# Add to index
start_add = time.time()
for i in range(num_vectors):
index.add(i, vectors[i])
print(f"Adding took: {time.time() - start_add:.4f}s")

# Benchmark search
num_queries = 100
query_vectors = np.random.randn(num_queries, dimension).astype(np.float32)
query_vectors /= np.linalg.norm(query_vectors, axis=1)[:, np.newaxis]

print(f"Running {num_queries} searches...")
start_search = time.time()

for i in range(num_queries):
index.search(query_vectors[i], k=5)

total_time = time.time() - start_search
avg_time = total_time / num_queries

print(f"Total search time: {total_time:.4f}s")
print(f"Average time per search: {avg_time:.4f}s")

return avg_time

if __name__ == "__main__":
benchmark_search()
62 changes: 62 additions & 0 deletions tests/test_vector_db_isolated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

import pytest
import os
import shutil
from pathlib import Path
import numpy as np
import sys

# Add project root to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from memory.lightweight_vector_db import LightweightVectorDB, HNSWIndex

@pytest.fixture
def vector_db():
# Setup
db_path = "tests/temp_vector_db"
if os.path.exists(db_path):
shutil.rmtree(db_path)

db = LightweightVectorDB(db_path=db_path)
yield db

# Teardown
if os.path.exists(db_path):
shutil.rmtree(db_path)

def test_store_and_search(vector_db):
documents = [
("Python is a programming language", {"category": "programming"}),
("Machine learning is cool", {"category": "ai"}),
("Banana is a fruit", {"category": "food"}),
]

for text, meta in documents:
vector_db.store_and_index(text, meta)

# Search
# Note: EmbeddingModel in this codebase is a simulation that returns random vectors seeded by text hash
# So it should be deterministic.

results = vector_db.search("programming", k=1)
assert len(results) >= 1
# We can't guarantee semantic match with random embeddings, but we check execution

# To test correctness, we should use HNSWIndex directly with known vectors

def test_hnsw_index_direct():
index = HNSWIndex(dimension=384)
vec1 = np.random.randn(384).astype(np.float32)
vec1 /= np.linalg.norm(vec1)

vec2 = np.random.randn(384).astype(np.float32)
vec2 /= np.linalg.norm(vec2)

index.add(1, vec1)
index.add(2, vec2)

# Search for vec1, should be closest to vec1
results = index.search(vec1, k=1)
assert results[0][0] == 1
assert results[0][1] < 0.0001 # Distance should be near 0