Skip to content

search-algolia

@evolve-framework/search-algolia · v1.0.0

Algolia as the search provider for an Evolve shop. It has two parts that deploy separately. The index adapter writes the catalog into Algolia, and the query adapter answers product search from it, with facets, sorting and Algolia's personalization. Both plug into @evolve-framework/search, so the indexing and the storefront's queries work the same as with any other provider.

When to use

  • A shop's product search and product listings come from Algolia. Category search is not implemented here yet, and the adapter reports it as unsupported. The commercetools CatalogModule then answers category search from commercetools itself.
  • An indexer service keeps Algolia in step with commercetools or another catalog: give its SearchIndexerModule an AlgoliaIndexAdapter.
  • You are moving off @evolve-framework/commercetools/algolia, which this package replaces. The stored record shape is unchanged, so moving needs no reindex.

Get started

  1. Install the package with the peer dependencies it needs. It comes from the Evolve registry, which needs a one-time sign-in.

    pnpm add @evolve-framework/search-algolia @evolve-framework/core @evolve-framework/search
    Requirements3
    @evolve-framework/core>= 1.0.0 < 2.0.0
    @evolve-framework/search>= 0.2.0 < 2.0.0
    @types/node>=22optional

For example:

import { AlgoliaIndexAdapter } from "@evolve-framework/search-algolia/indexing";
import { AlgoliaQueryAdapter } from "@evolve-framework/search-algolia/query";
const appId = process.env.ALGOLIA_APP_ID ?? "";
// The indexer writes, so it holds the write key.
export const indexAdapter = new AlgoliaIndexAdapter({
appId,
writeApiKey: process.env.ALGOLIA_WRITE_API_KEY ?? "",
});
// The service answering product search only reads, with a search-only key.
export const queryAdapter = new AlgoliaQueryAdapter({
appId,
searchApiKey: process.env.ALGOLIA_SEARCH_API_KEY ?? "",
});

Adapters

AlgoliaIndexAdapter

Writes documents to Algolia.

Every method throws on failure rather than logging and returning: a swallowed error acks the queue message that triggered the write, losing the update with no trace.

import { AlgoliaIndexAdapter } from "@evolve-framework/search-algolia";
new AlgoliaIndexAdapter(options: AlgoliaIndexAdapterOptions<TRecord>)
OptionTypeDescription
appIdstring

The Algolia application.

writeApiKeystring

A key allowed to write and configure indices. Never give it a browser.

resolveIndexName?IndexNameResolver

Override index naming to match an existing convention. Defaults to defaultIndexName.

toRecord?((document: ProductDocument, scope: IndexScope) => TRecord | TRecord[])

Override how a document becomes stored records. Returning several records fans one document out; the sync engine's deleteBySource still cleans them up. Defaults to toAlgoliaRecord.

More

The scope is passed so a record can be written for the target it is going to — flattening localized values to the index's language, say.

batchSize?number

Algolia accepts up to 1000 objects per batch.

atomicReindex?boolean

Rebuild through a temporary index that is moved into place. Defaults to true, and there is rarely a reason to turn it off — a move to a destination that has replicas is supported and propagates the new data to them.

More

Setting it to false does not make this adapter rebuild in place: it drops beginFullReindex and clears capabilities.fullReindex, leaving the sync engine to write over the live index. That is visible to shoppers while it runs, and it leaves behind documents that no longer exist, because nothing clears the index first.

MethodDescription
init(): Promise<void>One-shot, idempotent initialization (build SDK clients, etc.).
upsertDocuments( scope: IndexScope, documents: ProductDocument[], ): Promise<void>Write documents into the scope's target for each one's type, replacing any stored document with the same id as a whole.
deleteDocuments( scope: IndexScope, ids: string[], documentTypes: string[], ): Promise<void>Remove ids from every declared type's index.
deleteBySource( scope: IndexScope, sourceIds: string[], options: DeleteBySourceOptions, ): Promise<void>Remove every record a product owns, except the ids just written, across every declared type's index.
applySettings(scope: IndexScope, settings: unknown): Promise<void>Push provider index configuration (Algolia settings and replicas, …).

AlgoliaQueryAdapter

Reads documents back out of Algolia.

Separate from the index adapter so an API deployment holds only a search key.

import { AlgoliaQueryAdapter } from "@evolve-framework/search-algolia";
new AlgoliaQueryAdapter(options: AlgoliaQueryAdapterOptions<TRecord>)
OptionTypeDescription
appIdstring

The Algolia application.

searchApiKeystring

A search-only key: this adapter never writes.

resolveIndexName?IndexNameResolver

Defaults to defaultIndexName.

toDocument?((record: TRecord, locale: string) => ProductDocument)

Inverse of the index adapter's toRecord. Defaults to toProductDocument.

More

The request's locale is passed so a record whose localized values were flattened for its index can be read back under the locale being asked for.

buildQuery?((queries: SearchForHits[], query: ProductSearchQuery) => SearchForHits[])

Adjust the queries before they are sent — rule contexts, optional filters, distinct, analytics tags, personalization. Receives the defaults so it can extend rather than replace them.

distinct?number | boolean

Deduplicate hits by the index's attributeForDistinct.

More

Required by the variant-record model: with a record per variant, a search without this returns every variant of a product as its own row. Set it to true (one per group) or a count. Leave unset when records are per product.

scopeGranularity?"language" | "locale" | "store"

Must match the scopeGranularity of the document source the index is built from, such as CommercetoolsCatalogSource, so both halves resolve the same index name.

More

"language" (the default) and "locale" both read from an index named after the requested locale — which name resolver is configured decides whether the region survives into it. "store" reads from one index holding every language, which also means the query needs a language filter and per-request queryLanguages — see buildQuery.

paginationLimitedTo?number

The index's paginationLimitedTo setting, when it was changed from Algolia's default of 1,000: hits past it cannot be paged to.

clickAnalytics?boolean

Ask Algolia for a query id on every search, which click and conversion events send back so Insights attributes them to the search. Off by default, as Algolia only issues one when asked.

querySuggestions?boolean

Read suggestions from the index's Algolia Query Suggestions index, named by resolveIndexName with suggestions: true. A search that finds nothing then tries the top suggestion instead. Off by default, as the index has to be set up in Algolia first.

MethodDescription
init(): Promise<void>Create the Algolia client from appId and searchApiKey.
search( query: ProductSearchQuery, ): Promise<SearchResponse<ProductDocument>>Run a query and return the documents that matched.
suggest(query: SuggestQuery): Promise<SuggestResponse>Implement when capabilities.suggestions is true.

Entry points

@evolve-framework/search-algoliaBoth adapters, the index naming (defaultIndexName, localeIndexName), and the record mappers (toAlgoliaRecord and toProductDocument, toVariantRecord and fromVariantRecord)30 exports
@evolve-framework/search-algolia/indexingAlgoliaIndexAdapter2 exports
@evolve-framework/search-algolia/queryAlgoliaQueryAdapter, plus buildQueries and mapFacets for a query adapter of your own5 exports