django-haystack autocomplete returns too wide results

|▌冷眼眸甩不掉的悲伤 提交于 2019-12-12 01:57:26

问题


I have created an Index with field title_auto:

class GameIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, model_attr='title')
    title = indexes.CharField(model_attr='title')
    title_auto = indexes.NgramField(model_attr='title')

Elastic search settings look like this:

ELASTICSEARCH_INDEX_SETTINGS = {
    'settings': {
        "analysis": {
            "analyzer": {
                "ngram_analyzer": {
                    "type": "custom",
                    "tokenizer": "lowercase",
                    "filter": ["haystack_ngram"],
                    "token_chars": ["letter", "digit"]
                },
                "edgengram_analyzer": {
                    "type": "custom",
                    "tokenizer": "lowercase",
                    "filter": ["haystack_edgengram"]
                }
            },
            "tokenizer": {
                "haystack_ngram_tokenizer": {
                    "type": "nGram",
                    "min_gram": 1,
                    "max_gram": 15,
                },
                "haystack_edgengram_tokenizer": {
                    "type": "edgeNGram",
                    "min_gram": 1,
                    "max_gram": 15,
                    "side": "front"
                }
            },
            "filter": {
                "haystack_ngram": {
                    "type": "nGram",
                    "min_gram": 1,
                    "max_gram": 15
                },
                "haystack_edgengram": {
                    "type": "edgeNGram",
                    "min_gram": 1,
                    "max_gram": 15
                }
            }
        }
    }
}

I try to do autocomplete search, it works, however returns too many irrelevant results:

qs = SearchQuerySet().models(Game).autocomplete(title_auto=search_phrase)

OR

qs = SearchQuerySet().models(Game).filter(title_auto=search_phrase)

Both of them produce the same output.

If search_phrase is "monopoly", first results contain "Monopoly" in their titles, however, as there are only 2 relevant items, it returns 51. The others have nothing to do with "Monopoly" at all.

So my question is - how can I change relevance of the results?


回答1:


It's hard to tell for sure since I haven't seen your full mapping, but I suspect the problem is that the analyzer (one of them) is being used for both indexing and searching. So when you index a document, lots of ngram terms get created and indexed. If you search and your search text is also analyzed the same way, lots of search terms get generated. Since your smallest ngram is a single letter, pretty much any query is going to match a lot of documents.

We wrote a blog post about using ngrams for autocomplete that you might find helpful, here: http://blog.qbox.io/multi-field-partial-word-autocomplete-in-elasticsearch-using-ngrams. But I'll give you a simpler example to illustrate what I mean. I'm not super familiar with haystack so I probably can't help you there, but I can explain the issue with ngrams in Elasticsearch.

First I'll set up an index that uses an ngram analyzer for both indexing and searching:

PUT /test_index
{
   "settings": {
       "number_of_shards": 1,
      "analysis": {
         "filter": {
            "nGram_filter": {
               "type": "nGram",
               "min_gram": 1,
               "max_gram": 15,
               "token_chars": [
                  "letter",
                  "digit",
                  "punctuation",
                  "symbol"
               ]
            }
         },
         "analyzer": {
            "nGram_analyzer": {
               "type": "custom",
               "tokenizer": "whitespace",
               "filter": [
                  "lowercase",
                  "asciifolding",
                  "nGram_filter"
               ]
            }
         }
      }
   },
   "mappings": {
        "doc": {
            "properties": {
                "title": {
                    "type": "string", 
                    "analyzer": "nGram_analyzer"
                }
            }
        }
   }
}

and add some docs:

PUT /test_index/_bulk
{"index":{"_index":"test_index","_type":"doc","_id":1}}
{"title":"monopoly"}
{"index":{"_index":"test_index","_type":"doc","_id":2}}
{"title":"oligopoly"}
{"index":{"_index":"test_index","_type":"doc","_id":3}}
{"title":"plutocracy"}
{"index":{"_index":"test_index","_type":"doc","_id":4}}
{"title":"theocracy"}
{"index":{"_index":"test_index","_type":"doc","_id":5}}
{"title":"democracy"}

and run a simple match search for "poly":

POST /test_index/_search
{
    "query": {
        "match": {
           "title": "poly"
        }
    }
}

it returns all five documents:

{
   "took": 3,
   "timed_out": false,
   "_shards": {
      "total": 1,
      "successful": 1,
      "failed": 0
   },
   "hits": {
      "total": 5,
      "max_score": 4.729521,
      "hits": [
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "2",
            "_score": 4.729521,
            "_source": {
               "title": "oligopoly"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "1",
            "_score": 4.3608603,
            "_source": {
               "title": "monopoly"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "3",
            "_score": 1.0197333,
            "_source": {
               "title": "plutocracy"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "4",
            "_score": 0.31496215,
            "_source": {
               "title": "theocracy"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "5",
            "_score": 0.31496215,
            "_source": {
               "title": "democracy"
            }
         }
      ]
   }
}

This is because the search term "poly" gets tokenized into the terms "p", "o", "l", and "y", which, since the "title" field in each of the documents was tokenized into single-letter terms, matches every document.

If we rebuild the index with this mapping instead (same analyzer and docs):

"mappings": {
  "doc": {
     "properties": {
        "title": {
           "type": "string",
           "index_analyzer": "nGram_analyzer",
           "search_analyzer": "standard"
        }
     }
  }
}

the query will return what we expect:

POST /test_index/_search
{
    "query": {
        "match": {
           "title": "poly"
        }
    }
}
...
{
   "took": 1,
   "timed_out": false,
   "_shards": {
      "total": 1,
      "successful": 1,
      "failed": 0
   },
   "hits": {
      "total": 2,
      "max_score": 1.5108256,
      "hits": [
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "1",
            "_score": 1.5108256,
            "_source": {
               "title": "monopoly"
            }
         },
         {
            "_index": "test_index",
            "_type": "doc",
            "_id": "2",
            "_score": 1.5108256,
            "_source": {
               "title": "oligopoly"
            }
         }
      ]
   }
}

Edge ngrams work similarly, except that only terms that start at the beginning of the words will be used.

Here is the code I used for this example:

http://sense.qbox.io/gist/b24cbc531b483650c085a42963a49d6a23fa5579




回答2:


Unfortunately at this point in time there seems to be no way (apart from implementing a custom backend) to configure search analyzers and index analyzers through Django-Haystack separately. In case Django-Haystack autocomplete returns too wide results you can make use of the score value provided with each search result to optimize the output.

if search_query != "":
# Use autocomplete query or filter
# with results_filtered being a SearchQuerySet()
    results_filtered = results_filtered.filter(text=search_query)

#Remove objects with a low score
for result in results_filtered:
    if result.score < SEARCH_SCORE_THRESHOLD:
        results_filtered = results_filtered.exclude(id=result.id)

It worked reasonable well for me without having to define my own backend and scheme building.



来源:https://stackoverflow.com/questions/29008725/django-haystack-autocomplete-returns-too-wide-results

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!