memory-eternal 0.9.14 → 0.9.15
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/.claude-plugin/plugin.json +34 -34
- package/.codex-plugin/plugin.json +29 -29
- package/.cursor-plugin/plugin.json +28 -28
- package/README.en.md +365 -365
- package/README.md +389 -389
- package/lib/client.js +17 -17
- package/lib/minhash.js +99 -99
- package/lib/vault.js +941 -941
- package/package.json +131 -131
- package/web/app.js +19 -19
package/lib/minhash.js
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
// MinHash + LSH:相似卡候选生成(把 O(n²) 精确比对换成「签名分桶 → 少量精确校验」)。
|
|
2
|
-
//
|
|
3
|
-
// 用法:先给每张卡的 bigram 哈希集合算一个 k 维最小哈希签名;再按 band 分桶,
|
|
4
|
-
// 同一 band 撞在一起的卡互为候选;最后只对候选做**精确** Jaccard(保证判定语义不变)。
|
|
5
|
-
// 纯函数,方便单测。
|
|
6
|
-
|
|
7
|
-
const M1 = 0x85ebca6b
|
|
8
|
-
const M2 = 0xc2b2ae35
|
|
9
|
-
|
|
10
|
-
/** 32 位整数混合(murmur3 finalizer 变体)—— 用作第 seed 号哈希函数。 */
|
|
11
|
-
export function mix32(x, seed = 0) {
|
|
12
|
-
let h = (x ^ Math.imul(seed + 1, 0x9e3779b1)) >>> 0
|
|
13
|
-
h ^= h >>> 16
|
|
14
|
-
h = Math.imul(h, M1) >>> 0
|
|
15
|
-
h ^= h >>> 13
|
|
16
|
-
h = Math.imul(h, M2) >>> 0
|
|
17
|
-
h ^= h >>> 16
|
|
18
|
-
return h >>> 0
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* k 维最小哈希签名。
|
|
23
|
-
* @param {Iterable<number>} grams 哈希后的 bigram 集合
|
|
24
|
-
* @param {number} k 签名长度
|
|
25
|
-
* @returns {Uint32Array}
|
|
26
|
-
*/
|
|
27
|
-
export function minhashSignature(grams, k = 64) {
|
|
28
|
-
const sig = new Uint32Array(k).fill(0xffffffff)
|
|
29
|
-
for (const g of grams) {
|
|
30
|
-
for (let i = 0; i < k; i++) {
|
|
31
|
-
const h = mix32(g, i)
|
|
32
|
-
if (h < sig[i]) sig[i] = h
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return sig
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** 两个签名的一致位比例 = Jaccard 的无偏估计。 */
|
|
39
|
-
export function jaccardEstimate(a, b) {
|
|
40
|
-
if (!a || !b || a.length === 0 || a.length !== b.length) return 0
|
|
41
|
-
let same = 0
|
|
42
|
-
for (let i = 0; i < a.length; i++) if (a[i] === b[i]) same++
|
|
43
|
-
return same / a.length
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* LSH 分桶:把签名切成 bands 段,每段 rows 行,拼成桶 key。
|
|
48
|
-
* @returns {string[]} 长度 = bands(每段一个 key)
|
|
49
|
-
*/
|
|
50
|
-
export function lshBandKeys(sig, bands = 32, rows = 2) {
|
|
51
|
-
const keys = []
|
|
52
|
-
for (let b = 0; b < bands; b++) {
|
|
53
|
-
let key = ''
|
|
54
|
-
for (let r = 0; r < rows; r++) key += sig[b * rows + r].toString(36) + '.'
|
|
55
|
-
keys.push(key)
|
|
56
|
-
}
|
|
57
|
-
return keys
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 生成候选:同一 band 撞在一起的卡互为候选,按「撞上的 band 数」降序截断。
|
|
62
|
-
* @param {Map<string, Uint32Array>} signatures id -> 签名
|
|
63
|
-
* @param {{bands?:number, rows?:number, maxPerCard?:number, minHits?:number}} [opts]
|
|
64
|
-
* @returns {Map<string, string[]>} id -> 候选 id 列表(已按命中数排序、已截断)
|
|
65
|
-
*/
|
|
66
|
-
export function lshCandidates(signatures, opts = {}) {
|
|
67
|
-
const bands = opts.bands || 32
|
|
68
|
-
const rows = opts.rows || 2
|
|
69
|
-
const maxPerCard = opts.maxPerCard || 40
|
|
70
|
-
const minHits = opts.minHits || 1
|
|
71
|
-
const hits = new Map() // id -> Map(otherId -> count)
|
|
72
|
-
const buckets = new Map() // bandKey -> id[]
|
|
73
|
-
for (const [id, sig] of signatures) {
|
|
74
|
-
const keys = lshBandKeys(sig, bands, rows)
|
|
75
|
-
for (const key of keys) {
|
|
76
|
-
const arr = buckets.get(key)
|
|
77
|
-
if (arr) arr.push(id)
|
|
78
|
-
else buckets.set(key, [id])
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
for (const arr of buckets.values()) {
|
|
82
|
-
if (arr.length < 2 || arr.length > 400) continue // 超大桶基本是「全库同质」,跳过以免退化
|
|
83
|
-
for (let i = 0; i < arr.length; i++) {
|
|
84
|
-
for (let j = i + 1; j < arr.length; j++) {
|
|
85
|
-
const a = arr[i], b = arr[j]
|
|
86
|
-
let ma = hits.get(a); if (!ma) { ma = new Map(); hits.set(a, ma) }
|
|
87
|
-
ma.set(b, (ma.get(b) || 0) + 1)
|
|
88
|
-
let mb = hits.get(b); if (!mb) { mb = new Map(); hits.set(b, mb) }
|
|
89
|
-
mb.set(a, (mb.get(a) || 0) + 1)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const out = new Map()
|
|
94
|
-
for (const [id, map] of hits) {
|
|
95
|
-
const ranked = [...map.entries()].filter(([, n]) => n >= minHits).sort((x, y) => y[1] - x[1])
|
|
96
|
-
out.set(id, (ranked.length <= maxPerCard ? ranked : ranked.slice(0, maxPerCard)).map(([other]) => other))
|
|
97
|
-
}
|
|
98
|
-
return out
|
|
99
|
-
}
|
|
1
|
+
// MinHash + LSH:相似卡候选生成(把 O(n²) 精确比对换成「签名分桶 → 少量精确校验」)。
|
|
2
|
+
//
|
|
3
|
+
// 用法:先给每张卡的 bigram 哈希集合算一个 k 维最小哈希签名;再按 band 分桶,
|
|
4
|
+
// 同一 band 撞在一起的卡互为候选;最后只对候选做**精确** Jaccard(保证判定语义不变)。
|
|
5
|
+
// 纯函数,方便单测。
|
|
6
|
+
|
|
7
|
+
const M1 = 0x85ebca6b
|
|
8
|
+
const M2 = 0xc2b2ae35
|
|
9
|
+
|
|
10
|
+
/** 32 位整数混合(murmur3 finalizer 变体)—— 用作第 seed 号哈希函数。 */
|
|
11
|
+
export function mix32(x, seed = 0) {
|
|
12
|
+
let h = (x ^ Math.imul(seed + 1, 0x9e3779b1)) >>> 0
|
|
13
|
+
h ^= h >>> 16
|
|
14
|
+
h = Math.imul(h, M1) >>> 0
|
|
15
|
+
h ^= h >>> 13
|
|
16
|
+
h = Math.imul(h, M2) >>> 0
|
|
17
|
+
h ^= h >>> 16
|
|
18
|
+
return h >>> 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* k 维最小哈希签名。
|
|
23
|
+
* @param {Iterable<number>} grams 哈希后的 bigram 集合
|
|
24
|
+
* @param {number} k 签名长度
|
|
25
|
+
* @returns {Uint32Array}
|
|
26
|
+
*/
|
|
27
|
+
export function minhashSignature(grams, k = 64) {
|
|
28
|
+
const sig = new Uint32Array(k).fill(0xffffffff)
|
|
29
|
+
for (const g of grams) {
|
|
30
|
+
for (let i = 0; i < k; i++) {
|
|
31
|
+
const h = mix32(g, i)
|
|
32
|
+
if (h < sig[i]) sig[i] = h
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return sig
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 两个签名的一致位比例 = Jaccard 的无偏估计。 */
|
|
39
|
+
export function jaccardEstimate(a, b) {
|
|
40
|
+
if (!a || !b || a.length === 0 || a.length !== b.length) return 0
|
|
41
|
+
let same = 0
|
|
42
|
+
for (let i = 0; i < a.length; i++) if (a[i] === b[i]) same++
|
|
43
|
+
return same / a.length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* LSH 分桶:把签名切成 bands 段,每段 rows 行,拼成桶 key。
|
|
48
|
+
* @returns {string[]} 长度 = bands(每段一个 key)
|
|
49
|
+
*/
|
|
50
|
+
export function lshBandKeys(sig, bands = 32, rows = 2) {
|
|
51
|
+
const keys = []
|
|
52
|
+
for (let b = 0; b < bands; b++) {
|
|
53
|
+
let key = ''
|
|
54
|
+
for (let r = 0; r < rows; r++) key += sig[b * rows + r].toString(36) + '.'
|
|
55
|
+
keys.push(key)
|
|
56
|
+
}
|
|
57
|
+
return keys
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 生成候选:同一 band 撞在一起的卡互为候选,按「撞上的 band 数」降序截断。
|
|
62
|
+
* @param {Map<string, Uint32Array>} signatures id -> 签名
|
|
63
|
+
* @param {{bands?:number, rows?:number, maxPerCard?:number, minHits?:number}} [opts]
|
|
64
|
+
* @returns {Map<string, string[]>} id -> 候选 id 列表(已按命中数排序、已截断)
|
|
65
|
+
*/
|
|
66
|
+
export function lshCandidates(signatures, opts = {}) {
|
|
67
|
+
const bands = opts.bands || 32
|
|
68
|
+
const rows = opts.rows || 2
|
|
69
|
+
const maxPerCard = opts.maxPerCard || 40
|
|
70
|
+
const minHits = opts.minHits || 1
|
|
71
|
+
const hits = new Map() // id -> Map(otherId -> count)
|
|
72
|
+
const buckets = new Map() // bandKey -> id[]
|
|
73
|
+
for (const [id, sig] of signatures) {
|
|
74
|
+
const keys = lshBandKeys(sig, bands, rows)
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const arr = buckets.get(key)
|
|
77
|
+
if (arr) arr.push(id)
|
|
78
|
+
else buckets.set(key, [id])
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const arr of buckets.values()) {
|
|
82
|
+
if (arr.length < 2 || arr.length > 400) continue // 超大桶基本是「全库同质」,跳过以免退化
|
|
83
|
+
for (let i = 0; i < arr.length; i++) {
|
|
84
|
+
for (let j = i + 1; j < arr.length; j++) {
|
|
85
|
+
const a = arr[i], b = arr[j]
|
|
86
|
+
let ma = hits.get(a); if (!ma) { ma = new Map(); hits.set(a, ma) }
|
|
87
|
+
ma.set(b, (ma.get(b) || 0) + 1)
|
|
88
|
+
let mb = hits.get(b); if (!mb) { mb = new Map(); hits.set(b, mb) }
|
|
89
|
+
mb.set(a, (mb.get(a) || 0) + 1)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const out = new Map()
|
|
94
|
+
for (const [id, map] of hits) {
|
|
95
|
+
const ranked = [...map.entries()].filter(([, n]) => n >= minHits).sort((x, y) => y[1] - x[1])
|
|
96
|
+
out.set(id, (ranked.length <= maxPerCard ? ranked : ranked.slice(0, maxPerCard)).map(([other]) => other))
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|