# Product Search for B2B catalogs In [Design B2B catalogs](/learning-model-b2b-commerce/design-b2b-catalogs/overview.md) you used Product Selections and Stores to control which Products a buyer sees. Each buyer also trades through a distribution Channel that carries their negotiated Price, which you configure in [Configure B2B pricing](/learning-model-b2b-commerce/configure-b2b-pricing/overview.md); for search, what matters is that this Channel is the key you scope prices by. Search is where those two decisions become visible to the buyer: the same query, run for Horizon Hotels and for Pacific Property Group, must return each buyer's assortment at each buyer's price. This page shows how to express that scoping with Product Search. For the storefront mechanics of Product Search, such as faceting, filtering, and pagination, see the [Implement product discovery and presentation](/learning-implement-product-discovery-and-presentation/build-product-listing-pages/overview.md) path. ## Use the GraphQL productsSearch query Product Search is ID-first, so a raw search returns Product IDs and you fetch the data to render separately. The recommended way to do both in one request is the GraphQL [`productsSearch`](/api/graphql.md) query: you search and select only the fields you need for the catalog page in a single call, which keeps payloads small. The older REST data-integration path, the `productProjectionParameters` field on `ProductSearchRequest` (the `ProductSearchProjectionParams` type, with `priceCurrency`, `priceChannel`, and `priceCustomerGroup`), was **deprecated in December 2025**. Use the GraphQL `productsSearch` query for new B2B implementations. The `productsSearch` query is currently in [public beta](/offering/compatibility.md#public-beta). ## Two concerns: scope the match, then resolve the price B2B search scoping is two separate mechanisms, and keeping them apart is the key to getting it right: 1. **Scope which variants match**: constrain the search result to the buyer's context using `exact` expressions in the `query` on [searchable price fields](/api/projects/product-search.md#searchable-product-fields): `variants.prices.channel`, `variants.prices.customerGroup`, `variants.prices.currencyCode`, and `variants.prices.country`. Assortment scope uses `stores`, `variants.stores`, `productSelections`, and `variants.productSelections`. 2. **Resolve the price to display**: select the variant's `price(...)` field in the GraphQL response, passing the same context as price-selection arguments (`currency`, `country`, `channelId`, `customerGroupId`). This returns the single Price the buyer should see, applying the same [price selection](/learning-model-b2b-commerce/configure-b2b-pricing/price-selection.md) the platform uses at Cart time. Filtering decides which Products appear; the `price` field decides what number is shown for each. A query that filters by Channel but forgets to resolve the price would return the right Products with no price; one that resolves the price but forgets to filter would show every Product, priced for one buyer. ## Worked example: Horizon Hotels' scoped catalog Horizon Hotels trades through the `horizon-hotels-pricing` distribution Channel (id `chan-hh-001`) in `AUD`. The following `productsSearch` query returns laptops in their assortment, each with the negotiated Channel-scoped Price resolved for display. ```graphql { productsSearch( query: { and: [ { fullText: { field: "name", value: "laptop", language: "en-AU" } } { exact: { field: "variants.prices.channel", value: "chan-hh-001" } } { exact: { field: "variants.prices.currencyCode", value: "AUD" } } ] } limit: 20 ) { total results { product { masterData { current { masterVariant { sku price(currency: "AUD", channelId: "chan-hh-001") { value { centAmount currencyCode } } } } } } } } } ``` Reading the query: - The `and` block combines a full-text match on `name` with two `exact` filters that **scope the match** to Products having an `AUD` Price on Horizon Hotels' Channel. - The `price(currency: "AUD", channelId: "chan-hh-001")` selection **resolves the price** to the buyer's negotiated Channel-scoped Price. The argument names are `channelId` and `customerGroupId` (the resource IDs), not the resource objects. - Selecting only `sku` and `price` keeps the response small because the ID-first design rewards requesting only what the catalog page renders. The example selects the `masterVariant` to keep the query compact. In a production catalog, confirm that the variant you display is the one the buyer's search matched. Product Search can match only some Product Variants when you filter on `variants.*` fields, so use matching-variant handling when the catalog page must show the exact variant that carried the buyer-scoped Price. To scope a different buyer, change the Channel id (and add `customerGroupId` if the buyer's pricing is Customer-Group-scoped) in both the filter and the `price` selection. Run the identical shape for Pacific Property Group's Channel and the same catalog returns that buyer's prices instead. ## Connecting search to assortment Filtering by price scopes pricing; it does not by itself limit the catalog to a buyer's permitted assortment. That limit comes from the Store and Product Selection scope you built in Module 3, exposed in the search index as `stores`, `variants.stores`, `productSelections`, and `variants.productSelections`. To return only Products available in a buyer's Store, add an `exact` filter on `variants.stores` (or `stores`) for that Store's id. ```graphql { exact: { field: "variants.stores", value: "store-hh-au" } } ``` The mental model: **Product Selections and Stores decide which Products are in scope; Channel and Customer Group decide the price**; Product Search combines both into one buyer-specific catalog view. Product Search must be activated for the Project before it returns results, and it deactivates automatically after 30 days with no search calls. If search requests fail with an `ObjectNotFound` error, the API is not enabled. Activate it in the Merchant Center under **Settings > Project settings > Storefront Search**, or with the [Change Product Search Indexing Enabled](/api/projects/project.md#change-product-search-indexing-enabled) update action. ## Key takeaways - Use the GraphQL `productsSearch` query to search and fetch only the product fields you need in one call; the REST `productProjectionParameters` path is deprecated. - B2B scoping is two mechanisms: `exact` filters on `variants.prices.*` and assortment fields scope which variants match, and the variant `price(...)` field resolves what price to display. - Pass price context as `currency`, `country`, `channelId`, and `customerGroupId` arguments to the `price` field; they take resource IDs. - When filters target `variants.*` fields, make sure the catalog page displays the matched Product Variant, not an unrelated variant from the same Product. - Scope a buyer's assortment with `variants.stores` / `stores` and `variants.productSelections` / `productSelections`, the index fields backed by Module 3's Store and Product Selection design. - Product Search must be activated and stays active only while it is queried; an unconfigured API returns `ObjectNotFound`. ## Related pages - [Area overview page with navigation](/learning-model-b2b-commerce.md) - [Previous page: Why B2B search is different](/learning-model-b2b-commerce/discover-and-order-products-in-b2b/why-b2b-search-is-different.md) - [Next page: Faceting and filtering on price](/learning-model-b2b-commerce/discover-and-order-products-in-b2b/faceting-and-filtering-on-price.md)