深度解析ElasticSearch同义词管理:打造更智能的搜索体验

本文将会介绍ElasticSearch中的同义词管理方案,分别为同义词库和同义词API。

ElasticSearch同义词搜索

在ElasticSearch中引入同义词(synonyms)搜索功能的主要目的是提升搜索的智能性和用户体验,具体体现在以下几个方面:

  1. 覆盖多种表达方式
    用户输入的搜索词可能存在多种表达方式(如方言、行业术语或个人习惯用语),但这些词实际指向同一个概念。
    • 例如,“土豆”与“马铃薯”本质上是同义词,但如果搜索引擎只识别一种表达方式,就可能错过相关结果。
  2. 提升搜索精准度与全面性
    如果未配置同义词,用户可能无法获取到完整或准确的搜索结果,影响体验。通过将不同的表达方式映射为统一标准形式,搜索引擎可以更加全面、精准地返回用户需要的数据。

假设用户在搜索 “土豆”,而数据库中的记录使用了 “马铃薯”
- 未配置同义词:ElasticSearch只能严格匹配“土豆”,可能会返回空结果。
- 配置同义词:ElasticSearch将“土豆”映射到“马铃薯”,从而返回所有包含“马铃薯”的相关记录,解决用户问题。

本文将会介绍ElasticSearch中的两种同义词管理方案:静态同义词库动态同义词API

静态同义词库

ElasticSearch中提供了同义词库文件配置,只需要在设置索引(index)时配置好对应的同义词库文件即可。通过外部同义词文件(如synonyms.txt)管理同义词列表,ES在启动时加载该文件。

  • 优点:易于维护和更新同义词列表。
  • 缺点:更新同义词文件后需要重新加载索引或重启节点。

以下是示例同义词文件(synonyms.txt):

1
2
3
西红柿,番茄
土豆,马铃薯
自行车,脚踏车,单车

在上述文件中,每一行中以英文逗号隔开,代表这些短语都互为同义词。

my_index索引为例,我们引入同义词库的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
PUT my_index
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"analysis": {
"filter": {
"word_sync": {
"type": "synonym",
"synonyms_path": "/usr/share/elasticsearch/plugins/ik/config/synonyms.txt"
}
},
"analyzer": {
"ik_sync_smart": {
"filter": [
"word_sync"
],
"type": "custom",
"tokenizer": "ik_smart"
}
}
}
},
"mappings": {
"properties": {
"title": {
"analyzer": "ik_sync_smart",
"type": "text"
},
"content": {
"analyzer": "ik_sync_smart",
"type": "text"
}
}
}
}

在上述配置中,请留意同义词库文件(synonyms.txt)的完整路径为正确地址。另外,对title, content字段在使用ik分词器的同时引入同义词管理。

我们插入两条样例数据:

1
2
3
4
5
PUT my_index/_doc/1
{
"title": "测试1",
"content": "我喜欢吃土豆。"
}
1
2
3
4
5
PUT my_index/_doc/2
{
"title": "测试2",
"content": "我喜欢骑自行车。"
}

查看所有数据(前10条足够用):

1
2
3
4
5
6
7
GET my_index/_search
{
"query": {
"match_all": { }
},
"size": 10
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"took": 120,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "my_index",
"_id": "1",
"_score": 1,
"_source": {
"title": "测试1",
"content": "我喜欢吃土豆。"
}
},
{
"_index": "my_index",
"_id": "2",
"_score": 1,
"_source": {
"title": "测试2",
"content": "我喜欢骑自行车。"
}
}
]
}
}

对content字段进行检索,我们输入马铃薯

1
2
3
4
5
6
7
8
GET my_index/_search
{
"query": {
"match": {
"content": "马铃薯"
}
}
}

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1.07389,
"hits": [
{
"_index": "my_index",
"_id": "1",
"_score": 1.07389,
"_source": {
"title": "测试1",
"content": "我喜欢吃土豆。"
}
}
]
}
}

这说明我们的同义词配置在content字段已经生效了!

动态同义词API

上述的静态同义词库在实际使用时,会碰到问题:如果同义词库需要频繁更新或者动态更新,都需要重启ElasticSearch服务才会生效,这在很多场景下是无法容忍的。因此,从 8.10 版本之后,ElasticSearch引入了同义词API,允许通过API动态管理同义词集。

  • 优点:无需重启或重新索引即可更新同义词,管理更灵活。
  • 缺点:需要使用ES 8.10及以上版本。

我们通过API来创建一个名为synonyms_test的同义词库。

1
2
3
4
5
6
7
8
9
10
11
12
13
PUT _synonyms/synonyms_test
{
"synonyms_set": [
{
"id": "土豆",
"synonyms": "土豆,马铃薯"
},
{
"id": "番茄",
"synonyms": "番茄,西红柿"
}
]
}

查看该同义词库的内容:

1
GET _synonyms/synonyms_test

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"count": 2,
"synonyms_set": [
{
"id": "土豆",
"synonyms": "土豆,马铃薯"
},
{
"id": "番茄",
"synonyms": "番茄,西红柿"
}
]
}

也可查看该同义词库中具体一条数据的内容:

1
GET _synonyms/synonyms_test/土豆

返回结果如下:

1
2
3
4
{
"id": "土豆",
"synonyms": "土豆,马铃薯"
}

我们创建一个使用该同义词库的索引(index),配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
PUT syno_test
{
"settings": {
"index": {
"analysis": {
"analyzer": {
"synonym_analyzer": {
"tokenizer": "ik_smart",
"filter": [
"my_synonyms"
]
}
},
"filter": {
"my_synonyms": {
"type": "synonym",
"synonyms_set": "synonyms_test",
"updateable": true
}
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "synonym_analyzer"
}
}
}
}

插入两条样例数据:

1
2
3
4
PUT syno_test/_doc/1
{
"title": "我喜欢吃番茄,骑自行车。"
}
1
2
3
4
PUT syno_test/_doc/2
{
"title": "我喜欢骑自行车。"
}

如果我们对title字段进行搜索,输入西红柿

1
2
3
4
5
6
7
8
GET syno_test/_search
{
"query": {
"match": {
"title": "西红柿"
}
}
}

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 0.9227538,
"hits": [
{
"_index": "syno_test",
"_id": "1",
"_score": 0.9227538,
"_source": {
"title": "我喜欢吃番茄,骑自行车。"
}
}
]
}
}

但如果搜索关键词为单车,则返回结果如下:

1
2
3
4
5
6
7
8
9
10
{
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}

此时,我们将单车的同义词进行动态更新,命令如下:

1
2
3
4
PUT _synonyms/synonyms_test/单车
{
"synonyms": "自行车,脚踏车,单车"
}

查看该同义词库(命令为:GET _synonyms/synonyms_test),返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"count": 3,
"synonyms_set": [
{
"id": "单车",
"synonyms": "自行车,脚踏车,单车"
},
{
"id": "土豆",
"synonyms": "土豆,马铃薯"
},
{
"id": "番茄",
"synonyms": "番茄,西红柿"
}
]
}

可以看到,此时这个同义词库中已经动态更新了单车的同义词。

我们再次对title字段输入单车进行检索,返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.13786995,
"hits": [
{
"_index": "syno_test",
"_id": "2",
"_score": 0.13786995,
"_source": {
"title": "我喜欢骑自行车。"
}
},
{
"_index": "syno_test",
"_id": "1",
"_score": 0.12562492,
"_source": {
"title": "我喜欢吃番茄,骑自行车。"
}
}
]
}
}

可以看到,此时可以检索出单车的相关内容,说明我们刚才动态更新的同义词条对title字段的检索生效了!

同义词库的删除命令为DELETE _synonyms/synonyms_test,请谨慎使用

注意:如果未删除使用了该同义词库的索引,那么该删除同义词库的命令将会运行失败。

使用Python实现动态同义词管理

对于上述的动态同义词API方案,如果使用Python代码实现(注意第三方模块的elasticsearch要与使用的ES版本一致,笔者这版的版本为8.13.0),示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# -*- coding: utf-8 -*-
import json

from elasticsearch import Elasticsearch

# 创建 Elasticsearch 客户端实例,连接到本地的 Elasticsearch 服务
es = Elasticsearch("http://localhost:9200")

# 定义同义词集的 ID
set_id = "python_synonyms_test"
# 定义同义词规则的 ID
rule_id = "rule1"


# 查看同义词集列表
def list_synonym_sets():
try:
response = es.transport.perform_request(method="GET", url="/_synonyms")
print("\n同义词集列表:")
print(response)
except Exception as e:
print(f"\n获取同义词集列表时出错: {e}")


# 查看特定同义词集的详细信息
def get_synonym_set(set_id):
try:
response = es.transport.perform_request("GET", f"/_synonyms/{set_id}")
print(f"\n同义词集 '{set_id}' 的详细信息:")
print(response)
except Exception as e:
print(f"\n获取同义词集 '{set_id}' 时出错: {e}")


# 查看特定同义词规则
def get_synonym_rule(set_id, rule_id):
try:
response = es.transport.perform_request("GET", f"/_synonyms/{set_id}/{rule_id}")
print(f"\n同义词集 '{set_id}' 中规则 '{rule_id}' 的详细信息:")
print(response)
except Exception as e:
print(f"\n获取同义词规则 '{rule_id}' 时出错: {e}")


# 更新同义词集
def update_synonym_set(set_id, synonyms_payload):
try:
response = es.transport.perform_request(method="PUT",
url=f"/_synonyms/{set_id}",
headers={"Content-Type": "application/json"},
body=synonyms_payload)
print(f"\n同义词集 '{set_id}' 更新成功:")
print(response)
except Exception as e:
print(f"\n更新同义词集 '{set_id}' 时出错: {e}")


# 更新特定同义词规则
def update_synonym_rule(set_id, rule_id, synonym_rule):
try:
response = es.transport.perform_request(method="PUT",
url=f"/_synonyms/{set_id}/{rule_id}",
headers={"Content-Type": "application/json"},body=synonym_rule)
print(f"\n同义词集 '{set_id}' 中规则 '{rule_id}' 更新成功:")
print(response)
except Exception as e:
print(f"\n更新同义词规则 '{rule_id}' 时出错: {e}")


# 示例同义词集的定义
synonyms_payload = {
"synonyms_set": [
{
"id": "rule1",
"synonyms": "自行车,脚踏车"
},
{
"id": "rule2",
"synonyms": "手机,移动电话"
}
# 可以添加更多的同义词规则
]
}

# 示例同义词规则的定义
synonym_rule = {
"synonyms": "自行车,脚踏车,单车"
}

# 调用函数示例
if __name__ == "__main__":
# 查看同义词集列表
list_synonym_sets()
# 更新同义词集
update_synonym_set(set_id, synonyms_payload)
# 查看更新同义词集后的列表
print("\n更新后的同义词集列表:")
list_synonym_sets()

# 查看特定同义词集
get_synonym_set(set_id)

# 查看特定同义词规则
get_synonym_rule(set_id, rule_id)
# 更新特定同义词规则
update_synonym_rule(set_id, rule_id, synonym_rule)
# 查看更新后的特定同义词规则
print(f"{set_id}中的{rule_id}更新后的同义词规则:")
get_synonym_rule(set_id, rule_id)

运行后输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同义词集列表:
{'count': 1, 'results': [{'synonyms_set': 'synonyms_test', 'count': 3}]}

同义词集 'python_synonyms_test' 更新成功:
{'result': 'created', 'reload_analyzers_details': {'_shards': {'total': 32, 'successful': 16, 'failed': 0}, 'reload_details': []}}

更新后的同义词集列表:

同义词集列表:
{'count': 2, 'results': [{'synonyms_set': 'python_synonyms_test', 'count': 2}, {'synonyms_set': 'synonyms_test', 'count': 3}]}

同义词集 'python_synonyms_test' 的详细信息:
{'count': 2, 'synonyms_set': [{'id': 'rule1', 'synonyms': '自行车,脚踏车'}, {'id': 'rule2', 'synonyms': '手机,移动电话'}]}

同义词集 'python_synonyms_test' 中规则 'rule1' 的详细信息:
{'id': 'rule1', 'synonyms': '自行车,脚踏车'}

同义词集 'python_synonyms_test' 中规则 'rule1' 更新成功:
{'result': 'updated', 'reload_analyzers_details': {'_shards': {'total': 32, 'successful': 16, 'failed': 0}, 'reload_details': []}}
python_synonyms_test中的rule1更新后的同义词规则:

同义词集 'python_synonyms_test' 中规则 'rule1' 的详细信息:
{'id': 'rule1', 'synonyms': '自行车,脚踏车,单车'}

总结

总结下上文中介绍的两种ElasticSearch中的同义词管理方案:同义词文件管理和同义词API管理,不同处如下:

特性 同义词文件管理 同义词API管理
适用场景 - 同义词更新频率较低的系统。
- 规模较小且易于手动管理的同义词列表。
- 需要频繁更新同义词的系统。
- 同义词列表较大或复杂,需动态管理。
更新方式 - 修改同义词文件后,需重新加载索引或重启节点以使更改生效。 - 通过API实时更新同义词,无需重启或重新索引,变更可立即生效。
维护成本 - 需要手动编辑和同步同义词文件,可能增加运维工作量。 - 通过API集中管理,降低手动操作和维护成本。
灵活性 - 更新过程较为繁琐,灵活性较低。 - 提供更高的灵活性,支持细粒度的同义词管理。
Elasticsearch版本 - 适用于所有支持同义词文件的Elasticsearch版本。 - 需要Elasticsearch 8.10及以上版本。

笔者从事NLP工作以来,对同义词抽取有相关方面工作,可参考文章NLP(四十九)别名发现模型的初次尝试,对应的Github项目为https://github.com/percent4/alias_find_system.

该项目也是笔者的研究方向之一,但囿于精力和时间未能持续更新迭代,希望后面有时间可以好好维护,并成为中文同义词库方面的基础性工作之一。也欢迎有兴趣的读者与我联系~

欢迎关注我的公众号NLP奇幻之旅,原创技术文章第一时间推送。

欢迎关注我的知识星球“自然语言处理奇幻之旅”,笔者正在努力构建自己的技术社区。


深度解析ElasticSearch同义词管理:打造更智能的搜索体验
https://percent4.github.io/深度解析ElasticSearch同义词管理:打造更智能的搜索体验/
作者
Jclian91
发布于
2025年1月8日
许可协议