@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/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.