Guides / Building Search UI / UI & UX patterns

Geo Search with React InstantSearch

We released React InstantSearch Hooks, a new InstantSearch library for React. We recommend using React InstantSearch Hooks in new projects or upgrading from React InstantSearch.

Overview

We will see how we can leverage the geo search capabilities of Algolia with the geoSearch widget. The widget is implemented on top of Google Maps but the core logic is not tied to any map provider. You can build your own map widget with the connectGeoSearch connector and a different provider (e.g. Leaflet).

Before diving into the usage keep in mind that you are responsible for loading the Google Maps library. We provide a component to load the library (GoogleMapLoader) but its usage is not required to use the geo widget. You can use any strategy you want to load Google Maps. You can find more information about that in the Google Maps documentation.

Dataset

We’ll use a dataset of the 3000+ of the biggest airports in the world.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
 {
    "objectID": "3797",
    "name": "John F Kennedy Intl",
    "city": "New York",
    "country": "United States",
    "iata_code": "JFK",
    "links_count": 911,
    "_geoloc": {
      "lat": 40.639751,
      "lng": -73.778925
    }
  }
]

To be able to display hits on the map, we have the latitude and longitude stored in the record. We recommend storing it in the _geoloc attribute as it will allow you to do geo-filtering and and geo-sorting.

You can download the dataset on GitHub. Have a look at how to import it in Algolia.

Configure Index Settings

When displaying on a map, you still want the relevance to be good. For that let’s configure the index.

  • searchable attributes: we’re going to search in our 4 textual attributes: name, city, country and iata_code.
  • custom ranking: let’s use the number of other connected airports links_count as a ranking metric. The more connections the better.
1
2
3
4
$index->setSettings([
  'searchableAttributes' => ['name', 'city', 'country', 'iata_code'],
  'customRanking' => ['desc(links_count)']
]);

Hits on the map

This is the most simple use case for the geoSearch widget. It displays your results on a Google Maps. Note that in the example we explicitly set the height of the parent container. This is a requirement of Google Maps; don’t forget to set it, otherwise the map won’t be displayed.

The widget will set the zoom and position of the map according to the hits retrieved by the search. In case the search doesn’t return any results the map will fall back to its initialZoom and initialPosition By default, once you move the map the widget will use the bounding box of the map to filter the results. You can disable this behavior with the prop enableRefineOnMapMove You can find all the available options of the widget in the documentation.

Since the component is built on top of Google Maps you might want to apply some options to your maps. For example you might want to switch on a satellite image rather than a normal street map. You can provide these extra options directly on the geoSearch component and they will be forwarded to the Google Maps instance. The same pattern is also applied to the Marker component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-dom';
import { GoogleMapsLoader, GeoSearch, Marker } from 'react-instantsearch-dom-maps';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="airports" searchClient={searchClient}>
    <div style={{ height: 500 }}>
      <GoogleMapsLoader apiKey="GOOGLE_MAPS_API_KEY">
        {google => (
          <GeoSearch
            google={google}
            mapTypeId={google.maps.MapTypeId.SATELLITE}
          >
            {({ hits }) => (
              <div>
                {hits.map(hit => <Marker key={hit.objectID} hit={hit} />)}
              </div>
            )}
          </GeoSearch>
        )}
      </GoogleMapsLoader>
    </div>
  </InstantSearch>
);

You can find more examples in our widget showcase.

Custom Marker

The standard marker API of Google Maps let you update the image of the marker. Images are nice but what about adding information stored in your hits on the marker with a custom styling. To achieve this we expose another component, CustomMarker. This component accepts any React element as children. It means that you can create a marker with your own React component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from "react-instantsearch-dom";
import { GoogleMapsLoader, GeoSearch, CustomMarker } from "react-instantsearch-dom-maps";

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="airports" searchClient={searchClient}>
    <div style={{ height: 500 }}>
      <GoogleMapsLoader apiKey="GOOGLE_MAPS_API_KEY">
        {google => (
          <GeoSearch google={google}>
            {({ hits }) => (
              <div>
                {hits.map(hit => (
                  <CustomMarker key={hit.objectID} hit={hit}>
                    <span style={{ backgroundColor: "#FFF", fontSize: "1rem" }}>
                      {hit.city}
                    </span>
                  </CustomMarker>
                ))}
              </div>
            )}
          </GeoSearch>
        )}
      </GoogleMapsLoader>
    </div>
  </InstantSearch>
);

You can find more examples in our widget showcase.

Control

A common pattern that comes with a geo search experience is the ability to control how the refinement behaves from the map. By default the widget will use the map bounding box to filter the results as soon the map has moved. But sometimes it’s better for the experience to let the user choose when he wants to apply the refinement. This way he can explore the map without having the results changing every time. For this pattern we provide the Control component. This component lets the user control how the refinement behaves directly from the widget.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from "react-instantsearch-dom";
import { GoogleMapsLoader, GeoSearch, Control, Marker } from "react-instantsearch-dom-maps";

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="airports" searchClient={searchClient}>
    <div style={{ height: 500 }}>
      <GoogleMapsLoader apiKey="GOOGLE_MAPS_API_KEY">
        {google => (
          <GeoSearch google={google}>
            {({ hits }) => (
              <div>
                <Control />
                {hits.map(hit => <Marker key={hit.objectID} hit={hit} />)}
              </div>
            )}
          </GeoSearch>
        )}
      </GoogleMapsLoader>
    </div>
  </InstantSearch>
);

You can find more examples in our widget showcase.

Did you find this page helpful?