开发日志:ES嵌套文档
知了开发日志:ES嵌套文档
与专用的关系型数据库存储有所不同,Elasticsearch 并没有对处理实体之间的关系给出直接的方法。在知识管理应用之前的版本开发中,附件与评论的存储都需要要以类似于「数组」或「列表」的方式存储下来,所以早些版本采用了直接将列表字符串存储入ES的一个字段中,但很显然这并不利于我们的检索。好在,ES给出了我们新的数据建模方式——嵌套文档(Nested)和父子文档(Join),本次文档将着重介绍目前知识管理应用采用的数据建模方式——嵌套文档(Nested)。
1.为什么使用嵌套文档(Nested)
①数据建模的一致性。由于在 Elasticsearch 中单个文档的增删改都是原子性操作,那么将相关实体数据都存储在同一文档中也就理所当然,在前一版本使用父子文档(Join)时,每一个附件都会生成一个新的子文档,这在本质上与父文档是具有同等的数据地位的,这造成了在Elasticsearch中父文档与子文档杂糅,十分混乱,不够优雅!
②查询效率的提升。嵌套文档(Nested)先天的查询效率要高于父子文档(Join)。嵌套对象通过冗余数据来提高查询性能,适用于读多写少的场景。父子文档类似关系型数据库中的关联关系,适用于写多的场景,减少了文档修改的范围。很显然对于评论和附件都是读多写少的场景,因此选择嵌套索引对于性能来说非常划算!
③该场景下父子文档(Join)增删改查相当复杂。举一个例子,当Permissioned
字段(即资源是否公开字段)被用户修改,我们需要既需要修改父文档的公开状态也需要修改子文档,而使用嵌套文档所有嵌套对象都是自动处理的,因为他们本来就属于同一个文档。
2.嵌套文档(Nested)的实现原理
事实上Elasticsearch没有内部对象的概念。因此,就比如说在我们的知识管理应用中,一个文档有作者、创建时间、标题、正文、附件等属性,附件这个属性是一个对象,它又包含了附件的名字、内容、解析状态等等,ES并不能理解这样的嵌套对象,它将对象层次结构扁平化为字段名称和值的简单列表。比如说像这样的嵌套对象:
"user_id": 144,
"id": 17760,
"text": "<p><span style=\"font-size: 18pt;\">附件解析太牛啦!!</span></p>",
"comment": [
{
"resourceId": 17760,
"user_id": 144,
"comment": "好资源!!!",
"time": 1648291958000
},
{
"resourceId": 17760,
"user_id": 144,
"comment": "赞!!",
"time": 1648292134000
}
]
ES会这样存储起来:
"user_id": 144,
"id": 17760,
"text": "<p><span style=\"font-size: 18pt;\">附件解析太牛啦!!</span></p>",
"comment.resourceId": [17760,17760]
"comment.user_id": [144,144]
"comment.comment": ["好资源!!!","赞!!"]
"comment.time": [1648291958000,1648292134000]
comment.resourceId
和comment.user_id
等字段被扁平化为多值字段。
3.知识管理应用中数据建模的具体实现
在之后重建ES索引时可采用如下建模方法:
PUT
http://192.168.36.136:9200/knowledge_ms_v3/
{
"settings": {
"number_of_shards": "3",
"number_of_replicas": "1",
"analysis": {
"analyzer": {
"douhao": {
"type": "pattern",
"pattern": ","
}
}
}
},
"mappings": {
"resource": {
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"attachment": {
"type": "nested",
"properties": {
"resourceId": {
"type": "keyword"
},
"attachmentId": {
"type": "keyword"
},
"createTime": {
"type": "keyword"
},
"name": {
"type": "keyword"
},
"url": {
"type": "keyword"
},
"attach_text": {
"type": "text"
},
"status": {
"type": "keyword"
}
}
},
"comment": {
"type": "nested",
"properties": {
"resourceId": {
"type": "keyword"
},
"user_id": {
"type": "keyword"
},
"comment": {
"type": "text"
},
"time": {
"type": "keyword"
},
"userName": {
"type": "keyword"
},
"picName": {
"type": "keyword"
}
}
},
"collection": {
"type": "keyword"
},
"create_time": {
"type": "date"
},
"crop_id": {
"type": "long"
},
"edit_time": {
"type": "keyword"
},
"group_id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "douhao"
},
"id": {
"type": "keyword"
},
"label_id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "douhao"
},
"opposition": {
"type": "keyword"
},
"pageview": {
"type": "keyword"
},
"permissionId": {
"type": "keyword"
},
"recognition": {
"type": "keyword"
},
"superior": {
"type": "keyword"
},
"text": {
"type": "text",
"term_vector": "with_positions_offsets",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"title": {
"type": "text",
"term_vector": "with_positions_offsets",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"type": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"user_id": {
"type": "keyword"
}
}
}
}
}
4.嵌套查询在Java中的支持
嵌套查询在知识管理应用中主要在public Page<ResourceES> searchResourceES()
函数中实现。
首先我们构建一个BoolQueryBuilder
BoolQueryBuilder reBuilder = new BoolQueryBuilder();
然后我们使用嵌套查询对象NestedQueryBuilder
分别构建附件与评论的嵌套查询:
NestedQueryBuilder nestedQuery = new NestedQueryBuilder("attachment", new MatchQueryBuilder("attachment.attach_text", query), ScoreMode.Total).boost(5.0f);
NestedQueryBuilder nestedQuery2 = new NestedQueryBuilder("comment", new MatchQueryBuilder("comment.comment", query), ScoreMode.Total).boost(10.0f);
然后我们要将两个嵌套查询对象与对标题与正文搜索的多字段查询合并到BoolQueryBuilder中:
reBuilder = reBuilder.should(multiMatchQuery(query, "title", "text")).boost(1.0f).should(nestedQuery).should(nestedQuery2);
最后我们对BoolQueryBuilder对象进行一些约束和排序,放入函数中进行查询,返回到ES对象中:
resourcePage = elasticsearchTemplate.queryForPage(searchQuery, ResourceES.class);
特别要注意的是,我们需要将Java对象中的附件与索引的属性进行修改,即要将附件与评论的数据类型由先前的String改为Object,否则无法将ES返回的结果映射到我们的对象上。
5.使用HTTP进行查询设计
虽然ES为我们设计了非常方便好用的JavaAPI,但是我们仍然可以根据自己的需要进行查询的设计。
上述JavaAPI对应的HTTP方法:
POST
http://192.168.36.136:9200/knowledge_ms_v3/resource/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "query"
}
},
{
"match": {
"text": "query"
}
},
{
"nested": {
"path": "attachment",
"query": {
"bool": {
"must": [
{
"match": {
"attachment.attach_text": "query"
}
}
]
}
}
}
},
{
"nested": {
"path": "comment",
"query": {
"bool": {
"must": [
{
"match": {
"comment.comment": "query"
}
}
]
}
}
}
}
]
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?