本文将会介绍ElasticSearch中的同义词管理方案,分别为同义词库和同义词API。
ElasticSearch同义词搜索
在ElasticSearch中引入同义词(synonyms)搜索功能的主要目的是提升搜索的智能性和用户体验,具体体现在以下几个方面:
- 覆盖多种表达方式:
用户输入的搜索词可能存在多种表达方式(如方言、行业术语或个人习惯用语),但这些词实际指向同一个概念。
- 例如,“土豆”与“马铃薯”本质上是同义词,但如果搜索引擎只识别一种表达方式,就可能错过相关结果。
- 提升搜索精准度与全面性:
如果未配置同义词,用户可能无法获取到完整或准确的搜索结果,影响体验。通过将不同的表达方式映射为统一标准形式,搜索引擎可以更加全面、精准地返回用户需要的数据。
假设用户在搜索 “土豆”,而数据库中的记录使用了
“马铃薯”:
-
未配置同义词: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
| import json
from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
set_id = "python_synonyms_test"
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集中管理,降低手动操作和维护成本。 |
灵活性 |
- 更新过程较为繁琐,灵活性较低。 |
- 提供更高的灵活性,支持细粒度的同义词管理。 |
Elasticsearch版本 |
- 适用于所有支持同义词文件的Elasticsearch版本。 |
- 需要Elasticsearch 8.10及以上版本。 |
笔者从事NLP工作以来,对同义词抽取有相关方面工作,可参考文章NLP(四十九)别名发现模型的初次尝试,对应的Github项目为https://github.com/percent4/alias_find_system.
该项目也是笔者的研究方向之一,但囿于精力和时间未能持续更新迭代,希望后面有时间可以好好维护,并成为中文同义词库方面的基础性工作之一。也欢迎有兴趣的读者与我联系~
欢迎关注我的公众号NLP奇幻之旅,原创技术文章第一时间推送。
欢迎关注我的知识星球“自然语言处理奇幻之旅”,笔者正在努力构建自己的技术社区。