在工作中,经常听到有人反馈关系型数据库(MySQL、PostgreSQL 等)搜索功能存在 “LIKE 搜索速度慢” 的问题。尤其是在处理大量数据的系统中,LIKE 搜索往往会导致性能下降,搜索响应延迟的问题屡见不鲜。因此,越来越多的案例开始考虑从关系型数据库迁移到 Elasticsearch 来解决这一问题。
Elasticsearch 是一款能够实现高速、灵活全文搜索的强大搜索引擎。但要充分发挥其性能,恰当的数据设计与查询设计至关重要。
本文将聚焦于 “如何在 Elasticsearch 中高速实现类似 SQL LIKE 搜索的部分匹配搜索” 进行讲解。
1. 数据类型的差异
在 Elasticsearch 中进行字符串搜索前,首先需要理解 keyword 型与 text 型的区别。向 Elasticsearch 中注册字符串型字段时,需预先设定该字段采用哪种数据类型。
keyword 型:适用于精确字符串匹配
keyword 型是将字符串以原始形式注册到索引中的数据类型。由于其能高速执行完全匹配搜索、排序、聚合等操作,因此像 ID、商品分类这类短字符串通常会注册为 keyword 型。
text 型:适用于全文搜索
text 型擅长自然语言处理与全文搜索。设置为该类型的字段在注册到索引时,会通过分析器(Analyzer)将文本按单词或短语拆分后再进行注册。
文本按何种规则拆分为令牌(Token)由分析器决定,需根据搜索需求设计合适的分析器。
参考文档:analyzer | Elasticsearch Guide [8.16] | Elastic
我们可使用分析 API(Analyze API)确认字符串是如何被拆分令牌并注册的。以下是使用默认分析器(standard Analyzer)进行令牌化的示例。
POST _analyze
{
"text": "Elasticsearch is powerful"
}
从结果可以看出,文本被拆分为 “elasticsearch”“is”“powerful” 三个令牌。
{
"tokens": [
{
"token": "elasticsearch",
"start_offset": 0,
"end_offset": 13,
"type": "<ALPHANUM>",
"position": 0
},
{
"token": "is",
"start_offset": 14,
"end_offset": 16,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "powerful",
"start_offset": 17,
"end_offset": 25,
"type": "<ALPHANUM>",
"position": 2
}
]
}
通常情况下,部分匹配搜索的目标字段会注册为 text 型。
2. 实现部分匹配搜索的方法及特点
keyword 型字段的部分匹配搜索
对于 keyword 型字段,也可使用 wildcard 查询实现部分匹配搜索。wildcard 查询与 LIKE 搜索类似,是按特定模式匹配字符串的查询,其使用方式与 SQL 的 LIKE 搜索直观上较为接近。
但需注意,wildcard 查询的计算成本极高,索引规模越大,对搜索系统产生不良影响的可能性就越高。
wildcard 查询示例(搜索以 “Elasticsearch” 开头的字符串)
GET test_index/_search
{
"query": {
"wildcard": {
"message": {
"value": "Elasticsearch*"
}
}
}
}
text 型字段的部分匹配搜索
对 text 型字段进行部分匹配搜索时,通常使用 match 查询或 match_phrase 查询。与 text 型字段拆分令牌后注册到索引的逻辑相同,搜索字符串也会被拆分令牌,只要目标字段与搜索字符串的令牌能够匹配,对应的结果就会命中。其中,match 查询适用于单词搜索,match_phrase 查询适用于短语搜索。
match 查询示例(搜索以 “Elasticsearch” 开头的字符串)
GET test_index/_search
{
"query": {
"match": {
"message": "Elasticsearch"
}
}
}
3. 恰当的分析器设计
在 Elasticsearch 中进行字符串搜索时,虽然默认使用 match 或 match_phrase 查询,但如果分析器设置不当,可能会出现无法返回预期结果、反而返回大量无关结果等问题。
例如,在第 1 部分的示例中,若向已注册的 test_index 搜索 “power” 字符串,将无法命中结果。
搜索 “Elastic” 的查询
GET test_index/_search
{
"query": {
"match": {
"message": "power"
}
}
}
搜索结果
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
无法命中的原因是,索引中注册的令牌为 “elasticsearch”“is”“powerful”,而 “power” 这一令牌并未被注册。因此,要实现预期的搜索结果,必须分别合理设计:
- 数据注册时使用的分析器
- 搜索字符串使用的分析器
4. 对 text 型字段实现类 LIKE 从句的搜索方法
要对 text 型字段实现类似 LIKE 从句的 “搜索包含特定模式字符串” 的功能,可通过对应用了包含 N-gram 令牌生成器(tokenizer)的分析器的字段执行 match_phrase 查询来实现。
使用 N-gram 令牌生成器时,注册的字符串会被机械地按任意长度拆分令牌。例如,将 “Elasticsearch” 这一字符串按 2 个字符(bi-gram)拆分 N-gram 令牌,会得到 “El”“la”“as”……“rc”“ch” 等令牌,这些令牌会被注册到索引中。
可在创建索引时按如下方式设置分析器,为特定字段配置包含 N-gram 令牌生成器的分析器。
PUT test_index
{
"mappings": {
"properties": {
"message": {
"type": "text",
"analyzer": "my_analyzer"
}
}
},
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 2
}
}
}
}
}
即便使用 match_phrase 搜索 “lastic”,搜索字符串也会被拆分为 “la”“as”“st”“ti”“ic” 等令牌,只有包含所有这些令牌的字符串才会命中。通过这种方式,即可实现相当于 LIKE 搜索的部分匹配搜索。
GET test_index/_search
{
"query": {
"match_phrase": {
"message": "lastic"
}
}
}
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.1507283,
"hits": [
{
"_index": "test_index",
"_id": "ESDup5MBGJks9_KvUZZQ",
"_score": 1.1507283,
"_source": {
"message": "Elasticsearch is powerful"
}
}
]
}
}
5. 总结
本次介绍了在 Elasticsearch 中实现类 LIKE 搜索的部分匹配方法。
若仅需实现部分匹配搜索,使用 wildcard 查询即可达成,但从系统运行时的搜索性能等角度考量,尽管需要提前进行分析器(Analyzer)设置等额外工作,采用 N-gram 结合 match_phrase 查询仍是基础且推荐的方案。
当然,根据具体的搜索需求,还需要进行更细致的分析器及查询设计。尤其在搜索性能与搜索精度之间如何取得平衡,是运用 Elasticsearch 过程中无法回避的关键问题。希望本文能为 Elasticsearch 的搜索设计提供参考。
发表回复