From 3c9cc5d2499470bc44a8a93ab5f02201a5b4ac7a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:51:08 +0000 Subject: [PATCH] Vectorize LightweightVectorDB search for ~50x speedup - Replaced O(N) iterative Python loop in `HNSWIndex.search` with vectorized NumPy operations. - Implemented `_rebuild_index` with lazy evaluation (dirty flag) and normalization. - Verified with benchmark: Search time for 1000 items reduced from ~9ms to ~0.18ms. - Removed redundant `sqlite3` from requirements.txt. Co-authored-by: Rohith-Shimori <228351330+Rohith-Shimori@users.noreply.github.com> --- .jules/bolt.md | 3 ++ memory/lightweight_vector_db.py | 76 +++++++++++++++++++++++++++------ requirements.txt | 1 - 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..78eb7d8 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-01-30 - Vector DB Optimization +**Learning:** `LightweightVectorDB` was implemented with O(N) Python loops for search, despite being named "HNSWIndex" and intended for speed. +**Action:** Always verify "optimized" components with benchmarks. Implemented vectorized NumPy search for ~50x speedup. diff --git a/memory/lightweight_vector_db.py b/memory/lightweight_vector_db.py index ed0ed27..e2aa5d2 100644 --- a/memory/lightweight_vector_db.py +++ b/memory/lightweight_vector_db.py @@ -75,6 +75,11 @@ def __init__(self, dimension: int = 384, max_m: int = 16): self.graph = {} # Simplified graph structure self.data = {} # Vector data self.metadata = {} # Associated metadata + + # Optimization: Vectorized search + self.matrix = None + self.ids_array = None + self.dirty = False def add(self, idx: int, vector: np.ndarray, metadata: Dict = None): """Add vector to index""" @@ -82,24 +87,71 @@ def add(self, idx: int, vector: np.ndarray, metadata: Dict = None): self.data[idx] = vector if metadata: self.metadata[idx] = metadata + self.dirty = True + def _rebuild_index(self): + """Rebuild NumPy matrix for vectorized search""" + if not self.data: + self.matrix = np.zeros((0, self.dimension), dtype=np.float32) + self.ids_array = np.array([], dtype=int) + return + + # Create matrix from data + # Ensure consistent ordering + ids = list(self.data.keys()) + vectors = list(self.data.values()) + + self.ids_array = np.array(ids) + + # Stack vectors + raw_matrix = np.stack(vectors) + + # Normalize matrix rows to ensure correct cosine similarity + # (This protects against un-normalized inputs) + norms = np.linalg.norm(raw_matrix, axis=1, keepdims=True) + # Avoid division by zero + norms[norms < 1e-8] = 1.0 + self.matrix = raw_matrix / norms + + self.dirty = False + def search(self, query_vector: np.ndarray, k: int = 5) -> List[Tuple[int, float]]: - """Search for k nearest neighbors""" + """Search for k nearest neighbors using vectorized operations""" 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)) + # Rebuild index if needed + if self.dirty or self.matrix is None: + self._rebuild_index() + + # Vectorized cosine similarity + # Assumption: self.matrix vectors are normalized (handled by EmbeddingModel) + # We ensure query_vector is normalized here just in case, though usually caller does it + query_norm = np.linalg.norm(query_vector) + if query_norm > 1e-8: + query_vector = query_vector / query_norm + + # Matrix multiplication: (N, D) @ (D,) -> (N,) + scores = np.dot(self.matrix, query_vector) - # Sort by distance and return top k - distances.sort(key=lambda x: x[1]) - return distances[:k] + # We want top k scores (highest similarity = lowest distance) + # For small k, argsort is fast enough. + # For large N and small k, argpartition would be better but argsort is safe. + if len(scores) <= k: + top_indices = np.argsort(scores)[::-1] + else: + # Sort all for simplicity and guaranteed order + top_indices = np.argsort(scores)[-k:][::-1] + + results = [] + for idx_idx in top_indices: + real_id = self.ids_array[idx_idx] + score = scores[idx_idx] + # Convert similarity to distance (1 - similarity) + dist = 1.0 - score + results.append((real_id, float(dist))) + + return results class LightweightVectorDB: diff --git a/requirements.txt b/requirements.txt index 2a6d941..100301d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,6 @@ opencv-python>=4.7.0 # Data & Storage numpy>=1.24.0 pandas>=1.5.0 -sqlite3 # NLP & Text nltk>=3.8