通过同义词过滤器与自定义词库实现语义扩展搜索
通过将同义词组配置到自定义同义词词库,并结合dynamic_synonym同义词过滤器在写入与查询阶段对词语进行自动扩展,使任意同义词均能双向命中相关文档,最终实现搜索召回率大幅提升。
应用场景
在内容搜索、客服问答等场景中,用户可能会使用不同的词语表达同一意思。例如,文档记录的是“开心”,而用户搜索的是“高兴”,如果缺乏同义词机制,这篇文档将无法被召回,造成漏检。通过配置自定义同义词词库,并利用dynamic_synonym同义词过滤器,可在查询和写入阶段自动扩展同义词,从而实现跨词语的语义级搜索,显著提升搜索召回率。
典型应用:
- 内容平台搜索:用户使用口语化词汇搜索时,能精准匹配书面化文档内容。例如,搜索“笔记本”可召回“便携式电脑”的文章。
- 电商搜索扩展:商品描述使用正式名称,用户搜索时常用俗称或缩写。例如,搜索“手机”可召回描述为“移动终端”的商品。
- 医疗问答系统:同一疾病或药物存在多种称谓,患者使用通俗名称也能检索到相关内容。例如,搜索“心梗”可匹配数据库中记录的“心肌梗死”。
- 法律咨询检索:法律术语有正式与通俗表达之分,用户即使不熟悉专业术语,也能使用通俗说法找到所需内容。例如,搜索“离婚”可匹配文书中的“解除婚姻关系”。
- 企业知识库问答:支持缩写、别称与正式名称并行使用,确保检索覆盖全面。例如,搜索“OA系统”可召回关于“办公自动化系统”的文档。
方案架构
- 索引阶段:原始数据经过同义词扩展后,所有同义词均被写入倒排索引。例如原文含“开心”,索引中同时存储“高兴”、“快乐”、“愉快”,无论用户使用哪个词搜索均可命中。
- 查询阶段:用户输入的关键词同样经过同义词扩展,再与倒排索引匹配,实现双向语义覆盖。
- 词库动态加载:同义词词库存储于OBS,更新后无需重启集群,新写入数据即可使用新词库。
- 自定义分词器:由分析器standard/ik_smart/ik_max_word(基础分词)+ 同义词过滤器dynamic_synonym组合成自定义的分词器。
方案优势
- 语义级搜索覆盖:突破字面匹配限制,实现“搜一个词、覆盖一组意思”的语义扩展搜索,大幅提升召回率。
- 零代码配置:同义词规则以文本文件维护,业务运营人员即可自行添加新词组,无需修改任何代码或配置。
- 动态更新:词库文件通过OBS动态加载,更新词库无需重启集群,线上业务不受影响。
- 灵活的词组管理:一行定义一组同义词,支持任意数量词语组合,按业务需求灵活扩展。
- 与主词词库协同:同义词词库与主词词库可同时配置使用,确保同义词组内的专有词语在分词阶段不被错误拆分。
约束限制
- 集群版本要求:2018年3月10日之前创建的集群,不支持自定义词库功能。本文的示例代码基于OpenSearch集群和Elasticsearch 7.x版本的集群,Elasticsearch集群版本低于7.x的操作命令请参见示例代码(Elasticsearch版本<7.x)。
- 分词粒度影响:同义词扩展基于基础分词结果进行,如果某词在基础分词阶段已被拆分,则同义词扩展可能失效,建议将同义词组中的关键词同时加入主词词库,确保其在分词阶段被识别为独立词条后再进行同义词扩展。
前提条件
- 目标集群处于“可用”状态,无正在进行中的任务。
- 确认操作账号已具备以下权限(可以在IAM服务的“权限管理 > 权限”中查看和配置):
- 配置自定义词库的权限:
"css:IKThesaurus:*"
- OBS桶及对象的读取权限:
obs:bucket:getBucketLocation obs:bucket:getBucketStoragePolicy obs:bucket:listAllMyBuckets obs:object:getObject
- 如果OBS桶使用SSE-KMS加密模式时,还需KMS权限:
"kms:cmk:create", "kms:dek:create", "kms:cmk:get", "kms:dek:decrypt", "kms:cmk:list"
- 配置自定义词库的权限:
步骤一:准备词库文件
准备同义词词库,并上传至OBS桶中。
步骤二:配置自定义词库
为集群配置自定义的同义词词库,使dynamic_synonym过滤器能够加载同义词规则。
- 登录云搜索服务管理控制台。
- 在左侧导航栏,根据集群类型选择“集群管理 > Elasticsearch”或“集群管理 > OpenSearch”。
- 在集群列表,单击目标集群名称,进入集群详情页。
- 选择“集群配置 > 自定义词库”,进入自定义词库配置页面。
- 根据业务需求配置自定义词库。
表1 配置自定义词库 参数名称
操作说明
OBS桶
选择词库文件所在的OBS桶。
主词词库
保持默认值“不更新”,表示不配置该词库。
停词词库
保持默认值“不更新”,表示不配置该词库。
同义词词库
选择“更新 > 选择”,选择OBS桶中已上传的同义词词库文件“synonym_custom.txt”,单击“确定”。
- 配置完成后,单击“保存”,在弹窗中单击“确定”,启动词库更新。
下方呈现词库配置信息。等待约1分钟,词库状态从“更新中”变更为“成功”,表示词库更新完成。
图2 词库状态
步骤三:创建索引
创建索引并自定义分词器(组合分词和同义词扩展),使索引在写入和查询时都进行同义词扩展。
- 在自定义词库页面,单击页面右上角的“Kibana”或“Dashboards”。
- 在Kibana或Dashboards左侧导航栏选择“Dev Tools”,进入操作页面。
- 执行以下命令,创建索引book并自定义分词器ik_synonym(组合ik_smart分析器和dynamic_synonym过滤器)。
PUT book { "settings": { "analysis": { "filter": { "my_synonym": { "type": "dynamic_synonym" } }, "analyzer": { "ik_synonym": { "filter": [ "my_synonym" ], "type": "custom", "tokenizer": "ik_smart" } } } }, "mappings": { "properties": { "content": { "type": "text", "analyzer": "ik_synonym", "search_analyzer": "ik_synonym" } } } }参数说明:- my_synonym:自定义过滤器的名称。
- ik_synonym: 自定义分词器的名称。
- ik_smart:IK分词器提供的粗粒度分析器。
预期结果:{ "acknowledged" : true, "shards_acknowledged" : true, "index" : "book" }
步骤四:验证同义词分词效果
索引创建成功后,先验证同义词分词效果再导入数据,确保词库和分词器配置正确。
GET /book/_analyze
{
"analyzer": "ik_synonym",
"text": "我今天特别高兴"
} {
"tokens" : [
{ "token" : "我", "start_offset" : 0, "end_offset" : 1, "type" : "CN_WORD", "position" : 0 },
{ "token" : "今天", "start_offset" : 1, "end_offset" : 3, "type" : "CN_WORD", "position" : 1 },
{ "token" : "特别", "start_offset" : 3, "end_offset" : 5, "type" : "CN_WORD", "position" : 2 },
{ "token" : "高兴", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 3 },
{ "token" : "开心", "start_offset" : 5, "end_offset" : 7, "type" : "SYNONYM", "position" : 3 },
{ "token" : "快乐", "start_offset" : 5, "end_offset" : 7, "type" : "SYNONYM", "position" : 3 },
{ "token" : "愉快", "start_offset" : 5, "end_offset" : 7, "type" : "SYNONYM", "position" : 3 }
]
} 结果解读:position 3位置同时出现“高兴”、“开心”、“快乐”和“愉快”四个词,说明同义词扩展已生效。使用这四个词中任意一个搜索,均可命中该文档。
步骤五:写入测试数据
写入测试数据,用于验证同义词搜索效果。
执行以下命令,将测试文本写入索引book。
POST /book/_bulk
{"index":{"_id":"1"}}
{"content":"我今天特别高兴"}
{"index":{"_id":"2"}}
{"content":"孩子们玩得十分开心"}
{"index":{"_id":"3"}}
{"content":"你看他兴高采烈的"} 预期结果:
{
"took" : 9,
"errors" : false,
"items" : [
{ "index" : { "_index" : "book", "_type" : "_doc", "_id" : "1", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 0, "_primary_term" : 1, "status" : 201 } },
{ "index" : { "_index" : "book", "_type" : "_doc", "_id" : "2", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 1, "_primary_term" : 1, "status" : 201 } },
{ "index" : { "_index" : "book", "_type" : "_doc", "_id" : "3", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 }, "_seq_no" : 2, "_primary_term" : 1, "status" : 201 } }
]
} 步骤六:执行同义词搜索
通过同义词搜索验证自定义词库的配置效果。
GET /book/_search
{
"query": {
"match": {
"content": "快乐"
}
}
} 预期结果:
{
"took" : 15,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.8380518,
"hits" : [
{
"_index" : "book",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.8380518,
"_source" : {
"content" : "我今天特别高兴"
}
},
{
"_index" : "book",
"_type" : "_doc",
"_id" : "2",
"_score" : 0.8380518,
"_source" : {
"content" : "孩子们玩得十分开心"
}
}
]
}
} 结果解读:文档1原文含“高兴”,文档2原文含“开心”,与搜索关键词“快乐”是同义词组,因此都被成功召回;文档3无相关词语,未被召回;说明同义词扩展生效。
示例代码(Elasticsearch版本<7.x)
当Elasticsearch集群版本低于7.x时,需要自定义type名称,参考以下命令。
- 执行以下命令,创建索引book并指定自定义分词器。
PUT book { "settings": { "analysis": { "filter": { "my_synonym": { "type": "dynamic_synonym" } }, "analyzer": { "ik_synonym": { "filter": [ "my_synonym" ], "type": "custom", "tokenizer": "ik_smart" } } } }, "mappings": { "type1": { "properties": { "content": { "type": "text", "analyzer": "ik_synonym", "search_analyzer": "ik_synonym" } } } } } - 执行以下命令,将测试数据写入索引book。
POST /_bulk { "index" : { "_index" : "book", "_type" : "type1", "_id" : "1" } } { "content" : "我今天特别高兴" } { "index" : { "_index" : "book", "_type" : "type1", "_id" : "2" } } { "content" : "孩子们玩得十分开心" } { "index" : { "_index" : "book", "_type" : "type1", "_id" : "3" } } { "content" : "你看他兴高采烈的" } - 执行以下命令,通过同义词搜索验证配置结果。
GET /book/type1/_search { "query": { "match": { "content": "快乐" } } }