Solr Search in SAP Commerce: Architecture and Indexing Done Right
SAP Commerce search indexing: indexed types, full and partial runs, two-phase commits, value providers, and safe scheduling.
Search is where most storefront revenue decisions actually happen: a customer who searches converts at a multiple of one who browses, and a customer who gets "no results" for a product you stock is a refund you never got the chance to lose. Under the storefront, SAP Commerce delivers search through Apache Solr, with the platform contributing the indexing pipeline, business tooling, and query integration. This guide covers the half of the system that runs before any customer types a query: getting data into the index correctly and efficiently. A companion guide covers query tuning and troubleshooting.
The Shape of the System#
The database is normalized: products, prices, stock, classification attributes, and media live in separate tables connected by relations. Solr stores denormalized documents: one document per searchable item, carrying every field that search, faceting, or the listing page needs. Indexing is the transformation between the two worlds, and almost every search problem (stale prices, missing products, slow indexing, wrong facets) is a problem in that transformation.
The platform pieces:
- Indexed types: any composed type can become a Solr indexed type. Products ship preconfigured; custom types (stores, articles, FAQs) are added through Solr Item Type Management in Backoffice or ImpEx. One deliberate practice: give Backoffice its own indexed type even for the same composed type, because back-office search (find items to edit, all catalog versions) and storefront search (find products to buy, online only) have different field lists, different queries, and different freshness needs.
- Indexer queries: FlexibleSearch queries per indexed type that select what gets indexed, configured under Facet Search Config, Indexed Types, Indexer Queries.
- Value providers: the converters of the indexing world, turning models into document field values.
- Identity providers: define document identity in the index; the default works for standard product cases.
- Results converters: transform Solr documents back into DTOs for the storefront, so listing pages render from the index without touching the database.
On CCv2 the Solr infrastructure is provisioned for you (a ZooKeeper ensemble plus Solr nodes), the Solr version is pinned per commerce update release, and your solrconfig.xml and schema customizations ship from the repository via the manifest's useConfig.solr block. Version compatibility is the first checkpoint of every platform upgrade: check the third-party compatibility matrix before assuming your Solr customizations carry over.
Indexing Strategies: Full, Update, Delete#
Three operations exist, and a healthy project uses all three on different schedules.
Full indexing rebuilds from scratch: delete everything, re-export, re-index. On a large catalog it is expensive, so it typically runs once a day, immediately after catalog synchronization completes (indexing before sync finishes indexes the old catalog and is a classic scheduling bug). Full indexing supports two commit strategies, and the choice matters:
| Strategy | Behaviour | When |
|---|---|---|
| Direct | Documents are added or updated in place. On failure, previously committed documents survive, but the index is left in a mixed state. Replication pauses during the run | Standalone master/replica setups |
| Two-phase | Indexing writes into a shadow core; on success an atomic swap brings the new index online, on failure everything rolls back | The safe default, and strongly recommended on Solr Cloud |
The Solr Cloud detail that makes two-phase non-negotiable there: cloud nodes stay in sync, so a direct-mode full index deletes documents across all nodes at the start of the run. If the rebuild then takes forty minutes, your storefront searches an empty or partial index for forty minutes. Two-phase keeps the old index serving until the new one is complete.
Update indexing re-indexes only documents changed within a window. It does not pause replication, runs fast, and is the workhorse for freshness. The pattern: an update job every 5 to 15 minutes whose indexer query selects deltas:
SELECT {p:PK} FROM {Product AS p}
WHERE {p:modifiedtime} >= ?lastIndexTime
Delete indexing removes documents. Remember that update indexing never removes anything: a product that went offline stays in the index until either the nightly full index rebuilds without it or a delete job takes it out. If products deactivate during the day and must vanish from search promptly, you need the delete job on a schedule, not just the full rebuild at 4 a.m.
Partial Updates: Fast Freshness for Volatile Fields#
Default update indexing recreates the whole document: all 30 properties, all value providers, even if only the price changed. Partial indexing updates named properties only, leaving the rest of the document untouched. For the classic volatile pair, price and stock, this is dramatically cheaper: two value providers run instead of thirty, and the database sees a fraction of the queries.
Design rules for partial updates, learned the hard way:
- Pick properties that change often and independently: price, stock, promotion flags. Classification data that changes twice a year gains nothing from partial machinery.
- The delta query must actually catch the change. A price edit does not touch
Product.modifiedtimeunless something marks the product. Your indexer query for a price partial update has to detect PriceRow changes (join or subselect on the price table's modified time), or the job will run happily and index nothing. - Removals are the trap. Partial update writes values the provider produces; when data is deleted, the provider often produces nothing, and the old value survives in the document. Concretely: remove a product's JPY price entirely and
priceValue_jpy_doublekeeps its stale value, still visible to Japanese shoppers. Either avoid true removals (set price to zero or set an end date so the provider emits an explicit null-set for the field) or route removals through a full document update. - Mind the limitations: partially updated fields must be
stored="true"in the schema, and partial updates must not target fields used for spellchecking and suggestions. - Agree the freshness SLA in requirements. "How stale may stock be?" has a business answer (say, 15 minutes), and that answer, not engineering taste, sets the partial update schedule.
Value Providers: Where Indexing Time Goes#
Every property of every document of every indexing run flows through a value provider, which makes providers the highest-multiplier code in the search stack. A provider that costs 5 extra milliseconds adds 40 minutes to a full index of half a million products with ten localized fields. Rules:
- Question whether you need a custom provider at all. The SpEL value provider covers a surprising range declaratively:
(#item instanceof T(de.hybris.platform.variants.model.VariantProductModel)) ? baseProduct.code : null
- No per-item queries. A FlexibleSearch inside
getFieldValues()runs once per product; on a big catalog that is hundreds of thousands of queries. Providers that need related data should load it in bulk (cached lookup tables, preloaded maps) rather than querying per item. - Unit test providers like the hot path they are, including null cases (product without price, variant without base) because indexing runs meet every data pathology your catalog contains.
- Run indexer queries as admin, restrict at query time. Trying to encode visibility rules into indexing (running indexer queries as restricted users) produces an index you can never share across use cases. Index everything the storefront could ever show, and let search restrictions and query filters enforce visibility per request.
Scheduling: The Daily Rhythm That Works#
The reference cadence for a production catalog:
| Job | Schedule | Notes |
|---|---|---|
| Catalog sync | Nightly, low traffic | Prerequisite for full index |
| Full index (two-phase) | Nightly, triggered after sync completes | As few runs per day as freshness allows |
| Update index | Every 5 to 15 minutes | Delta query on ?lastIndexTime |
| Partial update (price/stock) | Every 5 to 60 minutes, per SLA | Only if these fields change intraday |
| Delete job | Hourly or as needed | Only removals; full index also cleans up nightly |
Chain the full index to the sync job's completion (a composite cronjob or trigger-on-success), never to a fixed clock time: the night the sync runs long is exactly the night a clock-scheduled index would index half a catalog. All indexing jobs belong on the background processing aspect on CCv2, pinned via node groups.
What to Verify Before Calling Search "Done"#
- Two-phase commit for full indexing (mandatory on Solr Cloud), and the storefront keeps serving during a full rebuild
- Update job catches every change class it claims to (product edits, price changes, stock movements), verified by editing an item and watching the document
- A deactivated product disappears from search within the agreed window (delete job or documented acceptance of the nightly rebuild)
- Partial update removal semantics tested with actual data deletion, not just value changes
- Full index duration measured and trending; a run that grew from 40 to 90 minutes is telling you about a provider or the database
- Separate indexed type for Backoffice search configured
- Indexing jobs pinned to background processing and sequenced after catalog sync
Indexing is unglamorous exactly the way plumbing is: invisible while correct, catastrophic while wrong. Get the document freshness rules and the commit strategy right, keep value providers cheap, and the query-side tuning in the companion guide has a solid foundation to stand on.