-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathembedder.test.ts
More file actions
120 lines (103 loc) · 4.5 KB
/
Copy pathembedder.test.ts
File metadata and controls
120 lines (103 loc) · 4.5 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
import { describe, it, expect } from 'vitest';
import { SimpleEmbedder, isCJK } from '../src/core/retrieval/embedder';
describe('embedder', () => {
describe('isCJK', () => {
it('should identify CJK Unified characters', () => {
expect(isCJK('桑')).toBe(true);
expect(isCJK('图')).toBe(true);
expect(isCJK('树')).toBe(true);
});
it('should identify CJK Extension A characters', () => {
// CJK Ext-A range: 0x3400–0x4DBF
expect(isCJK('\u3447')).toBe(true);
});
it('should identify Hiragana and Katakana', () => {
expect(isCJK('\u3042')).toBe(true); // Hiragana あ
expect(isCJK('\u30AB')).toBe(true); // Katakana カ
});
it('should identify Hangul', () => {
expect(isCJK('\uAC00')).toBe(true); // Hangul 가
});
it('should not identify ASCII as CJK', () => {
expect(isCJK('a')).toBe(false);
expect(isCJK('Z')).toBe(false);
expect(isCJK('0')).toBe(false);
});
it('should not identify common punctuation as CJK', () => {
expect(isCJK('.')).toBe(false);
expect(isCJK('-')).toBe(false);
expect(isCJK(' ')).toBe(false);
});
});
describe('SimpleEmbedder', () => {
const embedder = new SimpleEmbedder();
it('should produce vectors of the correct dimension', async () => {
const vec = await embedder.embed('test query');
expect(vec.length).toBe(512);
});
it('should produce normalized vectors (L2 norm ≈ 1)', async () => {
const vec = await embedder.embed('test query');
let norm = 0;
for (const v of vec) norm += v * v;
norm = Math.sqrt(norm);
expect(Math.abs(norm - 1)).toBeLessThan(0.01);
});
it('should produce non-zero vectors for meaningful text', async () => {
const vec = await embedder.embed('桑基图 sankey');
const nonZeroCount = vec.filter(v => v !== 0).length;
expect(nonZeroCount).toBeGreaterThan(0);
});
it('should produce similar vectors for synonymous terms', async () => {
const vec1 = await embedder.embed('桑基图');
const vec2 = await embedder.embed('sankey');
// Both should expand to include each other's tokens via synonyms
// so their vectors should have some overlap
let dot = 0;
for (let i = 0; i < vec1.length; i++) dot += vec1[i] * vec2[i];
// Cosine similarity should be positive (they share synonym-expanded tokens)
expect(dot).toBeGreaterThan(0);
});
it('should produce different vectors for unrelated terms', async () => {
const vec1 = await embedder.embed('桑基图');
const vec2 = await embedder.embed('热力图');
let dot = 0;
for (let i = 0; i < vec1.length; i++) dot += vec1[i] * vec2[i];
// They may share some unigram overlap but should not be identical
expect(Math.abs(dot - 1)).toBeGreaterThan(0.01);
});
it('should support synchronous embedding via embedSync', () => {
const vec = embedder.embedSync('test query');
expect(vec.length).toBe(512);
});
it('should support batch embedding', async () => {
const vecs = await embedder.embedBatch(['query one', 'query two']);
expect(vecs.length).toBe(2);
expect(vecs[0].length).toBe(512);
expect(vecs[1].length).toBe(512);
});
it('should handle empty text gracefully', async () => {
const vec = await embedder.embed('');
// Empty text should still produce a 512-d vector (all zeros or near-zero)
expect(vec.length).toBe(512);
});
it('should weight CJK trigrams higher than unigrams', async () => {
// A query with trigrams should produce a vector with higher peak values
// than one with only unigrams (due to trigram weight = 2.0 vs unigram = 0.15)
const vecTrigram = await embedder.embed('矩形树图');
const maxTrigram = Math.max(...vecTrigram.map(Math.abs));
const vecUnigram = await embedder.embed('图型');
const maxUnigram = Math.max(...vecUnigram.map(Math.abs));
// Trigrams get higher weight, so peak values should be larger
expect(maxTrigram).toBeGreaterThan(maxUnigram * 0.5);
});
it('should expand synonyms during embedding', async () => {
// '树图' should get synonym expansion to 'treemap'
// So the embedding of '树图' should share hash buckets with 'treemap'
const vec1 = await embedder.embed('树图');
const vec2 = await embedder.embed('treemap');
let dot = 0;
for (let i = 0; i < vec1.length; i++) dot += vec1[i] * vec2[i];
expect(dot).toBeGreaterThan(0);
});
});
});