Skip to main content

Overview

This guide walks you through building a custom search provider for Spree. By the end, you’ll have a fully functional search integration that:
  • Powers product search, filtering, sorting, and faceted navigation
  • Handles multi-locale and multi-currency indexing automatically
  • Integrates with the Store API without any frontend changes
  • Supports background indexing and bulk reindex
Before starting, make sure you understand how search and filtering works in Spree.
Spree ships with a built-in Meilisearch provider. If Meilisearch fits your needs, you don’t need to build a custom provider — just configure it.

Architecture

The controller builds a base ActiveRecord scope for security and visibility, then delegates everything else to your search provider.

Step 1: Create the Provider Class

Create a class that inherits from Spree::SearchProvider::Base and implements search_and_filter:
app/models/my_app/search_provider/typesense.rb
indexing_required? returning true tells the SearchIndexable concern to enqueue background jobs when products are created, updated, or destroyed.

Step 2: Implement search_and_filter

This is the core method. It receives a base AR scope (already filtered for security) and must return a SearchResult:
app/models/my_app/search_provider/typesense.rb
Always filter by locale, currency, store_ids, status='active', and discontinue_on in your search engine — not just in the AR scope. This ensures pagination counts are accurate. The AR scope is a safety net, not the primary filter.

Step 3: Implement Indexing

Products are indexed as one document per market × locale combination. The ProductPresenter handles this automatically:
app/models/my_app/search_provider/typesense.rb
The ProductPresenter returns an array of documents. For a store with US (USD/English) and EU (EUR/German+French) markets, one product produces 3 documents — each with flat name, price, locale, currency fields.

Step 4: Implement Bulk Reindex

Use preload_associations_lazily to avoid N+1 queries:
app/models/my_app/search_provider/typesense.rb
Use flat_map (not map) because ProductPresenter#call returns an array of documents per product.

Step 5: Register the Provider

config/initializers/spree.rb
Then reindex:

Provider Contract Reference

SearchResult

ProductPresenter

Indexing Lifecycle

The Spree::SearchIndexable concern on Product provides: Background jobs (IndexJob, RemoveJob) fire on after_commit when indexing_required? is true.

Important: Prefixed IDs

Always use prefixed IDs (ctg_abc, prod_xyz, optval_abc) when indexing. Never use raw database IDs — Spree supports UUID primary keys.