Listing groups
are used in
Performance Max retail campaigns to specify which products to include or exclude
in each asset group. As such, listing groups are applied at the AssetGroup
level using AssetGroupListingGroupFilter
objects. This is similar to other types of retail campaigns in which product
groups are applied at the AdGroup
level.
All asset groups in Performance Max retail campaigns require a valid product
partition tree composed of AssetGroupListingGroupFilter
objects. You can use a
single node tree that includes all products in the Merchant Center account to
meet this requirement.
This is referred to as product groups in the UI. You can group using multiple dimensions, letting you include or exclude products.
Consider the tree below, where at the first level, the products have been divided by condition into New, Used and other product conditions. At the second level, the products in other product conditions have been divided by brand as "CoolBrand" products, "CheapBrand", and other brands.
Each node in the tree is either a subdivision or a unit, as defined by ListingGroupType
. A subdivision
introduces a new level in the tree, while units are leaves of the tree. Each
subdivision must always be completely partitioned, so it must contain a node
representing Other. In the example, the root and Product Condition:
(Other)nodes are subdivisions. This tree structure with subdivisions and
units lets you set bids at the unit level and also ensures that every
product falls into one and only one unit node in the tree.
Nodes are objects of the ListingGroupInfo
class, which contains the ListingGroupType
field that indicates if
the nodes are unit or subdivision. Setting ListingGroupInfo
to listing_group
of AdGroupCriterion
will link it to the AdGroup
.
You need at least one unit node to make a tree valid. That unit can be the root node, which then becomes the "All Products" division. Ads won't serve until you create a valid listing group tree.
Performance Max listing groups
Listing groups in Performance Max campaigns work best when targeting groups of products, so this should be preferred over targeting individual products by item ID. You can use different dimensions such as custom labels or brand in your product feed to group products.
Code example
Java
/** * Runs the example. * * @param googleAdsClient the Google Ads API client. * @param customerId the client customer ID. * @param assetGroupId the asset group id for the Performance Max campaign. * @param replaceExistingTree option to remove existing product tree from the passed in asset * group. * @throws GoogleAdsException if an API request failed with one or more service errors. */ private void runExample ( GoogleAdsClient googleAdsClient , long customerId , long assetGroupId , boolean replaceExistingTree ) throws Exception { String assetGroupResourceName = ResourceNames . assetGroup ( customerId , assetGroupId ); List<MutateOperation> operations = new ArrayList <> (); if ( replaceExistingTree ) { List<AssetGroupListingGroupFilter> existingListingGroupFilters = getAllExistingListingGroupFilterAssetsInAssetGroup ( googleAdsClient , customerId , assetGroupResourceName ); if ( ! existingListingGroupFilters . isEmpty ()) { // A special factory object that ensures the creation of remove operations in the // correct order (child listing group filters must be removed before their parents). AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory = new AssetGroupListingGroupFilterRemoveOperationFactory ( existingListingGroupFilters ); operations . addAll ( removeOperationFactory . removeAll ()); } } // Uses a factory to create all the MutateOperations that manipulate a specific // AssetGroup for a specific customer. The operations returned by the factory's methods // are used to construct a new tree of filters. These filters can have parent-child // relationships, and also include a special root that includes all children. // // When creating these filters, temporary IDs are used to create the hierarchy between // each of the nodes in the tree, beginning with the root listing group filter. // // The factory created below is specific to a customerId and assetGroupId. AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory = new AssetGroupListingGroupFilterCreateOperationFactory ( customerId , assetGroupId , TEMPORARY_ID_LISTING_GROUP_ROOT ); // Creates the operation to add the root node of the tree. operations . add ( createOperationFactory . createRoot ()); // Creates an operation to add a leaf node for new products. ListingGroupFilterDimension newProductDimension = ListingGroupFilterDimension . newBuilder () . setProductCondition ( ProductCondition . newBuilder () . setCondition ( ListingGroupFilterProductCondition . NEW ) . build ()) . build (); operations . add ( createOperationFactory . createUnit ( TEMPORARY_ID_LISTING_GROUP_ROOT , createOperationFactory . nextId (), newProductDimension )); // Creates an operation to add a leaf node for used products. ListingGroupFilterDimension usedProductDimension = ListingGroupFilterDimension . newBuilder () . setProductCondition ( ProductCondition . newBuilder () . setCondition ( ListingGroupFilterProductCondition . USED ) . build ()) . build (); operations . add ( createOperationFactory . createUnit ( TEMPORARY_ID_LISTING_GROUP_ROOT , createOperationFactory . nextId (), usedProductDimension )); // This represents the ID of the "other" category in the ProductCondition subdivision. This ID // is saved because the node with this ID will be further partitioned, and this ID will serve as // the parent ID for subsequent child nodes of the "other" category. long otherSubdivisionId = createOperationFactory . nextId (); // Creates an operation to add a subdivision node for other products in the ProductCondition // subdivision. ListingGroupFilterDimension otherProductDimension = ListingGroupFilterDimension . newBuilder () . setProductCondition ( ProductCondition . newBuilder (). build ()) . build (); operations . add ( // Calls createSubdivision because this listing group will have children. createOperationFactory . createSubdivision ( TEMPORARY_ID_LISTING_GROUP_ROOT , otherSubdivisionId , otherProductDimension )); // Creates an operation to add a leaf node for products with the brand "CoolBrand". ListingGroupFilterDimension coolBrandProductDimension = ListingGroupFilterDimension . newBuilder () . setProductBrand ( ProductBrand . newBuilder (). setValue ( "CoolBrand" ). build ()) . build (); operations . add ( createOperationFactory . createUnit ( otherSubdivisionId , createOperationFactory . nextId (), coolBrandProductDimension )); // Creates an operation to add a leaf node for products with the brand "CheapBrand". ListingGroupFilterDimension cheapBrandProductDimension = ListingGroupFilterDimension . newBuilder () . setProductBrand ( ProductBrand . newBuilder (). setValue ( "CheapBrand" ). build ()) . build (); operations . add ( createOperationFactory . createUnit ( otherSubdivisionId , createOperationFactory . nextId (), cheapBrandProductDimension )); // Creates an operation to add a leaf node for other products in the ProductBrand subdivision. ListingGroupFilterDimension otherBrandProductDimension = ListingGroupFilterDimension . newBuilder () . setProductBrand ( ProductBrand . newBuilder (). build ()) . build (); operations . add ( createOperationFactory . createUnit ( otherSubdivisionId , createOperationFactory . nextId (), otherBrandProductDimension )); try ( GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient . getLatestVersion (). createGoogleAdsServiceClient ()) { MutateGoogleAdsRequest request = MutateGoogleAdsRequest . newBuilder () . setCustomerId ( Long . toString ( customerId )) . addAllMutateOperations ( operations ) . build (); MutateGoogleAdsResponse response = googleAdsServiceClient . mutate ( request ); printResponseDetails ( request , response ); } }
C#
/// <summary> /// Runs the code example. /// </summary> /// <param name="client">The Google Ads client.</param> /// <param name="customerId">The Google Ads customer ID.</param> /// <param name="assetGroupId">The asset group id for the Performance Max campaign.</param> /// <param name="replaceExistingTree">Option to remove existing product tree /// from the passed in asset group.</param> public void Run ( GoogleAdsClient client , long customerId , long assetGroupId , bool replaceExistingTree ) { GoogleAdsServiceClient googleAdsServiceClient = client . GetService ( Services . V21 . GoogleAdsService ); string assetGroupResourceName = ResourceNames . AssetGroup ( customerId , assetGroupId ); // We use a factory to create all the MutateOperations that manipulate a specific // AssetGroup for a specific customer. The operations returned by the factory's methods // are used to optionally remove all AssetGroupListingGroupFilters from the tree, and // then to construct a new tree of filters. These filters can have a parent-child // relationship, and also include a special root that includes all children. // // When creating these filters, we use temporary IDs to create the hierarchy between // the root listing group filter, and the subdivisions and leave nodes beneath that. // // The factory specific to a customerId and assetGroupId is created below. AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory = new AssetGroupListingGroupFilterCreateOperationFactory ( customerId , assetGroupId , TEMPORARY_ID_LISTING_GROUP_ROOT ); MutateGoogleAdsRequest request = new MutateGoogleAdsRequest { CustomerId = customerId . ToString () }; if ( replaceExistingTree ) { List<AssetGroupListingGroupFilter> existingListingGroupFilters = GetAllExistingListingGroupFilterAssetsInAssetGroup ( client , customerId , assetGroupResourceName ); if ( existingListingGroupFilters . Count > 0 ) { // A special factory object that ensures the creation of remove operations in the // correct order (child listing group filters must be removed before their parents). AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory = new AssetGroupListingGroupFilterRemoveOperationFactory ( existingListingGroupFilters ); request . MutateOperations . AddRange ( removeOperationFactory . RemoveAll ()); } } request . MutateOperations . Add ( createOperationFactory . CreateRoot ()); request . MutateOperations . Add ( createOperationFactory . CreateUnit ( TEMPORARY_ID_LISTING_GROUP_ROOT , createOperationFactory . NextId (), new ListingGroupFilterDimension () { ProductCondition = new ListingGroupFilterDimension . Types . ProductCondition () { Condition = ListingGroupFilterProductCondition . New } } ) ); request . MutateOperations . Add ( createOperationFactory . CreateUnit ( TEMPORARY_ID_LISTING_GROUP_ROOT , createOperationFactory . NextId (), new ListingGroupFilterDimension () { ProductCondition = new ListingGroupFilterDimension . Types . ProductCondition () { Condition = ListingGroupFilterProductCondition . Used } } ) ); // We save this ID because create child nodes underneath it. long subdivisionIdConditionOther = createOperationFactory . NextId (); request . MutateOperations . Add ( // We're calling CreateSubdivision because this listing group will have children. createOperationFactory . CreateSubdivision ( TEMPORARY_ID_LISTING_GROUP_ROOT , subdivisionIdConditionOther , new ListingGroupFilterDimension () { // All sibling nodes must have the same dimension type. We use an empty // ProductCondition to indicate that this is an "Other" partition. ProductCondition = new ListingGroupFilterDimension . Types . ProductCondition () } ) ); request . MutateOperations . Add ( createOperationFactory . CreateUnit ( subdivisionIdConditionOther , createOperationFactory . NextId (), new ListingGroupFilterDimension () { ProductBrand = new ProductBrand () { Value = "CoolBrand" } } ) ); request . MutateOperations . Add ( createOperationFactory . CreateUnit ( subdivisionIdConditionOther , createOperationFactory . NextId (), new ListingGroupFilterDimension () { ProductBrand = new ProductBrand () { Value = "CheapBrand" } } ) ); request . MutateOperations . Add ( createOperationFactory . CreateUnit ( subdivisionIdConditionOther , createOperationFactory . NextId (), new ListingGroupFilterDimension () { ProductBrand = new ProductBrand () } ) ); MutateGoogleAdsResponse response = googleAdsServiceClient . Mutate ( request ); PrintResponseDetails ( request , response ); }
PHP
/** * Runs the example. * * @param GoogleAdsClient $googleAdsClient the Google Ads API client * @param int $customerId the customer ID * @param int $assetGroupId the asset group ID * @param bool $replaceExistingTree true if it should replace the existing listing group * tree on the asset group */ public static function runExample( GoogleAdsClient $googleAdsClient, int $customerId, int $assetGroupId, bool $replaceExistingTree ) { // We create all the mutate operations that manipulate a specific asset group for a specific // customer. The operations are used to optionally remove all asset group listing group // filters from the tree, and then to construct a new tree of filters. These filters can // have a parent-child relationship, and also include a special root that includes all // children. // // When creating these filters, we use temporary IDs to create the hierarchy between // the root listing group filter, and the subdivisions and leave nodes beneath that. $mutateOperations = []; if ($replaceExistingTree === true) { $existingListingGroupFilters = self::getAllExistingListingGroupFilterAssetsInAssetGroup( $googleAdsClient, $customerId, ResourceNames::forAssetGroup($customerId, $assetGroupId) ); if (count($existingListingGroupFilters) > 0) { $mutateOperations = array_merge( $mutateOperations, // Ensures the creation of remove operations in the correct order (child listing // group filters must be removed before their parents). self::createMutateOperationsForRemovingListingGroupFiltersTree( $existingListingGroupFilters ) ); } } $mutateOperations[] = self::createMutateOperationForRoot( $customerId, $assetGroupId, self::LISTING_GROUP_ROOT_TEMPORARY_ID ); // The temporary ID to be used for creating subdivisions and units. static $tempId = self::LISTING_GROUP_ROOT_TEMPORARY_ID - 1; $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ 'product_condition' => new ProductCondition([ 'condition' => ListingGroupFilterProductCondition::PBNEW ]) ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ 'product_condition' => new ProductCondition([ 'condition' => ListingGroupFilterProductCondition::USED ]) ]) ); // We save this ID to create child nodes underneath it. $conditionOtherSubdivisionId = $tempId--; // We're calling createMutateOperationForSubdivision() because this listing group will // have children. $mutateOperations[] = self::createMutateOperationForSubdivision( $customerId, $assetGroupId, $conditionOtherSubdivisionId, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ // All sibling nodes must have the same dimension type. We use an empty // ProductCondition to indicate that this is an "Other" partition. 'product_condition' => new ProductCondition() ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, new ListingGroupFilterDimension( ['product_brand' => new ProductBrand(['value' => 'CoolBrand'])] ) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, new ListingGroupFilterDimension([ 'product_brand' => new ProductBrand(['value' => 'CheapBrand']) ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, // All other product brands. new ListingGroupFilterDimension(['product_brand' => new ProductBrand()]) ); // Issues a mutate request to create everything and prints its information. $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); $response = $googleAdsServiceClient->mutate( MutateGoogleAdsRequest::build($customerId, $mutateOperations) ); self::printResponseDetails($mutateOperations, $response); }