最近在做一個微服務項目,項目中基本用Redis和ES替代了Mysql,Mysql更像是一個備份。但是在開發(fā)中我遇到了一個ES查詢問題——ES的嵌套查詢。項目中存在一對多的關系,使用Mysql只要關聯(lián)查詢就行了,但是改用ES之后一直沒能直接實現(xiàn)我的需求,最后還是沒有解決我的問題,下面就簡單梳理一下。因為最近好幾個小伙伴問怎么搭梯子,所以最后會附上一個搭建梯子教程。
還是使用spring boot來整合ES,我根據(jù)項目的需求自己簡單的創(chuàng)建一個項目,一個一對多的查詢。
首先創(chuàng)建ES一個document(和Mysql的model基本一樣),代碼如下:
@Document(indexName = "team_index",type = "team_type", shards = 2, replicas = 1,createIndex = false)
public class TeamType {
@Id
@Field(type = FieldType.Keyword, store = true)
private String id;
@Field(type = FieldType.Keyword, store = true)
private Long teamId;
@Field(type = FieldType.Nested, store = true)
private List<MemberType> memberTypes;
@Field(type = FieldType.Keyword, store = true)
private String skuCode;
@Field(type = FieldType.Keyword, store = true)
private String ownerId;
@Field(type = FieldType.Keyword, store = true)
private String status;
@Field(type = FieldType.Integer, store = true)
private Integer needAmount;
// 省略部分字段
......
}
// MemberType
public class MemberType {
@Id
@Field(type = FieldType.Keyword, store = true)
private String id;
@Field(type = FieldType.Keyword, store = true)
private String teamId;
@Field(type = FieldType.Keyword, store = true)
private String userId;
@Field(type = FieldType.Boolean, store = true)
private boolean captain;
@Field(type = FieldType.Keyword, store = true)
private String orderId;
// 省略部分字段
......
}
根據(jù)代碼就可以看出在TeamType內(nèi)嵌套了一個MemberType列表,這是一個很簡單的一對多的關系。現(xiàn)在我的需求是這樣的我需要先對TeamType進行一次過濾查詢出滿足條件的對象,然后還需要對TeamType內(nèi)部的MemberType也進行一個過濾。
1、創(chuàng)建索引,添加數(shù)據(jù)
首先我啟動ES和kibana,然后通過調用接口方式創(chuàng)建索引并做好映射關系,當然也可以通過直接操作ES完成,不過既然是項目中使用我覺得自己寫個接口調用一下更方便一點,首先是創(chuàng)建索引:
public Map<String, Object> addIndex() {
Map<String,Object> resultMap = new HashMap<>();
if (elasticsearchTemplate.indexExists("team_index")) {
elasticsearchTemplate.deleteIndex("team_index");
}
elasticsearchTemplate.createIndex("team_index");
elasticsearchTemplate.putMapping(TeamType.class);
elasticsearchTemplate.refresh(TeamType.class);
resultMap.put("success",true);
resultMap.put("message","創(chuàng)建索引成功");
return resultMap;
}
接下來就是將數(shù)據(jù)從數(shù)據(jù)庫的數(shù)據(jù)存放到ES。在數(shù)據(jù)庫中我的Team和Member是分表存放的,二者通過Team的主鍵進行關聯(lián)。而在ES中只有TeamType一個類型,所以查詢從數(shù)據(jù)庫查詢出數(shù)據(jù)后需要自己拼裝一下。因為整個業(yè)務很簡單,封裝這一點也比較簡單,先從數(shù)據(jù)庫查詢所有Team,然后遍歷查詢所有的Member即可(或者使用之前說過的Mybatis一對多查詢),代碼如下:
public void addDataToES() {
List<Team> teamList = teamRepository.findAll();
for(Team team : teamList) {
List<Member> memberList = memberRepository.queryAllByTeamId(team.getId());
List<MemberType> memberTypeList = toTypeList(memberList);
TeamType teamType = new TeamType(team,memberTypeList);
teamTypeRepository.save(teamType);
teamTypeRepository.refresh();
}
}
現(xiàn)在索引建了、數(shù)據(jù)也存放了,接下來就是查詢
2、使用ES查詢
首先通過使用kibana來查詢,看下ES中的數(shù)據(jù)是否完整,
GET team_index/_search
{
"query": {
"match_all": {}
}
}
因為數(shù)據(jù)比較少,所以就貼提下查詢的結果:
{
"took": 62,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "team_index",
"_type": "team_type",
"_id": "2",
"_score": 1,
"_source": {
"id": "2",
"teamId": 2,
"memberTypes": [
{
"id": "5",
"teamId": "2",
"userId": "U22222222",
"captain": false,
"orderId": "O888888888",
"createTime": "2019-07-09T05:18:00.303+0000",
"updateTime": "2019-07-09T05:18:09.365+0000"
},
{
"id": "4",
"teamId": "2",
"userId": "U11111111",
"captain": false,
"orderId": "O999999999",
"createTime": "2019-07-09T05:17:34.585+0000",
"updateTime": "2019-07-09T05:17:46.820+0000"
}
],
"skuCode": "8887545454514",
"ownerId": "U00000001",
"status": "success",
"needAmount": 6,
"createTime": "2019-07-09T05:18:56.112+0000",
"updateTime": "2019-07-09T05:19:16.234+0000"
}
},
{
"_index": "team_index",
"_type": "team_type",
"_id": "1",
"_score": 1,
"_source": {
"id": "1",
"teamId": 1,
"memberTypes": [
{
"id": "3",
"teamId": "1",
"userId": "U77777777",
"captain": true,
"orderId": "O333333333",
"createTime": "2019-07-09T05:17:15.285+0000",
"updateTime": "2019-07-09T05:17:25.145+0000"
},
{
"id": "2",
"teamId": "1",
"userId": "U88888888",
"captain": false,
"orderId": "O222222222",
"createTime": "2019-07-09T05:16:45.362+0000",
"updateTime": "2019-07-09T05:16:57.684+0000"
},
{
"id": "1",
"teamId": "1",
"userId": "U99999999",
"captain": false,
"orderId": "O111111111",
"createTime": "2019-07-09T05:16:21.608+0000",
"updateTime": "2019-07-09T05:16:32.736+0000"
}
],
"skuCode": "9944545412121",
"ownerId": "U55555555",
"status": "success",
"needAmount": 10,
"createTime": "2019-07-09T05:18:27.576+0000",
"updateTime": "2019-07-09T05:18:50.830+0000"
}
}
]
}
}
查詢結果出來是2個TeamType,現(xiàn)在需求是這樣的:先對TeamType進行一個過濾,然后對其內(nèi)部嵌套的MemberType再進行一個過濾。以上面的查詢結果為例,我要查詢"status"等于"success",然后內(nèi)部的MemberType的"captain"屬性為false。也就是說我期望的結果如下:
{
"hits": {
"total": 2,
"max_score": 1,
"hits": [
{
"_index": "team_index",
"_type": "team_type",
"_id": "2",
"_score": 1,
"_source": {
"id": "2",
"teamId": 2,
"memberTypes": [
{
"id": "5",
"teamId": "2",
"userId": "U22222222",
"captain": false,
"orderId": "O888888888",
"createTime": "2019-07-09T05:18:00.303+0000",
"updateTime": "2019-07-09T05:18:09.365+0000"
},
{
"id": "4",
"teamId": "2",
"userId": "U11111111",
"captain": false,
"orderId": "O999999999",
"createTime": "2019-07-09T05:17:34.585+0000",
"updateTime": "2019-07-09T05:17:46.820+0000"
}
],
"skuCode": "8887545454514",
"ownerId": "U00000001",
"status": "success",
"needAmount": 6,
"createTime": "2019-07-09T05:18:56.112+0000",
"updateTime": "2019-07-09T05:19:16.234+0000"
}
},
{
"_index": "team_index",
"_type": "team_type",
"_id": "1",
"_score": 1,
"_source": {
"id": "1",
"teamId": 1,
"memberTypes": [
{
"id": "2",
"teamId": "1",
"userId": "U88888888",
"captain": false,
"orderId": "O222222222",
"createTime": "2019-07-09T05:16:45.362+0000",
"updateTime": "2019-07-09T05:16:57.684+0000"
},
{
"id": "1",
"teamId": "1",
"userId": "U99999999",
"captain": false,
"orderId": "O111111111",
"createTime": "2019-07-09T05:16:21.608+0000",
"updateTime": "2019-07-09T05:16:32.736+0000"
}
],
"skuCode": "9944545412121",
"ownerId": "U55555555",
"status": "success",
"needAmount": 10,
"createTime": "2019-07-09T05:18:27.576+0000",
"updateTime": "2019-07-09T05:18:50.830+0000"
}
}
]
}
}
按照原有想法,先過濾"status"為"success"的,然后內(nèi)部去過濾"memberTypes.captain"為false。但是實際使用并不是這么回事,我使用嵌套查詢嘗試了一下,但是沒有得到期望的結果,查詢?nèi)缦拢?/p>
GET team_index/_search
{
"query": {
"bool": {
"must": [
{"match": {
"status": "success"
}},
{
"nested": {
"path": "memberTypes",
"query": {
"match": {
"memberTypes.captain": false
}
}
}
}
]
}
}
}
使用這個查詢出來的結果和不使用嵌套查詢結果是一樣的,當然也可能是因為我數(shù)據(jù)量少的原因。但是嵌套查詢條件確實沒有生效,查詢結果中依然有"captain"為true的數(shù)據(jù)。不知道是不是我的思路有問題,如果有了解ES的可以告知一下,網(wǎng)上找了很久也沒有找到合適的方法。如果根據(jù)結果來推導,會不會是這么一個邏輯,嵌套查詢查找"captain"為false的TeamType,也就是說只要TeamType中的memberTypes有一個滿足"captain"為false,那么就會將這個TeamType返回,而返回的結果是不過濾內(nèi)部的MemberType的。為此我又專門添加了一條TeamType數(shù)據(jù),并將其內(nèi)部的MemberType的"captain"全部設置成true,然后再次查詢,發(fā)現(xiàn)查詢的結果依然是全部數(shù)據(jù),也就是說嵌套的"captain"為false條件沒有生效,所以我的猜想是錯的。
目前完全進入了死胡同,感覺這么簡單的查詢,自己卻沒有辦法解決。最后對查詢的結果在代碼里面對其進行篩選,不過這個方法感覺有點low。 代碼如下:
public Map<String, Object> queryMembers(Integer teamId) {
Map<String,Object> resultMap = new HashMap<>();
NestedQueryBuilder nestedQueryBuilder =
new NestedQueryBuilder("memberTypes",termQuery("memberTypes.captain",false),ScoreMode.None);
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery()
.must(termQuery("teamId",teamId)).must(nestedQueryBuilder);
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withIndices("team_index")
.withTypes("team_type")
.withQuery(boolQueryBuilder)
.build();
List<TeamType> teamTypeList = new ArrayList<>();
Iterable<TeamType> teamTypes = teamTypeRepository.search(searchQuery);
Iterator<TeamType> iterator = teamTypes.iterator();
while (iterator.hasNext()) {
TeamType teamType = iterator.next();
List<MemberType> memberTypeList = teamType.getMemberTypes();
teamType.setMemberTypes(memberTypeList.stream().filter(m -> !m.isCaptain()).collect(Collectors.toList()));
teamTypeList.add(teamType);
}
resultMap.put("result",teamTypeList);
return resultMap;
}
不知道是不是自己使用查詢的姿勢不正確,但是不管是在java代碼還是在kibana里面操作,都無法得到自己想要的結果,最后只能在代碼中多結果再次進行一次過濾。官方文檔中的說明也不是很清楚,自己看著感覺疑問挺多的。自己網(wǎng)上找了一些嵌套查詢的例子,但是都只給查詢的DSL,并沒有返回相應的結果。
自己也不知道怎么解決了,問了幾個同事也都沒有解決,如果哪位小伙伴對這個問題比較熟悉,希望指點一下,自己現(xiàn)在也不知道到底問題處在哪里了。
另外有好幾個小伙伴問我自建梯子的問題,因為我現(xiàn)在用的GCP服務器在海外(以前在日本,現(xiàn)在我遷到印度了),所以如果自建梯子必須要有海外的服務器,網(wǎng)上有如何薅羊毛的教程,一般都是一年或者送一定金額,具體可以網(wǎng)上百度一下。比如AWS、GCP,但是AWS速度比較慢,GCP的話目前好像國內(nèi)沒法注冊了。你也可以自己買一個小型機,一月好像5刀左右吧。我將搭梯子的教程放到github,我以前在簡書發(fā)過一次AWS的,但是被官方給鎖定了,所以就和這次的項目代碼放我的github了,有需要的可以自取。公眾號如果不能打開github鏈接,建議到簡書查看鏈接。