Django 中可配置化的 Haystack Elasticseach Backend

Haystack Configurable Elasticseach Backend

March 31, 2018 - 4 minute read -
django haystack elasticsearch

一句话

在使用 Django 开发 web 应用,Haystack 作为搜索接口,Elasticsearch 作为搜索引擎背景下,因为 Haystack 硬编码了 Elasticsearch 的 Settings 和 Mappings,所以当我们需要自定义 Elasticsearch 的 aAalysis 时,就需要扩展 Haystack 提供的 Elasticsearch backend ,实现一个可配置化的 search backend

背景

本文假设你对 ElasticSeach / Haystack / Django 已经有一定的了解。

在我们使用 Django 做 web 应用开发的时候,如果我们需要集成搜索功能,haystack 这个第三方的应用是个不错的选择。

Haystack 作为应用的搜索接口,提供了一个统一的、模块化的、Django 原生的 API 支持,并且以可插拔的方式,支持多种类型的搜索引擎作为搜索后端,在我们的实践中,使用 ElasticSearch 作为搜索后端引擎。

这篇文章我们将介绍在使用 ElasticSearch 作为搜索引擎时,为什么 以及 如何 扩展 haystack 的 Search Backend?

使用 Haystack 提供的 Elasticsearch Backend 可以满足大部分的索引和搜索需求,但是如果我们需要如下的两个功能,那就需要扩展它原生提供的 Search Backend 了。

  • 需要自定义索引或是数据字段的 Analysis 功能,例如:analyzer / filter / tokens
  • 经常不定时的会修改数据字段的类型

因为 Haystck 将索引的 settings 硬编码在它的 Elasticsearch Backend 中了,这导致我们无法自定义地修改索引和字段的 settings,即便是我们使用 Elasticsearch 提供的方式,单独对 Elasticsearch 的 settings 进行了修改,但在我们下次在 rebuild index 时,它还是会覆盖掉我们的修改。

为此,我们需要在 Haystack 层面实现一个可配置的 Search Backend,可以实现灵活的修改 index 的设置(主要是 analysis 相关),以及数据字段的设置。

Configurable Elasticseach Backend

class ConfigurableElasticBackend(ElasticsearchSearchBackend):

    DEFAULT_ANALYZER = "snowball"

    def __init__(self, connection_alias, **connection_options):
        super(ConfigurableElasticBackend, self).__init__(
            connection_alias, **connection_options)

        user_settings = getattr(settings, 'ELASTICSEARCH_INDEX_SETTINGS')
        user_analyzer = getattr(settings, 'ELASTICSEARCH_DEFAULT_ANALYZER')

        if user_settings:
            setattr(self, 'DEFAULT_SETTINGS', user_settings)
        if user_analyzer:
            setattr(self, 'DEFAULT_ANALYZER', user_analyzer)

现在我们已经支持自定义 index 的 settings 和默认的 analyzer 了,但是如果我们想要基于字段的 analyzer 呢? 很简单,只需要在字段的设置中添加一条 analyer 参数就行,通过扩展 Field 类,实现添加一个 analyer 关键字参数:

Mixin:

class ConfigurableFieldMixin(object):

    def __init__(self, **kwargs):
        self.analyzer = kwargs.pop('analyzer', None)
        super(ConfigurableFieldMixin, self).__init__(**kwargs)

SubClass:

from haystack.fields import CharField as BaseCharField

class CharField(ConfigurableFieldMixin, BaseCharField):
    pass

Search Fields:

from myapp import models
from haystack import indexes


class MyAppIndex(indexes.ModelSearchIndex, indexes.Indexable):

    name = indexes.NgramField(model_attr='name', null=True, analyzer='my_ngram_analyzer')

    class Meta:
        model = models.MyModel
        fields = ['name']

现在我们可以为每个字段添加不同的 Analyer 了,但是在实际 Indexing 数据时,并不会使用我们上面指定的 Analyer,因为 haystack 在构建 schema 的时候硬编码了字段的默认 analyer,我们的修改,会被 haystack 在构建索引时覆盖掉,所以我们还需要对 elasticsearch backend 的 build_schema 方法做一下扩展

class ConfigurableElasticBackend(ElasticsearchSearchBackend):

    DEFAULT_ANALYZER = "snowball"

    def __init__(self, connection_alias, **connection_options):
        super(ConfigurableElasticBackend, self).__init__(
            connection_alias, **connection_options)

        user_settings = getattr(settings, 'ELASTICSEARCH_INDEX_SETTINGS')
        user_analyzer = getattr(settings, 'ELASTICSEARCH_DEFAULT_ANALYZER')

        if user_settings:
            setattr(self, 'DEFAULT_SETTINGS', user_settings)
        if user_analyzer:
            setattr(self, 'DEFAULT_ANALYZER', user_analyzer)

    def build_schema(self, fields):
        content_field_name, mapping = super(ConfigurableElasticBackend,
                                            self).build_schema(fields)

        for field_name, field_class in fields.items():
            field_mapping = mapping[field_class.index_fieldname]

            if hasattr(field_class, 'analyzer'):
                field_mapping['analyzer'] = field_class.analyer
                mapping[field_class.index_fieldname] = field_mapping
        return content_field_name, mapping

这样我们就完整的实现了基于字段的自定义 analyer,同样的道理,如果需要,我们也可以实现字段上的其他设置。

最后,更新 Django settings:

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'myapp.backends.ConfigurableElasticSearchEngine',
        'URL': env_var('HAYSTACK_URL', 'http://127.0.0.1:9200/'),
        'INDEX_NAME': 'haystack',
    },
}

ELASTICSEARCH_INDEX_SETTINGS =
 {
    'settings': {
        "max_result_window": 20000,
        "analysis": {
            "analyzer": {
                "my_ngram_analyzer": {
                    "type": "custom",
                    "tokenizer": "lowercase",
                    "filter": ["haystack_ngram"]
                }
            },
            "tokenizer": {
                "haystack_ngram_tokenizer": {
                    "type": "nGram",
                    "min_gram": 3,
                    "max_gram": 15,
                }
            },
            "filter": {
                "haystack_ngram": {
                    "type": "nGram",
                    "min_gram": 3,
                    "max_gram": 15
                }
            }
        }
    }
}

ELASTICSEARCH_DEFAULT_ANALYZER = "snowball"

在上述的几个几个步骤中我们实现了可配置化的 Search Backend 和更加灵活的字段设置,但是这是否真的到这里就完了呢?答案是否定的!

一般情况下,当我们在增加了新的数据字段时,Haystack 会帮助我们更新 Index 的设置,该设置也会在 Elasticsearch 中即时生效。

NOTES: 索引数据的更新,需要我们手工使用 Haystack 提供的更新索引的命令(update index)或是在代码中 (使用信号) 实现从数据库中加载数据。

但当我们在增加了新的 Analyzer 或是修改现有的字段时(也就是文章开始部分介绍的需要扩展 Haystack 来支持的两个功能),需要重新索引来保证设置生效,数据被正确的索引。

而 Haystack 提供了重新索引的方式,是删除并重新构建该索引结构,最后从数据库中重新加载数据,它称之为重建索引(rebuild index)。

很明显,在我们重新构建索引的过程中,索引数据会丢失,这对于在线的 web 应用来说是不可接受的。 并且因为从数据库重新加载数据,这也导致重建索引的过程将持续很长时间。

那有没有可以帮助我们实现不存在索引数据丢失并且快速重新索引的方式呢?

请参考下篇文章,如何实现零宕机重新索引 Elasticsearch 索引数据

参考