Add InstantSearch and Autocomplete to your search experience in just 5 minutes
A good starting point for building a comprehensive search experience is a straightforward app template. When crafting your application’s ...
Senior Product Manager
A good starting point for building a comprehensive search experience is a straightforward app template. When crafting your application’s ...
Senior Product Manager
The inviting ecommerce website template that balances bright colors with plenty of white space. The stylized fonts for the headers ...
Search and Discovery writer
Imagine an online shopping experience designed to reflect your unique consumer needs and preferences — a digital world shaped completely around ...
Senior Digital Marketing Manager, SEO
Winter is here for those in the northern hemisphere, with thoughts drifting toward cozy blankets and mulled wine. But before ...
Sr. Developer Relations Engineer
What if there were a way to persuade shoppers who find your ecommerce site, ultimately making it to a product ...
Senior Digital Marketing Manager, SEO
This year a bunch of our engineers from our Sydney office attended GopherCon AU at University of Technology, Sydney, in ...
David Howden &
James Kozianski
Second only to personalization, conversational commerce has been a hot topic of conversation (pun intended) amongst retailers for the better ...
Principal, Klein4Retail
Algolia’s Recommend complements site search and discovery. As customers browse or search your site, dynamic recommendations encourage customers to ...
Frontend Engineer
Winter is coming, along with a bunch of houseguests. You want to replace your battered old sofa — after all, the ...
Search and Discovery writer
Search is a very complex problem Search is a complex problem that is hard to customize to a particular use ...
Co-founder & former CTO at Algolia
2%. That’s the average conversion rate for an online store. Unless you’re performing at Amazon’s promoted products ...
Senior Digital Marketing Manager, SEO
What’s a vector database? And how different is it than a regular-old traditional relational database? If you’re ...
Search and Discovery writer
How do you measure the success of a new feature? How do you test the impact? There are different ways ...
Senior Software Engineer
Algolia's advanced search capabilities pair seamlessly with iOS or Android Apps when using FlutterFlow. App development and search design ...
Sr. Developer Relations Engineer
In the midst of the Black Friday shopping frenzy, Algolia soared to new heights, setting new records and delivering an ...
Chief Executive Officer and Board Member at Algolia
When was your last online shopping trip, and how did it go? For consumers, it’s becoming arguably tougher to ...
Senior Digital Marketing Manager, SEO
Have you put your blood, sweat, and tears into perfecting your online store, only to see your conversion rates stuck ...
Senior Digital Marketing Manager, SEO
“Hello, how can I help you today?” This has to be the most tired, but nevertheless tried-and-true ...
Search and Discovery writer
We invited our friends at Starschema to write about an example of using Algolia in combination with MongoDB. We hope that you enjoy this four-part series by Full Stack Engineer Soma Osvay.
If you’d like to look back, here are the other links:
Part 1 – Use-case, architecture, and current challenges
Part 2 – Proposed solution and design
Part 3 – Data pipeline implementation
In this post, I will implement the frontend that will consume the Algolia index we created in part 3 of this series. The created web application is available here: https://algolia-listings.starschema.com/. You can try it out with our sample data by default, but if you’ve been following along and experimenting with the format we used in the last article, you can hook up your own index! The code of the application is available on GitHub. A public demo is hosted on StackBlitz.
The frontend of our existing consumer-facing application is written with jQuery and vanilla JavaScript, so I need to make sure that the Algolia sample application that I am developing is compatible with those technologies. Thankfully, Algolia’s InstantSearch.js is compatible with vanilla frontends without any customization, so I am able to use the default sample implementation in the Algolia documentation. Before starting, I drew up a quick sketch of what I wanted to create with the Algolia search UI. I took our existing search page as a baseline and extended it with a few extra features that we’re currently missing:
I used the official Algolia Getting started guide to set up my project and start coding. The npx create-instantsearch-app
created a very well-prepared and well-structured application that I could modify to match my use-case.
Before starting the Algolia specific development, I had to make sure that my Algolia app ID and API key aren’t hard coded in my application so I can share the source code easily. To do this, we created a form that asks for your Algolia credentials and stores them in the browser localStorage
to use in queries.
First, I had to change the layout of the generated index.html
file that was created along with the project. This is because I wanted to have a different search layout than Algolia originally created for me based on my UI plans. After a few iterations, I actually deviated from my original plan: I added a Clear Refinements button on the top of the filter section, scrapped pagination because I realized Algolia supports Infinite Scrolling, and I decided to implement GeoSearch (so that our end users can search by location in addition to text). My updated Algolia-specific HTML and CSS looks like this:
<!--Used to host the Algolia search UI-->
<div id="algolia-container">
<!-- The search box. Located on the top of the page, spanning the entire width -->
<div id="searchbox"></div>
<!-- The filters section, located on the left -->
<div class="filters">
<h2>Filters</h2>
<!-- Clear filters button location -->
<div id="clear-refinements"></div>
<!-- Country filter location -->
<h3>Country</h3>
<div id="country-list"></div>
<!-- Property Types filter location -->
<h3>Property Types</h3>
<div id="property-list"></div>
<!-- Review scores filter location -->
<h3>Review Scores</h3>
<div id="review-scores"></div>
<!-- Price filter location -->
<h3>Price</h3>
<div id="price"></div>
<!-- Cleaning fee filter location -->
<h3>Cleaning Fee</h3>
<div id="cleaning-fee"></div>
</div>
<!-- The results panel, located to the right side from the filters -->
<div class="results">
<h2>Results</h2>
<!-- The pagination container, which was later changed to infinite scroll results container -->
<div class="pagination-container">
<!-- The container which displays the number of results, and a link which can load the next set of results. These are modified from js -->
<div id="result-count">
<span></span>
<a href="#"></a>
</div>
<!-- The location of the control that controls how many results are loaded at once -->
<div id="per-page"></div>
</div>
<!-- The location for the map which shows the results on the world map -->
<div id="map-display">
<h3>Map</h3>
<div id="geo-search"></div>
</div>
<!-- The location of the search result details -->
<div id="details-display">
<h3>Details</h3>
<div id="hits"></div>
</div>
</div>
</div>
Algolia’s InstantSearch.js is able to create its own controls automatically, so I just have to provide the layout itself.
I also put a little effort into making this look nice with some CSS:
#algolia-container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
display: grid;
grid-template-columns: 200px 1fr;
grid-template-rows: auto auto;
gap: 20px;
}
#searchbox {
grid-column: 1 / 3;
grid-row: 1;
}
.filters {
grid-row: 2;
grid-column: 1;
}
.results {
grid-row: 2;
grid-column: 2;
}
.pagination-container {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 20px;
}
.ais-GeoSearch-map {
height: 500px; /* You can change this height */
}
Next, I initialized the Algolia JS engine that will query my index and display the results.
const {
algoliasearch,
instantsearch
} = window;
// Initialize the Algolia Search client and connect to our index
const searchClient = algoliasearch(appId, searchKey);
const search = instantsearch({
indexName: indexName,
routing: true,
searchClient,
});
Algolia has designed their frontend functionality into “widgets”, UI components that you can dynamically hydrate empty HTML nodes with. Algolia uses these to add a search box, a filtering and faceting interface, infinite scroll, and many other functionalities. We can add widgets by calling addWidgets
and providing the required widgets as an array.
search.addWidgets([
// The widget definitions go here
]);
I’m going to step through all the widgets I used, but just keep in mind that the following code samples all take place inside that array parameter to addWidgets
.
instantsearch.widgets.searchBox({
container: '#searchbox',
placeholder: 'Search real-estate listings',
autofocus: true
})
InfiniteHits
instead of just Hits
to enable infinite scrolling.
instantsearch.widgets.infiniteHits({
container: '#hits',
templates: {
// Item template will be filled out later
item: `Sample item template`
}
})
This control will contain the item template, which is a piece of HTML code that displays the individual search result. I’ll come back to this — right now there’s just a placeholder there.
instantsearch.widgets.hitsPerPage({
container: '#per-page',
items: [{
label: 'Load 5 results per page',
value: 5
},
{
label: 'Load 10 results per page',
value: 10
},
{
label: 'Load 20 results per page',
value: 20
},
{
label: 'Load 50 results per page',
value: 50,
default: true
},
{
label: 'Load 100 results per page',
value: 100
},
{
label: 'Load 200 results per page',
value: 200
}
]
})
instantsearch.widgets.clearRefinements({
container: '#clear-refinements',
templates: {
resetLabel: 'Clear filters',
},
})
instantsearch.widgets.refinementList({
container: '#property-list',
attribute: 'property_type'
}),
instantsearch.widgets.refinementList({
container: '#country-list',
attribute: 'address.country'
})
instantsearch.widgets.ratingMenu({
container: '#review-scores',
attribute: 'scores.stars'
})
instantsearch.widgets.rangeSlider({
container: '#price',
attribute: 'price',
precision: 10
}),
instantsearch.widgets.rangeSlider({
container: '#cleaning-fee',
attribute: 'cleaning_fee',
precision: 5
})
instantsearch.widgets.geoSearch({
container: '#geo-search',
googleReference: window.google,
initialPosition: {
lat: 48.864716,
lng: 2.349014,
},
initialZoom: 10,
builtInMarker: {
createOptions(item) {
return {
title: item.name,
};
},
events: {
click({
event,
item,
marker,
map
}) {
// to be implemented later
},
},
},
})
customInfiniteHits({})
Then, outside of the widget array, I create a custom function that runs when the hits change and returns new HTML for the hit list. I configured it to have customized views when there are no search results for the current geo-search or when there are no search results at all. When there are results, it displays the result count and allows the user to request that the next page of results be appended to the bottom of the current page.
// Define custom logic that runs when the results changed.
// This will call a function that displays the total result count and allows the user to Load more or clear filters if needed.
// The hitsChanged method is defined below to keep the initialization code clean.
// Documentation: <https://www.algolia.com/doc/api-reference/widgets/infinite-hits/js/>
const renderInfiniteHits = hitsChanged;
const customInfiniteHits = instantsearch.connectors.connectInfiniteHits(
renderInfiniteHits
);
// This function is called every time the search results change
// It checks how many results there are in total and either:
// - displays a 'No results found' message to the user along with the possibility to Clear Filters. It also hides the result container and possibly the map too
// - displays information about how many total results there are, and how many are currently displayed. It also allows the users to click a 'Load more' button on the top of the page.
function hitsChanged(renderOptions) {
// if this is an initial render and we got no results object, skip
if (!renderOptions.results) {
return;
}
// get the reference for the Result Count elements on the UI (its text and link as well)
const resultCountElement = document.getElementById('result-count');
const textEl = resultCountElement.querySelector('span');
const linkEl = resultCountElement.querySelector('a');
// check if we have any results
const hasHits = renderOptions.results.nbHits !== 0;
// check if we have any search query
const hasQuery = !!renderOptions.results.query;
// check if we have any geo bounding box (query made on map)
const hasGeoQuery = !!renderOptions.results._state.insideBoundingBox;
// We show/hide result Details depending if we have results or not
document.getElementById('details-display').style.display = hasHits ? 'block' : 'none';
// We hide the Map display if we have no results AND there was no map query applied (so the query can be undone)
document.getElementById('map-display').style.display = hasHits || hasGeoQuery ? 'block' : 'none';
// Update the Result count container to display information about results
if (hasHits) {
// if there are any results
if (renderOptions.isLastPage) {
// if we are on last page, show text and a link to clear filters
textEl.innerText = `Showing all ${renderOptions.hits.length} results`;
linkEl.style.display = 'none';
linkEl.innerText = 'Clear all filters';
linkEl.href = '.';
linkEl.style.display = 'inline';
linkEl.onclick = null;
} else {
// if we are not on last page, show text and link to load more items
textEl.innerText = `Showing top ${renderOptions.hits.length} results of ${renderOptions.results.nbHits}`;
linkEl.href = '';
linkEl.innerText = 'Load more';
linkEl.style.display = 'inline';
linkEl.href = "#";
linkEl.onclick = () => {
renderOptions.showMore();
return false;
};
}
} else {
// if we have no results, construct message
let statusMessage = 'No results have been found';
if (hasQuery) {
statusMessage += ` for '${renderOptions.results.query}'`;
}
if (hasGeoQuery) {
statusMessage += ' in selected map area';
}
// show message and link to clear filters
textEl.innerText = statusMessage
linkEl.innerText = 'Clear all filters';
linkEl.href = '.';
linkEl.style.display = 'inline';
linkEl.onclick = null;
}
}
After adding my widgets, the UI looked like the following:
When adding the widgets, I left the search result template empty, so the item details were not yet displayed properly. I changed the InfiniteHits
widget configuration to this:
instantsearch.widgets.infiniteHits({
container: '#hits',
templates: {
// We use no 'empty' template, as our results container will be hidden if there are no results.
// The item template is heavily modified to draw the details of a listing in detail
item: `
<article id="hit-{{objectID}}">
<div class="name-container">
<h1 class="name">{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}</h1>
{{#scores}}
<div class="stars">
<svg aria-hidden="true">
{{#scores.has_one}}
<use xlink:href="#ais-RatingMenu-starSymbol"></use>
{{/scores.has_one}}
{{^scores.has_one}}
<use xlink:href="#ais-RatingMenu-starEmptySymbol"></use>
{{/scores.has_one}}
</svg>
<svg aria-hidden="true">
{{#scores.has_two}}
<use xlink:href="#ais-RatingMenu-starSymbol"></use>
{{/scores.has_two}}
{{^scores.has_two}}
<use xlink:href="#ais-RatingMenu-starEmptySymbol"></use>
{{/scores.has_two}}
</svg>
<svg aria-hidden="true">
{{#scores.has_three}}
<use xlink:href="#ais-RatingMenu-starSymbol"></use>
{{/scores.has_three}}
{{^scores.has_three}}
<use xlink:href="#ais-RatingMenu-starEmptySymbol"></use>
{{/scores.has_three}}
</svg>
<svg aria-hidden="true">
{{#scores.has_four}}
<use xlink:href="#ais-RatingMenu-starSymbol"></use>
{{/scores.has_four}}
{{^scores.has_four}}
<use xlink:href="#ais-RatingMenu-starEmptySymbol"></use>
{{/scores.has_four}}
</svg>
<svg aria-hidden="true">
{{#scores.has_five}}
<use xlink:href="#ais-RatingMenu-starSymbol"></use>
{{/scores.has_five}}
{{^scores.has_five}}
<use xlink:href="#ais-RatingMenu-starEmptySymbol"></use>
{{/scores.has_five}}
</svg>
</div>
{{/scores}}
</div>
{{#description}}
<div class="description">
<div class="title">Description{{}}</div>
<p class="desc">{{#helpers.highlight}}{ "attribute": "description" }{{/helpers.highlight}}</p>
</div>
{{/description}}
{{#summary}}
<div class="summary">
<div class="title">Summary</div>
<p class="desc">{{#helpers.highlight}}{ "attribute": "summary" }{{/helpers.highlight}}</p>
</div>
{{/summary}}
{{#space}}
<div class="space">
<div class="title">Space</div>
<p class="desc">{{#helpers.highlight}}{ "attribute": "space" }{{/helpers.highlight}}</p>
</div>
{{/space}}
{{#neighborhood}}
<div class="neigh">
<div class="title">Neighborhood</div>
<p class="desc">{{#helpers.highlight}}{ "attribute": "neighborhood_overview" }{{/helpers.highlight}}</p>
</div>
{{/neighborhood}}
{{#transit}}
<div class="transit">
<div class="title">Transit</div>
<p class="desc">{{#helpers.highlight}}{ "attribute": "transit" }{{/helpers.highlight}}</p>
</div>
{{/transit}}
<div class="info">
{{#property_type}}
<div>
<span class="title">Property Type:</span>
<span>{{property_type}}</span>
</div>
{{/property_type}}
{{#address}}
<div>
<span class="title">Address:</span>
<span>{{#helpers.highlight}}{ "attribute": "address.street" }{{/helpers.highlight}}</span>
</div>
{{/address}}
{{#price}}
<div>
<span class="title">Price:</span>
<b>{{price}}$ per night</b>
{{#cleaning_fee}}
<span> + {{cleaning_fee}}$ cleaning fee</span>
{{/cleaning_fee}}
{{#security_deposit}}
<span> + {{security_deposit}}$ security deposit</span>
{{/security_deposit}}
</div>
{{/price}}
{{#accommodates}}
<div>
<span class="title">Accommodates:</span>
<b>{{accommodates}} people</b>
{{#bedrooms}}
<span> in {{bedrooms}} bedroom(s)</span>
{{/bedrooms}}
{{#beds}}
<span>, {{beds}} bed(s)</span>
{{/beds}}
{{#bathrooms}}
<span> with {{bathrooms}} bathroom(s)</span>
{{/bathrooms}}
</div>
{{/accommodates}}
</div>
{{#images.picture_url}}
<img class="image" src="{{images.picture_url}}">
{{/images.picture_url}}
</article>
`
}
})
The Algolia item template uses Mustache for adding variables. It is important to add the variables as attributesToRetrieve
when you are creating the index so that they are present in the code when Algolia constructs the item’s HTML. I also added the following CSS to display the items properly:
article {
display: grid;
width: 100%;
grid-template-columns: 1fr 400px;
column-gap: 20px;
grid-template-rows: auto auto auto auto auto auto auto 1fr;
}
article .title {
margin-top: 8px;
font-size: 14px;
color: gray;
}
article p {
display: block;
margin: 0px;
}
article .name-container {
grid-row: 1;
grid-column: 1 / 3;
}
article .name-container > * {
display: inline-block;
vertical-align: middle;
}
.stars {
margin-left: 8px;
height: 24px;
}
.stars svg {
width: 24px;
height: 24px;
display: inline-block;
fill: yellowgreen;
}
article .description {
grid-row: 2;
grid-column: 1;
}
article .summary {
grid-row: 3;
grid-column: 1;
}
article .space {
grid-row: 4;
grid-column: 1;
}
article .neigh {
grid-row: 5;
grid-column: 1;
}
article .transit {
grid-row: 6;
grid-column: 1;
}
article .info {
grid-row: 7;
grid-column: 1;
}
article .info > * {
margin-top: 8px;
}
article .image {
grid-column: 2;
grid-row: 2 / 9;
margin-top: 8px;
width: 100%;
}
article .properties {
display: grid;
grid-template-columns: auto auto;
gap: 4px;
grid-auto-flow: column;
width: fit-content;
}
article .properties .key {
color: gray;
font-size: 12px;
}
article .properties .value {
font-size: 12px;
}
And now the search results look like this:
Overall, I found that the Algolia InstantSearch.js framework provides an incredibly flexible way of creating your own search interfaces in your web applications. The UI building feels smooth, and the widget system is a wonderful tool to build good-looking interfaces very quickly. Using the custom connectors, I also realized that Algolia can fit into more complex use-cases as well, which require a more customized approach. The generated HTML can be styled easily to look much different than the prototype.
My entire Algolia frontend implementation was a few hundred lines of code, compared to the thousands of lines I would have had to write if I was going to implement this kind of capability myself. Most of the code was taken up by my custom generalization and connector logic, and Algolia let me tie all of those complex features in with the default InstantSearch.js widgets with just a few commands.
The searching is astonishingly fast, the index outperforms my expectations by quite a bit. The average search or filter action shows a result within 60ms, much quicker than our usual expectation of a whole second.
During this proof of concept, I found that:
Based on these findings, I will definitely recommend to my team that we use Algolia in production.
Full Stack Engineer, Starschema
Powered by Algolia Recommend