Quick Start: Search
Welcome to the Particular Audience (PA) Quick Start guide for Search! By the end of this tutorial you will have a functioning search UI widget that you can place on your website.
The PA Search API is flexible - it allows for a whole range of functionality from a single endpoint. This includes:
  • Faceted search
  • Personalised search
  • Image search
  • Synonyms
  • And more
To learn how to trigger all the functionality, please see the Search API reference.
In this example we will just focus on getting a basic faceted search example functioning.
Here’s what you will finish with:

💻 Building a simple UI with React

To get started, let’s create a React project.
1
yarn create react-app pa-search-ui
Copied!
You should now have an empty React JS project.
Let’s now start building!
🧙 Tips: We’re using Ant Design as our UI framework. It has nice looking UI components to help us build faster!
1
cd pa-search-ui
2
yarn add antd
3
yarn start
Copied!

Adding the PA Sandbox Tag

Let’s go ahead and add in the PA Sandbox Tag. This (to an extent) mimics the production PA Tag that will be given to you. The tag also helps get the CustomerId for you from PA’s servers.
To add the tag, navigate to public/index.htmland add the following script in the head tag.
public/index.html
1
<script>window.addPAC = function (c, e) { document.cookie = 'PAC=' + c + ';' + e + ';' }</script>
2
<script src="https://cdn.particularaudience.com/js/sandbox/t.js"></script>
Copied!
🧙 Tips: The PA Tag does a lot of house keeping work such as track user behaviour data and automatically fetch the CustomerID for you.

Boilerplate React code

Next, let’s go to src/App.js and add some boilerplate code.
This piece of code contains three key areas:
  1. 1.
    When our query changes, we want to initiate a search
  2. 2.
    When filters change, we want to initiate a search
  3. 3.
    Display search results
1
import React, { useState, useEffect, useRef } from "react";
2
import { Input, Checkbox, Tag } from 'antd';
3
import "antd/dist/antd.css";
4
5
const { Search } = Input;
6
7
const WEBSITE_ID = '11111111-1111-1111-1111-111111111111'
8
9
const filterTitleMapping = {
10
product_type: "Categories",
11
brand: "Brands"
12
}
13
14
const STYLES = {
15
app: {
16
padding: 32
17
},
18
body: {
19
display: 'flex'
20
},
21
header: {
22
textAlign: 'left',
23
marginBottom: 32
24
},
25
subtitle: {
26
display: 'flex',
27
justifyContent: 'space-between'
28
},
29
filterContainer: {
30
textAlign: 'left',
31
flex: 1,
32
},
33
resultsContainer: {
34
display: 'grid',
35
gridGap: '16px 16px',
36
gridTemplateColumns: 'auto auto auto auto',
37
flex: 2
38
},
39
slot: {
40
border: '1px solid black',
41
padding: 8
42
},
43
checkbox: {
44
width: '100%',
45
margin: '2px 0'
46
},
47
filterTitle: {
48
margin: '16px 0 8px 0',
49
fontWeight: 'bold'
50
},
51
suggestions: {
52
color: '#1a73e8',
53
cursor: 'pointer'
54
}
55
}
56
57
function App() {
58
59
const [searchResults, setSearchResults] = useState([])
60
const [selectedFilters, setSelectedFilters] = useState({})
61
const [filters, setFilters] = useState({})
62
const [isLoading] = useState(false)
63
const [customerId, setCustomerId] = useState(null)
64
const [query, setQuery] = useState('')
65
66
const initialRender = useRef(true);
67
68
const fetchSearch = async (updateFilter) => {
69
if (customerId) {
70
let scope = {}
71
Object.keys(selectedFilters).forEach((key) => {
72
if (selectedFilters[key].length > 0) {
73
scope[key] = selectedFilters[key]
74
}
75
})
76
77
const payload = {
78
customer_id: customerId,
79
website_id: WEBSITE_ID,
80
q: query,
81
client: "sandbox",
82
personalise: false,
83
search_fields: [
84
"title",
85
"descriptions"
86
],
87
fuzzy: false,
88
size: 20,
89
scope: scope,
90
aggregations: ["product_type", "brand"]
91
}
92
93
const res = await fetch(`https://search.stg.p-a.io/search`,
94
{
95
method: 'POST',
96
headers: {
97
'Accept': 'application/json',
98
'Content-Type': 'application/json'
99
},
100
body: JSON.stringify(payload)
101
}
102
)
103
if (res.ok) {
104
const resultsJson = await res.json()
105
setSearchResults(resultsJson?.payload?.results)
106
if (updateFilter) {
107
setFilters(resultsJson?.payload?.aggregations)
108
}
109
}
110
}
111
};
112
113
useEffect(() => {
114
// Get PA CustomerID
115
const customerIdFromCookie = getCookie('PAC')
116
setCustomerId(customerIdFromCookie)
117
}, []);
118
119
useEffect(() => {
120
if (initialRender.current) {
121
initialRender.current = false;
122
} else {
123
// Reset existing filters
124
setSelectedFilters({})
125
setFilters({})
126
// Update filters when the query changes
127
fetchSearch(true)
128
}
129
}, [query]);
130
131
useEffect(() => {
132
// When filters change, only do search, do not update existing filters
133
fetchSearch(false)
134
}, [selectedFilters]);
135
136
137
const getCookie = (cookieName) => {
138
let name = cookieName + '='
139
let cookieAttributes = document.cookie.split(';')
140
for (var i = 0; i < cookieAttributes.length; i++) {
141
let cookieAttribute = cookieAttributes[i]
142
while (cookieAttribute.charAt(0) === ' ') {
143
cookieAttribute = cookieAttribute.substring(1)
144
}
145
if (cookieAttribute.indexOf(name) === 0) {
146
return cookieAttribute.substring(name.length, cookieAttribute.length)
147
}
148
}
149
return ''
150
}
151
152
const onCheckboxChange = (e, value, key) => {
153
let cpSelectedFilters = {...selectedFilters}
154
if (!(key in cpSelectedFilters)) {
155
cpSelectedFilters[key] = []
156
}
157
if (e.target.checked) {
158
cpSelectedFilters[key].push(value)
159
} else {
160
const index = cpSelectedFilters[key].indexOf(value);
161
if (index > -1) {
162
cpSelectedFilters[key].splice(index, 1);
163
}
164
}
165
166
setSelectedFilters(cpSelectedFilters)
167
}
168
169
170
return (
171
<div style={STYLES.app} className="App">
172
<h1>Particular Audience React Search UI</h1>
173
<div style={STYLES.header}>
174
<Search
175
onChange={(event) => {
176
setQuery(event.target.value)
177
}}
178
placeholder="Search for your favourite product"
179
loading={isLoading}
180
value={query}
181
style={{marginBottom: 8}}
182
/>
183
<div style={STYLES.subtitle}>
184
<span>
185
Try search terms such as
186
<span onClick={() => { setQuery('shirt') }} style={STYLES.suggestions}> shirt</span>,
187
<span onClick={() => { setQuery('shoes')}} style={STYLES.suggestions}> shoes</span> or
188
<span onClick={() => { setQuery('skirt')}} style={STYLES.suggestions}> skirt</span>.
189
</span>
190
<span>
191
⚡ Super fast search. Check out the search network calls for speed.
192
</span>
193
</div>
194
</div>
195
<div style={STYLES.body}>
196
<div style={STYLES.filterContainer}>
197
{Object.keys(filters).map(key => {
198
let els = []
199
els.push(
200
<div key={key} style={STYLES.filterTitle}>{filterTitleMapping[key]}</div>
201
)
202
filters[key].forEach((value, index) => {
203
if (index < 10) {
204
els.push(<Checkbox key={value.key} onChange={(e) => { onCheckboxChange(e, value.key, key) }} style={STYLES.checkbox}>{value.key} <Tag>{value.doc_count}</Tag></Checkbox>)
205
}
206
})
207
208
return els
209
})}
210
</div>
211
<div style={STYLES.resultsContainer}>
212
{
213
searchResults.map((recommendation) => {
214
return (
215
<div key={recommendation.sku_id} style={STYLES.slot}>
216
<img width={150} height={150} src={recommendation.image_link} />
217
<p>${recommendation?.attributes?.sale_price}</p>
218
<p>{recommendation.title}</p>
219
</div>
220
)
221
})
222
}
223
</div>
224
</div>
225
</div>
226
);
227
}
228
229
export default App;
Copied!

Sending a search request

A search request is relatively easy to send. The required fields are:
  • client (to be provided by PA)
  • website_id (to be provided by PA)
  • q (query term to search for)
  • customer_id (available in a cookie)
Once these are filled in, you are able to execute a search request!
Example payload:
1
const payload = {
2
customer_id: CUSTOMER_ID,
3
website_id: WEBSITE_ID,
4
q: query,
5
client: "sandbox",
6
personalise: false,
7
search_fields: [
8
"title",
9
"descriptions"
10
],
11
fuzzy: false,
12
size: 20,
13
scope: scope,
14
aggregations: ["product_type", "brand"]
15
}
Copied!

Aggregations

The PA API is flexible - you are able to aggregate on any field provided to us. Aggregations is how we show the faceted filters on the left hand side of the demo.
To enable aggregations, simply provide an array of attributes you would like to aggregate in your POST request.
🧠 Note: In this demo we are aggregating by “product_type” and “brand” however you are able to aggregate by any field provided to us in the product feed.
The sandbox has not been indexed to provide personalized or image search. To enable personalized search simply change the "personalise" boolean parameter to true.
To allow for image search, simply provide a publicly accessible image url in the image_link field.

Filtering results

To filter results, use the scope field in the JSON payload. Scope is a JSON object where the key is the field you’d like to filter and the value is an array of values you’d like to filter by.
Here is an example of filtering by product_type and price:
1
const payload = {
2
customer_id: CUSTOMER_ID,
3
website_id: WEBSITE_ID,
4
q: query,
5
client: "sandbox",
6
personalise: false,
7
search_fields: [
8
"title",
9
"descriptions"
10
],
11
fuzzy: false,
12
size: 20,
13
scope: {
14
product_type: ["example"],
15
price: {
16
max: 1000,
17
min: 200
18
}
19
},
20
aggregations: ["product_type", "brand"]
21
}
Copied!