This document describes the high-level architecture of the Algolia Search & Discovery
extension for Magento 2 (Algolia_AlgoliaSearch). If you want to familiarize yourself
with the codebase, you are in the right place.
See also AGENTS.md for verification commands, testing, CI/CD, and code style.
The extension synchronizes Magento 2 catalog data (products, categories, CMS pages, search suggestions, and additional sections) into Algolia indices, manages sorting replicas, captures analytics events, and renders client-side search UI. It runs as a standard Magento module installed via Composer.
There are two main data flows:
Indexing moves data from Magento to Algolia. It comes in two flavors:
Delta (incremental):
Catalog/CMS change
-> mview subscription or Plugin/Observer
-> executeList($ids) on the Magento indexer
-> BatchQueueProcessor.handleDeltaIndex()
-> buildIndexList queue jobs
-> IndexBuilder -> RecordBuilder -> AlgoliaConnector
-> records updated in-place on the live index
Full (complete rebuild):
CLI command (algolia:reindex:*) or executeFull() if enabled
-> BatchQueueProcessor.handleFullIndex()
-> buildIndexFull queue jobs
-> IndexBuilder -> RecordBuilder -> AlgoliaConnector
-> records written to the live index (default)
OR to a temporary index (if temp indexing enabled)
-> IndexMover atomically swaps temp -> production (when temp index used)
Frontend delivers search configuration from the server to client-side JS:
Admin config (system.xml)
-> ConfigHelper + specialized helpers
-> Block/Configuration.php serializes to a JS config object
-> autocomplete.js, instantsearch.js, insights.js, recommend.js
-> client-side rendering (no server-side search results)
Business logic for indexing, organized by entity type. Each entity follows a consistent
pattern - search for IndexBuilder, RecordBuilder, and BatchQueueProcessor to find
the implementations for Product, Category, Page, Suggestion, and AdditionalSection.
AlgoliaConnector is the single gateway to the Algolia PHP API client. All API calls
flow through it. IndexSettingsHandler pushes index configuration and handles replica
setting forwarding. AbstractIndexBuilder provides store emulation so records reflect
the correct store context during indexing.
Magento models, the indexer layer, and the queue system.
Model/Indexer/ contains 7 Magento indexers (registered in etc/indexer.xml): Products,
Categories, Pages, Suggestions, AdditionalSections, QueueRunner, and DeleteProduct. It
also contains three observer-style classes (ProductObserver, CategoryObserver,
PageObserver) that intercept entity save/delete via Magento's plugin system. These are
functionally Magento plugins (interceptors) despite their "Observer" naming and
Model/Indexer/ location; a future refactor may relocate them under Plugin/.
Queue is the cron-driven async job queue backed by the algoliasearch_queue table.
Job defines the handler whitelist (ALLOWED_HANDLERS) that restricts which
class/method pairs can be executed from the queue. IndicesConfigurator orchestrates
pushing index settings across all entity types and stores.
Configuration hub. ConfigHelper (~2,100 lines) is the central reader for all admin
settings. Specialized sub-helpers live in Helper/Configuration/ (AutocompleteHelper,
InstantSearchHelper, QueueHelper, PersonalizationHelper) and entity helpers in
Helper/Entity/. Helper/Adapter/ bridges to Magento's catalog search adapter.
Magento interceptors declared in etc/di.xml. These hook into Magento core to trigger
reindexing on catalog changes (StockItemObserver for inventory, CacheCleanProductPlugin)
and to capture analytics data (QuoteItem for conversion tracking).
Event observers bound in etc/events.xml, etc/frontend/events.xml, and
etc/adminhtml/events.xml. The Insights/ sub-directory captures analytics events
(add-to-cart, purchase, wishlist). Admin observers like SaveSettings trigger index
settings pushes when configuration changes.
Frontend rendering. Block/Configuration.php is the critical bridge - it serializes all
server-side config into a JS object consumed by every frontend script.
Block/Instant/ and Block/Navigation/ handle InstantSearch and faceted navigation.
Bundled JS libraries: autocomplete.js, instantsearch.js, insights.js, plus
hooks.js for extensibility. Minified vendor libraries in web/js/lib/. Templates
in templates/ and layout XML in layout/.
Magento XML configuration. Key files:
di.xml- DI wiring, plugin declarations, proxy definitionsindexer.xml- 7 indexer registrationsmview.xml- materialized view subscriptions for delta indexing (~12 catalog tables)crontab.xml- queue processing cron jobdb_schema.xml- extension tables (see Database Tables) plus columns onquote_item/sales_order_itemadminhtml/system.xml- all admin settingsconfig.xml- default valuesevents.xml,frontend/events.xml,adminhtml/events.xml- observer bindings
CLI commands organized by concern: Indexer/ (reindex per entity or all), Queue/
(process, clear), Replica/ (sync, rebuild, delete, disable-virtual), plus
SynonymDeduplicateCommand and BatchingOptimizeCommand.
Interface contracts. Builder/ defines IndexBuilderInterface, RecordBuilderInterface,
and BatchQueueProcessorInterface - the consistent per-entity pattern. Data/ holds data
transfer object interfaces. Insights/ and Product/ define service contracts.
ProcessQueue.php - the cron entry point that calls Queue.runCron().
Domain-specific exceptions that control indexing flow: ProductDisabledException,
ProductOutOfStockException, CategoryNotActiveException,
ReplicaLimitExceededException, etc.
The extension supports two distinct indexing modes. Understanding the distinction is critical for anyone working on the indexing pipeline.
Incremental, event-driven. When a product, category, or CMS page changes, one of two mechanisms detects it:
-
Materialized views (
etc/mview.xml) subscribe to ~12 catalog tables. When the indexer is in "Update by Schedule" mode, Magento collects changed entity IDs and passes them toexecuteList($ids). -
Plugin/Observer interceptors (ProductObserver, CategoryObserver, PageObserver) fire on entity save/delete and call
reindexRow($id)directly when the indexer is not in scheduled mode.
In both cases, BatchQueueProcessor.handleDeltaIndex() enqueues buildIndexList jobs
containing only the changed entity IDs. Records are updated in-place on the live index.
A complete rebuild of an entire entity index. This is triggered in two ways:
-
CLI commands (
algolia:reindex:products,algolia:reindex:all, etc.) - the recommended approach. -
Magento's
executeFull()on the registered indexers - disabled by default (see below).
Since v3.17, temporary indexing is optional. When enabled (requires the queue to be
active and the temp index setting turned on via QueueHelper::useTmpIndex()), full
indexing builds into a temporary index, then IndexMover atomically swaps it into
production so the live index is never in a partial state. When disabled (the default),
records are written directly to the live index during full reindex.
Magento's indexer framework exposes executeFull() on all registered indexers, and any
process can invoke it - not just Magento core, but also third-party extensions, ERP
integrations, and CLI tools that call reindexAll. This happens regardless of whether
the indexer is in "Update on Save" or "Update by Schedule" mode.
Before v3.16, this meant any of these callers could inadvertently trigger expensive full
Algolia reindexes, flooding the queue and starving delta jobs. The queue allocates only
33% of processing capacity to full reindex jobs (FULL_REINDEX_TO_REALTIME_JOBS_RATIO
in Queue), so uncontrolled full reindexes degraded freshness of incremental updates.
Since v3.16, each indexer's executeFull() has a guard clause (e.g.,
ConfigHelper::isProductsIndexerEnabled()). As of v3.18, all guards default to disabled
in etc/config.xml. Full reindexing should be an intentional, scheduled operation
invoked via the Algolia CLI commands.
Defined in etc/db_schema.xml:
| Table | Purpose |
|---|---|
algoliasearch_queue |
Pending indexing jobs |
algoliasearch_queue_log |
Per-run execution logs |
algoliasearch_queue_archive |
Completed/failed job archive |
algoliasearch_landing_page |
Built-in merchandising landing pages (see Cross-Cutting Concerns) |
algoliasearch_query |
Search query data for suggestions |
Additionally, quote_item and sales_order_item gain columns for Insights analytics tracking.
All indexing operations flow through a cron-driven async queue.
Jobs are persisted to algoliasearch_queue and processed by Cron/ProcessQueue.php.
Each job references a class and method that must appear in Job::ALLOWED_HANDLERS - a
hardcoded whitelist that prevents arbitrary code execution if the queue table is
compromised. Execution logs go to algoliasearch_queue_log; completed or failed jobs
are archived to algoliasearch_queue_archive.
The queue processes full reindex and delta jobs in a mixed ratio
(FULL_REINDEX_TO_REALTIME_JOBS_RATIO = 0.33), ensuring delta updates get at least 67%
of each processing cycle.
Every index is scoped to a single Magento store. Configuration and optionally credentials can differ per store.
The indexer layer iterates stores explicitly - search for the store iteration loop in any
Model/Indexer/ class. $storeId is threaded through nearly every Service method.
AbstractIndexBuilder handles store emulation so product data (prices, attributes,
visibility) reflects the correct store context.
Index names follow a consistent convention built by IndexNameFetcher:
| Component | Pattern | Example |
|---|---|---|
| Base | {prefix}{storeCode} |
magento_default |
| Entity index | {base}_{entitySuffix} |
magento_default_products |
| Temp index | {entityIndex}_tmp |
magento_default_products_tmp |
| Query suggestions (Magento) | {base}_query_suggestions |
magento_default_query_suggestions |
The prefix is configured via ConfigHelper::getIndexPrefix().
Starting with v3.18, the extension also supports
Algolia Query Suggestions
as an alternative to the built-in Magento suggestions index. When the "Use Algolia Query
Suggestions" mode is selected in the autocomplete configuration, the extension references
an index name configured directly in the Algolia dashboard rather than using the
_query_suggestions suffix convention. The Magento suggestions mode (which populates from
the search_query table) is now deprecated in favor of this approach.
Replica index naming:
| Sort type | Pattern | Example |
|---|---|---|
| Non-price | {primary}_{attribute}_{direction} |
magento_default_products_name_desc |
| Price (default) | {primary}_price_default_{direction} |
magento_default_products_price_default_asc |
| Price (customer group) | {primary}_price_group_{groupId}_{direction} |
magento_default_products_price_group_2_asc |
All Magento-managed replica names are prefixed with the primary index name. This is how
ReplicaManager distinguishes Magento replicas from non-Magento replicas (e.g., those
created in the Algolia dashboard or Merchandising Studio).
Algolia replicas enable sorting on attributes beyond the primary index's default ranking. The extension manages two types:
- Virtual replicas are lightweight references to the primary index where only
customRankingdiffers. They are more efficient and allow more replicas per index, but are subject to a configurable limit (default 20). - Standard replicas are full index copies with a completely replaced ranking configuration. They are more resource-intensive in Algolia.
The type is determined per sort attribute in the admin sorting configuration.
Customer group multiplication. Most sort attributes produce one replica per direction (asc/desc) - a straightforward one-to-one mapping. Price sorting is different. When customer groups are enabled and a price sort is configured, one replica is created for each customer group in the store's website. A single price sort config line produces N replicas where N is the number of non-excluded customer groups. This one-to-many relationship can quickly approach the replica limit.
Ownership boundary. ReplicaManager only manages replicas whose names are prefixed
with the primary index name. During sync, it merges Magento-managed replicas with any
non-Magento replicas before writing the replicas setting on the primary index. This
preserves replicas created externally (Algolia dashboard, Merchandising Studio).
Lifecycle. Replica config changes are detected via a registry (ReplicaState) that
captures before/after state when admin config is saved. On the next product indexing run,
ReplicaManager compares the current Magento sorting config to what Algolia has,
calculates a diff, and applies add/update/delete operations. Ranking is configured per
replica type: virtual replicas prepend the sort attribute to customRanking inherited
from the primary; standard replicas replace the full ranking array.
Validation. Replica count is validated against the limit before sync. Price-sort replicas are tracked separately to detect customer-group-driven overflow. On failure, the system attempts to revert config to the previous state.
The implementation lives primarily in Service/Product/ (ReplicaManager,
SortingTransformer) with change detection in Registry/ReplicaState and config hooks
in Model/Backend/.
-
Queue-first indexing. All indexing is async by default. This avoids blocking Magento admin operations and allows batch optimization.
-
Opt-in full reindexing. Magento's
executeFull()is guarded and disabled by default since v3.18. Any process can invokereindexAllon Magento's indexer framework, so full reindexing must be intentional via Algolia CLI commands to prevent uncontrolled expensive operations. -
Handler whitelist.
Job::ALLOWED_HANDLERSprevents arbitrary code execution from the queue. New queue handlers must be registered here or they are silently rejected. -
Centralized configuration. All admin settings are read through
ConfigHelperor its sub-helpers. No directScopeConfigInterfacereads elsewhere in the codebase. -
Entity-based service organization. Each indexable entity gets its own Service sub-directory with a consistent set of classes: IndexBuilder, RecordBuilder, BatchQueueProcessor. New entities should follow this pattern.
-
Single API gateway.
SearchClientinstantiation is managed bySearchClientProvider(viaSearchClientProviderInterface). All index operations go throughAlgoliaConnector, which delegates client access to the provider. Other classes never instantiateSearchClientdirectly or bypass the connector for index operations. Other Algolia API clients (e.g.,IngestionClient) follow the same provider pattern with their own dedicated providers. -
Frontend config bridge. Server-side config is serialized once in
Block/Configuration.php. There is no REST API for frontend config - JS reads from a script block injected into the page. -
Replica forwarding. Sorting replica settings are forwarded from the primary index via
IndexSettingsHandler, not configured independently.ReplicaManagerhandles the replica lifecycle. See Sorting Replicas for details.
These are properties the codebase maintains by convention.
- No direct
SearchClientinstantiation outsideSearchClientProvider. Each Algolia API client type has its own dedicated provider. - No direct Algolia index operations outside
AlgoliaConnector. - No direct
ScopeConfigInterfacereads outsideHelper/. - No REST/GraphQL endpoints for indexing control - indexing is triggered only through Magento's indexer framework, CLI commands, or the admin UI.
- No server-side rendering of search results - all search rendering is client-side JS.
- No cross-store index sharing - each store gets its own set of indices.
- No Magento model instance is stored in the backend session - persist a scalar id (or a plain array) and reload the entity where it's needed.
Store scoping. Every public method in the Service layer that touches indices accepts
$storeId. Never assume a single-store context.
Proxy pattern. Heavy services are wrapped in Magento Proxy classes for lazy loading.
Search for \Proxy in etc/di.xml to see which.
Events and extensibility. The extension fires Magento events at key points in the
indexing pipeline - external modules can observe these to modify records before they're
sent to Algolia. Frontend JS can be extended via hooks.js.
Insights/Analytics. A parallel subsystem (Service/Insights/, Observer/Insights/,
view/frontend/web/js/insights/) captures click, conversion, and view events. It runs
independently of the indexing pipeline.
Landing pages. Built-in merchandising via the algoliasearch_landing_page table and
LandingPageHelper. This subsystem is under reconsideration and may be deprecated or
reworked in a future release.
- Follow Magento 2 Coding Standards strictly
- Use Service Contracts for all public APIs
- Dependency Injection instead of
ObjectManager - Repository Pattern for data access
- Factory Pattern for object creation
- Plugin Pattern preferred over preferences/rewrites
Adding a new indexable entity:
- Create
Service/{Entity}/IndexBuilder,RecordBuilder,BatchQueueProcessor - Register the IndexBuilder methods in
Job::ALLOWED_HANDLERS - Add an indexer in
etc/indexer.xmland mview subscriptions inetc/mview.xml - Add a guard clause and config flag for full indexing (see existing indexers)
Adding admin configuration:
- Define the field in
etc/adminhtml/system.xml - Add a getter in
ConfigHelperor the appropriate sub-helper inHelper/Configuration/
Adding queue handlers:
Register the class and method in Job::ALLOWED_HANDLERS or the job will be silently
rejected at execution time.
- Instantiating the Algolia PHP client directly - use
AlgoliaConnector. - Reading config via
ScopeConfigInterface- useConfigHelperor a sub-helper. - Hardcoding store IDs or assuming single-store operation.
- Bypassing the queue for indexing operations.
- Storing a Magento model instance in the backend session.
Model\Jobcarries an injectedObjectManagerInterfacefor its handler dispatch, so serializing it atsession_write_close()fatals with "Serialization of 'Closure' is not allowed."