---
title: "Search inputs for retailers | Snowplow Signals"
description: "Snowplow Signals tells your search engine what this shopper typed, clicked and skipped this visit, so it can broaden the results or offer help."
source: https://signals.snowplow.io/solutions/retail/search
---

[Retail and e-commerce](https://signals.snowplow.io/solutions/retail) / Search inputs 

# Search results that know what this shopper tried. 

Signals tells your search engine what this shopper has typed, clicked and skipped this visit, so the fourth search can be ranked differently from the first.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher search conversion

## Turn more searches into orders by ranking the fourth search differently from the first. 

Search conversion moves when the ranker can tell a shopper's fourth query from their first. Signals gives your search engine this visit's searches, result clicks and dead ends, so it can loosen the match, weight the category the shopper has been browsing, drop what they opened and left, or put help above the list.

Four ways retailers use it

01 Shoppers on their third failed search Three searches, nothing clicked. Loosen the match or show categories. 02 Shoppers who search after browsing one category Twenty minutes in running shoes, then a search for "black". Weight running shoes. 03 Shoppers who open a result and come straight back Opened two results, back to the list both times. Move what they tried down. 04 Shoppers repeating a search from an earlier visit Searched for the same shirt last week and again tonight, no order. Offer the alert. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Loosen the match when nothing has been clicked

A shopper typed "linen shirt navy", then "navy linen shirt", then "blue linen shirt", and clicked nothing on any of the three lists. The ranker scores each query as it arrives, and what the shopper did with the two before it arrives after the third list is on the screen. Three searches with no click say the catalog's words are not this shopper's words. The next list should match more loosely, or show the nearest categories, while the shopper is still at the search box.

What Signals knows

`searches_session` 

Searches run this visit

`result_clicks_session` 

Results opened from a search this visit

`last_queries_session` 

The queries typed this visit

What your search engine does

Switches to a looser match and shows the nearest categories above the list once a visit reaches three searches with no result opened, and passes the earlier queries in as synonyms for this one.

Define it · PythonUse it · TypeScript

search.tsx · 16 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

search = Event(name="search", vendor="com.example", version="1-0-0")
search_result_click = Event(name="search_result_click", vendor="com.example", version="1-0-0")

searches_session = Attribute(
    name="searches_session",
    type="int32",
    events=[search],
    aggregation="counter",
    default_value=0,
)

result_clicks_session = Attribute(
    name="result_clicks_session",
    type="int32",
    events=[search_result_click],
    aggregation="counter",
    default_value=0,
)

last_queries_session = Attribute(
    name="last_queries_session",
    type="string_list",
    events=[search],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="search", major_version=1, path="query"),
)

# Keyed on the visit, so the counts start from zero each time the shopper comes back.
shop_search_dead_end = StreamAttributeGroup(
    name="shop_search_dead_end",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[searches_session, result_clicks_session, last_queries_session],
)
```

```
// In the code that sends a search request to your search engine
const shopper = await signals.getServiceAttributes({
  name: "shop_search_dead_end",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { searches_session: 3, result_clicks_session: 0, last_queries_session: ["linen shirt navy", "navy linen shirt", "blue linen shirt"] }

if (shopper.searches_session >= 3 && shopper.result_clicks_session === 0) {
  search.query({
    q: query,
    synonyms: shopper.last_queries_session,
    match: "loose",
    showCategories: true,
  });
}
```

### Weight the category the shopper has been browsing

A shopper has spent the visit in running shoes, opened six product pages, and then typed "black" into the search box. On its own the word matches dresses, phone cases and belts, and the first screen of results shows all of them. The browsing that would narrow it happened a minute ago, on pages the search request does not carry. The list should lead with black running shoes and leave the rest below.

What Signals knows

`top_category_session` 

The product category viewed most this visit

`product_views_session` 

Product pages opened this visit

What your search engine does

Boosts the category this shopper has viewed most this visit when the query is short or spans several categories, and skips the boost when the visit has no browsing behind it yet.

Define it · PythonUse it · TypeScript

search.tsx · 14 lines Copy Copied 

```
from snowplow_signals import Attribute, EntityProperty, Event, StreamAttributeGroup, domain_sessionid

product_view = Event(name="product_view", vendor="com.example", version="1-0-0")

top_category_session = Attribute(
    name="top_category_session",
    type="string",
    events=[product_view],
    aggregation="most_frequent",
    property=EntityProperty(vendor="com.example", name="product", major_version=1, path="category"),
)

product_views_session = Attribute(
    name="product_views_session",
    type="int32",
    events=[product_view],
    aggregation="counter",
    default_value=0,
)

shop_search_category = StreamAttributeGroup(
    name="shop_search_category",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[top_category_session, product_views_session],
)
```

```
// In the code that sends a search request to your search engine
const shopper = await signals.getServiceAttributes({
  name: "shop_search_category",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { top_category_session: "running_shoes", product_views_session: 6 }

if (shopper.product_views_session >= 3) {
  search.query({
    q: query,
    boost: { category: shopper.top_category_session },
  });
}
```

### Down-weight what the shopper opened and left

A shopper searched for "desk lamp", opened the first result, came back, opened the second, came back. When they refine the search to "desk lamp brass", both lamps are still at the top of the list. The ranker scores the new query against the catalog, and the two rejections are a minute old and still on their way to anything it reads. The products this shopper has seen and left should sit below the ones they have not.

What Signals knows

`products_opened_session` 

Products opened from search results this visit

`searches_session` 

Searches run this visit

What your search engine does

Passes the products this shopper has opened from results this visit as a demotion list on the next search, so the lamps they have seen drop below the ones they have not and the refined query shows something new first.

Define it · PythonUse it · TypeScript

search.tsx · 14 lines Copy Copied 

```
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, domain_sessionid,
)

search = Event(name="search", vendor="com.example", version="1-0-0")
search_result_click = Event(name="search_result_click", vendor="com.example", version="1-0-0")

products_opened_session = Attribute(
    name="products_opened_session",
    type="string_list",
    events=[search_result_click],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="search_result_click", major_version=1, path="product_id"),
)

searches_session = Attribute(
    name="searches_session",
    type="int32",
    events=[search],
    aggregation="counter",
    default_value=0,
)

shop_search_tried = StreamAttributeGroup(
    name="shop_search_tried",
    version=1,
    attribute_key=domain_sessionid,
    owner="you@example.com",
    attributes=[products_opened_session, searches_session],
)
```

```
// In the code that sends a search request to your search engine
const shopper = await signals.getServiceAttributes({
  name: "shop_search_tried",
  attribute_key: "domain_sessionid",
  identifier: sessionId,
});
// { products_opened_session: ["L-2041", "L-2088"], searches_session: 2 }

if (shopper.searches_session >= 2 && shopper.products_opened_session.length > 0) {
  search.query({
    q: query,
    demote: shopper.products_opened_session,
  });
}
```

### Put help above the list when the same search comes back without an order

A shopper searched for "blue linen shirt" last week and is typing it again tonight. Neither visit ended in an order. The second search reaches the ranker looking like the first, and the merchandising report that would catch a repeated query is read after the shopper has gone again. The second attempt is where a back-in-stock alert, a size filter or a chat prompt should sit above the results.

What Signals knows

`queries_30d` 

The queries this shopper has typed in the last 30 days

`search_visits_30d` 

Separate visits with a search in the last 30 days

`orders_30d` 

Orders placed in the last 30 days

What your search engine does

Shows a back-in-stock alert or a help prompt above the results when the query matches one this shopper typed on an earlier visit and no order has followed, and keeps the list itself unchanged for everyone else.

Define it · PythonUse it · TypeScript

search.tsx · 17 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AtomicProperty, Event, EventProperty, StreamAttributeGroup, domain_userid,
)

search = Event(name="search", vendor="com.example", version="1-0-0")
order_placed = Event(name="order_placed", vendor="com.example", version="1-0-0")

queries_30d = Attribute(
    name="queries_30d",
    type="string_list",
    events=[search],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="search", major_version=1, path="query"),
    period=timedelta(days=30),
)

search_visits_30d = Attribute(
    name="search_visits_30d",
    type="int32",
    events=[search],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="domain_sessionid"),
    period=timedelta(days=30),
)

orders_30d = Attribute(
    name="orders_30d",
    type="int32",
    events=[order_placed],
    aggregation="counter",
    period=timedelta(days=30),
    default_value=0,
)

# Keyed on the browser rather than the visit, so a query from last week counts.
shop_search_repeat = StreamAttributeGroup(
    name="shop_search_repeat",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[queries_30d, search_visits_30d, orders_30d],
)
```

```
// In the code that sends a search request to your search engine.
// Read before this search is tracked, so the list holds earlier searches only.
const shopper = await signals.getServiceAttributes({
  name: "shop_search_repeat",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { queries_30d: ["blue linen shirt", "linen shirt"], search_visits_30d: 2, orders_30d: 0 }

const repeated = shopper.queries_30d.includes(query.toLowerCase());

if (repeated && shopper.search_visits_30d >= 2 && shopper.orders_30d === 0) {
  search.query({
    q: query,
    banner: { kind: "back_in_stock_or_help", query },
  });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
