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.

yarn create react-app pa-search-ui

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!

cd pa-search-ui
yarn add antd
yarn start

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
<script>window.addPAC = function (c, e) { document.cookie = 'PAC=' + c + ';' + e + ';' }</script>
<script src="https://cdn.particularaudience.com/js/sandbox/t.js"></script>

🧙 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. When our query changes, we want to initiate a search

  2. When filters change, we want to initiate a search

  3. Display search results

import React, { useState, useEffect, useRef } from "react";
import { Input, Checkbox, Tag } from 'antd';
import "antd/dist/antd.css";

const { Search } = Input;

const WEBSITE_ID = '11111111-1111-1111-1111-111111111111'

const filterTitleMapping = {
  product_type: "Categories",
  brand: "Brands"
}

const STYLES = {
  app: {
    padding: 32
  },
  body: {
    display: 'flex'
  },
  header: {
    textAlign: 'left', 
    marginBottom: 32
  },
  subtitle: {
    display: 'flex',
    justifyContent: 'space-between'
  },
  filterContainer: {
    textAlign: 'left',
    flex: 1,
  },
  resultsContainer: {
    display: 'grid',
    gridGap: '16px 16px',
    gridTemplateColumns: 'auto auto auto auto',
    flex: 2
  },
  slot: {
    border: '1px solid black',
    padding: 8
  },
  checkbox: {
    width: '100%', 
    margin: '2px 0'
  },
  filterTitle: {
    margin: '16px 0 8px 0', 
    fontWeight: 'bold'
  },
  suggestions: {
    color: '#1a73e8',
    cursor: 'pointer'
  }
}

function App() {

  const [searchResults, setSearchResults] = useState([])
  const [selectedFilters, setSelectedFilters] = useState({})
  const [filters, setFilters] = useState({})
  const [isLoading] = useState(false)
  const [customerId, setCustomerId] = useState(null)
  const [query, setQuery] = useState('')

  const initialRender = useRef(true);

  const fetchSearch = async (updateFilter) => {
    if (customerId) {
      let scope = {}
      Object.keys(selectedFilters).forEach((key) => {
        if (selectedFilters[key].length > 0) {
          scope[key] = selectedFilters[key]
        }
      })

      const payload = {
        customer_id: customerId,
        website_id: WEBSITE_ID,
        q: query,
        client: "sandbox",
        personalise: false,
        search_fields: [
            "title",
            "descriptions"
        ],
        fuzzy: false,
        size: 20,
        scope: scope,
        aggregations: ["product_type", "brand"]
      }
      
      const res = await fetch(`https://search.stg.p-a.io/search`,
        {
          method: 'POST',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(payload)
        }
      )
      if (res.ok) {
        const resultsJson = await res.json()
        setSearchResults(resultsJson?.payload?.results)
        if (updateFilter) {
          setFilters(resultsJson?.payload?.aggregations)
        }
      }
    }
  };

  useEffect(() => {
    // Get PA CustomerID
    const customerIdFromCookie = getCookie('PAC')
    setCustomerId(customerIdFromCookie)
  }, []);

  useEffect(() => {
    if (initialRender.current) {
      initialRender.current = false;
    } else {
      // Reset existing filters
      setSelectedFilters({})
      setFilters({})
      // Update filters when the query changes
      fetchSearch(true)
    }
  }, [query]);

  useEffect(() => {
    // When filters change, only do search, do not update existing filters
    fetchSearch(false)
  }, [selectedFilters]);


  const getCookie = (cookieName) => {
    let name = cookieName + '='
    let cookieAttributes = document.cookie.split(';')
    for (var i = 0; i < cookieAttributes.length; i++) {
      let cookieAttribute = cookieAttributes[i]
      while (cookieAttribute.charAt(0) === ' ') {
        cookieAttribute = cookieAttribute.substring(1)
      }
      if (cookieAttribute.indexOf(name) === 0) {
        return cookieAttribute.substring(name.length, cookieAttribute.length)
      }
    }
    return ''
  }

  const onCheckboxChange = (e, value, key) => {
    let cpSelectedFilters = {...selectedFilters}
    if (!(key in cpSelectedFilters)) {
      cpSelectedFilters[key] = []
    }
    if (e.target.checked) {
      cpSelectedFilters[key].push(value)
    } else {
      const index = cpSelectedFilters[key].indexOf(value);
      if (index > -1) {
        cpSelectedFilters[key].splice(index, 1);
      }
    }
    
    setSelectedFilters(cpSelectedFilters)
  }


  return (
    <div style={STYLES.app} className="App">
      <h1>Particular Audience React Search UI</h1>
      <div style={STYLES.header}>
        <Search 
            onChange={(event) => {
              setQuery(event.target.value)
            }}
            placeholder="Search for your favourite product" 
            loading={isLoading}
            value={query}
            style={{marginBottom: 8}}
          />
        <div style={STYLES.subtitle}>
          <span>
            Try search terms such as 
            <span onClick={() => { setQuery('shirt') }} style={STYLES.suggestions}> shirt</span>, 
            <span onClick={() => { setQuery('shoes')}} style={STYLES.suggestions}> shoes</span> or 
            <span onClick={() => { setQuery('skirt')}} style={STYLES.suggestions}> skirt</span>.
          </span>
          <span>
            ⚡ Super fast search. Check out the search network calls for speed.
          </span>
        </div>
      </div>
      <div style={STYLES.body}>
        <div style={STYLES.filterContainer}>
          {Object.keys(filters).map(key => {
            let els = []
            els.push(
              <div key={key} style={STYLES.filterTitle}>{filterTitleMapping[key]}</div>
            )
            filters[key].forEach((value, index) => {
              if (index < 10) {
                els.push(<Checkbox key={value.key} onChange={(e) => { onCheckboxChange(e, value.key, key) }} style={STYLES.checkbox}>{value.key} <Tag>{value.doc_count}</Tag></Checkbox>)
              }
            })
            
            return els
          })}
        </div>
        <div style={STYLES.resultsContainer}>
          {
            searchResults.map((recommendation) => {
              return (
                <div key={recommendation.sku_id} style={STYLES.slot}>
                  <img width={150} height={150} src={recommendation.image_link} />
                  <p>${recommendation?.attributes?.sale_price}</p>
                  <p>{recommendation.title}</p>
                </div>
              )
            })
          }
        </div>
      </div>
    </div>
  );
}

export default App;

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:

const payload = {
        customer_id: CUSTOMER_ID,
        website_id: WEBSITE_ID,
        q: query,
        client: "sandbox",
        personalise: false,
        search_fields: [
            "title",
            "descriptions"
        ],
        fuzzy: false,
        size: 20,
        scope: scope,
        aggregations: ["product_type", "brand"]
      }

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:

const payload = {
        customer_id: CUSTOMER_ID,
        website_id: WEBSITE_ID,
        q: query,
        client: "sandbox",
        personalise: false,
        search_fields: [
            "title",
            "descriptions"
        ],
        fuzzy: false,
        size: 20,
        scope: {
          product_type: ["example"],
          price: {
            max: 1000,
            min: 200
          }
        },
        aggregations: ["product_type", "brand"]
      }

Last updated