PETPET · Search and product variants
Why I stopped rebuilding product groups during search
Search could find the right variant and still spend too long figuring out how it belonged on screen. I moved that repeated catalog assembly out of the request and made the API read a group that was already prepared.
October 30, 2026 · 5 min read
One product on screen can be several records underneath
Think of the same food sold in a small bag, a large bag, and a different flavor. The app wants to present those as one product with choices. In the catalog, each choice is a SKU, while the shared product family is an SPU.
Search often finds a SKU first. Before the API can return the card the app expects, it has to work out which family that SKU belongs to, gather the other eligible choices, pick a representative variant, and assemble the group’s display information.
The request was doing catalog assembly over and over
The old path did that work after search had already found its candidates. It mapped products to their SPUs, loaded the SPU records, calculated group summaries, and fetched the member variants. For a typical grouping pass, that meant four MongoDB reads; attributes could add another read when the response needed them.
None of those reads was unreasonable on its own. The waste was repeating the same structural work for groups whose membership and display shape had not changed. The request was rebuilding information that could have been prepared when the catalog changed instead.
I gave the repeatable work to a worker
When an SPU or its members change, the system records a small event in an outbox. A dispatcher publishes that event to Pub/Sub, and a Cloud Run worker loads the affected catalog records and compiles a read model: the product-family details, eligible variants, default choice, attributes, and search grouping data.
The compiled result and a product-to-SPU index are stored separately from the source catalog. That gives Search a direct way to map the variants it just found to the prepared groups, without scanning and rebuilding each group from scratch on every request.
Search reads the group; it does not stop checking the product
The API looks up the SPUs for the current page in a batch, then loads their read models in another batch. Redis can serve warm read models; MongoDB remains the backing store. If a model is missing or the lookup fails, the rollout can fall back to the earlier grouping path while that behavior is still enabled.
Precomputing the group does not make the worker the authority on every fact a shopper sees. Availability, visibility, and other changeable product details still belong to the serving path when they need a current check. The read model saves the repeated assembly work; it is not a reason to return an old product state blindly.
The improvement was in a specific step
In the Search enrichment step, the MongoDB reads went from four to two for the typical grouping path: one batched lookup for the product-to-SPU mapping and one for the prepared groups. A Redis hit can avoid the second MongoDB read, but the important change was removing the repeated aggregation from the request—not claiming that every part of Search became twice as fast.
Keeping that scope clear mattered to me. A useful performance number should describe the stage that changed, not quietly turn into a claim about the entire endpoint.
What I kept from the old path
I did not want a missing read model to become a broken search result. The legacy path stayed available as a compatibility fallback during rollout, and the source product collections remained the source of truth. That meant I could move the expensive preparation earlier without making the new model a single point of failure.
The pattern I keep coming back to is simple: prepare stable relationships when data changes, then let the request focus on the page it needs to serve. Keep the facts that can change quickly close to the moment they are shown.