Django Haystack + Elasticsearch: промбемы автодополнения
В своих проектах часто использую Django Haystack для поиска, в качестве бекэнда выступает Elasticsearch. К сожалению, Haystack предоставляет довольно мало возможностей для настройки индексов, все они вшиты в код. Из-за чего стречаются малкие и серьезные проблемы. С одной из таких проблем столкнулся, когда нужно было реализовать поиск с автодополнением (autocomplete).
Обратимся к документации:
import datetime
from haystack import indexes
from myapp.models import Note
class NoteIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
author = indexes.CharField(model_attr='user')
pub_date = indexes.DateTimeField(model_attr='pub_date')
# We add this for autocomplete.
content_auto = indexes.EdgeNgramField(model_attr='content')
def get_model(self):
return Note
def index_queryset(self, using=None):
"""Used when the entire index for model is updated."""
return Note.objects.filter(pub_date__lte=datetime.datetime.now())
from haystack.query import SearchQuerySet
SearchQuerySet().autocomplete(content_auto='old')
Сказано, что при поиске строки old, мы получим слудуюшие результаты:
goldfish, cuckold и older
Все хорошо, пока мы не включили в поиск слово длиннее трех символов. При поиске строки gold, результаты будут неожиданными:
goldfish, cuckold, older, golrang, golf
То есть в поисковую выдачу попадут слова включающе любые три символа в поисковом запросе. Причина этому дефольные настройки Elasticsearch, которые прадлагает Django Haystack. Как видим, edge_ngram
имеет стандартный анализатор. Но нам нужно, чтобы поиск находил, только точные совпадения и отсекал лишнее. Чтобы решить эту задачу, мы должны в маппинге явно указать, index_analyzer
и search_analyzer
. Это можно сделать явно, запросом к ES:
PUT /my_index/my_type/_mapping
{
"my_type": {
"properties": {
"content_auto": {
"index_analyzer": "autocomplete",
"search_analyzer": "standard"
}
}
}
}
Теперь можно убедиться, что все работает правильно и в результатах нет мусора.
К сожалению, при перестроении индекса, все опять сломается. Чтобы этого не произошло придется написать свою версию ElasticsearchSearchBackend
, указав свои настройки для edge_ngram
в FIELD_MAPPINGS
.
from haystack.backends.elasticsearch_backend import ElasticsearchSearchBackend, ElasticsearchSearchEngine, \
FIELD_MAPPINGS, DEFAULT_FIELD_MAPPING
from haystack.constants import DJANGO_CT, DJANGO_ID
FIELD_MAPPINGS['edge_ngram'] = {'type': 'string', 'index_analyzer': 'edgengram_analyzer', 'search_analyzer': 'standard'}
class ElasticsearchCustomBackend(ElasticsearchSearchBackend):
DEFAULT_SETTINGS = {
'settings': {
"analysis": {
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "lowercase",
"filter": ["haystack_ngram"]
},
"edgengram_analyzer": {
"type": "custom",
"tokenizer": "lowercase",
"filter": ["haystack_edgengram"]
}
},
"tokenizer": {
"haystack_ngram_tokenizer": {
"type": "nGram",
"min_gram": 3,
"max_gram": 15,
},
"haystack_edgengram_tokenizer": {
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 15,
"side": "front"
}
},
"filter": {
"haystack_ngram": {
"type": "nGram",
"min_gram": 3,
"max_gram": 15
},
"haystack_edgengram": {
"type": "edgeNGram",
"min_gram": 2,
"max_gram": 15
}
}
}
}
}
def build_schema(self, fields):
content_field_name = ''
mapping = {
DJANGO_CT: {'type': 'string', 'index': 'not_analyzed', 'include_in_all': False},
DJANGO_ID: {'type': 'string', 'index': 'not_analyzed', 'include_in_all': False},
}
for field_name, field_class in fields.items():
field_mapping = FIELD_MAPPINGS.get(field_class.field_type, DEFAULT_FIELD_MAPPING).copy()
if field_class.boost != 1.0:
field_mapping['boost'] = field_class.boost
if field_class.document is True:
content_field_name = field_class.index_fieldname
# Do this last to override `text` fields.
if field_mapping['type'] == 'string':
if field_class.indexed is False or hasattr(field_class, 'facet_for'):
field_mapping['index'] = 'not_analyzed'
del field_mapping['analyzer']
mapping[field_class.index_fieldname] = field_mapping
return (content_field_name, mapping)
class ElasticsearchSearchCustomEngine(ElasticsearchSearchEngine):
backend = ElasticsearchCustomBackend
Осталось добавить наш бекэнд в настройки.