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 that depends on the core and schemas layers:
packages/search-typesense/
├── src/
│ ├── index.ts
│ ├── module.ts
│ ├── search-provider.ts
│ ├── mappers/
│ │ └── product.ts
│ └── client.ts
└── package.json
{
"name": "@evolve-packages/search-typesense",
"dependencies": {
"@evolve-framework/core": "workspace:*",
"@evolve-framework/schemas": "workspace:*",
"@evolve-framework/commercetools": "workspace:*",
"typesense": "^1.8.0"
}
}
2. Implement a SearchProvider
The catalog module delegates search to a SearchProvider abstraction. Create
a subclass that implements search against your engine:
// src/search-provider.ts
import { SearchProvider } from "@evolve-framework/commercetools";
export class TypesenseSearchProvider extends SearchProvider {
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
ProductsResult type defined in @evolve-framework/schemas.
3. Create the GraphQL module
Subclass CatalogGraphQLModule and override the search-related resolvers.
The productSearch resolver dispatches to your search provider based on the
module config:
// src/module.ts
import { CatalogGraphQLModule } from "@evolve-framework/commercetools";
import { TypesenseSearchProvider } from "./search-provider.ts";
export class TypesenseCatalogModule extends CatalogGraphQLModule {
getResolvers() {
const resolvers = super.getResolvers();
const provider = new TypesenseSearchProvider();
resolvers.Query.productSearch = async (_parent, args, context) => {
return provider.searchProducts(context, args);
};
return resolvers;
}
}
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 server.ts, swap the default module for your new
one and pass the search engine configuration:
// backend/services/catalog-commercetools/src/server.ts
import { GraphQLCompositeModule } from "@evolve-framework/commercetools";
import { TypesenseCatalogModule } from "@evolve-packages/search-typesense";
const module = new GraphQLCompositeModule([
new TypesenseCatalogModule({
config: {
searchEngine: "typesense",
typesenseHost: config.TYPESENSE_HOST,
typesenseApiKey: config.TYPESENSE_API_KEY,
},
}),
]);
The config is passed through to 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/.
Further reading
- Framework architecture for the three-layer design and module system
- Customization for overriding resolvers and subclassing modules
- Messaging and events for product sync through the event bus