Cloudant blog Home Search

Building a Store Finder

Note: Given the age of this blog post, several links may be broken.

In this post we’ll be creating a web-based store finder using Cloudant. A user visits your website and wants to know which of your branches is closest to their current location.


Photo by Sherzod Max on Unsplash

To build this need a search index that can sort search results by distance but first, we need a database of branches.

Data preparation🔗

Let’s say we have a list of branches that looks like this:

130 Turves Road, Cheadle Hulme, Cheadle, Cheshire, SK8 6AW
2-8 High Street, Witney, Oxfordshire, OX28 6HA
34 Church Wk, Caterham, Surrey, CR3 6RT
49 Calverley Road, Tunbridge Wells, Kent, TN1 2UU
8 Pitsea Centre, Northlands Pavement, Pitsea, Basildon, Essex, SS13 3DU
36 The Broadway, London, E15 1NG
14-16 Station Road, West Drayton, Middlesex

Our first task is to split the addresses into meaningful columns:

address1 address2 address3 city county postcode
130 Turves Road Cheadle Hulme Cheadle Cheshire SK8 6AW
2-8 High Street Witney Oxfordshire OX28 6HA
34 Church Wk Caterham Surrey CR3 6RT
49 Calverley Road Tunbridge Wells Kent TN1 2UU
8 Pitsea Centre Northlands Pavement Pitsea Basildon Essex SS13 3DU
36 The Broadway London E15 1NG
14-16 Station Road West Drayton Middlesex UB7 7BY

Next we need to to be able to locate each of our addresses on a map, by calculating a latitude and longitude of each branch. In the UK, the Office for National Statistics publishes a list of postcodes and their longitude/latitude so with with a suitable tool, we can lookup each branch’s postcode and add additional columns for the positional data:

address1 address2 address3 city county postcode longitude latitude
130 Turves Road Cheadle Hulme Cheadle Cheshire SK8 6AW -2.206689 53.373407
2-8 High Street Witney Oxfordshire OX28 6HA -1.484964 51.785435
34 Church Wk Caterham Surrey CR3 6RT -0.077256 51.281556
49 Calverley Road Tunbridge Wells Kent TN1 2UU 0.266187 51.134097
8 Pitsea Centre Northlands Pavement Pitsea Basildon Essex SS13 3DU 0.504061 51.566237
36 The Broadway London E15 1NG 0.001901 51.541677
14-16 Station Road West Drayton Middlesex UB7 7BY -0.474192 51.509073

Importing tabular data into Cloudant🔗

It’s easy to import data in CSV (comma-separated values) or TSV (tab-separated values) into Cloudant. couchimport allows data to be bulk imported into a Cloudant database from a variety of formats. If our data is in a text file called branches.txt with TAB characters separating the columns and the first row containing column headings, then couchimport can be used like so:

# import the tabular data from "branches.txt" into Cloudant database "branches"
cat branches.txt | couchimport --db branches

The couchimport utility expects the Cloudant URL, including credentials, to be stored in an environment variable. See the README for details.

One subtlety of importing data from a text file, is that there is no sense of data type. If we want the latitude and longitude to be treated as numbers instead of strings, then we need to transform the data on its way into Cloudant. Create a transform function like this:

module.exports = function (doc) {
  doc.longitude = parseFloat(doc.longitude)
  doc.latitude = parseFloat(doc.latitude)
  return doc

and tell couchimport the path of the transform function:

# import data while transforming longitude & latitude into numbers
cat branches.txt | couchimport --db branches --transform './transform.js'

Creating a ‘find my nearest’ index🔗

Cloudant Search has a mode which allows search results to be ordered by distance from a supplied point. First we need to create the index by uploading a JavaScript function that decides which attributes to index:

// index town, latitude & longitude
function(doc) {
  index('latitude', doc.latitude)
  index('longitude', doc.longitude)

When we come to query the index we need to do two things:

  1. Supply a reasonable lat/long range around the centre point of our user’s location. If we don’t bound the query by a geographical area, Cloudant would have to use all the data in the database to get the result. The size of the region will depend on the density of your data - if you have branches on every street corner, a small area will suffice but a bigger geography will be needed for sparser data sets.
  2. Instruct Cloudant to sort the data by proximity to our centre point. Cloudant Search has a specific sort parameter which orders by distance from a point

If the centre point of our search is lat=51.508056 & long=-0.128056 (Trafalgar Square, London), then we could pick a region +-0.5° of latitude & longitude, so our query in Lucene query language becomes:

// find documents +-0.5° around our centre point
latitude:[51.0080 TO 52.0080] AND longitude:[-0.628056 TO 0.371944]

and our sort parameter is:

// sort by distance from the supplied point with distance units of kilometres

The full URL, with special characters escaped correctly, becomes:[51.0080%20TO%2052.0080]%20AND%20longitude%3A[-0.628056%20TO%200.371944]&sort=%22%3Cdistance,longitude,latitude,-0.128056,51.508056,km%3E%22&include_docs=true&limit=10

which with additional parameters include_docs=true&limit=10 returns the ten nearest branches to our chosen location.

Putting it all together🔗

It’s relatively simple to get a user’s location (with their permission) in a web application. This tutorial explains it in easy steps. For dedicated apps for Apple and Android devices, the platform SDKs have their own location APIs.

Here’s a single-page web app that:

  • Asks permission of the user to get their location.
  • Gets the browser’s location.
  • Creates a query from the browser to a Cloudant service, asking for stores around the browser’s location.
  • Presents the results as a list of stores.

If you’re in the UK, you can try it here.


Extra ideas🔗

To further improve the store finder it could also:

  • Fall back to asking for the user’s “city” and perform a fielded search for documents matching the supplied city, if the user declines to give us their latitude & longitude.
  • Present the results on a map.
  • Allow multi-page result sets to be accessed with an “infinite scroll” mechanism (see the bookmark parameter).
  • Provide links for directions, detecting which mapping app the user is likely to use.
  • Add extra meta data: phone numbers, URLs, opening times, store features etc.
  • Allow filtering of results by features e.g only show stores that are currently open