type Doc<TMeta = Record<string, unknown>> = {
    id: string;
    fields: Record<string, string>;
    meta?: TMeta;
};
interface Adapter<R, D = Doc> {
    fromRow(row: R): D;
    toRow?(doc: D, original?: R): R;
}
type DeltaOp<TRow = unknown> = {
    op: 'add';
    row: TRow;
} | {
    op: 'update';
    row: TRow;
} | {
    op: 'remove';
    id: string;
};
type DeltaPayload<TRow = unknown> = {
    version: string | number;
    changes: DeltaOp<TRow>[];
};
type BootstrapPayload<TRow = unknown> = {
    version: string | number;
    rows: TRow[];
};
type SyncConfig<TRow = unknown> = {
    bootstrap?: () => Promise<BootstrapPayload<TRow>>;
    fetchDelta?: (sinceVersion: string | number) => Promise<DeltaPayload<TRow>>;
    pollIntervalMs?: number;
    subscribe?: (onChange: (delta: DeltaPayload<TRow>) => void) => () => void;
};
interface Signal<TDoc = Doc, TCtx = unknown> {
    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<string, number>, weights: Record<string, number>) => number);
type ScoreExplanation = {
    docId: string;
    total: number;
    contributions: Array<{
        signalId: string;
        value: number;
        weight: number;
    }>;
};

type Posting = {
    docId: string;
    fieldFreqs: Record<string, number>;
};
/** Tokeniser signature — feeds the inverted index. */
type Extract = (text: string) => string[];
declare class SearchStore {
    postings: Map<string, Posting[]>;
    docs: Map<string, Doc>;
    fieldStats: Map<string, {
        totalLength: number;
        docCount: number;
    }>;
    totalDocs: number;
    features: Map<string, string[]>;
    add(doc: Doc, extract: Extract): void;
    remove(docId: string): void;
    candidates(queryFeats: string[]): Set<string>;
    postingsFor(feature: string): Posting[] | undefined;
    avgFieldLen(field: string): number;
}

interface StorageAdapter {
    open(name: string): Promise<void>;
    get<T = unknown>(key: string): Promise<T | undefined>;
    set(key: string, value: unknown): Promise<void>;
    delete(key: string): Promise<void>;
    iterate(prefix: string): AsyncIterable<[string, unknown]>;
    close(): Promise<void>;
}
type StorageKind = 'memory' | 'indexeddb' | 'localstorage' | StorageAdapter;

type SearchContext = {
    query: string;
    queryFeatures: string[];
    now: number;
    userId?: string;
    customMeta?: Record<string, unknown>;
    /** Internal — injected by Search.query for scorers that need store access (e.g. bm25Field). */
    _store?: SearchStore;
};
type SearchScorer = Signal<Doc, SearchContext> & {
    /**
     * 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<TRow = unknown> = {
    id?: string;
    adapter?: Adapter<TRow, Doc>;
    storage?: StorageKind;
    sync?: SyncConfig<TRow>;
    /**
     * 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<string, number>;
    topK?: number;
    scoreThreshold?: number;
};
type SearchResult<TDoc = Doc> = {
    doc: TDoc;
    score: number;
    explanation?: ScoreExplanation;
};
type QueryOpts = {
    topK?: number;
    explain?: boolean;
    scoreThreshold?: number;
    customMeta?: Record<string, unknown>;
};

type FieldWeightConfig = {
    weight: number;
    saturation?: number;
};
type BM25FieldOpts = {
    weights: Record<string, FieldWeightConfig>;
    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;

/**
 * learn-to-rank — logistic regression over per-field BM25 contributions.
 *
 * Input: click events `{query, shown: [{docId, fields_score: {title, brand, ...}}], clicked: docId}`
 * Output: per-field weights that maximize click-through.
 *
 * Algorithm: minibatch gradient descent on binary cross-entropy with L2.
 * Pure TS, no external ML deps. Good enough for ~100k events.
 */
type ClickEvent = {
    query: string;
    shown: Array<{
        docId: string;
        fieldScores: Record<string, number>;
    }>;
    clickedDocId: string;
};
type LearnOpts = {
    fields: string[];
    iterations?: number;
    learningRate?: number;
    l2?: number;
    batchSize?: number;
};
type LearnedWeights = {
    weights: Record<string, number>;
    loss: number;
    iterations: number;
};
declare function learnRankingWeights(events: ClickEvent[], opts: LearnOpts): LearnedWeights;

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<TRow = unknown> {
    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<TRow>);
    init(): Promise<void>;
    addDoc(row: TRow): Promise<void>;
    addDocs(rows: TRow[]): Promise<void>;
    removeDoc(id: string): Promise<void>;
    updateDoc(row: TRow): Promise<void>;
    query(text: string, opts?: QueryOpts): Promise<SearchResult[]>;
    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<SearchResult[]>): void;
    snapshot(): Promise<{
        docs: Doc[];
    }>;
    restore(snap: {
        docs: Doc[];
    }): Promise<void>;
    dispose(): Promise<void>;
    getDoc(id: string): Doc | undefined;
    allDocs(): Iterable<Doc>;
    getFeatures(id: string): string[] | undefined;
    _store(): SearchStore;
    private persist;
    private startSync;
    private applyDeltaPayload;
}
declare function createSearch<TRow = unknown>(opts: SearchOpts<TRow>): Search<TRow>;

export { type ClickEvent, type LearnOpts, type LearnedWeights, type QueryOpts, Search, type SearchContext, type SearchOpts, type SearchResult, type SearchScorer, builtin, createSearch, learnRankingWeights };
