type Doc> = { id: string; fields: Record; meta?: TMeta; }; interface Adapter { fromRow(row: R): D; toRow?(doc: D, original?: R): R; } type DeltaOp = { op: 'add'; row: TRow; } | { op: 'update'; row: TRow; } | { op: 'remove'; id: string; }; type DeltaPayload = { version: string | number; changes: DeltaOp[]; }; type BootstrapPayload = { version: string | number; rows: TRow[]; }; type SyncConfig = { bootstrap?: () => Promise>; fetchDelta?: (sinceVersion: string | number) => Promise>; pollIntervalMs?: number; subscribe?: (onChange: (delta: DeltaPayload) => void) => () => void; }; interface Signal { id: string; compute(doc: TDoc, ctx: TCtx): number; meta?: { range?: [number, number]; description?: string; }; } type CombinerKind = 'product' | 'weighted_sum' | 'log_sum'; type Combiner = CombinerKind | ((scores: Record, weights: Record) => number); type ScoreExplanation = { docId: string; total: number; contributions: Array<{ signalId: string; value: number; weight: number; }>; }; type Posting = { docId: string; fieldFreqs: Record; }; /** Tokeniser signature — feeds the inverted index. */ type Extract = (text: string) => string[]; declare class SearchStore { postings: Map; docs: Map; fieldStats: Map; totalDocs: number; features: Map; private docFieldLengths; add(doc: Doc, extract: Extract): void; remove(docId: string): void; candidates(queryFeats: string[]): Set; postingsFor(feature: string): Posting[] | undefined; avgFieldLen(field: string): number; } interface StorageAdapter { open(name: string): Promise; get(key: string): Promise; set(key: string, value: unknown): Promise; delete(key: string): Promise; iterate(prefix: string): AsyncIterable<[string, unknown]>; close(): Promise; } type StorageKind = 'memory' | 'indexeddb' | 'localstorage' | StorageAdapter; type SearchContext = { query: string; queryFeatures: string[]; now: number; userId?: string; customMeta?: Record; /** Internal — injected by Search.query for scorers that need store access (e.g. bm25Field). */ _store?: SearchStore; }; type SearchScorer = Signal & { /** * Optional gate: if returns false, the doc is filtered out entirely * (e.g. inventoryFilter for out-of-stock items). */ gate?(doc: Doc, ctx: SearchContext): boolean; }; type SearchOpts = { id?: string; adapter?: Adapter; storage?: StorageKind; sync?: SyncConfig; /** * Optional host-supplied tokeniser. Same function is used to index docs * AND to parse queries — symmetry is what makes BM25 matching work. * * Default: a Unicode-aware universal tokeniser (NFKC normalise + * whitespace split for whitespace scripts; codepoint + bigram for CJK, * Thai, Lao, Khmer, Burmese; lowercase throughout). Handles 200+ * languages out of the box with no per-locale packs. * * Plug in your own when you have domain-specific tokenisation rules * (e.g. medical term splitter, product SKU boundaries, ICD codes). */ extractFeatures?: (text: string) => string[]; scorers: SearchScorer[]; combiner?: Combiner; weights?: Record; topK?: number; scoreThreshold?: number; }; type SearchResult = { doc: TDoc; score: number; explanation?: ScoreExplanation; }; type QueryOpts = { topK?: number; explain?: boolean; scoreThreshold?: number; customMeta?: Record; }; type FieldWeightConfig = { weight: number; saturation?: number; }; type BM25FieldOpts = { weights: Record; k1?: number; b?: number; }; /** * Per-field BM25 with saturation. Reads the store from ctx._store (injected by Search). */ declare function bm25Field(opts: BM25FieldOpts): SearchScorer; type RecencyOpts = { field: string; halfLifeDays?: number; }; declare function recencyDecay(opts: RecencyOpts): SearchScorer; type NewProductBoostOpts = { field: string; withinDays?: number; multiplier?: number; }; declare function newProductBoost(opts: NewProductBoostOpts): SearchScorer; type BestsellerBoostOpts = { field: string; method?: 'log_normalize' | 'rank_inverse'; maxBoost?: number; }; declare function bestsellerBoost(opts: BestsellerBoostOpts): SearchScorer; declare function promoBoost(opts?: { field?: string; }): SearchScorer; declare function inventoryFilter(opts?: { field?: string; min?: number; }): SearchScorer; declare function activeFilter(opts?: { field?: string; }): SearchScorer; declare const builtin: { bm25Field: typeof bm25Field; recencyDecay: typeof recencyDecay; newProductBoost: typeof newProductBoost; bestsellerBoost: typeof bestsellerBoost; promoBoost: typeof promoBoost; inventoryFilter: typeof inventoryFilter; activeFilter: typeof activeFilter; }; declare class Search { private id; private store; private adapter; private storage; private scorers; private weights; private extract; private combiner; private topK; private scoreThreshold; private opts; private opened; private lowConfHook?; private pollTimer?; private unsubscribe?; constructor(opts: SearchOpts); init(): Promise; addDoc(row: TRow): Promise; addDocs(rows: TRow[]): Promise; removeDoc(id: string): Promise; updateDoc(row: TRow): Promise; query(text: string, opts?: QueryOpts): Promise; explain(text: string, docId: string): { docId: string; total: number; gated: boolean; contributions: never[]; } | { docId: string; total: number; contributions: { signalId: string; value: number; weight: number; }[]; gated?: undefined; } | undefined; onLowConfidence(fn: (q: string, c: SearchResult[]) => Promise): void; snapshot(): Promise<{ docs: Doc[]; }>; restore(snap: { docs: Doc[]; }): Promise; dispose(): Promise; getDoc(id: string): Doc | undefined; allDocs(): Iterable; getFeatures(id: string): string[] | undefined; _store(): SearchStore; private persist; private startSync; private applyDeltaPayload; } declare function createSearch(opts: SearchOpts): Search; export { type QueryOpts, Search, type SearchContext, type SearchOpts, type SearchResult, type SearchScorer, builtin, createSearch };