@oh-my-pi/pi-mnemopi 17.0.7 → 17.0.8
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/CHANGELOG.md +6 -0
- package/package.json +5 -4
- package/src/core/mmr.ts +30 -0
- package/src/core/shmr.ts +18 -11
- package/src/core/vector-index.ts +12 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.8] - 2026-07-22
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Optimized vector operations (exact vector-index search, SHMR similarity clustering, and default-similarity MMR rerank) by migrating hot loops to native batch kernels, resulting in significant performance improvements (up to 1.8x faster top-K search, 2.4x faster pairwise clustering, and 22-36x faster MMR reranking).
|
|
10
|
+
|
|
5
11
|
## [17.0.4] - 2026-07-18
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-mnemopi",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.8",
|
|
5
5
|
"description": "Local SQLite memory engine for Oh My Pi agents",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
"fmt": "biome format --write ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
43
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
44
|
-
"@oh-my-pi/pi-
|
|
42
|
+
"@oh-my-pi/pi-ai": "17.0.8",
|
|
43
|
+
"@oh-my-pi/pi-catalog": "17.0.8",
|
|
44
|
+
"@oh-my-pi/pi-natives": "17.0.8",
|
|
45
|
+
"@oh-my-pi/pi-utils": "17.0.8",
|
|
45
46
|
"lru-cache": "11.5.2"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
package/src/core/mmr.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { mmrRerankIndices } from "@oh-my-pi/pi-natives";
|
|
2
|
+
|
|
1
3
|
export interface MmrResult {
|
|
2
4
|
readonly content?: string;
|
|
3
5
|
readonly score?: number;
|
|
@@ -33,6 +35,34 @@ export function mmrRerank<T extends MmrResult>(
|
|
|
33
35
|
const first = sortedResults[0];
|
|
34
36
|
if (first === undefined) return [];
|
|
35
37
|
|
|
38
|
+
// Native batch kernel: one N-API crossing selects all indices. Only valid
|
|
39
|
+
// for the default Jaccard similarity; custom similarity functions stay in
|
|
40
|
+
// TS. Lone-surrogate contents also stay in TS: N-API converts them to
|
|
41
|
+
// U+FFFD, which would merge distinct tokens. NaN topK stays in TS so the
|
|
42
|
+
// pre-native contract (loop guard is false, first result still returned)
|
|
43
|
+
// is preserved.
|
|
44
|
+
if (
|
|
45
|
+
similarityFn === jaccardSimilarity &&
|
|
46
|
+
!Number.isNaN(limit) &&
|
|
47
|
+
sortedResults.every(result => (result.content ?? "").isWellFormed())
|
|
48
|
+
) {
|
|
49
|
+
// Pre-lowercase with JS semantics so contextual mappings (Final_Sigma:
|
|
50
|
+
// "ΟΣ" -> "ος") are applied before the context-insensitive native
|
|
51
|
+
// lowercase, which is idempotent on already-lowercased text.
|
|
52
|
+
const contents = sortedResults.map(result => (result.content ?? "").toLowerCase());
|
|
53
|
+
const scores = Float64Array.from(sortedResults, result => result.score ?? 0);
|
|
54
|
+
// Clamp before the u32 N-API boundary: Infinity or >= 2**32 would
|
|
55
|
+
// otherwise wrap (ToUint32) and silently return nothing.
|
|
56
|
+
const nativeLimit = Math.min(limit, sortedResults.length);
|
|
57
|
+
const picked = mmrRerankIndices(contents, scores, lambdaParam, nativeLimit);
|
|
58
|
+
const out: T[] = [];
|
|
59
|
+
for (const index of picked) {
|
|
60
|
+
const item = sortedResults[index];
|
|
61
|
+
if (item !== undefined) out.push(item);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
const selected: T[] = [first];
|
|
37
67
|
const remaining = sortedResults.slice(1);
|
|
38
68
|
|
package/src/core/shmr.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { cosineSimilarityPairs } from "@oh-my-pi/pi-natives";
|
|
3
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import * as embeddings from "./embeddings";
|
|
5
6
|
import { cosineSimilarity } from "./vector-math";
|
|
@@ -166,17 +167,23 @@ export async function clusterBySimilarity(items: readonly ShmrItem[], threshold:
|
|
|
166
167
|
if (items.length === 0) return [];
|
|
167
168
|
const vectors = await resolveItemVectors(items);
|
|
168
169
|
const adjacency: number[][] = Array.from({ length: items.length }, () => []);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
170
|
+
// Native batch kernel: one N-API crossing evaluates all O(n^2) pairs.
|
|
171
|
+
// Vectors are zero-padded to a shared dim, matching the TS `?? 0`
|
|
172
|
+
// missing-element semantics of cosineSimilarity.
|
|
173
|
+
let dim = 0;
|
|
174
|
+
for (const vector of vectors) if (vector.length > dim) dim = vector.length;
|
|
175
|
+
const flat = new Float64Array(vectors.length * dim);
|
|
176
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
177
|
+
const vector = vectors[i];
|
|
178
|
+
if (vector === undefined) continue;
|
|
179
|
+
for (let col = 0; col < vector.length; col++) flat[i * dim + col] = vector[col] ?? 0;
|
|
180
|
+
}
|
|
181
|
+
const pairs = cosineSimilarityPairs(flat, vectors.length, dim, threshold);
|
|
182
|
+
for (let p = 0; p < pairs.length; p += 2) {
|
|
183
|
+
const i = pairs[p] ?? 0;
|
|
184
|
+
const j = pairs[p + 1] ?? 0;
|
|
185
|
+
adjacency[i]?.push(j);
|
|
186
|
+
adjacency[j]?.push(i);
|
|
180
187
|
}
|
|
181
188
|
const visited = new Set<number>();
|
|
182
189
|
const clusters: ShmrItem[][] = [];
|
package/src/core/vector-index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { vectorIndexTopK } from "@oh-my-pi/pi-natives";
|
|
2
|
+
|
|
1
3
|
export interface ExactVectorSearchHit<TId> {
|
|
2
4
|
id: TId;
|
|
3
5
|
score: number;
|
|
@@ -66,19 +68,16 @@ export function searchExactVectorIndex<TId>(
|
|
|
66
68
|
queryNormSq += value * value;
|
|
67
69
|
}
|
|
68
70
|
if (queryNormSq <= 0) return [];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
// Native batch kernel: one N-API crossing scores every row and ranks the
|
|
72
|
+
// top k with the same stable ordering as the TS sort. TS guards above
|
|
73
|
+
// (finite query, positive norm, non-empty index) are preserved. Clamp k to
|
|
74
|
+
// the row count before the u32 boundary: Infinity or >= 2**32 would
|
|
75
|
+
// otherwise wrap (ToUint32) and return no hits.
|
|
76
|
+
const topK = vectorIndexTopK(index.matrix, index.dimensions, Float64Array.from(query), Math.min(k, index.count));
|
|
71
77
|
const hits: ExactVectorSearchHit<TId>[] = [];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let score = 0;
|
|
76
|
-
for (let col = 0; col < queryDimensions; col += 1) {
|
|
77
|
-
score += index.matrix[offset + col] * ((query[col] ?? 0) / queryNorm);
|
|
78
|
-
}
|
|
79
|
-
hits.push({ id: index.ids[row] as TId, score });
|
|
78
|
+
for (let i = 0; i < topK.indices.length; i += 1) {
|
|
79
|
+
const row = topK.indices[i] ?? 0;
|
|
80
|
+
hits.push({ id: index.ids[row] as TId, score: topK.scores[i] ?? 0 });
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
hits.sort((a, b) => b.score - a.score);
|
|
83
|
-
return hits.slice(0, Math.min(k, hits.length));
|
|
82
|
+
return hits;
|
|
84
83
|
}
|