forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathembedVector.test.ts
More file actions
194 lines (172 loc) · 7.74 KB
/
Copy pathembedVector.test.ts
File metadata and controls
194 lines (172 loc) · 7.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { describe, expect, it } from 'vitest'
import {
contentHash,
dot,
formatForEmbedding,
l2Normalize,
scanTopKBySimilarity,
EMBED_DIM,
type VectorRow
} from './embedVector'
import { bufferToVector, vectorToBuffer } from '../ipc/taskEmbeddingVector'
// Port of macOS OCREmbeddingService.formatForEmbedding (OCREmbeddingService.swift:43-50).
// The app name and window title carry retrieval signal the OCR text often does not:
// "that mockup in Figma", "the Slack thread about billing".
describe('formatForEmbedding', () => {
it('prepends the app and window title, exactly as macOS composes it', () => {
expect(formatForEmbedding('the deck', 'Figma', 'Q3 mockups')).toBe(
'[Figma] Q3 mockups\nthe deck'
)
})
it('omits the title when there is none (macOS: "[app]\\n<ocr>")', () => {
expect(formatForEmbedding('the deck', 'Figma', '')).toBe('[Figma]\nthe deck')
})
it('emits no empty brackets when the app name is unknown', () => {
// macOS always has an app name and would emit a bare "[]"; the Windows
// foreground reader can come up empty, and "[]" is pure noise in the vector.
expect(formatForEmbedding('the deck', '', '')).toBe('the deck')
expect(formatForEmbedding('the deck', '', 'Q3 mockups')).toBe('Q3 mockups\nthe deck')
})
it('makes the same screen text in two different apps DISTINCT content', () => {
// This is the point of hashing the composed string (macOS does the same,
// OCREmbeddingService.swift:68): two apps showing identical text are two
// different things, and each earns its own vector...
const inFigma = formatForEmbedding('shared text', 'Figma', 'design')
const inSlack = formatForEmbedding('shared text', 'Slack', 'design')
expect(contentHash(inFigma)).not.toBe(contentHash(inSlack))
// ...while consecutive screenshots of the SAME window still collapse to one
// vector, which is where the ~20x dedup saving actually comes from.
expect(contentHash(inFigma)).toBe(
contentHash(formatForEmbedding('shared text', 'Figma', 'design'))
)
})
})
describe('l2Normalize', () => {
it('scales a vector to unit length', () => {
const v = l2Normalize(Float32Array.from([3, 4]))
expect(v[0]).toBeCloseTo(0.6, 6)
expect(v[1]).toBeCloseTo(0.8, 6)
expect(Math.hypot(...v)).toBeCloseTo(1, 6)
})
it('leaves an already-normalized vector alone', () => {
const v = l2Normalize(Float32Array.from([0, 1, 0]))
expect([...v]).toEqual([0, 1, 0])
})
// A zero vector has no direction — dividing by its norm would yield NaNs that
// then poison every similarity comparison.
it('returns a zero vector unchanged instead of producing NaN', () => {
const v = l2Normalize(new Float32Array(4))
expect([...v]).toEqual([0, 0, 0, 0])
})
})
describe('dot', () => {
// The whole reason vectors are normalized before storage: dot == cosine.
it('equals cosine similarity for normalized vectors', () => {
const a = l2Normalize(Float32Array.from([1, 1]))
const b = l2Normalize(Float32Array.from([1, 0]))
expect(dot(a, a)).toBeCloseTo(1, 6) // identical
expect(dot(a, b)).toBeCloseTo(Math.SQRT1_2, 6) // 45 degrees
expect(dot(b, l2Normalize(Float32Array.from([0, 1])))).toBeCloseTo(0, 6) // orthogonal
expect(dot(b, l2Normalize(Float32Array.from([-1, 0])))).toBeCloseTo(-1, 6) // opposite
})
// A stored vector from a different model must not score as a near-match.
it('scores 0 for mismatched dimensions rather than comparing a prefix', () => {
expect(dot(Float32Array.from([1, 0, 0]), Float32Array.from([1, 0]))).toBe(0)
})
})
describe('blob round-trip', () => {
it('preserves a full-dimension vector through the SQLite BLOB codec', () => {
const original = l2Normalize(Float32Array.from({ length: EMBED_DIM }, (_, i) => i % 7))
const blob = vectorToBuffer(original)
expect(blob.byteLength).toBe(EMBED_DIM * 4) // 12288 bytes
const restored = bufferToVector(blob)
expect(restored.length).toBe(EMBED_DIM)
expect(dot(original, restored)).toBeCloseTo(1, 5)
})
})
describe('scanTopKBySimilarity', () => {
const row = (hash: string, vec: number[]): VectorRow => ({
hash,
vec: l2Normalize(Float32Array.from(vec))
})
const query = l2Normalize(Float32Array.from([1, 0]))
/** Serve `rows` as pages, recording every yield between them. */
const pager = (rows: VectorRow[], yields: string[]) => ({
fetchChunk: (offset: number, limit: number) => rows.slice(offset, offset + limit),
yieldToEventLoop: async () => {
yields.push('yield')
}
})
const scan = async (rows: VectorRow[], limit: number, chunk = 2, yields: string[] = []) => {
const p = pager(rows, yields)
return scanTopKBySimilarity(p.fetchChunk, query, limit, p.yieldToEventLoop, chunk)
}
it('returns the most similar entries, strongest first', async () => {
const top = await scan([row('a', [0, 1]), row('b', [1, 0]), row('c', [1, 1])], 2)
expect(top.map((t) => t.hash)).toEqual(['b', 'c']) // exact match, then 45 degrees
expect(top[0].similarity).toBeCloseTo(1, 6)
expect(top[1].similarity).toBeCloseTo(Math.SQRT1_2, 6)
})
it('keeps the best K when there are more candidates than slots', async () => {
const rows = Array.from({ length: 50 }, (_, i) => row(`h${i}`, [i, 100 - i]))
const top = await scan(rows, 3)
expect(top.map((t) => t.hash)).toEqual(['h49', 'h48', 'h47']) // most x-aligned
})
// C2: better-sqlite3 is synchronous, so a single scan over every vector would
// freeze the main process. The scan MUST page and yield between pages — this is
// the assertion that the freeze is structurally impossible, not merely unlikely.
it('reads in bounded pages and yields the event loop between them', async () => {
const yields: string[] = []
const rows = Array.from({ length: 10 }, (_, i) => row(`h${i}`, [i, 1]))
const pages: number[] = []
const top = await scanTopKBySimilarity(
(offset, limit) => {
pages.push(limit)
return rows.slice(offset, offset + limit)
},
query,
3,
async () => {
yields.push('yield')
},
2
)
expect(pages.every((p) => p === 2)).toBe(true) // never asks for the whole table
expect(yields.length).toBeGreaterThanOrEqual(4) // yielded between pages
expect(top).toHaveLength(3) // and still ranked everything correctly
expect(top[0].hash).toBe('h9')
})
it('stops at a short page instead of scanning forever', async () => {
const yields: string[] = []
// 4 rows with a page size of 2: the second page is full, the third is empty
// and ends the scan. A bug here would loop on an infinite tail of empty pages.
const top = await scan(
[row('a', [1, 0]), row('b', [0, 1]), row('c', [1, 1]), row('d', [2, 0])],
4,
2,
yields
)
expect(top).toHaveLength(4)
})
it('handles fewer candidates than K, and an empty store', async () => {
expect(await scan([row('a', [1, 0])], 10)).toHaveLength(1)
expect(await scan([], 5)).toEqual([])
expect(await scan([row('a', [1, 0])], 0)).toEqual([])
})
// A vector written by a different model must not be ranked against this query.
it('scores a wrong-dimension vector 0 instead of matching on a prefix', async () => {
const top = await scan([row('a', [1, 0, 0]), row('b', [1, 0])], 2)
expect(top[0].hash).toBe('b')
expect(top[1]).toEqual({ hash: 'a', similarity: 0 })
})
})
describe('contentHash', () => {
it('is a stable 32-char key (first 16 bytes of SHA-256)', () => {
expect(contentHash('hello')).toHaveLength(32)
expect(contentHash('hello')).toBe(contentHash('hello'))
})
it('differs for different content, including whitespace-only differences', () => {
expect(contentHash('hello')).not.toBe(contentHash('hello '))
expect(contentHash('a')).not.toBe(contentHash('b'))
})
})