@seekdb/bm25 1.2.0
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/LICENSE +202 -0
- package/README.md +633 -0
- package/dist/index.cjs +277 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.mjs +255 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
# BM25 Embedding Function Guide
|
|
2
|
+
|
|
3
|
+
The BM25 (Best Matching 25) embedding function is a sparse embedding implementation that uses the BM25 ranking algorithm to convert text into sparse vectors for efficient full-text search.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
BM25 is a probabilistic information retrieval function that ranks documents based on query terms appearing in the document. It's widely used in search engines and document retrieval systems.
|
|
8
|
+
|
|
9
|
+
**Key Features:**
|
|
10
|
+
|
|
11
|
+
- Sparse vector output (high-dimensional, mostly zeros)
|
|
12
|
+
- Term frequency-based representation
|
|
13
|
+
- Document length normalization
|
|
14
|
+
- Stopword filtering
|
|
15
|
+
- Stemming support (Snowball)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @seekdb/bm25
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { SeekdbClient, SparseVectorIndexConfig, K } from "seekdb";
|
|
27
|
+
import { Bm25EmbeddingFunction } from "@seekdb/bm25";
|
|
28
|
+
|
|
29
|
+
const client = new SeekdbClient({
|
|
30
|
+
path: "./seekdb.db",
|
|
31
|
+
database: "test",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Create BM25 embedding function
|
|
35
|
+
const bm25 = new Bm25EmbeddingFunction();
|
|
36
|
+
|
|
37
|
+
// Create collection with BM25 index
|
|
38
|
+
const collection = await client.createCollection({
|
|
39
|
+
name: "bm25_collection",
|
|
40
|
+
schema: {
|
|
41
|
+
sparseVectorIndex: new SparseVectorIndexConfig({
|
|
42
|
+
sourceKey: K.DOCUMENT,
|
|
43
|
+
embeddingFunction: bm25,
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add documents - auto-vectorized
|
|
49
|
+
await collection.add({
|
|
50
|
+
ids: ["1", "2", "3"],
|
|
51
|
+
documents: [
|
|
52
|
+
"Machine learning is transforming artificial intelligence",
|
|
53
|
+
"Python programming language is popular",
|
|
54
|
+
"Vector databases enable semantic search",
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Search
|
|
59
|
+
const results = await collection.query({
|
|
60
|
+
queryTexts: "artificial intelligence",
|
|
61
|
+
queryKey: "sparseEmbedding",
|
|
62
|
+
nResults: 3,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration Parameters
|
|
67
|
+
|
|
68
|
+
### Constructor Options
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
interface Bm25EmbeddingArgs {
|
|
72
|
+
k?: number; // Term frequency parameter (default: 1.2)
|
|
73
|
+
b?: number; // Document length parameter (default: 0.75)
|
|
74
|
+
avgDocLength?: number; // Average document length (default: 256)
|
|
75
|
+
tokenMaxLength?: number; // Maximum token length (default: 40)
|
|
76
|
+
stopwords?: string[]; // Custom stopwords (optional)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### `k` (Term Frequency Saturation)
|
|
81
|
+
|
|
82
|
+
Controls how quickly term frequency saturates:
|
|
83
|
+
|
|
84
|
+
- **Range:** (0, +∞)
|
|
85
|
+
- **Default:** 1.2
|
|
86
|
+
- **Effect:**
|
|
87
|
+
- Higher `k`: Less saturation, term frequency has more impact
|
|
88
|
+
- Lower `k`: More saturation, diminishing returns for repeated terms
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// For documents where term frequency is important (e.g., product descriptions)
|
|
92
|
+
const bm25HighK = new Bm25EmbeddingFunction({ k: 1.5 });
|
|
93
|
+
|
|
94
|
+
// For documents where term presence matters more than frequency
|
|
95
|
+
const bm25LowK = new Bm25EmbeddingFunction({ k: 1.0 });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `b` (Document Length Normalization)
|
|
99
|
+
|
|
100
|
+
Controls how much document length affects ranking:
|
|
101
|
+
|
|
102
|
+
- **Range:** [0, 1]
|
|
103
|
+
- **Default:** 0.75
|
|
104
|
+
- **Effect:**
|
|
105
|
+
- `b = 0`: Ignore document length
|
|
106
|
+
- `b = 1`: Fully normalize by document length
|
|
107
|
+
- Higher `b`: Longer documents get penalized more
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// For short documents (tweets, titles) - reduce length penalty
|
|
111
|
+
const bm25ShortDocs = new Bm25EmbeddingFunction({ b: 0.3 });
|
|
112
|
+
|
|
113
|
+
// For long documents (articles, books) - apply length normalization
|
|
114
|
+
const bm25LongDocs = new Bm25EmbeddingFunction({ b: 0.9 });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `avgDocLength` (Average Document Length)
|
|
118
|
+
|
|
119
|
+
Used for document length normalization:
|
|
120
|
+
|
|
121
|
+
- **Range:** (0, +∞)
|
|
122
|
+
- **Default:** 256
|
|
123
|
+
- **Effect:** Sets the "average" document length for normalization
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// For Twitter data
|
|
127
|
+
const bm25Twitter = new Bm25EmbeddingFunction({
|
|
128
|
+
avgDocLength: 30,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// For blog posts
|
|
132
|
+
const bm25Blogs = new Bm25EmbeddingFunction({
|
|
133
|
+
avgDocLength: 500,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// For research papers
|
|
137
|
+
const bm25Papers = new Bm25EmbeddingFunction({
|
|
138
|
+
avgDocLength: 5000,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### `tokenMaxLength` (Maximum Token Length)
|
|
143
|
+
|
|
144
|
+
Filters tokens longer than this limit:
|
|
145
|
+
|
|
146
|
+
- **Range:** (0, +∞)
|
|
147
|
+
- **Default:** 40
|
|
148
|
+
- **Effect:** Excludes very long tokens (often URLs, special strings)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// For social media with hashtags/mentions
|
|
152
|
+
const bm25Social = new Bm25EmbeddingFunction({
|
|
153
|
+
tokenMaxLength: 20,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// For technical documents with long identifiers
|
|
157
|
+
const bm25Tech = new Bm25EmbeddingFunction({
|
|
158
|
+
tokenMaxLength: 100,
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `stopwords` (Custom Stopwords)
|
|
163
|
+
|
|
164
|
+
Words to exclude from tokenization:
|
|
165
|
+
|
|
166
|
+
- **Default:** Common English stopwords (a, an, the, is, etc.)
|
|
167
|
+
- **Effect:** Removes low-information words
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Add custom stopwords
|
|
171
|
+
const bm25Custom = new Bm25EmbeddingFunction({
|
|
172
|
+
stopwords: [
|
|
173
|
+
"please",
|
|
174
|
+
"thank",
|
|
175
|
+
"regards", // Email signatures
|
|
176
|
+
"click",
|
|
177
|
+
"here",
|
|
178
|
+
"now", // Spam-like words
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Use only custom stopwords (disable defaults)
|
|
183
|
+
const bm25NoStopwords = new Bm25EmbeddingFunction({
|
|
184
|
+
stopwords: [],
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Default Stopwords
|
|
189
|
+
|
|
190
|
+
The following stopwords are filtered by default:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
a, an, and, are, as, at, be, by, for, from, has, he, in, is, it,
|
|
194
|
+
its, of, on, that, the, to, was, were, will, with
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Text Processing Pipeline
|
|
198
|
+
|
|
199
|
+
BM25 applies the following text processing steps:
|
|
200
|
+
|
|
201
|
+
1. **Lowercase conversion** - Convert to lowercase
|
|
202
|
+
2. **Alphanumeric filtering** - Keep only letters, numbers, underscores, spaces
|
|
203
|
+
3. **Whitespace tokenization** - Split on whitespace
|
|
204
|
+
4. **Stopword removal** - Filter out stopwords
|
|
205
|
+
5. **Length filtering** - Remove tokens exceeding `tokenMaxLength`
|
|
206
|
+
6. **Stemming** - Apply Snowball English stemmer
|
|
207
|
+
7. **Hashing** - Convert tokens to integer hash values
|
|
208
|
+
8. **BM25 scoring** - Apply BM25 formula
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// Input text
|
|
212
|
+
const text = "Machine learning is transforming artificial intelligence";
|
|
213
|
+
|
|
214
|
+
// After processing
|
|
215
|
+
// Tokens: ["machin", "learn", "transform", "artifici", "intellig"]
|
|
216
|
+
|
|
217
|
+
// Sparse vector
|
|
218
|
+
{ 1234567: 0.8, 2345678: 0.5, 3456789: 0.6, ... }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## BM25 Formula
|
|
222
|
+
|
|
223
|
+
The BM25 score for a document `D` given a query `Q` is:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
score(D, Q) = sum over t in Q of IDF(t) * (f(t, D) * (k + 1)) / (f(t, D) + k * (1 - b + b * |D| / avgdl))
|
|
227
|
+
|
|
228
|
+
Where:
|
|
229
|
+
- t = term in query
|
|
230
|
+
- D = document
|
|
231
|
+
- f(t, D) = frequency of term t in document D
|
|
232
|
+
- |D| = length of document D (in tokens)
|
|
233
|
+
- avgdl = average document length in collection
|
|
234
|
+
- k = term frequency saturation parameter
|
|
235
|
+
- b = document length normalization parameter
|
|
236
|
+
- IDF(t) = inverse document frequency of term t
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Inverse Document Frequency (IDF)
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
IDF(t) = log((N - df(t) + 0.5) / (df(t) + 0.5))
|
|
243
|
+
|
|
244
|
+
Where:
|
|
245
|
+
- N = total number of documents
|
|
246
|
+
- df(t) = number of documents containing term t
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Note:** The current implementation computes BM25 scores per-document (not per-collection). The IDF component is simplified for single-document processing. For collection-wide IDF, you'd need to maintain term frequency statistics.
|
|
250
|
+
|
|
251
|
+
## Implementation Details
|
|
252
|
+
|
|
253
|
+
### Hashing
|
|
254
|
+
|
|
255
|
+
BM25 uses Murmur3 hash to convert stemmed tokens to integer keys:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const token = "learning";
|
|
259
|
+
const hash = murmur3(token); // e.g., 1234567890
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This allows sparse vectors to use integer keys while supporting unlimited vocabulary.
|
|
263
|
+
|
|
264
|
+
### Stemming
|
|
265
|
+
|
|
266
|
+
Snowball English stemmer is used for morphological normalization:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
running -> run
|
|
270
|
+
running -> run
|
|
271
|
+
studies -> studi
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Tokenization
|
|
275
|
+
|
|
276
|
+
Simple whitespace tokenization with preprocessing:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// Input: "Hello, World! How are you?"
|
|
280
|
+
// Processed: "hello world how are you"
|
|
281
|
+
// Tokens: ["hello", "world", "how", "are", "you"]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Usage Patterns
|
|
285
|
+
|
|
286
|
+
### Short Documents (Social Media)
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const bm25Twitter = new Bm25EmbeddingFunction({
|
|
290
|
+
k: 1.5, // Less saturation, freq matters
|
|
291
|
+
b: 0.3, // Less length normalization
|
|
292
|
+
avgDocLength: 30, // Short documents
|
|
293
|
+
tokenMaxLength: 20, # Short tokens
|
|
294
|
+
stopwords: ["rt", "via"] # Remove retweets
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Long Documents (Articles, Papers)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const bm25Articles = new Bm25EmbeddingFunction({
|
|
302
|
+
k: 1.0, # More saturation, presence matters
|
|
303
|
+
b: 0.9, # More length normalization
|
|
304
|
+
avgDocLength: 1000, # Long documents
|
|
305
|
+
tokenMaxLength: 40 # Standard token length
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Product Catalogs
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const bm25Products = new Bm25EmbeddingFunction({
|
|
313
|
+
k: 1.2,
|
|
314
|
+
b: 0.5, # Moderate length normalization
|
|
315
|
+
avgDocLength: 150,
|
|
316
|
+
stopwords: [
|
|
317
|
+
"product", "item", "sku", # Catalog-specific
|
|
318
|
+
"please", "click", "buy" # Marketing fluff
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Code Search
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
const bm25Code = new Bm25EmbeddingFunction({
|
|
327
|
+
k: 1.0,
|
|
328
|
+
b: 0.5,
|
|
329
|
+
avgDocLength: 200,
|
|
330
|
+
tokenMaxLength: 100, # Allow long identifiers
|
|
331
|
+
stopwords: ["var", "let", "const", "function"] # Remove keywords
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Configuration Validation
|
|
336
|
+
|
|
337
|
+
The BM25 embedding function validates its configuration:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { SeekdbValueError } from "seekdb";
|
|
341
|
+
|
|
342
|
+
// Valid configurations
|
|
343
|
+
const bm25 = new Bm25EmbeddingFunction({
|
|
344
|
+
k: 1.2, // OK
|
|
345
|
+
b: 0.75, // OK
|
|
346
|
+
avgDocLength: 256, // OK
|
|
347
|
+
tokenMaxLength: 40, // OK
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Invalid configurations - these throw SeekdbValueError
|
|
351
|
+
const invalid1 = new Bm25EmbeddingFunction({
|
|
352
|
+
k: -1.0, // Error: k must be positive
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const invalid2 = new Bm25EmbeddingFunction({
|
|
356
|
+
b: 1.5, // Error: b must be in [0, 1]
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const invalid3 = new Bm25EmbeddingFunction({
|
|
360
|
+
avgDocLength: 0, // Error: avgDocLength must be positive
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const invalid4 = new Bm25EmbeddingFunction({
|
|
364
|
+
tokenMaxLength: -5, // Error: tokenMaxLength must be positive
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Static Methods
|
|
369
|
+
|
|
370
|
+
### `validateConfig(config)`
|
|
371
|
+
|
|
372
|
+
Validate configuration before creating an instance:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { Bm25EmbeddingFunction } from "@seekdb/bm25";
|
|
376
|
+
|
|
377
|
+
const config = {
|
|
378
|
+
k: 1.2,
|
|
379
|
+
b: 0.75,
|
|
380
|
+
avg_doc_length: 256,
|
|
381
|
+
token_max_length: 40,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
Bm25EmbeddingFunction.validateConfig(config);
|
|
386
|
+
console.log("Configuration is valid");
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error("Invalid configuration:", error);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### `buildFromConfig(config)`
|
|
393
|
+
|
|
394
|
+
Create an instance from a configuration object:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
const config = {
|
|
398
|
+
k: 1.2,
|
|
399
|
+
b: 0.75,
|
|
400
|
+
avg_doc_length: 256,
|
|
401
|
+
token_max_length: 40,
|
|
402
|
+
stopwords: ["a", "an", "the"],
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const bm25 = Bm25EmbeddingFunction.buildFromConfig(config);
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Note:** Configuration keys use snake_case for `buildFromConfig`:
|
|
409
|
+
|
|
410
|
+
- `avgDocLength` → `avg_doc_length`
|
|
411
|
+
- `tokenMaxLength` → `token_max_length`
|
|
412
|
+
|
|
413
|
+
## Configuration Management
|
|
414
|
+
|
|
415
|
+
The BM25 embedding function supports configuration updates:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { Bm25EmbeddingFunction } from "@seekdb/bm25";
|
|
419
|
+
|
|
420
|
+
const bm25 = new Bm25EmbeddingFunction({
|
|
421
|
+
k: 1.2,
|
|
422
|
+
b: 0.75,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Validate configuration updates
|
|
426
|
+
bm25.validateConfigUpdate({
|
|
427
|
+
k: 1.5, // Allowed
|
|
428
|
+
b: 0.8, // Allowed
|
|
429
|
+
avg_doc_length: 300, // Allowed
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// This will throw
|
|
433
|
+
bm25.validateConfigUpdate({
|
|
434
|
+
unknown_param: "value", // Not allowed
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Mutable configuration keys:
|
|
439
|
+
|
|
440
|
+
- `k`
|
|
441
|
+
- `b`
|
|
442
|
+
- `avg_doc_length`
|
|
443
|
+
- `token_max_length`
|
|
444
|
+
- `stopwords`
|
|
445
|
+
|
|
446
|
+
## Complete Example
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
import { SeekdbClient, SparseVectorIndexConfig, K } from "seekdb";
|
|
450
|
+
import { Bm25EmbeddingFunction } from "@seekdb/bm25";
|
|
451
|
+
|
|
452
|
+
async function bm25Example() {
|
|
453
|
+
// 1. Create client
|
|
454
|
+
const client = new SeekdbClient({
|
|
455
|
+
path: "./seekdb.db",
|
|
456
|
+
database: "test"
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// 2. Configure BM25 for technical articles
|
|
460
|
+
const bm25 = new Bm25EmbeddingFunction({
|
|
461
|
+
k: 1.2, // Standard term frequency saturation
|
|
462
|
+
b: 0.75, # Standard length normalization
|
|
463
|
+
avgDocLength: 200, # Average article length
|
|
464
|
+
tokenMaxLength: 40, # Standard token length
|
|
465
|
+
stopwords: [
|
|
466
|
+
"the", "a", "an", "and", "or", "but", # Basic stopwords
|
|
467
|
+
"please", "thank", "regards", # Email signatures
|
|
468
|
+
"click", "here", "now", "download" # Marketing fluff
|
|
469
|
+
]
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
console.log("BM25 configuration:", bm25.getConfig());
|
|
473
|
+
|
|
474
|
+
// 3. Create collection
|
|
475
|
+
const collection = await client.createCollection({
|
|
476
|
+
name: "tech_articles",
|
|
477
|
+
schema: {
|
|
478
|
+
sparseVectorIndex: new SparseVectorIndexConfig({
|
|
479
|
+
sourceKey: K.DOCUMENT,
|
|
480
|
+
embeddingFunction: bm25,
|
|
481
|
+
prune: true, # Remove small values
|
|
482
|
+
refine: true, # Refine search results
|
|
483
|
+
drop_ratio_build: 0.5, # Drop 50% of smallest values
|
|
484
|
+
drop_ratio_search: 0.3 # Drop 30% during search
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 4. Add articles
|
|
490
|
+
const articles = [
|
|
491
|
+
{
|
|
492
|
+
id: "1",
|
|
493
|
+
title: "Introduction to Machine Learning",
|
|
494
|
+
content: "Machine learning is a subset of artificial intelligence that enables systems to learn from data without explicit programming."
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "2",
|
|
498
|
+
title: "Python for Data Science",
|
|
499
|
+
content: "Python is a powerful programming language widely used for data analysis, machine learning, and scientific computing."
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: "3",
|
|
503
|
+
title: "Vector Databases Explained",
|
|
504
|
+
content: "Vector databases store data as embeddings and enable similarity search for AI applications including RAG and recommendations."
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: "4",
|
|
508
|
+
title: "Deep Learning with Neural Networks",
|
|
509
|
+
content: "Deep learning uses neural networks with multiple layers to learn hierarchical representations of data."
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "5",
|
|
513
|
+
title: "Natural Language Processing",
|
|
514
|
+
content: "NLP enables computers to understand and generate human language using techniques like tokenization and transformers."
|
|
515
|
+
}
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
await collection.add({
|
|
519
|
+
ids: articles.map(a => a.id),
|
|
520
|
+
documents: articles.map(a => `${a.title}. ${a.content}`),
|
|
521
|
+
metadatas: articles.map(a => ({
|
|
522
|
+
title: a.title,
|
|
523
|
+
category: "Technology"
|
|
524
|
+
}))
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
console.log(`Added ${articles.length} articles`);
|
|
528
|
+
|
|
529
|
+
// 5. Search examples
|
|
530
|
+
const queries = [
|
|
531
|
+
"artificial intelligence and machine learning",
|
|
532
|
+
"Python programming language",
|
|
533
|
+
"neural networks",
|
|
534
|
+
"data science",
|
|
535
|
+
"language understanding"
|
|
536
|
+
];
|
|
537
|
+
|
|
538
|
+
for (const query of queries) {
|
|
539
|
+
const results = await collection.query({
|
|
540
|
+
queryTexts: query,
|
|
541
|
+
queryKey: "sparseEmbedding",
|
|
542
|
+
nResults: 3,
|
|
543
|
+
include: ["documents", "metadatas", "distances"]
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
console.log(`\nQuery: "${query}"`);
|
|
547
|
+
console.log("Results:");
|
|
548
|
+
for (let i = 0; i < results.ids[0].length; i++) {
|
|
549
|
+
console.log(` ${i + 1}. ${results.metadatas?.[0]?.[i]?.title}`);
|
|
550
|
+
console.log(` Score: ${results.distances?.[0]?.[i]?.toFixed(4)}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 6. Demonstrate parameter effects
|
|
555
|
+
console.log("\n\nDemonstrating parameter effects:");
|
|
556
|
+
|
|
557
|
+
// High k (less saturation) - favors repeated terms
|
|
558
|
+
const bm25HighK = new Bm25EmbeddingFunction({ k: 2.0, b: 0.75 });
|
|
559
|
+
console.log("\nHigh k (2.0) - less saturation:");
|
|
560
|
+
console.log(await testQuery(collection, "machine learning machine", bm25HighK));
|
|
561
|
+
|
|
562
|
+
// Low k (more saturation) - favors term presence
|
|
563
|
+
const bm25LowK = new Bm25EmbeddingFunction({ k: 0.8, b: 0.75 });
|
|
564
|
+
console.log("\nLow k (0.8) - more saturation:");
|
|
565
|
+
console.log(await testQuery(collection, "machine learning machine", bm25LowK));
|
|
566
|
+
|
|
567
|
+
// High b (more length normalization)
|
|
568
|
+
const bm25HighB = new Bm25EmbeddingFunction({ k: 1.2, b: 0.9 });
|
|
569
|
+
console.log("\nHigh b (0.9) - more length normalization:");
|
|
570
|
+
console.log(await testQuery(collection, "machine learning", bm25HighB));
|
|
571
|
+
|
|
572
|
+
// Low b (less length normalization)
|
|
573
|
+
const bm25LowB = new Bm25EmbeddingFunction({ k: 1.2, b: 0.3 });
|
|
574
|
+
console.log("\nLow b (0.3) - less length normalization:");
|
|
575
|
+
console.log(await testQuery(collection, "machine learning", bm25LowB));
|
|
576
|
+
|
|
577
|
+
// Cleanup
|
|
578
|
+
await client.deleteCollection("tech_articles");
|
|
579
|
+
await client.close();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function testQuery(
|
|
583
|
+
collection: any,
|
|
584
|
+
query: string,
|
|
585
|
+
embedding: any
|
|
586
|
+
): Promise<string> {
|
|
587
|
+
// This is for demonstration - in practice, you'd create separate collections
|
|
588
|
+
// or use a different query approach
|
|
589
|
+
return `Query: "${query}" with k=${embedding.k}, b=${embedding.b}`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
bm25Example().catch(console.error);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Comparison with Dense Embeddings
|
|
596
|
+
|
|
597
|
+
| Aspect | BM25 (Sparse) | Dense Embeddings |
|
|
598
|
+
| -------------------- | ------------------------ | ---------------------- |
|
|
599
|
+
| **Representation** | Term-based (tokens) | Semantic (neural) |
|
|
600
|
+
| **Dimension** | Very high (millions) | Low (128-1536) |
|
|
601
|
+
| **Training** | None (rule-based) | Requires training |
|
|
602
|
+
| **Memory** | Compact (non-zeros only) | Fixed per vector |
|
|
603
|
+
| **Interpretability** | High (see which terms) | Low (black box) |
|
|
604
|
+
| **Best For** | Exact keyword matching | Semantic similarity |
|
|
605
|
+
| **Cross-lingual** | No | Yes (with right model) |
|
|
606
|
+
|
|
607
|
+
## Best Practices
|
|
608
|
+
|
|
609
|
+
1. **Tune for your data:**
|
|
610
|
+
- Experiment with `k` and `b` on your specific dataset
|
|
611
|
+
- Use validation data to find optimal parameters
|
|
612
|
+
|
|
613
|
+
2. **Customize stopwords:**
|
|
614
|
+
- Add domain-specific stopwords
|
|
615
|
+
- Consider removing common terms in your domain
|
|
616
|
+
|
|
617
|
+
3. **Set appropriate document length:**
|
|
618
|
+
- Use actual average from your data
|
|
619
|
+
- Recalculate when adding significantly different content
|
|
620
|
+
|
|
621
|
+
4. **Combine with other methods:**
|
|
622
|
+
- Use hybrid search with dense vectors for semantic search
|
|
623
|
+
- Combine with full-text search for exact matching
|
|
624
|
+
|
|
625
|
+
5. **Monitor performance:**
|
|
626
|
+
- Check query latency with different parameters
|
|
627
|
+
- Adjust `drop_ratio_build` and `drop_ratio_search` as needed
|
|
628
|
+
|
|
629
|
+
## License
|
|
630
|
+
|
|
631
|
+
BM25 embedding function is part of SeekDB and licensed under Apache 2.0.
|
|
632
|
+
|
|
633
|
+
The Snowball stemmer is licensed under BSD 3-Clause.
|