如何在django_elasticsearch_dsl中实现类似Django icontains的模糊查询?
我之前也遇到过类似的问题——想用django_elasticsearch_dsl实现和Django icontains一样的任意子串匹配,而不是局限于完整单词。原来的match和match_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




