codepulse-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codepulse/__init__.py +5 -0
- codepulse/api/app.py +1336 -0
- codepulse/api/errors.py +172 -0
- codepulse/api/security.py +189 -0
- codepulse/auth.py +406 -0
- codepulse/cli.py +1052 -0
- codepulse/config.py +475 -0
- codepulse/data_location.py +78 -0
- codepulse/embeddings/chunker.py +138 -0
- codepulse/embeddings/embedder.py +120 -0
- codepulse/git_diff.py +201 -0
- codepulse/indexer/engine.py +2655 -0
- codepulse/indexer/ignore.py +120 -0
- codepulse/ingestion_safety.py +169 -0
- codepulse/jobs.py +257 -0
- codepulse/mcp_server.py +269 -0
- codepulse/models.py +140 -0
- codepulse/observability.py +130 -0
- codepulse/parser/__init__.py +8 -0
- codepulse/parser/base.py +58 -0
- codepulse/parser/factory.py +37 -0
- codepulse/parser/graph.py +502 -0
- codepulse/parser/javascript.py +428 -0
- codepulse/parser/module_graph.py +232 -0
- codepulse/parser/python.py +331 -0
- codepulse/rate_limit.py +142 -0
- codepulse/repository_identity.py +83 -0
- codepulse/search/bm25.py +342 -0
- codepulse/search/ranker.py +382 -0
- codepulse/search/tokenizer.py +80 -0
- codepulse/search/vector.py +357 -0
- codepulse/storage/db.py +526 -0
- codepulse/storage/migrations.py +347 -0
- codepulse/storage/schema_migrations/README +1 -0
- codepulse/storage/schema_migrations/env.py +70 -0
- codepulse/storage/schema_migrations/script.py.mako +28 -0
- codepulse/storage/schema_migrations/versions/135a7b064ea7_add_base_classes_and_relationship_type.py +82 -0
- codepulse/storage/schema_migrations/versions/20fc2d0607e2_add_file_content_hash_and_natural_key_.py +106 -0
- codepulse/storage/schema_migrations/versions/21a7cb7f1a85_initial_schema.py +105 -0
- codepulse/storage/schema_migrations/versions/2cd38eb12a7e_add_call_edge_repo_id_isolation_fix.py +112 -0
- codepulse/storage/schema_migrations/versions/3ba396e9c78f_add_symbol_natural_key_uniqueness.py +80 -0
- codepulse/storage/schema_migrations/versions/49572f834366_add_embedding_persistence_fields.py +102 -0
- codepulse/storage/schema_migrations/versions/7c3e5a91d2f4_add_module_edges.py +61 -0
- codepulse/storage/schema_migrations/versions/a41f0d2c6b8e_canonicalize_repository_urls.py +94 -0
- codepulse/storage/schema_migrations/versions/b0bdd63949f4_add_api_key_model.py +71 -0
- codepulse/storage/schema_migrations/versions/b9014c049e33_add_api_key_repository_role.py +65 -0
- codepulse/storage/schema_migrations/versions/c286d277ea21_add_repository_snapshot_model.py +62 -0
- codepulse/storage/schema_migrations/versions/de22df2fc3ce_add_index_job_model.py +59 -0
- codepulse/storage/schema_migrations/versions/f6e481353553_rename_dependency_to_call_edge_add_confidence.py +80 -0
- codepulse/symbol_identity.py +29 -0
- codepulse_engine-0.1.0.dist-info/METADATA +191 -0
- codepulse_engine-0.1.0.dist-info/RECORD +56 -0
- codepulse_engine-0.1.0.dist-info/WHEEL +5 -0
- codepulse_engine-0.1.0.dist-info/entry_points.txt +2 -0
- codepulse_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
- codepulse_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Any, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import sqlite_vec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VectorIndex:
|
|
11
|
+
"""
|
|
12
|
+
Embedded vector similarity index for dense code embeddings (Phase 2.2).
|
|
13
|
+
|
|
14
|
+
Replaces the pre-Phase-2.2 pure-NumPy brute-force implementation's
|
|
15
|
+
*internals* with the embedded `sqlite-vec` SQLite extension (roadmap
|
|
16
|
+
§36 Phase 2: "swap `VectorIndex` for an embedded ANN structure...
|
|
17
|
+
without changing the ranking contract"; §7 Storage Decision: "a local
|
|
18
|
+
vector index (e.g. `sqlite-vec` or a persisted HNSW structure via
|
|
19
|
+
`hnswlib`)") while preserving, unchanged, every part of the class's
|
|
20
|
+
external contract that `IndexingEngine`, `HybridRanker`,
|
|
21
|
+
`codepulse/api/app.py`'s `find_similar_code` endpoint, and the
|
|
22
|
+
existing test suite depend on:
|
|
23
|
+
|
|
24
|
+
- Constructor: `VectorIndex(dimension=384)`.
|
|
25
|
+
- `.dimension`: unchanged.
|
|
26
|
+
- `.doc_ids`: a real `list[str]`, reflecting the currently-active
|
|
27
|
+
document set (Phase 3.1 added `remove_docs`, below, so this is no
|
|
28
|
+
longer purely append-only -- see the module's own "Known
|
|
29
|
+
limitation" note for exactly what is, and is not, covered by it).
|
|
30
|
+
- `.vectors`: a genuine `numpy.ndarray` of shape `(num_docs,
|
|
31
|
+
dimension)`, mirroring every normalized vector ever added, in
|
|
32
|
+
insertion order -- `tests/test_embedding_persistence.py` asserts
|
|
33
|
+
`.vectors.shape`/`.vectors.shape[1]` directly. The actual similarity
|
|
34
|
+
search below never reads this array; it is maintained purely to
|
|
35
|
+
keep this pre-existing public contract intact across the
|
|
36
|
+
`sqlite-vec` swap.
|
|
37
|
+
- `add(doc_id, vector, metadata=None)`, `add_batch(doc_ids, vectors,
|
|
38
|
+
metadatas=None)`, `search(query_vector, top_k=20)` -- identical
|
|
39
|
+
signatures and return shapes (`{"doc_id", "similarity",
|
|
40
|
+
"relevance_score", "embedding_distance", "metadata"}` per result,
|
|
41
|
+
`relevance_score`/`embedding_distance` derived from `similarity` via
|
|
42
|
+
the exact same formulas the pre-Phase-2.2 implementation used).
|
|
43
|
+
|
|
44
|
+
Dependency note (disclosed, not silently chosen): §7 names two
|
|
45
|
+
candidate libraries, `sqlite-vec` or `hnswlib`, without choosing
|
|
46
|
+
between them. `hnswlib` was attempted first and found to have no
|
|
47
|
+
prebuilt wheel for this project's actual development platform
|
|
48
|
+
(Windows, CPython 3.11) -- PyPI carries only a source distribution,
|
|
49
|
+
and building it requires a C++ compiler (Microsoft Visual C++ Build
|
|
50
|
+
Tools) that is not installed and was not installed for this milestone
|
|
51
|
+
(installing system-level build tooling is outside this project's
|
|
52
|
+
scope). `sqlite-vec` (0.1.9) has a prebuilt `win_amd64` wheel and was
|
|
53
|
+
used instead -- verified directly (this module's own construction and
|
|
54
|
+
round-trip experiments performed before writing this adapter, not
|
|
55
|
+
assumed from documentation).
|
|
56
|
+
|
|
57
|
+
Known, disclosed characteristic of the underlying library actually
|
|
58
|
+
used: `sqlite-vec` 0.1.9's `vec0` virtual table performs an exact
|
|
59
|
+
(brute-force) distance computation over every stored vector, not an
|
|
60
|
+
approximate/graph-based nearest-neighbor structure the way `hnswlib`'s
|
|
61
|
+
HNSW algorithm would -- confirmed by direct testing (identical
|
|
62
|
+
results to the pre-Phase-2.2 NumPy brute-force implementation for
|
|
63
|
+
every query tried, not merely "close"). This still satisfies §7's own
|
|
64
|
+
literal wording (`sqlite-vec` is one of its two named options) and
|
|
65
|
+
remains a genuine embedded, compiled-native vector index rather than
|
|
66
|
+
a Python/NumPy one -- but is not literally an *approximate* nearest
|
|
67
|
+
neighbor structure at this library version. Retrieval remains fully
|
|
68
|
+
deterministic as a direct consequence (a permanent project invariant,
|
|
69
|
+
CLAUDE.md §4), which an approximate structure would not guarantee as
|
|
70
|
+
simply.
|
|
71
|
+
|
|
72
|
+
Known limitation, UPDATED by Phase 3.1, then RESOLVED (for the
|
|
73
|
+
full-reindex case specifically) by a later backend-hardening pass --
|
|
74
|
+
kept here for history rather than silently rewritten (disclosed, not
|
|
75
|
+
silently expanded scope -- see CODEPULSE_PROGRESS.md): Phase 2.2
|
|
76
|
+
shipped with `add`/`add_batch` as pure-append operations and no
|
|
77
|
+
delete capability at all, unlike Phase 2.1's `BM25Index.remove_repo`
|
|
78
|
+
-- meaning re-indexing an already-indexed repository silently
|
|
79
|
+
accumulated duplicate vector entries. Phase 3.1 added a real, general
|
|
80
|
+
`remove_docs(doc_ids)` method (needed for its own literal "(deleted)"
|
|
81
|
+
file-handling requirement), which `IndexingEngine.index_directory()`'s
|
|
82
|
+
incremental path uses to remove the OLD embeddings of a file whose
|
|
83
|
+
content changed before re-adding the new ones -- closing the
|
|
84
|
+
practical duplication gap for that specific, incremental-reindex
|
|
85
|
+
case. It deliberately did **not** add a `remove_repo`-style "delete
|
|
86
|
+
everything for this repo_id in one call" convenience at that time.
|
|
87
|
+
|
|
88
|
+
`add`/`add_batch` themselves remain pure-append, by design, with no
|
|
89
|
+
automatic dedup-on-add -- a caller who adds the exact same `doc_id`
|
|
90
|
+
twice WITHOUT first calling `remove_docs`/`remove_repo` still gets a
|
|
91
|
+
duplicate (see `remove_docs`'s own docstring, and
|
|
92
|
+
`test_known_limitation_repeated_add_of_the_same_doc_id_accumulates_
|
|
93
|
+
duplicates` in `tests/test_sqlite_vec_semantic_index.py`, which keeps
|
|
94
|
+
asserting exactly this at the `VectorIndex` level). **RESOLVED at the
|
|
95
|
+
`IndexingEngine` level**: `remove_repo(repo_id)` (below) now exists,
|
|
96
|
+
mirroring `BM25Index.remove_repo`'s exact semantics, and
|
|
97
|
+
`IndexingEngine.index_directory()` calls it before repopulating the
|
|
98
|
+
vector index on a FULL (non-incremental) re-index -- the same
|
|
99
|
+
replace-not-accumulate treatment Phase 2.1 already gave the lexical
|
|
100
|
+
index. A full re-index of an already-indexed repository therefore no
|
|
101
|
+
longer accumulates vector duplicates.
|
|
102
|
+
|
|
103
|
+
Thread-safety note (a necessary, disclosed adjustment, not a design
|
|
104
|
+
preference): the underlying `sqlite3.Connection` is constructed with
|
|
105
|
+
`check_same_thread=False`. FastAPI/Starlette route handlers defined as
|
|
106
|
+
plain `def` (every endpoint in this app, including the Phase 1.3
|
|
107
|
+
background-task indexing path) run on a shared threadpool, so the same
|
|
108
|
+
`IndexingEngine`/`VectorIndex` instance can legitimately be called
|
|
109
|
+
from more than one OS thread across its lifetime. Python's `sqlite3`
|
|
110
|
+
module refuses cross-thread use of one connection by default; the
|
|
111
|
+
pre-Phase-2.2 pure NumPy/list implementation had no such restriction,
|
|
112
|
+
so disabling this check is necessary purely to avoid this milestone
|
|
113
|
+
introducing a new hard-crash mode where none existed before -- not a
|
|
114
|
+
claim of full concurrent-write safety. This codebase has no locking
|
|
115
|
+
around `bm25_index`/`vector_index` today (unchanged, pre-existing,
|
|
116
|
+
out of this milestone's scope, matching Phase 2.1's own precedent).
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, dimension: int = 384) -> None:
|
|
120
|
+
self.dimension = dimension
|
|
121
|
+
self.doc_ids: list[str] = []
|
|
122
|
+
self.metadata: list[dict[str, Any]] = []
|
|
123
|
+
self.vectors: np.ndarray = np.empty((0, dimension), dtype=np.float32)
|
|
124
|
+
|
|
125
|
+
self._conn = sqlite3.connect(":memory:", check_same_thread=False)
|
|
126
|
+
self._conn.enable_load_extension(True)
|
|
127
|
+
sqlite_vec.load(self._conn)
|
|
128
|
+
self._conn.enable_load_extension(False)
|
|
129
|
+
self._conn.execute(
|
|
130
|
+
f"CREATE VIRTUAL TABLE vec_items USING vec0("
|
|
131
|
+
f"embedding float[{dimension}] distance_metric=cosine)"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Phase 3.1 (roadmap §37 item 3.1's "(deleted)" category needs a
|
|
135
|
+
# real delete capability on this index, which did not exist
|
|
136
|
+
# before this milestone -- see `remove_docs` below). A rowid is
|
|
137
|
+
# no longer derivable from list position (`rowid - 1 ==
|
|
138
|
+
# list-index`), since a deletion in the middle of `self.doc_ids`
|
|
139
|
+
# would break that correspondence for every entry after it.
|
|
140
|
+
# `_next_rowid` is a monotonic counter, NEVER decremented or
|
|
141
|
+
# reused, so a rowid freed by a delete can never collide with a
|
|
142
|
+
# rowid assigned to a still-live document later. `_rowid_by_doc_id`/
|
|
143
|
+
# `_doc_by_rowid` are the explicit, authoritative mappings
|
|
144
|
+
# `search()` and `remove_docs()` use instead of positional
|
|
145
|
+
# arithmetic.
|
|
146
|
+
self._next_rowid: int = 1
|
|
147
|
+
self._rowid_by_doc_id: dict[str, int] = {}
|
|
148
|
+
self._doc_by_rowid: dict[int, tuple[str, dict[str, Any]]] = {}
|
|
149
|
+
|
|
150
|
+
def add(self, doc_id: str, vector: np.ndarray, metadata: dict[str, Any] | None = None) -> None:
|
|
151
|
+
"""Adds a single normalized vector with doc_id and metadata."""
|
|
152
|
+
vec_2d = vector.reshape(1, -1) if vector.ndim == 1 else vector
|
|
153
|
+
self.add_batch([doc_id], vec_2d, [metadata] if metadata is not None else None)
|
|
154
|
+
|
|
155
|
+
def add_batch(
|
|
156
|
+
self,
|
|
157
|
+
doc_ids: list[str],
|
|
158
|
+
vectors: np.ndarray,
|
|
159
|
+
metadatas: list[dict[str, Any]] | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Adds a batch of vectors to the index."""
|
|
162
|
+
if len(doc_ids) == 0:
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Normalize -- unchanged from the pre-Phase-2.2 implementation.
|
|
166
|
+
norm = np.linalg.norm(vectors, axis=1, keepdims=True)
|
|
167
|
+
norm[norm == 0] = 1.0
|
|
168
|
+
normalized_vecs = (vectors / norm).astype(np.float32)
|
|
169
|
+
|
|
170
|
+
if self.vectors.shape[0] == 0:
|
|
171
|
+
self.vectors = normalized_vecs
|
|
172
|
+
else:
|
|
173
|
+
self.vectors = np.vstack([self.vectors, normalized_vecs])
|
|
174
|
+
|
|
175
|
+
resolved_metas = list(metadatas) if metadatas else [{} for _ in doc_ids]
|
|
176
|
+
|
|
177
|
+
# `vec_items`'s rowid is the ONLY handle sqlite-vec's own KNN query
|
|
178
|
+
# gives back (verified via direct experiment) -- it is not the
|
|
179
|
+
# CodePulse `doc_id` string. Each new rowid comes from the
|
|
180
|
+
# monotonic `_next_rowid` counter (Phase 3.1 -- see `__init__`'s
|
|
181
|
+
# comment on why this replaced the old "rowid - 1 == list index"
|
|
182
|
+
# arithmetic once deletion became possible).
|
|
183
|
+
rows = []
|
|
184
|
+
for i, doc_id in enumerate(doc_ids):
|
|
185
|
+
rowid = self._next_rowid
|
|
186
|
+
self._next_rowid += 1
|
|
187
|
+
rows.append((rowid, sqlite_vec.serialize_float32(normalized_vecs[i].tolist())))
|
|
188
|
+
self._rowid_by_doc_id[doc_id] = rowid
|
|
189
|
+
self._doc_by_rowid[rowid] = (doc_id, resolved_metas[i])
|
|
190
|
+
|
|
191
|
+
self._conn.executemany("INSERT INTO vec_items(rowid, embedding) VALUES (?, ?)", rows)
|
|
192
|
+
self._conn.commit()
|
|
193
|
+
|
|
194
|
+
self.doc_ids.extend(doc_ids)
|
|
195
|
+
self.metadata.extend(resolved_metas)
|
|
196
|
+
|
|
197
|
+
def remove_repo(self, repo_id: int) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Deletes every currently-indexed vector document belonging to
|
|
200
|
+
`repo_id`, mirroring `BM25Index.remove_repo`'s existing
|
|
201
|
+
repository-replacement semantics exactly (see
|
|
202
|
+
`IndexingEngine.index_directory()`'s call site, right before the
|
|
203
|
+
vector index is repopulated on a full re-index). Closes the gap
|
|
204
|
+
this class's own docstring has disclosed since Phase 2.2: `add`/
|
|
205
|
+
`add_batch` are pure-append with no dedup-on-add, and until now
|
|
206
|
+
there was no bulk "delete everything for this repo_id" operation
|
|
207
|
+
the way `BM25Index` has always had -- so a full, non-incremental
|
|
208
|
+
re-index of an already-indexed repository silently accumulated
|
|
209
|
+
duplicate vector entries forever. Identifies matching documents
|
|
210
|
+
via `self.metadata`'s `repo_id` field (populated by every caller
|
|
211
|
+
that adds vectors -- `IndexingEngine`'s indexing and hydration
|
|
212
|
+
paths both set it), then delegates to `remove_docs` for the
|
|
213
|
+
actual deletion bookkeeping (rowid mapping, `.vectors` array
|
|
214
|
+
alignment). Safe to call for a `repo_id` with no existing
|
|
215
|
+
documents -- a no-op, matching `remove_docs`'s own
|
|
216
|
+
no-op-safety precedent.
|
|
217
|
+
"""
|
|
218
|
+
doc_ids = [d for d, m in zip(self.doc_ids, self.metadata) if m.get("repo_id") == repo_id]
|
|
219
|
+
self.remove_docs(doc_ids)
|
|
220
|
+
|
|
221
|
+
def remove_docs(self, doc_ids: list[str]) -> None:
|
|
222
|
+
"""
|
|
223
|
+
Phase 3.1 (roadmap §37 item 3.1: git-diff change detection's
|
|
224
|
+
"(deleted)" category) -- deletes exactly the given documents by
|
|
225
|
+
their real `doc_id`. This is a genuinely new capability: no
|
|
226
|
+
version of `VectorIndex` before this milestone could delete
|
|
227
|
+
anything (see the class docstring's "Known limitation," which
|
|
228
|
+
this method does not fully resolve -- see below). Used when
|
|
229
|
+
`IndexingEngine.index_directory()` detects a specific set of
|
|
230
|
+
deleted files via `codepulse.git_diff` and needs to remove only
|
|
231
|
+
those files' embeddings, leaving every other document (in this
|
|
232
|
+
repository or any other) untouched. Safe to call with a `doc_id`
|
|
233
|
+
that is not currently indexed (a no-op for that one id).
|
|
234
|
+
|
|
235
|
+
Deliberately does NOT resolve Phase 2.2's disclosed
|
|
236
|
+
repository-replacement-idempotency gap on its own: this method
|
|
237
|
+
only removes documents a caller explicitly names (e.g. from a
|
|
238
|
+
git-diff deleted-files list); it is not a `remove_repo`-style
|
|
239
|
+
"delete everything for this repo_id" operation, and
|
|
240
|
+
`add`/`add_batch` still perform no automatic dedup on their own.
|
|
241
|
+
`IndexingEngine.index_directory()`'s Phase 3.1 incremental path
|
|
242
|
+
(see its own docstring) is what actually closes the practical
|
|
243
|
+
gap for the specific case of a file being re-indexed after a
|
|
244
|
+
real content change, by computing and passing the old doc_ids
|
|
245
|
+
for changed files into this method before re-adding them.
|
|
246
|
+
"""
|
|
247
|
+
to_remove = set(doc_ids)
|
|
248
|
+
if not to_remove:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
rowids_to_remove = [self._rowid_by_doc_id[d] for d in to_remove if d in self._rowid_by_doc_id]
|
|
252
|
+
if rowids_to_remove:
|
|
253
|
+
placeholders = ",".join("?" for _ in rowids_to_remove)
|
|
254
|
+
self._conn.execute(f"DELETE FROM vec_items WHERE rowid IN ({placeholders})", rowids_to_remove)
|
|
255
|
+
self._conn.commit()
|
|
256
|
+
|
|
257
|
+
for doc_id in to_remove:
|
|
258
|
+
rowid = self._rowid_by_doc_id.pop(doc_id, None)
|
|
259
|
+
if rowid is not None:
|
|
260
|
+
self._doc_by_rowid.pop(rowid, None)
|
|
261
|
+
|
|
262
|
+
keep_mask = [doc_id not in to_remove for doc_id in self.doc_ids]
|
|
263
|
+
self.doc_ids = [d for d, keep in zip(self.doc_ids, keep_mask) if keep]
|
|
264
|
+
self.metadata = [m for m, keep in zip(self.metadata, keep_mask) if keep]
|
|
265
|
+
if self.vectors.shape[0]:
|
|
266
|
+
self.vectors = self.vectors[np.array(keep_mask, dtype=bool)]
|
|
267
|
+
|
|
268
|
+
def search(self, query_vector: np.ndarray, top_k: int = 20) -> list[dict[str, Any]]:
|
|
269
|
+
"""
|
|
270
|
+
Calculates cosine similarities between query_vector and all indexed vectors.
|
|
271
|
+
Returns top_k most similar entries with scores in [0, 1] and distances in [0, 2].
|
|
272
|
+
"""
|
|
273
|
+
if self.vectors.shape[0] == 0:
|
|
274
|
+
return []
|
|
275
|
+
|
|
276
|
+
# Ensure query vector is normalized 1D -- unchanged from the
|
|
277
|
+
# pre-Phase-2.2 implementation.
|
|
278
|
+
q = query_vector.flatten().astype(np.float32)
|
|
279
|
+
q_norm = np.linalg.norm(q)
|
|
280
|
+
if q_norm > 0:
|
|
281
|
+
q = q / q_norm
|
|
282
|
+
|
|
283
|
+
# Phase 2.5 finding (discovered by the benchmark harness's own
|
|
284
|
+
# 10K-file tier run, not previously exercised by any Phase 2.2
|
|
285
|
+
# test -- those all used small corpora): sqlite-vec's `vec0` KNN
|
|
286
|
+
# query has a hard limit on `k`, enforced by the library itself
|
|
287
|
+
# (verified experimentally: `sqlite3.OperationalError: k value in
|
|
288
|
+
# knn query too large, provided 40000 and the limit is 4096`).
|
|
289
|
+
# `IndexingEngine.search()`'s repository-scoped candidate-pool
|
|
290
|
+
# sizing (Phase 0.6) intentionally requests `top_k =
|
|
291
|
+
# len(vector_index.doc_ids)` -- the entire corpus -- so
|
|
292
|
+
# `_repo_filter`'s post-filtering is never starved; at realistic
|
|
293
|
+
# scale that number now regularly exceeds sqlite-vec's own limit,
|
|
294
|
+
# which would otherwise crash every scoped semantic/hybrid search
|
|
295
|
+
# once a repository's embedded-chunk count passes ~4096. Clamped
|
|
296
|
+
# here, defensively, for every caller (not just the benchmark's
|
|
297
|
+
# own scoped-search path) -- this is the smallest number of
|
|
298
|
+
# candidates sqlite-vec itself will ever accept in one query, so
|
|
299
|
+
# any caller requesting more must be capped somewhere, and this is
|
|
300
|
+
# the one place that already owns the actual SQL call.
|
|
301
|
+
#
|
|
302
|
+
# Disclosed trade-off, not silently glossed over: capping means a
|
|
303
|
+
# very large multi-repository deployment (a query result set
|
|
304
|
+
# genuinely dominated by >4096 higher-scoring OTHER repositories'
|
|
305
|
+
# chunks before this repository's own best candidates appear) could
|
|
306
|
+
# in principle see `_repo_filter` starved below `limit` results for
|
|
307
|
+
# that one repository -- the same class of approximation the
|
|
308
|
+
# existing "pull the whole corpus" heuristic already was, not a new
|
|
309
|
+
# kind of incompleteness, and unreachable within this milestone's
|
|
310
|
+
# own single-repository-per-tier benchmark. Revisit only if this is
|
|
311
|
+
# ever observed to actually matter for a real multi-tenant corpus.
|
|
312
|
+
SQLITE_VEC_MAX_K = 4096
|
|
313
|
+
num_results = min(top_k, len(self.doc_ids), SQLITE_VEC_MAX_K)
|
|
314
|
+
if num_results <= 0:
|
|
315
|
+
return []
|
|
316
|
+
|
|
317
|
+
query_blob = sqlite_vec.serialize_float32(q.tolist())
|
|
318
|
+
rows = self._conn.execute(
|
|
319
|
+
"SELECT rowid, distance FROM vec_items WHERE embedding MATCH ? AND k = ? ORDER BY distance",
|
|
320
|
+
(query_blob, num_results),
|
|
321
|
+
).fetchall()
|
|
322
|
+
|
|
323
|
+
results = []
|
|
324
|
+
for rowid, distance in rows:
|
|
325
|
+
# Phase 3.1: looked up via the explicit `_doc_by_rowid`
|
|
326
|
+
# mapping (see `__init__`'s comment) rather than the old
|
|
327
|
+
# `rowid - 1 == list index` arithmetic, which stopped being
|
|
328
|
+
# valid the moment deletion (`remove_docs`) became possible.
|
|
329
|
+
doc_id, doc_metadata = self._doc_by_rowid[rowid]
|
|
330
|
+
# sqlite-vec's own cosine `distance` is `1 - cosine_similarity`
|
|
331
|
+
# (verified experimentally) -- recovered here and then run
|
|
332
|
+
# through the EXACT same similarity/relevance/distance formulas
|
|
333
|
+
# the pre-Phase-2.2 NumPy implementation used, so only the
|
|
334
|
+
# source of the raw similarity number changed, not its meaning.
|
|
335
|
+
# `distance` comes back NULL for a genuinely all-zero (and thus
|
|
336
|
+
# un-normalizable) query vector -- verified experimentally, not
|
|
337
|
+
# assumed; treated as similarity 0.0 (a tie across every
|
|
338
|
+
# candidate), matching what the old np.dot-based implementation
|
|
339
|
+
# would already produce for the same degenerate input. A real
|
|
340
|
+
# embedding is never all-zero (CLAUDE.md §4's "no silent
|
|
341
|
+
# pseudo-embedding fallback" invariant, enforced in
|
|
342
|
+
# codepulse/embeddings/embedder.py), so this path is not
|
|
343
|
+
# expected to be exercised by real traffic.
|
|
344
|
+
sim = 0.0 if distance is None else (1.0 - distance)
|
|
345
|
+
sim = max(-1.0, min(1.0, sim))
|
|
346
|
+
distance_val = max(0.0, 1.0 - sim)
|
|
347
|
+
relevance = max(0.0, min(1.0, (sim + 1.0) / 2.0))
|
|
348
|
+
|
|
349
|
+
results.append({
|
|
350
|
+
"doc_id": doc_id,
|
|
351
|
+
"similarity": sim,
|
|
352
|
+
"relevance_score": relevance,
|
|
353
|
+
"embedding_distance": distance_val,
|
|
354
|
+
"metadata": doc_metadata,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
return results
|