Download packages/minidb/src/trigram.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 3.56 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/minidb/src/trigram.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/minidb/src/trigram.ts
-
curl -L -o trigram.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/minidb/src/trigram.ts
3.56 kB
| // src/trigram.ts | |
| // | |
| // n-gram (2-gram + 3-gram) substring tokenizer for literal search. | |
| // | |
| // A TextIndex with this tokenizer indexes hashed character n-grams instead of | |
| // words, so symbol-heavy text (`C++`, `$\frac{a}{b}$`, `**passed**`, emoji) | |
| // becomes searchable as exact substrings. The index only yields candidates — | |
| // zero false positives is guaranteed by a downstream confirmation step that | |
| // re-checks `normalizeLiteral(doc).includes(normalizeLiteral(query))` against | |
| // the original text; hash collisions and non-contiguous n-gram matches only | |
| // cost extra confirmations. | |
| // | |
| // Deliberate deviations from the reference (Elasticsearch `wildcard` field): | |
| // - Case-insensitive: NFKC + lowercase, aligned with the default tokenizer. | |
| // - Windows are cut by Unicode code point (Array.from), not UTF-16 code | |
| // unit, so surrogate pairs (emoji) are never split. | |
| // - n-grams hash into 2^HASH_BITS buckets via the existing crc32 (no new | |
| // deps); 2-grams and 3-grams carry distinct tag prefixes so their buckets | |
| // never alias. | |
| import { crc32 } from './crc32.ts'; | |
| /** Tokenizer kinds that can be persisted in a text index definition | |
| * (`db.textindexes.json`). A definition without the field (written before | |
| * n-gram support existed) means 'default'. */ | |
| export type TextIndexTokenizerName = 'default' | 'ngram'; | |
| /** Width of the n-gram hash space in bits (4M buckets). Collisions only add | |
| * confirmation work downstream; they never affect correctness. */ | |
| const HASH_BITS = 22; | |
| const HASH_MASK = (1 << HASH_BITS) - 1; | |
| /** Normalize text for literal matching: Unicode NFKC (fullwidth `$` -> `$`, | |
| * compatibility glyphs folded) + lowercase. The search layer's confirmation | |
| * step must use this exact function so index and comparison agree. */ | |
| export function normalizeLiteral(text: string): string { | |
| return text.normalize('NFKC').toLowerCase(); | |
| } | |
| function termFor(gram: string, width: number): string { | |
| const hash = crc32(Buffer.from(gram, 'utf8')) & HASH_MASK; | |
| return String(width) + hash.toString(36); | |
| } | |
| /** Encode one n-gram (2 or 3 code points) as an index term: a width tag plus | |
| * the low HASH_BITS of its crc32 in base 36. Deterministic across processes. */ | |
| export function ngramTerm(gram: string): string { | |
| const width = Array.from(gram).length; | |
| if (width !== 2 && width !== 3) { | |
| throw new RangeError(`ngramTerm expects a 2- or 3-gram, got ${width} code points`); | |
| } | |
| return termFor(gram, width); | |
| } | |
| export interface NgramTokenizerOptions { | |
| /** Query side: a length-2 query emits only its 2-gram, a longer query only | |
| * its 3-grams (fewer, more selective terms). Index side (the default) | |
| * emits every 3-gram plus every 2-gram so both query shapes can match. | |
| * Text shorter than 2 normalized code points yields no terms — the search | |
| * layer must reject such queries itself. */ | |
| forQuery?: boolean; | |
| } | |
| /** Build a tokenizer that maps text to hashed n-gram terms (see file header). | |
| * Windows slide over code points, so astral characters stay whole. */ | |
| export function createNgramTokenizer(opts: NgramTokenizerOptions = {}): (text: string) => string[] { | |
| return (text) => { | |
| const chars = Array.from(normalizeLiteral(text)); | |
| const n = chars.length; | |
| const terms: string[] = []; | |
| if (n < 2) return terms; | |
| const widths = opts.forQuery ? (n === 2 ? [2] : [3]) : n >= 3 ? [3, 2] : [2]; | |
| for (const w of widths) { | |
| for (let i = 0; i + w <= n; i++) { | |
| terms.push(termFor(chars.slice(i, i + w).join(''), w)); | |
| } | |
| } | |
| return terms; | |
| }; | |
| } | |