Skip to content

Adding a Search Engine

The API uses an engine abstraction that makes it straightforward to add new search backends. Four engines are currently implemented: Elasticsearch, OpenSearch, Meilisearch, and Typesense.

Elasticsearch and OpenSearch share a base class (ElasticCompatEngine) since they use an identical query DSL. Subclasses only differ in client construction and response unwrapping. Meilisearch and Typesense implement SearchEngine directly because their API models are fundamentally different.

  • Directorysrc/engines/
    • engine.ts SearchEngine interface
    • elastic-compat.ts shared base class
    • elasticsearch.ts
    • opensearch.ts
    • meilisearch.ts
    • typesense.ts
    • index.ts engine factory map
  1. Implement the SearchEngine interface

    Create a new file in src/engines/ (e.g. typesense.ts). Your class must implement all five methods from src/engines/engine.ts:

    import type {
    FacetSearchOptions,
    FacetValue,
    SearchOptions,
    SearchResult,
    } from "../types.ts";
    export interface SearchEngine {
    search(query: string, options: SearchOptions): Promise<SearchResult>;
    getDocument(id: string): Promise<Record<string, unknown> | null>;
    searchFacetValues(
    field: string,
    query: string,
    options?: FacetSearchOptions,
    ): Promise<FacetValue[]>;
    getMapping(): Promise<Record<string, unknown>>;
    rawQuery(body: Record<string, unknown>): Promise<Record<string, unknown>>;
    }
    MethodPurpose
    searchFull-text search with pagination, facets, filters, boosts, highlights, histograms, and geo grid. Return a SearchResult.
    getDocumentFetch a single document by ID. Return null if not found.
    searchFacetValuesSearch within a facet field’s values by name. Used for autocomplete-style facet filtering.
    getMappingReturn the index schema/mapping. Structure depends on the engine.
    rawQueryPass a raw query body to the engine. The body shape is engine-specific.

    Your constructor receives the full IndexConfig and should extract what it needs (host, auth, index name).

    For engines with a completely different API model, implement SearchEngine directly — see src/engines/meilisearch.ts.

  2. Register the engine

    Add your engine to the factory map in src/engines/index.ts:

    import { TypesenseEngine } from "./typesense.ts";
    const engineFactories: Record<string, (config: IndexConfig) => SearchEngine> = {
    elasticsearch: (config) => new ElasticsearchEngine(config),
    opensearch: (config) => new OpenSearchEngine(config),
    meilisearch: (config) => new MeilisearchEngine(config),
    typesense: (config) => new TypesenseEngine(config),
    };
  3. Update the config schema

    Add the new engine name as a valid literal in src/config.ts:

    const IndexConfigSchema = t.Object({
    engine: t.Union([
    t.Literal("elasticsearch"),
    t.Literal("opensearch"),
    t.Literal("meilisearch"),
    t.Literal("typesense"),
    ]),
    // ...
    });

    Also update schemas/config.schema.json to include the new engine in the enum:

    "engine": {
    "type": "string",
    "enum": ["elasticsearch", "opensearch", "meilisearch", "typesense"]
    }