I’m going to walk through creating a search feature using Algolia. I’ll be using some new features of JavaScript as well, such as async/await, but it can be accomplished just as well without it, and I’ll offer up alternatives where it applies. This will cover the basic outline, with a link to a more developed version at the end of the post.
It’s not going to be a magic bullet for everyone, but it’s interesting to see how it works, and it can be a solution to add to your toolkit.
What Do I Need?
- An Algolia account (there’s a free tier, as long as you add their logo to your search area)
- Some content you want to be searchable
- A way add your records to an index (you can do this manually, use the API, or if you’re using a CMS or a framework there are plenty of solutions readily available)
"Record? Index? What are you on about?" An index is something that holds the data you want to be searchable, and a record is a piece of that data. For example, you could have an index called "posts" that are made up of records of which each represents a single post. Kinda like:
<ul data-index="posts">
<li data-record="post">
<h2>My Post Title</h2>
<p>Oh hey lorem ipsum, dolor sit amet consectetur? Haha, good one adipisicing elit...</p>
</li>
<li data-record="post">
...
</li>
...
</ul>
Or maybe I ruined it. Nam facilis doloribus? Essentially you can then tell Algolia which parts of your posts it can search on. This can be the title, some content, tags, categories etc… and you can weight them by importance. So a query matching one of your post titles would bump that result to the top over a match in the content of another post.
In the API section of the Algolia dashboard, you’ll find your Application ID, your Search-Only API Key, and your Admin API Key. If you’re using a CMS or framework with an Algolia integration available, there will be spaces for you to enter these. You can also restrict HTTP referrers to ensure search will only work on the domains of your choice.
The Code
I’ll be using the JavaScript search client, and more specifically the lite client which limits the usage to search only, which will do the job. It’ll also save on file size.
Let’s install it:
npm install algoliasearch --save
Next up we’ll set up our search input:
<div class="c-search js-search-container">
<div class="c-search__inner">
<label class="c-search__label" for="s">Search:</label>
<input type="search" class="c-search__input js-search-input" id="s">
<img src="/images/algolia.svg" class="c-search__credit">
<div class="js-search-results c-search__results"></div>
</div>
</div>
Those .js-
prefixed classes will be our hooks. They’re not for styling, so it makes the intentions clear when you’re looking at the code that some JavaScript is at play here.
For the JavaScript, we’ll grab the lite client, and scaffold out some code:
import algoliasearch from 'algoliasearch/lite'
export const Search = {
trigger: document.querySelectorAll('.js-search'),
input: document.querySelector('.js-search-input'),
resultsContainer: document.querySelector('.js-search-results'),
index: null,
init() {
// bind to `this` so we reference this object rather than the input when it's called
this.performSearch = this.performSearch.bind(this)
// supply our application id and search-only api key
let client = algoliasearch('APPLICATION_ID', 'SEARCH_ONLY_API_KEY')
// connect to our index
this.index = client.initIndex('INDEX_NAME')
// perform a live search as the user types into the input field
this.input.addEventListener('keyup', this.performSearch)
},
async performSearch(event) {},
displayResults(results) {},
emptyResultContainer() {},
// we'll build up the HTML to inject into the container here
getResultLink(result) {},
displayNoResults() {}
}
So we’re grabbing our .js-
prefixed elements here, and setting up the Algolia client with our credentials to prepare it for the search.
When they keyup
event is triggered, it’ll call the performSearch
method. It’s in here that the query to Algolia is made:
async performSearch(event) {
let query = event.target.value
try {
let content = await this.index.search({ query })
if (content.hits && content.hits.length) {
this.displayResults(content.hits)
} else {
this.displayNoResults()
}
} catch (e) {
console.log('Error performing search: ', e)
}
}
I’m using async/await here, but you can use promises as well:
performSearch(event) {
let query = event.target.value
this.emptyResultContainer()
this.index
.search({ query })
.then(content => {
if (content.hits && content.hits.length) {
this.displayResults(content.hits)
} else {
this.displayNoResults()
}
})
.catch(e => {
console.log('Error performing search: ', e)
})
}
We’re getting closer to displaying the results. To start with we’ll outline how the flow works. If we have results, display them, otherwise we’ll let the user know nothing was found. After this we’ll see about constructing the search hits to inject into the results container:
displayResults(results) {
results.forEach(result => {
let resultLink = this.getResultLink(result)
this.resultsContainer.appendChild(resultLink)
})
},
emptyResultContainer() {
while (this.resultsContainer.firstChild) {
this.resultsContainer.removeChild(this.resultsContainer.firstChild)
}
},
displayNoResults() {
let title = document.createElement('h4')
title.innerText = 'No results found'
this.resultsContainer.appendChild(title)
}
In displayResults
we’re calling getResultLink
which we’ll use to append the the results container:
getResultLink(result) {
let link = document.createElement('a')
let title = document.createElement('h4')
link.setAttribute('href', result.url)
title.innerText = result.title
link.appendChild(title)
return link
}
And finally here’s the snippet in its entirety:
import algoliasearch from 'algoliasearch/lite'
export const Search = {
trigger: document.querySelectorAll('.js-search'),
input: document.querySelector('.js-search-input'),
resultsContainer: document.querySelector('.js-search-results'),
index: null,
init() {
this.performSearch = this.performSearch.bind(this)
let client = algoliasearch('APPLICATION_ID', 'SEARCH_ONLY_API_KEY')
this.index = client.initIndex('posts')
this.input.addEventListener('keyup', this.performSearch)
},
performSearch(event) {
let query = event.target.value
this.emptyResultContainer()
this.index
.search({ query })
.then(content => {
if (content.hits && content.hits.length) {
this.displayResults(content.hits)
} else {
this.displayNoResults()
}
})
.catch(e => {
console.log('Error performing search: ', e)
})
},
displayResults(results) {
results.forEach(result => {
let resultLink = this.getResultLink(result)
this.resultsContainer.appendChild(resultLink)
})
},
emptyResultContainer() {
while (this.resultsContainer.firstChild) {
this.resultsContainer.removeChild(this.resultsContainer.firstChild)
}
},
getResultLink(result) {
let link = document.createElement('a')
let title = document.createElement('h4')
link.setAttribute('href', result.url)
title.innerText = result.title
link.appendChild(title)
return link
},
displayNoResults() {
let title = document.createElement('h4')
title.innerText = 'No results found'
this.resultsContainer.appendChild(title)
}
}
With that, you can call Search.init()
to kick it all off.
Lost and Found
No longer do your quality posts need to be buried pages deep, never to be seen again. We’ve gone through using the lite client to save on file size, but you can use other full-fledged solutions for the framework of your choice for a more out-of-the-box experience.
This doesn't take into account some accessibility updates which my latest implementation now includes.
Top comments (0)