You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在django_elasticsearch_dsl中实现类似Django icontains的模糊查询?

我之前也遇到过类似的问题——想用django_elasticsearch_dsl实现和Django icontains一样的任意子串匹配,而不是局限于完整单词。原来的matchmatch_phrase确实搞不定,因为它们都是基于分词后的完整词项来匹配的,没法处理"neve"找"never"这种部分单词的场景。下面给你两种可行的解决方案,按需选择:

方案1:用Wildcard查询快速实现(适合临时/小数据量场景)

这种方法不需要修改索引结构,直接调整查询逻辑就能实现类似icontains的效果。核心是用Elasticsearch的wildcard查询,通过在搜索文本前后加*来匹配任意位置的子串:

from django_elasticsearch_dsl import Q

# 注意用位运算符|组合查询,而不是Python的or
titles = TitleDocument.search().query(
    Q("wildcard", title=f"*{kwargs['text']}*") |
    Q("wildcard", slug=f"*{kwargs['text']}*") |
    Q("wildcard", page_title=f"*{kwargs['text']}*") |
    Q("wildcard", menu_title=f"*{kwargs['text']}*") |
    Q("wildcard", meta_description=f"*{kwargs['text']}*")
)

注意点:

  • 这种方法的缺点是性能较差,尤其是当搜索文本以*开头时,Elasticsearch无法利用倒排索引,需要扫描大量文档。如果你的数据量不大,或者只是临时用用,这个方法足够简单直接。
  • 如果只需要前缀匹配(比如输入"nev"找"never"),可以换成prefix查询,性能会好很多:Q("prefix", title=kwargs['text'])

方案2:用NGram分词器(推荐,适合长期/大数据量场景)

如果需要频繁做这种部分匹配搜索,推荐在索引层面配置NGram分词器。它会在索引文本时,把内容拆分成多个短的子串(比如"never"会被拆成"ne", "ev", "ve", "er"等),这样查询时用普通的match就能匹配到包含目标子串的内容,而且性能远优于wildcard。

步骤1:修改你的Document类,配置NGram分析器

from django_elasticsearch_dsl import Document, fields
from django_elasticsearch_dsl.registries import registry
from .models import Title

@registry.register_document
class TitleDocument(Document):
    class Index:
        name = 'titles'
        # 配置索引的分析器设置
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
            'analysis': {
                'analyzer': {
                    # 自定义NGram分析器
                    'ngram_analyzer': {
                        'type': 'custom',
                        'tokenizer': 'ngram_tokenizer',
                        'filter': ['lowercase']  # 统一转小写,实现大小写不敏感匹配
                    }
                },
                'tokenizer': {
                    # 定义NGram分词器的规则
                    'ngram_tokenizer': {
                        'type': 'ngram',
                        'min_gram': 2,  # 最小子串长度,比如2个字符
                        'max_gram': 15, # 最大子串长度,覆盖大部分单词
                        'token_chars': ['letter', 'digit']  # 只保留字母和数字作为分词字符
                    }
                }
            }
        }

    # 给需要匹配的字段指定自定义的NGram分析器
    title = fields.TextField(analyzer='ngram_analyzer', search_analyzer='standard')
    slug = fields.TextField(analyzer='ngram_analyzer', search_analyzer='standard')
    page_title = fields.TextField(analyzer='ngram_analyzer', search_analyzer='standard')
    menu_title = fields.TextField(analyzer='ngram_analyzer', search_analyzer='standard')
    meta_description = fields.TextField(analyzer='ngram_analyzer', search_analyzer='standard')

    class Django:
        model = Title
        fields = []

步骤2:重新索引数据

修改完Document类后,需要重新生成索引并同步数据:

python manage.py search_index --rebuild

步骤3:用普通Match查询实现子串匹配

现在你可以用更简洁的查询代码,效果和icontains完全一致,而且性能更好:

from django_elasticsearch_dsl import Q

# 用multi_match同时查询多个字段,更简洁
titles = TitleDocument.search().query(
    Q("multi_match", query=kwargs['text'], fields=['title', 'slug', 'page_title', 'menu_title', 'meta_description'])
)

为什么这个方法更好?

  • NGram分词器在索引阶段就把文本拆成了子串,查询时可以直接利用倒排索引,性能比wildcard高很多。
  • 配合lowercase过滤器,自动实现大小写不敏感匹配,和Django的icontains行为完全一致。

内容的提问来源于stack exchange,提问作者asda111sd

火山引擎 最新活动