/** * Native Typesense IndexerAdapter — the v1 default for catalog-plane search. * * Uses an injected `TypesenseClient` interface (mirroring the storage provider * binding pattern) so the package doesn't take a hard dep on the Typesense * HTTP SDK. Templates wire in the actual client. * * See `docs/architecture/catalog-architecture.md` §5.4.1 for design. */ import { type DocumentEmitter, type IndexerAdapter, type IndexerSlice } from "@voyant-travel/catalog-contracts/indexer/contract"; import type { FieldPolicyRegistry } from "../contract.js"; import { type TypesenseSearchQuery } from "./typesense-search-query.js"; export { buildDefaultTypesenseQueryBy, buildDefaultTypesenseSearchFields, buildSearchQuery, type TypesenseSearchQuery, } from "./typesense-search-query.js"; /** * Minimal interface the Typesense client must satisfy. Templates pass in * the real `typesense` SDK client (or a custom HTTP wrapper) and the adapter * uses only these methods. */ export interface TypesenseClient { collections(name?: string): { list(): Promise; create(schema: TypesenseCollectionSchema): Promise; update(schema: Partial): Promise; delete(): Promise; retrieve(): Promise; documents(): { import(documents: unknown[], options?: { action?: "upsert" | "create"; }): Promise; delete(query: { filter_by: string; }): Promise; search(query: TypesenseSearchQuery): Promise; }; }; } export interface TypesenseFieldSchema { name: string; type: "string" | "string[]" | "int32" | "int64" | "float" | "bool" | "object" | "float[]"; facet?: boolean; index?: boolean; optional?: boolean; sort?: boolean; num_dim?: number; vec_dist?: "cosine" | "ip"; } export interface TypesenseCollectionSchema { name: string; fields: TypesenseFieldSchema[]; default_sorting_field?: string; enable_nested_fields?: boolean; metadata?: Record; } export interface TypesenseSearchHit { document: Record; text_match?: number; vector_distance?: number; hybrid_search_info?: { rank_fusion_score: number; }; } export interface TypesenseSearchResponse { hits: TypesenseSearchHit[]; found: number; facet_counts?: Array<{ field_name: string; counts: Array<{ value: string | number; count: number; }>; }>; } /** * One row's outcome from Typesense's `documents/import` endpoint. The endpoint * returns HTTP 200 even when individual rows fail validation; each line of the * response body is a JSON object of this shape. Failures are easy to miss — * the whole point of inspecting the body is to not let them pass silently * (a bad field shape can make *every* document fail while the CLI exits 0). */ export interface TypesenseImportRowResult { success: boolean; error?: string; /** The offending document, serialized by Typesense. */ document?: string; code?: number; } /** Summary of the failed rows in one import response. */ export interface ImportFailureSummary { collection: string; /** Number of rows that failed to import. */ failed: number; /** Total rows the response reported on. */ total: number; /** Up to `sampleSize` representative row errors. */ samples: string[]; } /** How the adapter reacts to row-level import failures. */ export type ImportFailureMode = "throw" | "best-effort"; /** * Raised when Typesense reports row-level import failures and the adapter is * in `"throw"` mode (the default). Carries the failure counts so a CLI can * exit non-zero and a caller can decide whether to retry. */ export declare class TypesenseImportError extends Error { readonly collection: string; readonly failed: number; readonly total: number; readonly samples: string[]; constructor(summary: ImportFailureSummary); } /** * Normalizes the `documents/import` response into per-row results. The fetch * client returns the raw newline-delimited JSON body (a `string`); the * official `typesense` SDK returns an already-parsed array of objects. Any * other shape (e.g. a `{}` test double, or `undefined`) yields `[]` — there * is nothing to inspect, so it is treated as "no reported failures". */ export declare function parseTypesenseImportResults(result: unknown): TypesenseImportRowResult[]; /** * Inspects an import response and returns a failure summary, or `null` when * every reported row succeeded (or the response carried no inspectable rows). */ export declare function summarizeImportFailures(collection: string, result: unknown, sampleSize?: number): ImportFailureSummary | null; export interface TypesenseIndexerOptions { client: TypesenseClient; /** Embedding dimension shipped by the configured EmbeddingProvider. */ vectorDimensions?: number | null; /** Optional collection-name prefix (useful for multi-tenant single-cluster setups). */ collectionPrefix?: string; /** * Field-policy registries keyed by vertical. Seeds the per-vertical registry * cache so a search-only process (the worker, which never runs * `ensureCollection`) builds queries against the REAL policy — including * numeric sort/filter fields. Without this, search falls back to * `inferRegistryFromCollection`, which only knows string fields, so numeric * sorts (e.g. `price-asc` → `priceFromAmountCents`) silently no-op. */ registries?: ReadonlyMap; /** * How to treat row-level import failures. Typesense's bulk-import endpoint * returns HTTP 200 even when individual rows fail validation (e.g. a field * serialized as an object where the schema expects `string[]`), so a reindex * can silently leave a collection empty. Default `"throw"` raises a * {@link TypesenseImportError} so the reindex CLI exits non-zero; set * `"best-effort"` to log and keep going. */ importFailureMode?: ImportFailureMode; /** * Invoked with a summary whenever any row fails to import, regardless of * `importFailureMode` (in `"throw"` mode it fires just before the throw). * Defaults to a `console.warn`. Use to route failures to a structured logger. */ onImportFailure?: (summary: ImportFailureSummary) => void; /** Number of representative row errors included in summaries. Default 5. */ importErrorSampleSize?: number; /** * Retry delays for Typesense's transient collection-schema update lock. * Default is short exponential backoff; tests may pass zeroes. */ collectionUpdateRetryDelaysMs?: readonly number[]; } /** * Returns the Typesense collection name for one variant slice. Stable across * runs so existing collections survive deployments. */ export declare function collectionName(slice: IndexerSlice, prefix?: string): string; export declare function parseCollectionName(name: string, prefix?: string): IndexerSlice | undefined; /** * Builds a Typesense collection schema from the field-policy registry. Maps * field-policy types onto Typesense field types using `query` + `class` from * the policy. */ export declare function buildCollectionSchema(slice: IndexerSlice, registry: FieldPolicyRegistry, options?: { vectorDimensions?: number | null; collectionPrefix?: string; }): TypesenseCollectionSchema; export declare function createTypesenseIndexer(options: TypesenseIndexerOptions): IndexerAdapter; /** * Helper for verticals that want to register a `DocumentEmitter` against * this adapter. Currently a thin pass-through; reserved for future emitter * registry extensions. */ export declare function attachEmitter(emitter: DocumentEmitter): DocumentEmitter;