SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw History Blame Contribute Delete
2.96 kB
// src/text-index/tokenize.ts
//
// Tokenizer of the full-text index: Latin/number words + CJK unigrams &
// bigrams (no dictionary, zero dependencies, works for Chinese without a
// segmenter), plus the document text extraction shared with the generation
// build's worker. This file is part of the worker's import closure, so every
// relative import carries an explicit `.ts` specifier (see the IMPORT NOTE in
// worker/text-build-core.ts).
import { getPath } from '../query.ts';
const LATIN = /[a-z0-9]+/g;
const CJK = /[\u3400-\u9fff\u3040-\u30ff\uff00-\uffef]+/g;
// Postings records store the term length in a uint16. A single document with a
// longer token previously made every postings rebuild throw after the index had
// already been cleared, permanently poisoning the index (and compaction). Such
// tokens can never be real query terms — drop them at tokenization so one
// pathological document cannot destroy the index.
const MAX_TERM_CHARS = 0xffff;
// The same postings uint16 limit in UTF-8 BYTES (encodeRecord's unit). A
// custom tokenizer's output is validated against it at every write boundary
// (tokensFor): an overlong term rejects the write loudly instead of poisoning
// the next postings rebuild (review #27).
export const MAX_TERM_BYTES = 0xffff;
export const yieldToLoop = (): Promise<void> => new Promise((r) => setImmediate(r));
/** Tokenize text into terms (lowercased latin words + CJK uni/bigrams). */
export function tokenize(str: unknown): string[] {
const s = String(str).toLowerCase();
const terms: string[] = [];
const latin = s.match(LATIN);
// Loop-push instead of `terms.push(...latin)`: spreading a large match array
// (hundreds of thousands of tokens from a big doc) overflows the call stack.
// Latin matches are ASCII, so chars == utf8 bytes for the length guard.
if (latin) for (const t of latin) if (t.length <= MAX_TERM_CHARS) terms.push(t);
const runs = s.match(CJK) ?? [];
for (const r of runs) {
for (let i = 0; i < r.length; i++) {
terms.push(r[i]!);
if (i + 1 < r.length) terms.push(r[i]! + r[i + 1]!);
}
}
return terms;
}
function stringLeaves(obj: unknown, acc: string[] = []): string[] {
if (obj === null || obj === undefined) return acc;
if (typeof obj === 'string') {
acc.push(obj);
return acc;
}
if (typeof obj !== 'object') return acc;
for (const v of Object.values(obj as Record<string, unknown>)) stringLeaves(v, acc);
return acc;
}
/** Extract the indexable text of a document (shared with the generation
* build's worker: fields mode joins the configured paths, otherwise every
* string leaf). Internal to the package. */
export function extractText(fields: readonly string[] | null, doc: unknown): string {
if (fields && fields.length) {
return fields
.map((f) => getPath(doc, f))
.filter((v): v is string => typeof v === 'string')
.join(' ');
}
return stringLeaves(doc).join(' ');
}