Introduction
Recently, I’ve investigated autocomplete functionality of our system as there were a lot of complaints that it was returning irrelevant results. The approach we had taken was pretty naive: our backend wrapped query into wildcard symbols and executed it as query_string on fields __title, title and commonInfo.RealName. Index we’ve executed search upon contained entity with _title equal 3 foxes but autocomplete query 3 foxes suggested BRN-3 / QCK 3 / 19 foxes, AC d / 3 foxes, 3 BRW / 1 foxes. The exact match was nowhere in sight!
So I’ve chosen 3 foxes as my relevance baseline and turned my eye to specific Elasticsearch queries that facilitate autocomplete functionality.
Search as You Type
As the name implies, search as you type seemed a perfect fit for autocomplete functionality. To start off, I’ve changed the mapping of my __title field to search_as_you_type and performed bool_prefix query straight from the documentation.
{
"_source": [
"__title"
],
"from": 0,
"size": 3,
"query": {
"multi_match": {
"query": "3 foxe",
"type": "bool_prefix",
"fields": [
"__title",
"__title.2gram",
"__title.3gram"
]
}
}
}
Now this was better!
{
"took": 21,
"timed_out": false,
"_shards": {
"total": 33,
"successful": 33,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 272,
"relation": "eq"
},
"max_score": 4.5528774,
"hits": [
{
"_index": "data",
"_type": "_doc",
"_id": "cdf7f3aded8745d1827e9c92dea1e8b7",
"_score": 4.5528774,
"_source": {
"__title": "3 oxe/ 3 foxes"
}
},
{
"_index": "data",
"_type": "_doc",
"_id": "1a42873cead94f18a31d0b102b4fbdcd",
"_score": 4.285463,
"_source": {
"__title": "3 foxes"
}
},
{
"_index": "data",
"_type": "_doc",
"_id": "6f1588bbe1a440028af1de4337bf8fac",
"_score": 3.9906564,
"_source": {
"__title": "9 mfx/ 3 oxe/ 3 foxes"
}
}
]
}
}
But still, it was not good enough since the exact match was only second. Since out-of-the-box solutions didn’t help, I’ve decided to read on about search_as_you_type mapping and n-gram fields. After reading for a while, I’ve learned that n-grams are basically sequences of words extracted from the text mixed in a random order which allows searching words out of order in my autocomplete query. The downside of this is that the Elasticsearch cluster consumes extra memory to store n-grams which may affect the cluster state. And fancy search_as_you_type mapping just means that n-gram fields are created automatically.
Since typing out-of-order words wasn’t my use case, I’ve decided not to mess with it and improve my relevance query time instead of index-time.
Match Phrase Query
In order to boost exact match relevance, I’ve switched to match phrase prefix query.
{
"_source": [
"__title"
],
"from": 0,
"size": 3,
"query": {
"match_phrase_prefix": {
"__title": {
"query": "3 foxe"
}
}
}
}
Now that was what I was looking for.
{
"took": 10,
"timed_out": false,
"_shards": {
"total": 33,
"successful": 33,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 28,
"relation": "eq"
},
"max_score": 12.053555,
"hits": [
{
"_index": "data",
"_type": "_doc",
"_id": "1a42873cead94f18a31d0b102b4fbdcd",
"_score": 12.053555,
"_source": {
"__title": "3 foxes"
}
},
//omited for brevity
]
}
}
Can we wrap up at this point? Not so soon! As you may recall, our autocomplete functionality uses 3 fields, but we’ve examined only one of them. So how do we combine multiple fields? Since match_phrase_prefix doesn’t support multiple fields, the first guess was the plain old bool query.
{
"_source":[
"__title",
"title",
"commonInfo.RealNameShort"
],
"explain":false,
"from":0,
"size":3,
"query":{
"bool":{
"should":[
{
"match_phrase_prefix":{
"__title":{
"query":"3 foxe"
}
}
},
{
"match_phrase_prefix":{
"title":{
"query":"3 foxe"
}
}
},
{
"match_phrase_prefix":{
"commonInfo.RealNameShort":{
"query":"3 foxe"
}
}
}
]
}
}
}
And the result was,
{
"took": 13,
"timed_out": false,
"_shards": {
"total": 33,
"successful": 33,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 28,
"relation": "eq"
},
"max_score": 28.880083,
"hits": [
{
"_index": "data",
"_type": "_doc",
"_id": "15e4e503cc1d4284aeb34664cb61c5ae",
"_score": 28.880083,
"_source": {
"__title": "apt 3 foxes",
"commonInfo": {
"RealNameShort": "apt 3 foxes"
},
"title": "apartmetnt 3 foxes"
}
},
{
"_index": "data",
"_type": "_doc",
"_id": "83b2653a851c4ca19d3df0410ab1c41f",
"_score": 26.242756,
"_source": {
"__title": "rest/ 3 foxes",
"commonInfo": {
"RealNameShort": "rest/ 3 foxes"
},
"title": "restaraunt/ 3 foxes"
}
},
{
"_index": "data",
"_type": "_doc",
"_id": "1a42873cead94f18a31d0b102b4fbdcd",
"_score": 23.940828,
"_source": {
"__title": "3 foxes",
"title": "3 completely irrelevant to real name words",
"commonInfo": {
"RealNameShort": "3 foxes"
}
}
}
]
}
}
Join the conversation! Your thoughts help the community grow.