Elasticsearch: Building Autocomplete with Go (Completion Suggester)
This is a super-concise guide for making use of Go to use Elasticsearch’s autocomplete functionality. I will be making use of Olivere’s Go library for Elasticsearch which currently is the best available Go library in my opinion. We would look into making an index exclusively for suggesting, define it’s mapping, get suggestions from Go client and also look at a glimpse of how to get it working on the frontend. Bear in mind, this guide would be helpful to get you started on using the completion suggester (Autocomplete / Search-as-you-go) of Elasticsearch but won’t be exploring the full potential of what Elasticsearch is capable of. Now let’s get started.
Completion Suggester
As it is evident from the name, this suggester helps you complete words. It helps you get relevant completions to your typings almost instantaneously. It is not meant for spell correction or did-you-mean functionality, Elasticsearch has suggesters for those features too. You can read more about completion suggesters here.
Mapping
To make use of suggesters, we have to make use of completion
mapping type. Let’s say I have to get some stickers based on its tags. Here’s the mapping.
PUT stickers
{
"mappings": {
"icons": {
"properties": {
"tags": {
"type": "completion"
}
}
}
}
}
Here the field tags
has type completion
which means Elasticsearch would make use of words in the tags field for the autocomplete. Of course, you can define other fields in this mapping if you want, but only tags
would be considered for completion.
Indexing
Now to put some data into this index.
POST stickers/icons
{
"tags": {
"input": [
"Movie",
"Tickets",
"Movie Tickets",
"Ticket",
"Ticketing",
"Asterisk",
"Vending",
"Fares",
"Lottery",
"Discounted",
"Fare",
"Sale",
"Discount"
]
}
}
Now here I have generated some relevant tags for one sticker. Now we can also have weights for specific indexes or have weights for each tag as well which would prioritize them in suggestions. You can check them out here. I indexed some more stickers with their relevant tags.
Querying
To query the index and get suggestions in Kibana.
POST stickers/icon/_search?pretty
{
"suggest": {
"sticker-suggest": {
"prefix": "med",
"completion": {
"field": "tags",
"skip_duplicates":false,
"fuzzy": {
"fuzziness": 1
}
}
}
}
}
Here, sticker-suggest
is just my name for this suggest. prefix
is my typing; the term that would be searched for like in a search bar on the frontend.
For this, I get results like,
- Medical
- Medicine
- Melody
Now you would be wondering, Melody does not have “med” in it. That’s because I have set fuzziness
of 1
. That allows for terms that are slightly different from my typing. You can avoid it for precise suggestions only.
Getting suggestions through Go.
I am using Olivere’s Elasticsearch library. You can find it’s documentation over here. Here’s the basic method of connecting your Elasticsearch via Go.
client, err := elastic.NewSimpleClient(elastic.SetURL("http(s)://<Your Elasticsearch IP: PORT>"),
elastic.SetErrorLog(log.New(os.Stderr, "ELASTIC ", log.LstdFlags)),
elastic.SetInfoLog(log.New(os.Stdout, "", log.LstdFlags)))if err != nil {
panic(err)
}ctx := context.Background()
Here’s how you make your suggestion request for Elasticsearch
tagSuggester := elastic.NewCompletionSuggester("sticker-suggest").Fuzziness(1).Text(“med”).Field("tags").SkipDuplicates(true)searchSource := elastic.NewSearchSource().
Suggester(tagSuggester).
FetchSource(false).
TrackScores(true)searchResult, err := client.Search().
Index("stickers").
Type("icons").
SearchSource(searchSource).
Do(ctx)if err != nil {
panic(err)
}
Here, you can probably figure out tagSuggester
is making a template of my Completion Suggester with all my required options. searchSource
lets me configure my search request with NewSearchSource()
. client.Search()
indicates which index and type to look into and Do()
actually sends the request.
Parsing the Response
Here’s the part which took me a while to figure out.
stickerSuggest := searchResult.Suggest["sticker-suggest"]var results []stringfor _, options := range stickerSuggest {
for _, option := range options.Options {
fmt.Printf("%v ", option.Text)
results = append(results, option.Text)
}
}fmt.Println(results)
return results
Here, spellingSuggestions
traverses into my sticker-suggest part of my response, which actually contains the suggestions. Now the response structure of suggesters in Kibana would look like this.
"suggest": {
"sticker-suggest": [
{
"text": "med",
"offset": 0,
"length": 3,
"options": [
{
"text": "Medical",
. . .
},
{
"text": "Medicine",
. . .
},
. . .
]
}
]
}
So as you can see sticker-suggest and options are both arrays. So we have to traverse these arrays and get our results from the text field of each option. Then you can append them to an array if you wish and return them to the calling function.
Frontend on Angular5 / Jquery
I made use of mux which made URL routing really simple. I routed requests for a particular URL to my autocomplete function. I was using Angular5 at the time and made use of Jquery-UI’s autocomplete functionality which takes my response as the source and displays the results. Here’s how that function looked.
initAutoComplete() {
(<any>$('#sticker-query')).autocomplete({
source: 'http://' + this.elasticAPI + '/autocomplete',
minLength: 1,
delay: 0,
});
}
Where requests to my autocomplete route would get results from my function. Here’s what it would look in deployment.
Conclusion
Well, that brings us to the end of this article. I just want to say Elasticsearch is amazing and it provides us with amazing features besides autocompletion like Phrase suggesters and Context suggesters. Do check them out. Also, a big thanks to Olivere for making an Elasticsearch client for Go.