Skip to content

New: AI agent integration via Model Context Protocol (MCP).Learn more

Adding a search engine

Evolve’s three-layer architecture separates schema definitions from implementation. The catalog module defines search interfaces (product search, facets, categories) in @evolve-framework/schemas, while the actual search logic lives in an implementation package. This guide walks through creating a new implementation for a search engine such as Typesense, Meilisearch, or Elasticsearch.

  1. Create the implementation package

    Create a new package in your project repository that depends on the core and schemas layers:

    • Directorypackages/search-typesense/
      • Directorysrc/
        • index.ts
        • module.ts
        • search-provider.ts
        • Directorymappers/
          • product.ts
        • client.ts
      • package.json
    {
    "name": "@evolve-packages/search-typesense",
    "dependencies": {
    "@evolve-framework/core": "catalog:",
    "@evolve-framework/schemas": "catalog:",
    "@evolve-framework/commercetools": "catalog:",
    "typesense": "^1.8.0"
    }
    }

    The @evolve-framework/* packages are published to the private registry and pinned centrally in pnpm-workspace.yaml under catalog:, so the package uses the catalog: protocol instead of version numbers.

  2. Implement a SearchProvider

    The catalog module delegates search to a SearchProvider abstraction, defined in @evolve-framework/commercetools (src/lib/search/base.ts), with the built-in Algolia and commercetools providers as reference implementations. Create a provider with the same shape that implements search against your engine:

    src/search-provider.ts
    export class TypesenseSearchProvider {
    async searchProducts(context, args) {
    const client = this.createClient(context.config);
    const results = await client.collections("products").documents().search({
    q: args.query,
    filter_by: buildFilters(args.filters, context.storeContext),
    sort_by: mapSortOrder(args.sort),
    page: args.page,
    per_page: args.pageSize,
    facet_by: args.facets?.join(","),
    });
    return {
    results: results.hits.map(mapSearchHitToProduct),
    total: results.found,
    facets: mapFacets(results.facet_counts),
    };
    }
    async searchCategories(context, args) {
    // Implement category search
    }
    private createClient(config: Record<string, unknown>) {
    return new Typesense.Client({
    nodes: [{ host: config.typesenseHost, port: 443, protocol: "https" }],
    apiKey: config.typesenseApiKey,
    });
    }
    }

    Note that the results field (not items) and facets field match the SearchResult type defined alongside the SearchProvider abstraction.

  3. Create the GraphQL module

    Subclass CatalogModule and override the search-related resolvers. CatalogModule builds its resolver map in its constructor, so replace the productSearch resolver after calling super:

    src/module.ts
    import {
    CatalogModule,
    type ClientFactory,
    } from "@evolve-framework/commercetools";
    import { TypesenseSearchProvider } from "./search-provider.ts";
    export class TypesenseCatalogModule extends CatalogModule {
    constructor(options: { clientFactory: ClientFactory }) {
    super(options);
    const provider = new TypesenseSearchProvider();
    this.resolvers.Query.productSearch = async (_parent, args, context) => {
    return provider.searchProducts(context, args);
    };
    }
    }

    This replaces only the search resolvers. Product detail pages, categories, and other catalog queries continue to use the default commercetools resolvers.

  4. Wire into the catalog service

    In the catalog service’s module.ts, swap the default module for your new one:

    backend/services/catalog-commercetools/src/module.ts
    import { CompositeModule } from "@evolve-framework/core";
    import { TypesenseCatalogModule } from "@evolve-packages/search-typesense";
    import { clientFactory } from "#src/commercetools.ts";
    export const createModule = (): CompositeModule => {
    return new CompositeModule([new TypesenseCatalogModule({ clientFactory })]);
    };

    The search engine configuration goes through the per-request config object built in the service’s src/context.ts:

    backend/services/catalog-commercetools/src/context.ts
    config: {
    searchEngine: "typesense",
    typesenseHost: config.TYPESENSE_HOST,
    typesenseApiKey: config.TYPESENSE_API_KEY,
    },

    This is exposed as context.config so your search provider can access the credentials at request time.

  5. Sync product data

    Search engines need a product feed. Evolve publishes product-published events when products change. Create an event handler that indexes products into your search engine. See custom events for how to wire a handler.

  6. Add credentials to Terraform

    Store the search engine API key and host as secrets and inject them as environment variables. Follow the patterns in the existing Algolia configuration in terraform/.