seam-code 0.3.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.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
seam/query/semantic.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Semantic search read path — T5.
|
|
2
|
+
|
|
3
|
+
Leaf module: imports stdlib + seam.config + seam.analysis.embeddings.
|
|
4
|
+
numpy is imported LAZILY inside _semantic_candidates_impl only (never at module scope).
|
|
5
|
+
NO server/cli imports; NO direct fastembed import.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
rrf_merge(fts_ranked, semantic_ranked, k=60) -> list[int]
|
|
9
|
+
Pure Reciprocal Rank Fusion of two ranked id lists.
|
|
10
|
+
Fully unit-testable without a model.
|
|
11
|
+
|
|
12
|
+
cosine_sim(a_bytes, b_bytes) -> float
|
|
13
|
+
Cosine similarity between two float32 blobs.
|
|
14
|
+
Returns 0.0 on zero-norm vectors or dimension mismatch (safe fallback).
|
|
15
|
+
Pure-Python (struct.unpack) — importable even when numpy is absent.
|
|
16
|
+
|
|
17
|
+
semantic_candidates(conn, query, *, model, limit) -> list[tuple[int, float]]
|
|
18
|
+
Embed query via analysis.embeddings.embed_query, compute cosine similarity
|
|
19
|
+
against stored vectors, return top-k (symbol_id, score) pairs.
|
|
20
|
+
|
|
21
|
+
Returns [] if:
|
|
22
|
+
- fastembed unavailable
|
|
23
|
+
- embed_query returns b'' (model error)
|
|
24
|
+
- stored model != configured model (never silently mix models)
|
|
25
|
+
- no embeddings in DB
|
|
26
|
+
|
|
27
|
+
Design decisions:
|
|
28
|
+
- WHY numpy is lazy here (not at module scope): this module must be importable when
|
|
29
|
+
only the base install (no [semantic] extra) is present — numpy may be absent. numpy
|
|
30
|
+
is imported inside _semantic_candidates_impl, gated by is_available() being True
|
|
31
|
+
(which implies fastembed and thus numpy are present). A try/except ImportError falls
|
|
32
|
+
back to pure-Python cosine_sim so the module degrades safely even in pathological
|
|
33
|
+
environments where numpy is somehow missing despite fastembed being importable.
|
|
34
|
+
- Model-mismatch guard: the DB stores the model name per row. If no row matches the
|
|
35
|
+
requested model, we treat the entire store as incompatible and return []. This
|
|
36
|
+
prevents silently mixing vectors from different embedding spaces (different spaces
|
|
37
|
+
produce meaningless cosine scores without any visible error). The check runs before
|
|
38
|
+
any vector load or cosine computation.
|
|
39
|
+
- Brute-force cosine: O(n * dim). For 1k–20k symbols × 384 dim this is
|
|
40
|
+
~1–5ms with numpy — no ANN index needed at this scale. sqlite-vec is deferred.
|
|
41
|
+
- SEAM_SEMANTIC_SCAN_CAP bounds the SQL LIMIT so we never load more rows than
|
|
42
|
+
the configured cap (protects against unbounded memory use on very large indexes).
|
|
43
|
+
Logs at DEBUG when cap truncates the scan so operators can tune the knob.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import logging
|
|
47
|
+
import sqlite3
|
|
48
|
+
import struct
|
|
49
|
+
|
|
50
|
+
import seam.config as config
|
|
51
|
+
from seam.analysis.embeddings import embed_query, is_available
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── rrf_merge ─────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def rrf_merge(
|
|
60
|
+
fts_ranked: list[int],
|
|
61
|
+
semantic_ranked: list[int],
|
|
62
|
+
k: int = 60,
|
|
63
|
+
) -> list[int]:
|
|
64
|
+
"""Reciprocal Rank Fusion of two ranked id lists.
|
|
65
|
+
|
|
66
|
+
Combines a keyword-ranked list and a semantic-ranked list into a single
|
|
67
|
+
merged ranking using the RRF formula:
|
|
68
|
+
|
|
69
|
+
score(id) = Σ 1 / (k + rank_i(id))
|
|
70
|
+
|
|
71
|
+
where rank_i is the 1-based position of the id in list i (lists that do not
|
|
72
|
+
contain the id contribute 0). The merged list is sorted by score descending.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
fts_ranked: Symbol ids ranked by FTS5 relevance (best first).
|
|
76
|
+
semantic_ranked: Symbol ids ranked by cosine similarity (best first).
|
|
77
|
+
k: RRF smoothing constant. Default 60 (standard value from
|
|
78
|
+
the original Cormack et al. paper). Higher k flattens
|
|
79
|
+
rank differences; lower k amplifies them.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A deduplicated list of ids sorted by merged RRF score descending.
|
|
83
|
+
Ids that appear in both lists receive a higher score than ids in only one.
|
|
84
|
+
Stable: ties are broken by original list order (fts first, then semantic).
|
|
85
|
+
|
|
86
|
+
WHY RRF vs. score interpolation:
|
|
87
|
+
Score interpolation requires normalizing scores across heterogeneous systems
|
|
88
|
+
(BM25 vs cosine). RRF only needs rank positions, which are always comparable.
|
|
89
|
+
It consistently outperforms score interpolation in hybrid retrieval benchmarks
|
|
90
|
+
without any additional tuning. (Cormack, Clarke & Buettcher, SIGIR 2009.)
|
|
91
|
+
"""
|
|
92
|
+
if not fts_ranked and not semantic_ranked:
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# Build a score map: id → accumulated RRF score
|
|
96
|
+
scores: dict[int, float] = {}
|
|
97
|
+
|
|
98
|
+
# Add contributions from the FTS list (1-based rank)
|
|
99
|
+
for rank, doc_id in enumerate(fts_ranked, start=1):
|
|
100
|
+
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
|
|
101
|
+
|
|
102
|
+
# Add contributions from the semantic list
|
|
103
|
+
for rank, doc_id in enumerate(semantic_ranked, start=1):
|
|
104
|
+
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
|
|
105
|
+
|
|
106
|
+
# Sort by score descending; stable sort preserves insertion order for ties,
|
|
107
|
+
# which effectively prioritises fts_ranked for tie-breaking (fts ids were inserted first).
|
|
108
|
+
return sorted(scores, key=lambda doc_id: scores[doc_id], reverse=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── cosine_sim ────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cosine_sim(a_bytes: bytes, b_bytes: bytes) -> float:
|
|
115
|
+
"""Compute cosine similarity between two float32 byte blobs.
|
|
116
|
+
|
|
117
|
+
Uses struct.unpack (stdlib only — no numpy import here) to decode the vectors.
|
|
118
|
+
This keeps the module importable even when numpy is absent.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
float in [-1.0, 1.0]; 0.0 on any degenerate case (zero norm, dim mismatch,
|
|
122
|
+
empty input, corrupt bytes). Never raises.
|
|
123
|
+
|
|
124
|
+
WHY struct.unpack over numpy:
|
|
125
|
+
numpy is a fastembed transitive dep — when fastembed is absent, numpy may
|
|
126
|
+
also be absent. Using struct keeps this module dependency-free, ensuring
|
|
127
|
+
it is always importable without extras.
|
|
128
|
+
"""
|
|
129
|
+
if not a_bytes or not b_bytes:
|
|
130
|
+
return 0.0
|
|
131
|
+
|
|
132
|
+
n_a = len(a_bytes) // 4
|
|
133
|
+
n_b = len(b_bytes) // 4
|
|
134
|
+
|
|
135
|
+
if n_a != n_b or n_a == 0:
|
|
136
|
+
return 0.0
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
fmt = f"{n_a}f"
|
|
140
|
+
a = struct.unpack(fmt, a_bytes)
|
|
141
|
+
b = struct.unpack(fmt, b_bytes)
|
|
142
|
+
except struct.error:
|
|
143
|
+
# Corrupt bytes — degrade gracefully
|
|
144
|
+
return 0.0
|
|
145
|
+
|
|
146
|
+
dot = sum(ai * bi for ai, bi in zip(a, b))
|
|
147
|
+
norm_a = sum(ai * ai for ai in a) ** 0.5
|
|
148
|
+
norm_b = sum(bi * bi for bi in b) ** 0.5
|
|
149
|
+
|
|
150
|
+
if norm_a == 0.0 or norm_b == 0.0:
|
|
151
|
+
return 0.0
|
|
152
|
+
|
|
153
|
+
return dot / (norm_a * norm_b)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── semantic_candidates ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def semantic_candidates(
|
|
160
|
+
conn: sqlite3.Connection,
|
|
161
|
+
query: str,
|
|
162
|
+
*,
|
|
163
|
+
model: str,
|
|
164
|
+
limit: int,
|
|
165
|
+
) -> list[tuple[int, float]]:
|
|
166
|
+
"""Find the top-k symbol_ids by semantic similarity to the query.
|
|
167
|
+
|
|
168
|
+
Steps:
|
|
169
|
+
1. Check fastembed availability — return [] if absent.
|
|
170
|
+
2. Embed the query using analysis.embeddings.embed_query.
|
|
171
|
+
3. Verify DB has embeddings for the configured model (model-mismatch guard).
|
|
172
|
+
4. Load all stored vectors for the configured model.
|
|
173
|
+
5. Compute cosine similarity for each stored vector vs. the query vector.
|
|
174
|
+
6. Return the top-k (symbol_id, score) pairs sorted by score descending.
|
|
175
|
+
|
|
176
|
+
Returns [] if:
|
|
177
|
+
- fastembed is not available (graceful degradation)
|
|
178
|
+
- embed_query returns b'' (model load error)
|
|
179
|
+
- stored model != configured model (never silently mix model vectors)
|
|
180
|
+
- no embeddings present in the DB for the configured model
|
|
181
|
+
- any unexpected error occurs (logs a warning, never raises)
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
conn: Open SQLite connection with read access.
|
|
185
|
+
query: The search query string to embed.
|
|
186
|
+
model: FastEmbed model name (must match the model used at index time).
|
|
187
|
+
limit: Maximum number of (symbol_id, score) pairs to return.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
list[tuple[int, float]] — at most `limit` entries, sorted by score descending.
|
|
191
|
+
Each tuple is (symbol_id, cosine_score).
|
|
192
|
+
"""
|
|
193
|
+
# Step 1: Fast-path when fastembed absent
|
|
194
|
+
if not is_available():
|
|
195
|
+
logger.debug("semantic_candidates: fastembed unavailable — returning []")
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
return _semantic_candidates_impl(conn, query, model=model, limit=limit)
|
|
200
|
+
except Exception as exc: # noqa: BLE001
|
|
201
|
+
logger.warning(
|
|
202
|
+
"semantic_candidates: unexpected error for query %r (model=%r): %s: %s — "
|
|
203
|
+
"falling back to FTS5-only path.",
|
|
204
|
+
query,
|
|
205
|
+
model,
|
|
206
|
+
type(exc).__name__,
|
|
207
|
+
exc,
|
|
208
|
+
)
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _semantic_candidates_impl(
|
|
213
|
+
conn: sqlite3.Connection,
|
|
214
|
+
query: str,
|
|
215
|
+
*,
|
|
216
|
+
model: str,
|
|
217
|
+
limit: int,
|
|
218
|
+
) -> list[tuple[int, float]]:
|
|
219
|
+
"""Inner implementation — may raise; outer function catches everything.
|
|
220
|
+
|
|
221
|
+
WHY split: the outer guard converts any exception to [] and logs a warning,
|
|
222
|
+
keeping the read path non-crashing. Inner logic is cleaner without try/except.
|
|
223
|
+
|
|
224
|
+
Performance:
|
|
225
|
+
- SEAM_SEMANTIC_SCAN_CAP: SQL LIMIT caps how many rows are loaded.
|
|
226
|
+
- numpy fast path: when numpy is importable (it is a fastembed transitive dep,
|
|
227
|
+
so it is available whenever is_available() is True), all stored vectors are
|
|
228
|
+
decoded into a single (N, dim) float32 matrix and cosine is computed in one
|
|
229
|
+
matmul — ~1–5ms for 10k × 384-dim vs ~10–50ms for pure-Python per-row loop.
|
|
230
|
+
- Falls back to pure-Python cosine_sim if numpy import fails (defensive fallback).
|
|
231
|
+
"""
|
|
232
|
+
# Step 2: Embed the query
|
|
233
|
+
query_vec = embed_query(query, model)
|
|
234
|
+
if not query_vec:
|
|
235
|
+
logger.debug(
|
|
236
|
+
"semantic_candidates: embed_query returned empty bytes for %r — returning []",
|
|
237
|
+
query,
|
|
238
|
+
)
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
# Step 3: Model-mismatch guard.
|
|
242
|
+
# Check whether ANY embeddings row exists for the configured model.
|
|
243
|
+
# If only rows from a different model are present, return [] immediately — do not
|
|
244
|
+
# mix vectors from different embedding spaces (silently wrong scores).
|
|
245
|
+
model_check = conn.execute(
|
|
246
|
+
"SELECT COUNT(*) FROM embeddings WHERE model = ?", (model,)
|
|
247
|
+
).fetchone()[0]
|
|
248
|
+
|
|
249
|
+
if model_check == 0:
|
|
250
|
+
# No rows for this model. Could be: (a) no embeddings at all, (b) model mismatch.
|
|
251
|
+
total_embeddings = conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0]
|
|
252
|
+
if total_embeddings > 0:
|
|
253
|
+
# Model mismatch is a user misconfiguration — warn so it's visible.
|
|
254
|
+
logger.warning(
|
|
255
|
+
"semantic_candidates: model mismatch — DB has %d embedding(s) for a "
|
|
256
|
+
"different model; configured model=%r. "
|
|
257
|
+
"Run 'seam init --semantic' to rebuild embeddings with the current model.",
|
|
258
|
+
total_embeddings,
|
|
259
|
+
model,
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
logger.debug(
|
|
263
|
+
"semantic_candidates: no embeddings in DB for model=%r — returning [].",
|
|
264
|
+
model,
|
|
265
|
+
)
|
|
266
|
+
return []
|
|
267
|
+
|
|
268
|
+
# Step 4: Load stored vectors for this model, bounded by SEAM_SEMANTIC_SCAN_CAP.
|
|
269
|
+
# The LIMIT prevents loading an unbounded number of rows on very large indexes.
|
|
270
|
+
scan_cap = config.SEAM_SEMANTIC_SCAN_CAP
|
|
271
|
+
rows = conn.execute(
|
|
272
|
+
"SELECT symbol_id, vector FROM embeddings WHERE model = ? LIMIT ?",
|
|
273
|
+
(model, scan_cap),
|
|
274
|
+
).fetchall()
|
|
275
|
+
|
|
276
|
+
if not rows:
|
|
277
|
+
return []
|
|
278
|
+
|
|
279
|
+
# Log at DEBUG when the scan was capped (user might have more symbols than the cap).
|
|
280
|
+
actual_count = conn.execute(
|
|
281
|
+
"SELECT COUNT(*) FROM embeddings WHERE model = ?", (model,)
|
|
282
|
+
).fetchone()[0]
|
|
283
|
+
if actual_count > scan_cap:
|
|
284
|
+
logger.debug(
|
|
285
|
+
"semantic_candidates: scan capped at %d rows (index has %d for model=%r); "
|
|
286
|
+
"raise SEAM_SEMANTIC_SCAN_CAP to scan all vectors.",
|
|
287
|
+
scan_cap,
|
|
288
|
+
actual_count,
|
|
289
|
+
model,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Step 5: Compute cosine similarity — numpy fast path when available.
|
|
293
|
+
# numpy is a fastembed transitive dep: if is_available() is True, numpy is importable.
|
|
294
|
+
# The try/except ImportError is a defensive fallback for pathological environments.
|
|
295
|
+
try:
|
|
296
|
+
import numpy as np
|
|
297
|
+
|
|
298
|
+
# Decode query vector: (dim,) float32
|
|
299
|
+
q_arr = np.frombuffer(query_vec, dtype=np.float32) # shape (dim,)
|
|
300
|
+
|
|
301
|
+
# Build (N, dim) matrix from all stored blobs in one pass.
|
|
302
|
+
mat = np.stack(
|
|
303
|
+
[np.frombuffer(bytes(row["vector"]), dtype=np.float32) for row in rows]
|
|
304
|
+
) # shape (N, dim)
|
|
305
|
+
|
|
306
|
+
sym_ids = [row["symbol_id"] for row in rows]
|
|
307
|
+
|
|
308
|
+
# Cosine similarity: (mat @ q) / (||mat|| * ||q||) per row.
|
|
309
|
+
# dot: shape (N,)
|
|
310
|
+
dots = mat @ q_arr
|
|
311
|
+
norms_mat = np.linalg.norm(mat, axis=1) # (N,)
|
|
312
|
+
norm_q = float(np.linalg.norm(q_arr))
|
|
313
|
+
|
|
314
|
+
if norm_q == 0.0:
|
|
315
|
+
return []
|
|
316
|
+
|
|
317
|
+
# Zero-norm rows get cosine=0 (safe: norms_mat == 0 → division → 0 via clip).
|
|
318
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
319
|
+
cosines = np.where(norms_mat == 0.0, 0.0, dots / (norms_mat * norm_q))
|
|
320
|
+
|
|
321
|
+
# Pair (symbol_id, score) and get top-k via argsort.
|
|
322
|
+
top_k_indices = np.argsort(-cosines)[:limit]
|
|
323
|
+
return [(int(sym_ids[i]), float(cosines[i])) for i in top_k_indices]
|
|
324
|
+
|
|
325
|
+
except ImportError:
|
|
326
|
+
# numpy absent (defensive fallback — should not happen when is_available() is True).
|
|
327
|
+
logger.debug(
|
|
328
|
+
"semantic_candidates: numpy not available — falling back to pure-Python cosine."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Pure-Python fallback (slower but correct; keeps the function working without numpy).
|
|
332
|
+
scored: list[tuple[int, float]] = []
|
|
333
|
+
for row in rows:
|
|
334
|
+
score = cosine_sim(query_vec, bytes(row["vector"]))
|
|
335
|
+
scored.append((row["symbol_id"], score))
|
|
336
|
+
|
|
337
|
+
# Step 6: Sort by score descending, return top-k
|
|
338
|
+
scored.sort(key=lambda t: t[1], reverse=True)
|
|
339
|
+
return scored[:limit]
|