@wrongstack/vector-memory 0.320.1 → 1.0.2
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.
- package/dist/index.js +32 -22
- package/dist/sage-fusion.d.ts +18 -3
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -249,6 +249,11 @@ function encodeVector(vec) {
|
|
|
249
249
|
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
250
250
|
}
|
|
251
251
|
function decodeVector(buf) {
|
|
252
|
+
if (buf.byteLength % 4 !== 0) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`decodeVector: invalid vector byteLength ${buf.byteLength} (must be a multiple of 4)`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
252
257
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
253
258
|
const copy = new Float32Array(buf.byteLength / 4);
|
|
254
259
|
for (let i = 0; i < copy.length; i++) {
|
|
@@ -264,6 +269,12 @@ import * as path from "node:path";
|
|
|
264
269
|
import { withFileLock } from "@wrongstack/core/utils";
|
|
265
270
|
import { loadRuntimeDatabaseSync } from "@wrongstack/persistence";
|
|
266
271
|
import { cosineSimilarity, HashingEmbeddingProvider } from "@wrongstack/sage";
|
|
272
|
+
var DEFAULT_SEARCH_LIMIT = 10;
|
|
273
|
+
function normalizeLimit(limit) {
|
|
274
|
+
if (limit === void 0 || !Number.isFinite(limit)) return DEFAULT_SEARCH_LIMIT;
|
|
275
|
+
const floored = Math.floor(limit);
|
|
276
|
+
return floored < 1 ? DEFAULT_SEARCH_LIMIT : floored;
|
|
277
|
+
}
|
|
267
278
|
var DEFAULT_DIRECTORY = ".wrongstack/vector-memory";
|
|
268
279
|
var DEFAULT_FILENAME = "vector-memory.db";
|
|
269
280
|
var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
|
|
@@ -525,7 +536,7 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
525
536
|
}
|
|
526
537
|
async search(query, opts = {}) {
|
|
527
538
|
this.assertOpen();
|
|
528
|
-
const limit = opts.limit
|
|
539
|
+
const limit = normalizeLimit(opts.limit);
|
|
529
540
|
const threshold = opts.threshold ?? 0;
|
|
530
541
|
const includeVectors = opts.includeVectors === true;
|
|
531
542
|
if (typeof query !== "string" || query.trim().length === 0) return [];
|
|
@@ -554,7 +565,7 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
554
565
|
const vec = decodeVector(row.vec_blob);
|
|
555
566
|
const raw = cosineSimilarity(queryVec, vec);
|
|
556
567
|
const score = Math.max(0, Math.min(1, raw));
|
|
557
|
-
if (score < threshold) continue;
|
|
568
|
+
if (!Number.isFinite(score) || score < threshold) continue;
|
|
558
569
|
if (top.length >= limit && score <= (top[top.length - 1]?.score ?? 0)) continue;
|
|
559
570
|
let at = top.length;
|
|
560
571
|
while (at > 0 && (top[at - 1]?.score ?? 0) < score) at--;
|
|
@@ -1214,30 +1225,26 @@ async function fuseWithVectorMemory(query, lexical, options = {}) {
|
|
|
1214
1225
|
}
|
|
1215
1226
|
const sageById = /* @__PURE__ */ new Map();
|
|
1216
1227
|
for (const memory of lexical) sageById.set(memory.id, memory);
|
|
1217
|
-
for (const hit of vectorHits) {
|
|
1218
|
-
const sageId = hit.entry.metadata?.["sageId"];
|
|
1219
|
-
if (typeof sageId === "string" && !sageById.has(sageId)) {
|
|
1220
|
-
continue;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
1228
|
const lexicalRanked = lexical.map((memory, index) => ({
|
|
1224
1229
|
memory,
|
|
1225
1230
|
rankScore: lexicalRankScore(index, lexical.length),
|
|
1226
1231
|
lexicalScore: lexicalRankScore(index, lexical.length)
|
|
1227
1232
|
}));
|
|
1228
1233
|
const vectorRanked = [];
|
|
1234
|
+
const seenVectorSageIds = /* @__PURE__ */ new Set();
|
|
1229
1235
|
for (let i = 0; i < vectorHits.length; i++) {
|
|
1230
1236
|
const hit = vectorHits[i];
|
|
1231
1237
|
const sageId = hit.entry.metadata?.["sageId"];
|
|
1232
|
-
if (typeof sageId !== "string") continue;
|
|
1238
|
+
if (typeof sageId !== "string" || seenVectorSageIds.has(sageId)) continue;
|
|
1233
1239
|
const memory = sageById.get(sageId);
|
|
1234
1240
|
if (!memory) continue;
|
|
1241
|
+
seenVectorSageIds.add(sageId);
|
|
1235
1242
|
vectorRanked.push({ memory, vectorScore: hit.score });
|
|
1236
1243
|
}
|
|
1237
1244
|
const fused = /* @__PURE__ */ new Map();
|
|
1238
1245
|
for (let i = 0; i < lexicalRanked.length; i++) {
|
|
1239
1246
|
const c = lexicalRanked[i];
|
|
1240
|
-
const rrf =
|
|
1247
|
+
const rrf = (1 - weight) * (1 / (k + i + 1));
|
|
1241
1248
|
fused.set(c.memory.id, {
|
|
1242
1249
|
memory: c.memory,
|
|
1243
1250
|
vectorScore: null,
|
|
@@ -1581,12 +1588,16 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
|
|
|
1581
1588
|
preview: previewText(mem.text, 140)
|
|
1582
1589
|
});
|
|
1583
1590
|
}
|
|
1591
|
+
const overlapById = /* @__PURE__ */ new Map();
|
|
1592
|
+
for (const row of overlap) overlapById.set(row.id, row);
|
|
1584
1593
|
for (const hit of vectorHits) {
|
|
1585
1594
|
const sageId = hit.entry.metadata?.["sageId"];
|
|
1586
1595
|
if (typeof sageId !== "string") continue;
|
|
1587
|
-
const existing =
|
|
1596
|
+
const existing = overlapById.get(sageId);
|
|
1588
1597
|
if (existing) {
|
|
1589
|
-
existing.vectorScore
|
|
1598
|
+
if (existing.vectorScore === null) {
|
|
1599
|
+
existing.vectorScore = hit.score;
|
|
1600
|
+
}
|
|
1590
1601
|
continue;
|
|
1591
1602
|
}
|
|
1592
1603
|
if (seenIds.has(sageId)) continue;
|
|
@@ -1598,8 +1609,8 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
|
|
|
1598
1609
|
preview: previewText(hit.entry.text, 140)
|
|
1599
1610
|
});
|
|
1600
1611
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1612
|
+
const finalOverlap = [];
|
|
1613
|
+
for (const row of overlap) {
|
|
1603
1614
|
if (row.vectorScore === null) {
|
|
1604
1615
|
lexicalOnly.push({
|
|
1605
1616
|
id: row.id,
|
|
@@ -1607,26 +1618,25 @@ async function runSearchRace(query, lexical, vectorStore, options = {}) {
|
|
|
1607
1618
|
vectorScore: null,
|
|
1608
1619
|
preview: row.preview
|
|
1609
1620
|
});
|
|
1610
|
-
|
|
1621
|
+
} else {
|
|
1622
|
+
finalOverlap.push(row);
|
|
1611
1623
|
}
|
|
1612
1624
|
}
|
|
1613
|
-
const lexicalCount = lexicalOnly.length +
|
|
1614
|
-
const vectorCount = vectorOnly.length +
|
|
1625
|
+
const lexicalCount = lexicalOnly.length + finalOverlap.length;
|
|
1626
|
+
const vectorCount = vectorOnly.length + finalOverlap.length;
|
|
1615
1627
|
const denom = Math.max(lexicalCount, vectorCount, 1);
|
|
1616
1628
|
return {
|
|
1617
1629
|
query,
|
|
1618
1630
|
lexicalOnly,
|
|
1619
1631
|
vectorOnly,
|
|
1620
|
-
|
|
1621
|
-
// so every remaining row holds a real number here.
|
|
1622
|
-
overlap,
|
|
1632
|
+
overlap: finalOverlap,
|
|
1623
1633
|
metrics: {
|
|
1624
1634
|
lexicalCount,
|
|
1625
1635
|
vectorCount,
|
|
1626
|
-
overlapCount:
|
|
1636
|
+
overlapCount: finalOverlap.length,
|
|
1627
1637
|
lexicalOnlyRatio: lexicalCount === 0 ? 0 : lexicalOnly.length / lexicalCount,
|
|
1628
1638
|
vectorOnlyRatio: vectorCount === 0 ? 0 : vectorOnly.length / vectorCount,
|
|
1629
|
-
agreementRatio:
|
|
1639
|
+
agreementRatio: finalOverlap.length / denom
|
|
1630
1640
|
}
|
|
1631
1641
|
};
|
|
1632
1642
|
}
|
package/dist/sage-fusion.d.ts
CHANGED
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The fusion is a Reciprocal Rank Fusion (RRF)-style blend rather than a
|
|
8
8
|
* raw cosine-blend, so it stays robust when one side returns nothing
|
|
9
|
-
* (e.g. embedding provider unavailable).
|
|
10
|
-
*
|
|
9
|
+
* (e.g. embedding provider unavailable).
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT — the vector channel can only RE-RANK the lexical list, never
|
|
12
|
+
* extend it. A vector hit is kept only when its `metadata.sageId` resolves to
|
|
13
|
+
* a memory already present in `lexical`, because the caller's lexical list is
|
|
14
|
+
* the authoritative source of `Sage` objects and this function will not
|
|
15
|
+
* fabricate one (pinned by tests/sage-fusion.test.ts). Therefore:
|
|
16
|
+
* - `lexical` may be `[]` → the result is `[]`, whatever the vector channel
|
|
17
|
+
* found. This is NOT a fallback to pure vector ranking.
|
|
11
18
|
* - `vector` may be `[]` → fusion falls back to pure lexical ranking.
|
|
12
19
|
* - either may be `undefined` → fusion silently drops that channel.
|
|
13
20
|
*
|
|
@@ -40,6 +47,10 @@ export interface SageFusionOptions {
|
|
|
40
47
|
/**
|
|
41
48
|
* Cosine threshold below which a vector-only hit is dropped (no lexical
|
|
42
49
|
* counterpart to lift it). Default 0 — keep all, let RRF decide.
|
|
50
|
+
*
|
|
51
|
+
* Currently inert: vector-only hits cannot occur (see the module docstring),
|
|
52
|
+
* so nothing ever reaches this threshold. Kept for API stability and for
|
|
53
|
+
* the day the vector channel is allowed to extend the candidate set.
|
|
43
54
|
*/
|
|
44
55
|
vectorOnlyThreshold?: number | undefined;
|
|
45
56
|
}
|
|
@@ -51,7 +62,11 @@ export interface SageFusionHit {
|
|
|
51
62
|
lexicalScore: number | null;
|
|
52
63
|
/** 0..1, monotonically higher = better. */
|
|
53
64
|
finalScore: number;
|
|
54
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Where the candidate came from. `'vector'` is currently unreachable —
|
|
67
|
+
* every candidate originates in the lexical list (see the module
|
|
68
|
+
* docstring); the vector channel only upgrades one to `'both'`.
|
|
69
|
+
*/
|
|
55
70
|
source: 'lexical' | 'vector' | 'both';
|
|
56
71
|
}
|
|
57
72
|
declare function lexicalRankScore(index: number, total: number): number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/vector-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "WrongStack Vector Memory — an additional vector-search memory store powered by @huggingface/transformers (local ONNX embeddings), alongside the SAGE lexical memory system.",
|
|
6
6
|
"repository": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@wrongstack/core": "0.
|
|
31
|
-
"@wrongstack/persistence": "0.
|
|
32
|
-
"@wrongstack/sage": "0.
|
|
30
|
+
"@wrongstack/core": "1.0.2",
|
|
31
|
+
"@wrongstack/persistence": "1.0.2",
|
|
32
|
+
"@wrongstack/sage": "1.0.2"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
35
|
"@huggingface/transformers": "^4.2.0"
|