Skip to main content

Using Search Rules

Shoppers searching for drill should not always see the same ranking as every other query. Search rules let you boost or bury products, pin or hide specific products, rewrite a query, or add a filter when a shopper query — or a browse listing — matches a trigger you define.

Search rules live in rule groups. Each group is a named campaign container. You attach one or more groups to a search profile with the ordered rule_groups attribute. Until a group is attached, its rules have no effect on search.

No reindexing required

Search rules are evaluated when a search runs, not when products are indexed. Creating or updating rules does not require a catalog reindex. Conditions and filters must still reference indexed fields — list indexed fields to see what is available, and enable custom attributes through indexable fields first if needed.

Always-On Boosts vs Query-Triggered Rules​

Catalog Search supports two related merchandising tools on a search profile:

ToolWhen it appliesConfigure with
Always-on boost / buryEvery search that uses the profileProfile boosts
Search rulesOnly when a rule trigger matches the shopper query or browseRule groups attached via profile rule_groups

Use always-on boosts for storewide policies (for example, always promote in-stock featured products). Use search rules for query- or browse-specific campaigns (for example, boost Milwaukee when the shopper searches for drill).

When a search has both, rule boosts are resolved among themselves first, then combined with the profile boosts. Each product receives the single highest matching weight. See Profile boosts and rule boosts.

How Search Rules Work​

  1. Create a rule group with a unique slug.
  2. Add rules to the group. Each rule has a trigger, one or more actions, an execution_order, and an optional stop_after_match flag.
  3. Attach the group UUID to a search profile's rule_groups array. Earlier entries in the array run first across groups.
  4. Search with that profile. Matching rules apply their actions; unmatched queries keep the profile's normal ranking.

Rules are evaluated in one profile-wide pipeline: profile rule_groups order, then each rule's execution_order inside that group (lower runs first). execution_order does not sort across groups.

A profile that lists the seasonal group before the brand group runs every seasonal rule before any brand rule:

Group positionexecution_orderWhen it runs
Seasonal (first)1001st
Seasonal (first)2002nd
Brand (second)503rd

The brand rule stays third. Its lower execution_order applies only inside the brand group.

Attaching rule groups activates the rules engine

A non-empty rule_groups array turns on query-time rules for every search that uses that profile. Rule actions still apply only when a trigger matches. Set rule_groups to [] to return to profile-only behavior.

Triggers​

Each rule has a trigger that decides when the rule matches.

matchWhen the rule runsquery
exactNormalized shopper query equals the trigger textRequired
containsTrigger text appears as a substring in the normalized queryRequired
browseNon-autocomplete browse search (q=*), such as category or “view all” pagesOmit

Query text is normalized before matching: trimmed, lowercased, and with whitespace collapsed.

TriggerExampleMatchesDoes not match
Exact drillmatch: exact, query: drilldrill, Drillcordless drill
Contains drillmatch: contains, query: drilldrill, cordless drill driverhammer
Browsematch: browseq=* browse listingsTyped search queries, autocomplete, empty or whitespace-only queries

Actions​

A rule must include at least one action.

Boost and bury​

Boost and bury use the same condition syntax as profile boosts. Positive weights promote matching products. Negative weights bury them. A weight of 0 is neutral: the product ranks as if it matched no boost clause.

These two rules both match a shopper search for drill. The first moves featured products up by 100. The second moves clearance products down by -100.

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "boost-featured-drills",
"execution_order": 100,
"trigger": { "query": "drill", "match": "exact" },
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).is_featured:=true", "weight": 100 }
]
}
}
}
}'
curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "bury-clearance-drills",
"execution_order": 200,
"trigger": { "query": "drill", "match": "exact" },
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).is_clearance:=true", "weight": -100 }
]
}
}
}
}'

Putting both weights in the boosts array of one rule produces the same checklist. The weights are sorted from the highest number to the lowest. Rule order and the order of objects in boosts do not set the check order. 100 is checked before -100:

CheckFromConditionWeight
1boost-featured-drillsis_featured:=true100
2bury-clearance-drillsis_clearance:=true-100

A shopper search for drill runs both rules:

curl "https://useast.api.elasticpath.com/v2/pcm/catalog/search?q=drill&search_profile=default" \
-H "Authorization: Bearer $ACCESS_TOKEN"

Each product starts at check 1 and stops at the first row it matches. That weight is the only one used.

ProductCheck 1, featured 100Check 2, clearance -100Weight used
Featured drillMatches. Stop.Not used100
Featured clearance drillMatches. Stop.Not used100
Clearance drillNo matchMatches. Stop.-100
Standard drillNo matchNo matchnone

100 and -100 are never added. The featured clearance drill matches check 1, so the -100 from the clearance rule is skipped. When two rules use the same condition text, one weight is chosen before this checklist is built. See Same condition on more than one rule.

Same condition on more than one rule​

When two matching rules use the same condition text, pipeline order picks one weight for that condition:

Earlier ruleLater ruleWeight kept
Positive or 0Positive or 0Earlier weight
Boost or 0Bury (negative)Later bury
BuryMore negative buryMore negative weight
BuryBoost or 0Earlier bury

An earlier rule that boosts BRAND-NAME:=Milwaukee by 100, and a later rule that buries that same condition by -100, leaves the condition at -100. Reverse those rules and the bury stays: a later boost on that condition does not restore the products.

Different conditions, including a zero-weight exception​

Different condition text is all kept. Ordering by weight decides which clause a product hits first. When two clauses share a weight, the clause from the earlier rule is checked first.

Use 0 on a narrower, different condition to exempt products from a broader bury. 0 is ordered before every negative weight, so products that match the exception receive 0 and rank with products that matched no clause. The two clauses can sit on one rule, or on two matching rules. The conditions must be different text:

{
"boosts": [
{
"condition": "extensions.products(Details).brand:=Nike && extensions.products(Details).is_featured:=true",
"weight": 0
},
{
"condition": "extensions.products(Details).brand:=Nike",
"weight": -100
}
]
}
ProductBrandFeaturedClause it hitsWeight
Nike Air MaxNikeyesFeatured Nike, checked first because 0 outranks -1000
Nike ClearanceNikenoNike bury-100
Adidas RunnerAdidasyesNo clausenone

Nike Air Max stays with the ordinary results. Nike Clearance is buried. Adidas Runner is untouched.

The exception works because featured Nike products match a condition the other Nike products do not. Putting 0 on the same condition text, brand:=Nike, follows the table above instead:

Earlier weight on brand:=NikeLater weight on brand:=NikeEvery Nike product
0-100Buried at -100. The later bury replaces the 0.
-1000Buried at -100. The later 0 does not clear the bury.

Profile boosts and rule boosts​

Search rules are resolved with each other first. Always-on profile boosts are then combined with that result. For one condition, the higher weight is kept, whether it came from the profile or from a rule.

A storewide profile boosts Nike on every search, and a drill campaign buries Nike when the shopper searches for drill:

SourceConditionWeight
Profile boostsbrand:=Nike100
Matching search rulebrand:=Nike-50

Both name brand:=Nike. 100 is higher than -50, so Nike products stay at 100 on that drill search. The rule bury does not demote them.

The same comparison runs the other way. A profile that buries out-of-season Nike, and a campaign rule that boosts Nike for this query:

SourceConditionWeight
Profile boostsbrand:=Nike-100
Matching search rulebrand:=Nike50

50 is higher than -100, so Nike products receive 50 for this search. The profile bury does not apply to them on this query.

When more than one rule names that condition, those rules are reduced to one weight before the profile is considered. On brand:=Nike:

StepWhat is comparedWeight kept
Earlier ruleBoost 4040
Later ruleBury -80-80. Among rules, the bury replaces the earlier boost.
ProfileBoost 100, compared with the merged rule weight -80100. 100 is higher than -80.

Nike products are boosted by the profile. The campaign bury never becomes the weight, because the profile clause is higher.

Different conditions from the profile and from rules are both kept. Each product still receives the highest clause it matches. A profile clause is_featured:=true at 100 and a rule clause is_clearance:=true at -100 produce:

ProductFeaturedClearanceWeight
Featured clearance drillyesyes100
Clearance drillnoyes-100
Featured drillyesno100

Shopper-selected sort is the primary ordering. The boost step ranks products that sort leaves tied, and it occupies one sort field. The platform accepts at most three sort fields. When the shopper sort already uses three fields, the boost step is omitted and neither profile nor rule boosts change ranking. Test campaigns with the storefront sorts shoppers use. See How Results Are Ordered for how boosts combine with relevance and request sorts.

Pin and hide​

Pins place products at one-based global result positions. Active filters still apply, so a pinned product that fails the current filters does not appear. A pin can include a product that does not match the query text when that product passes the filters.

When two matching rules pin different products to the same position, the earlier rule in the pipeline keeps that position. Within one rule, each product and each position can appear only once.

Hides remove products from results. Matching rules combine their hide lists. If the same product is both pinned and hidden, hide wins.

{
"pins": [
{ "product_id": "978dfdd0-7cbf-454d-90b4-3d61de60ca46", "position": 1 }
],
"hides": [
{ "product_id": "15b0b18e-48bb-47c8-96e8-3d5739e68dfd" }
]
}

Query rewrite​

Replace the shopper query when the rule matches. Use this for common typos or alternate terms that should resolve to a canonical query.

The first rewrite in the pipeline is the replacement. That match ends the current pass, so later rules in the pass are not evaluated. Actions from rules that already matched in the pass still apply.

Matching then runs once more against the replacement. Rules that already ran are skipped. A replace_query on that second pass is ignored. stop_after_match ends only the pass it occurs in: a stop on the rewriting rule does not cancel the second pass, and a stop during the second pass skips later rules in that pass only.

A browse trigger cannot rewrite the query. The replacement must be non-empty text, and it cannot be *.

{
"replace_query": "drill"
}

Rule filter​

Add an indexed-field filter only when the trigger matches. When several matching rules set filter_by, those expressions are ANDed together, then ANDed with profile filters and any shopper filters on the request.

{
"filter_by": "extensions.products(Details).is_in_stock:=true"
}

Ordering and stop_after_match​

ControlScopeBehavior
rule_groups array positionAcross attached groupsEarlier UUID runs first
execution_orderWithin a groupLower positive integer runs first; must be unique per group
stop_after_matchCurrent matching passWhen true and the rule matches, later rules in that pass are skipped, including later groups

stop_after_match applies that rule, keeps every earlier match in the pass, and skips the rest of the pass. A rule that does not match does not stop anything. After a query rewrite, the second pass still runs. See Query rewrite.

Use stop_after_match when a campaign must block broader rules that would otherwise also match.

Quick Reference​

Rule groups and rules are managed through the following endpoints, relative to your API base URL (for example https://useast.api.elasticpath.com/v2):

OperationMethod & Path
Create a rule groupPOST /pcm/catalogs/search/rule-groups
List rule groupsGET /pcm/catalogs/search/rule-groups
Get a rule groupGET /pcm/catalogs/search/rule-groups/{rule_group_id}
Update a rule groupPUT /pcm/catalogs/search/rule-groups/{rule_group_id}
Delete a rule groupDELETE /pcm/catalogs/search/rule-groups/{rule_group_id}
Create a rulePOST /pcm/catalogs/search/rule-groups/{rule_group_id}/rules
List rulesGET /pcm/catalogs/search/rule-groups/{rule_group_id}/rules
Get a ruleGET /pcm/catalogs/search/rule-groups/{rule_group_id}/rules/{rule_id}
Update a rulePUT /pcm/catalogs/search/rule-groups/{rule_group_id}/rules/{rule_id}
Delete a ruleDELETE /pcm/catalogs/search/rule-groups/{rule_group_id}/rules/{rule_id}
Move a rulePOST /pcm/catalogs/search/rule-groups/{rule_group_id}/rules/{rule_id}/move

A rule group has these attributes:

AttributeTypeRequiredDescription
slugstringYesUnique identifier within the tenant. Pattern: ^[a-zA-Z0-9_.-]+$.
descriptionstringNoHuman-readable summary of the group's intent.

A search rule has these attributes:

AttributeTypeRequiredDescription
slugstringNoHuman-readable identifier within the group.
descriptionstringNoSummary of what the rule does.
execution_orderintegerYesLower values run first within the group. Must be unique and at least 1.
triggerobjectYesWhen the rule matches. See Triggers.
actionsobjectYesAt least one of boosts, pins, hides, replace_query, or filter_by.
stop_after_matchbooleanNoWhen true and this rule matches, skip later rules in the current pass, including later groups. Default false.

JSON:API types are catalog_search_rule_group and catalog_search_rule.

Creating a Rule Group​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule_group",
"attributes": {
"slug": "seasonal-drill-campaign",
"description": "Merchandising rules for drill searches"
}
}
}'

Example response:

{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "catalog_search_rule_group",
"attributes": {
"slug": "seasonal-drill-campaign",
"description": "Merchandising rules for drill searches"
},
"meta": {
"owner": "store",
"created_at": "2026-03-05T12:00:00Z",
"updated_at": "2026-03-05T12:00:00Z"
}
}
}

Creating Boost or Bury Rules​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "boost-featured-drills",
"execution_order": 100,
"trigger": { "query": "drill", "match": "exact" },
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).is_featured:=true", "weight": 100 },
{ "condition": "extensions.products(Details).is_clearance:=true", "weight": -100 }
]
}
}
}
}'

Contains trigger example​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "boost-milwaukee-cordless",
"execution_order": 200,
"trigger": { "query": "cordless", "match": "contains" },
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).BRAND-NAME:=Milwaukee", "weight": 50 }
]
}
}
}
}'

Pinning and Hiding Products​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "pin-and-hide-products",
"execution_order": 300,
"trigger": { "query": "cordless drill", "match": "contains" },
"actions": {
"pins": [
{ "product_id": "978dfdd0-7cbf-454d-90b4-3d61de60ca46", "position": 1 }
],
"hides": [
{ "product_id": "15b0b18e-48bb-47c8-96e8-3d5739e68dfd" }
]
}
}
}
}'

Browse Rules​

A browse rule matches every non-autocomplete search with q=*: category pages, collection pages, and “view all” listings. Shopper filters on that listing still apply. Autocomplete does not evaluate browse rules. Exact and contains rules do not match q=*.

Profile field weights, synonyms, and typo tolerance rank typed search. A browse request has no query text for those settings to score. Listing order is the shopper’s sort, then boosts.

Profile boosts and filters run on every search that uses the profile, including keyword search. A browse rule keeps the same merchandising on listings. Attach one group to many profiles when those listings should share a policy and each profile should keep its own keyword tuning.

A browse rule matches every listing for the profiles that attach it. It does not target a single category. A boost or filter condition applies on each of those listings. On a shared condition, a higher profile boost still wins over a lower browse bury. Give the browse clause the higher weight, or leave that condition off the profile, when the listing policy must win.

GoalBrowse ruleWhy it stays off the profile
Rank listings on a profile that also serves searchBoost featured or high-margin products, bury clearanceThose clauses would also change keyword ranking
One listing policy on many profilesAttach the same browse group to web, app, and wholesale profilesEach profile keeps its own field weights and keyword rules. Edit the listing policy once
Full category grids, findable SKUsfilter_by in-stockA profile filter would hide that SKU from search
Heroes and exclusions on gridsPin the campaign product, hide parts and accessoriesThose products stay searchable by name
A campaign on some profilesShared base browse group, plus a seasonal group listed first only on profiles in the campaignEach profile chooses which groups to attach and in which order
Segment listingsRetail and wholesale profiles attach different browse groupsKeyword tuning stays on each profile. Only the listing policy diverges
curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "browse-featured-products",
"execution_order": 100,
"trigger": { "match": "browse" },
"actions": {
"filter_by": "extensions.products(Details).is_in_stock:=true",
"boosts": [
{ "condition": "extensions.products(Details).is_featured:=true", "weight": 100 }
]
}
}
}
}'

Query Rewrite Rules​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "rewrite-drill-typo",
"execution_order": 50,
"trigger": { "query": "dril", "match": "exact" },
"actions": { "replace_query": "drill" }
}
}
}'

Stopping Later Matching Rules​

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"slug": "exclusive-sale-campaign",
"execution_order": 50,
"stop_after_match": true,
"trigger": { "query": "sale", "match": "exact" },
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).is_sale:=true", "weight": 100 }
]
}
}
}
}'

Attaching Rule Groups to a Search Profile​

A rule group has no effect until it is attached to a search profile:

curl -X PUT https://useast.api.elasticpath.com/v2/pcm/catalogs/search-profiles/{profile_id} \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_profile",
"attributes": {
"rule_groups": [
"550e8400-e29b-41d4-a716-446655440000",
"660e8400-e29b-41d4-a716-446655440000"
]
}
}
}'

Rules for attaching groups:

  • A profile can reference multiple rule groups; array order sets cross-group precedence.
  • A profile may only reference rule groups visible to its tenant (organization/store scope follows the same model as search profiles).
  • Set rule_groups to [] to detach all groups and disable the query-time rules engine for that profile.

Updating and Moving Rules​

Update a rule directly, including a sparse execution_order:

curl -X PUT https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules/8fccaa19-dba9-4621-8d11-31a222a68c7c \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"execution_order": 150,
"actions": {
"boosts": [
{ "condition": "extensions.products(Details).is_featured:=true", "weight": 150 }
]
}
}
}
}'

Move a rule relative to another rule in the same group:

curl -X POST https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules/8fccaa19-dba9-4621-8d11-31a222a68c7c/move \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"type": "catalog_search_rule",
"attributes": {
"placement": "before",
"target_rule_id": "a1000001-0001-4000-8000-000000000099"
}
}
}'

placement supports before, after, first, and last. Supply target_rule_id only for before and after.

The service preserves sparse execution_order values when it can (for example moving a rule into an unused integer gap). When no gap remains between neighboring rules, the group is renumbered so each rule still has a unique execution_order.

Matched Rules in Search Responses​

When rules match a full search request, the response can include matched_rules with the rule id and parent rule_group_id. The payload lists identifiers only — not the full rule bodies — so storefronts and tooling can confirm which campaigns fired.

Search rules are not applied to autocomplete

Autocomplete (search-as-you-type) does not evaluate search rules and does not return matched_rules, even when the selected search profile has rule groups attached.

Deleting Rules and Rule Groups​

Delete a single rule:

curl -X DELETE https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000/rules/8fccaa19-dba9-4621-8d11-31a222a68c7c \
-H "Authorization: Bearer $ACCESS_TOKEN"

Deleting a rule is allowed even when its parent group is still attached to a profile.

Delete a rule group:

curl -X DELETE https://useast.api.elasticpath.com/v2/pcm/catalogs/search/rule-groups/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $ACCESS_TOKEN"

A rule group cannot be deleted while any search profile still references it. Remove the group from every profile's rule_groups first, or the delete returns 409. Deleting a group cascade-deletes the rules inside it.

Organization and Store Scope​

Rule groups follow the same organization and store tenancy model as search profiles:

  • Organization-scoped groups can be attached to organization and store search profiles (subject to visibility rules for the tenant).
  • Store-scoped groups can be attached only where that store's profiles allow.

Validation and Errors​

StatusWhen
400Invalid slug, duplicate execution_order within a group, invalid trigger, empty actions, or invalid boost/filter syntax
400Profile rule_groups contains an invalid UUID, a duplicate, or a group that does not exist / is out of scope
403A store tries to update or delete an organization-scoped group or rule without permission
404The rule group or rule does not exist
409The rule group is still referenced by one or more search profiles

Best Practices​

  1. Attach groups intentionally. Creating rules alone does nothing until the group is on a profile's rule_groups list.
  2. Choose the right trigger. Use exact for precise campaign queries, contains for multi-word shopper phrasing, and browse for category or all-products listings.
  3. Plan group order and execution_order. Pipeline order decides same-condition conflicts: an earlier boost or 0 beats a later boost or 0, a later bury replaces an earlier boost, and an earlier bury stays in place.
  4. Use stop_after_match sparingly. It skips later rules in the current pass. Rules that already matched still apply. It does not cancel the rematch that follows a query rewrite.
  5. Merchandise only on indexed fields. Boost, bury, and rule filters cannot use sale windows or custom relationships until those fields are indexed.
  6. Test with real storefront sorts. Confirm campaigns under the same profile, filters, and shopper sort options production will use.
  7. Detach before deleting a group. Remove the group from every profile first to avoid a 409.
Ask External AI