黑夜给了我黑色的眼睛,我却用它寻找光明。前端
通过了解简单的API和简单搜索,已经基本上能应付大部分的使用场景。但是非关系型数据库数据的文档数据每每又多又杂,各类各样冗余的字段,组成了一条"记录"。复杂的数据结构,带来的就是复杂的搜索。因此在进入本章节前,咱们要构建一个尽量"复杂"的数据结构。java
下面分为两个场景,场景1偏向数据结构上的复杂而且介绍聚合查询、指定字段返回、深分页,场景2偏向搜索精度上的复杂。git
存储一个公司的员工,员工信息包含姓名、工号、性别、出生年月日、岗位、上级、下级、所在部门、进入公司时间、修改时间、建立时间。其中员工工号做为主键ID全局惟一,员工只有一个直属上级,但有多个下级,能够经过父子文档实现。员工有可能属于多个部门(特别是领导可能兼任多个部门的负责人)。程序员
建立索引并定义映射结构:github
PUT http://localhost:9200/company { "mappings":{ "employee":{ "properties":{ "id":{ "type":"keyword" }, "name":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "sex":{ "type":"keyword" }, "age":{ "type":"integer" }, "birthday":{ "type":"date" }, "position":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "level":{ "type":"join", "relations":{ "superior":"staff", "staff":"junior" } }, "departments":{ "type":"text", "analyzer":"ik_smart", "fields":{ "keyword":{ "type":"keyword", "ignore_above":256 } } }, "joinTime":{ "type":"date" }, "modified":{ "type":"date" }, "created":{ "type":"date" } } } } }
接下来是构造数据,咱们构造几条关键数据。spring
更为全面直观的数据以下表所示:数据库
姓名 | 工号 | 性别 | 年龄 | 出生年月日 | 岗位 | 上级 | 下级 | 部门 | 进入公司时间 | 修改时间 | 建立时间 |
---|---|---|---|---|---|---|---|---|---|---|---|
张三 | 1 | 男 | 49 | 1970-01-01 | 董事长 | / | 李四 | / | 1990-01-01 | 1562167817000 | 1562167817000 |
李四 | 2 | 男 | 39 | 1980-04-03 | 总经理 | 张三 | 王5、赵6、孙7、周八 | 市场部、研发部 | 2001-02-02 | 1562167817000 | 1562167817000 |
王五 | 3 | 女 | 27 | 1992-09-01 | 销售 | 李四 | / | 市场部 | 2010-07-01 | 1562167817000 | 1562167817000 |
赵六 | 4 | 男 | 29 | 1990-10-10 | 销售 | 李四 | / | 市场部 | 2010-08-08 | 1562167817000 | 1562167817000 |
孙七 | 5 | 男 | 26 | 1993-12-10 | 前端工程师 | 李四 | / | 研发部 | 2016-07-01 | 1562167817000 | 1562167817000 |
周八 | 6 | 男 | 25 | 1994-05-11 | Java工程师 | 李四 | / | 研发部 | 2018-03-10 | 1562167817000 | 1562167817000 |
插入6条数据:json
POST http://localhost:9200/company/employee/1?routing=1 { "id":"1", "name":"张三", "sex":"男", "age":49, "birthday":"1970-01-01", "position":"董事长", "level":{ "name":"superior" }, "joinTime":"1990-01-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/2?routing=1 { "id":"2", "name":"李四", "sex":"男", "age":39, "birthday":"1980-04-03", "position":"总经理", "level":{ "name":"staff", "parent":"1" }, "departments":["市场部","研发部"], "joinTime":"2001-02-02", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/3?routing=1 { "id":"3", "name":"王五", "sex":"女", "age":27, "birthday":"1992-09-01", "position":"销售", "level":{ "name":"junior", "parent":"2" }, "departments":["市场部"], "joinTime":"2010-07-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/4?routing=1 { "id":"4", "name":"赵六", "sex":"男", "age":29, "birthday":"1990-10-10", "position":"销售", "level":{ "name":"junior", "parent":"2" }, "departments":["市场部"], "joinTime":"2010-08-08", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/5?routing=1 { "id":"5", "name":"孙七", "sex":"男", "age":26, "birthday":"1993-12-10", "position":"前端工程师", "level":{ "name":"junior", "parent":"2" }, "departments":["研发部"], "joinTime":"2016-07-01", "modified":"1562167817000", "created":"1562167817000" }
POST http://localhost:9200/company/employee/6?routing=1 { "id":"6", "name":"周八", "sex":"男", "age":28, "birthday":"1994-05-11", "position":"Java工程师", "level":{ "name":"junior", "parent":"2" }, "departments":["研发部"], "joinTime":"2018-03-10", "modified":"1562167817000", "created":"1562167817000" }
GET http://localhost:9200/company/employee/_search { "query":{ "match":{ "departments":"研发部" } } }
GET http://localhost:9200/company/employee/_search { "query": { "bool":{ "must":[{ "match":{ "departments":"市场部" } },{ "match":{ "departments":"研发部" } }] } } }
*被搜索的字段是一个数组类型,但对查询语句并无特殊的要求。数组
GET http://localhost:9200/company/employee/_search { "query": { "has_parent":{ "parent_type":"superior", "query":{ "match":{ "name":"张三" } } } } }
GET http://localhost:9200/company/employee/_search { "query": { "has_parent":{ "parent_type":"staff", "query":{ "match":{ "name":"李四" } } } } }
GET http://localhost:9200/company/employee/_search { "query": { "has_child":{ "type":"junior", "query":{ "match":{ "name":"王五" } } } } }
ES中的聚合查询相似MySQL中的聚合函数(avg、max等),例如计算员工的平均年龄。前端工程师
GET http://localhost:9200/company/employee/_search?pretty { "size": 0, "aggs": { "avg_age": { "avg": { "field": "age" } } } }
指定字段返回值在查询结果中指定须要返回的字段。例如只查询张三的生日。
GET http://localhost:9200/company/employee/_search?pretty { "_source":["name","birthday"], "query":{ "match":{ "name":"张三" } } }
ES的深分页是一个老生常谈的问题。用过ES的都知道,ES默认查询深度不能超过10000条,也就是page * pageSize < 10000。若是须要查询超过1万条的数据,要么经过设置最大深度,要么经过scroll
滚动查询。若是调整配置,即便能查出来,性能也会不好。但经过scroll
滚动查询的方式带来的问题就是只能进行"上一页"、"下一页"的操做,而不能进行页码跳转。
scroll
原理简单来说,就是一批一批的查,上一批的最后一个数据,做为下一批的第一个数据,直到查完全部的数据。
首先须要初始化查询
GET http://localhost:9200/company/employee/_search?scroll=1m { "query":{ "match_all":{} }, "size":1, "_source": ["id"] }
像普通查询结果同样进行查询,url中的scroll=1m指的是游标查询的过时时间为1分钟,每次查询就会更新,设置过长占会用过多的时间。
接下来就能够经过上述API返回的_scroll_id
进行滚动查询,假设上面的结果返回"_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFBFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABQhZNaTc3RVFYQ1N4cV91bUlRVHQyQVpRAAAAAAAAAUMWTWk3N0VRWENTeHFfdW1JUVR0MkFaUQAAAAAAAAFEFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABRRZNaTc3RVFYQ1N4cV91bUlRVHQyQVpR"
。
GET http://localhost:9200/_search/scroll { "scroll":"1m", "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAFBFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABQhZNaTc3RVFYQ1N4cV91bUlRVHQyQVpRAAAAAAAAAUMWTWk3N0VRWENTeHFfdW1JUVR0MkFaUQAAAAAAAAFEFk1pNzdFUVhDU3hxX3VtSVFUdDJBWlEAAAAAAAABRRZNaTc3RVFYQ1N4cV91bUlRVHQyQVpR" }
这种方式有一个小小的弊端,若是超过过时时间就不能继续往下查询,这种查询适合一次全量查询全部数据。但现实状况有多是用户在一个页面停留很长时间,再点击上一页或者下一页,此时超过过时时间页面不能再进行查询。因此还有另一种方式,范围查询。
假设员工数据中的工号ID是按递增且惟一的顺序,那么咱们能够经过范围查询进行分页。
例如,按ID递增排序,第一查询ID>0的数据,数据量为1。
GET http://localhost:9200/company/employee/_search { "query":{ "range":{ "id":{ "gt":0 } } }, "size":1, "sort":{ "id":{ "order":"asc" } } }
此时返回ID=1的1条数据,咱们再继续查询ID>1的数据,数据量仍然是1。
GET http://localhost:9200/company/employee/_search { "query":{ "range":{ "id":{ "gt":1 } } }, "size":1, "sort":{ "id":{ "order":"asc" } } }
这样咱们一样作到了深分页的查询,而且没有过时时间的限制。
存储商品数据,根据商品名称搜索商品,要求准确度高,不能搜索洗面奶结果出现面粉。
因为这个场景主要涉及的是搜索的精度问题,因此并不会有复杂的数据结构,只有一个title字段。
定义一个只包含title字段且分词器默认为standard
的索引:
PUT http://localhost:9200/ware_index { "mappings": { "ware": { "properties": { "title":{ "type":"text" } } } } }
插入两条数据:
POST http://localhost:9200/ware_index/ware { "title":"洗面奶" }
POST http://localhost:9200/ware_index/ware { "title":"面粉" }
搜索关键字"洗面奶":
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match":{ "title":"洗面奶" } } }
搜索结果出现了"洗面奶"和"面粉"两个风马牛不相及的结果,这显然不符合咱们的预期。
缘由在分词一章中已经说明,text
类型默认分词器为standard
,它会将中文字符串一个字一个字拆分,也就是将"洗面奶"拆分红了"洗"、"面"、"奶",将"面粉"拆分红了"面"、"粉"。而match
会将搜索的关键词拆分,也就拆分红了"洗"、"面"、"奶",最后两个"面"都能匹配上,也就出现了上述结果。因此对于中文的字符串搜索咱们须要指定分词器,而经常使用的分词器是ik_smart
,它会按照最大粒度拆分,若是采用ik_max_word
它会将词按照最小粒度拆分,也有可能形成上述结果。
DELETE http://localhost:9200/ware_index
删除索引,从新建立并指定title字段的分词器为ik_smart
。
PUT http://localhost:9200/ware_index { "mappings":{ "ware":{ "properties":{ "id":{ "type":"keyword" }, "title":{ "type":"text", "analyzer":"ik_smart" } } } } }
这时若是插入“洗面奶”和“面粉”,搜索“洗面奶”是结果就只有一条。但此时咱们插入如下两条数据:
POST http://localhost:9200/ware_index/ware { "id":"1", "title":"新但愿牛奶" }
POST http://localhost:9200/ware_index/ware { "id":"2", "title":"春秋上新短袖" }
搜索关键字”新但愿牛奶“:
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match":{ "title":"新但愿牛奶" } } }
搜索结果出现了刚插入的2条,显然第二条”春秋上新短袖“并非咱们想要的结果。出现这种问题的缘由一样是由于分词的问题,在ik
插件的词库中并无"新但愿"一词,因此它会把搜索的关键词"新但愿"拆分为"新"和"但愿",一样在"春秋上新短袖"中"新"也并无组合成其它词语,它也被单独拆成了"新",这就形成了上述结果。解决这个问题的办法固然能够在ik
插件中新增"新但愿"词语,若是咱们在分词中所作的那样,但也有其它的办法。
match_phrase
,短语查询,它会将搜索关键字"新但愿牛奶"拆分红一个词项列表"新 但愿 牛奶",对于搜索的结果须要彻底匹配这些词项,且位置对应,本例中的"新但愿牛奶"文档数据从词项和位置上彻底对应,故经过match_phrase
短语查询可搜索出结果,且只有一条数据。
POST http://localhost:9200/ware_index/ware/_search { "query":{ "match_phrase":{ "title":"新但愿牛奶" } } }
尽管这能知足咱们的搜索结果,可是用户实际在搜索中经常多是"牛奶 新但愿"这样的顺序,但遗憾的是根据match_phrase
短语匹配的要求是须要被搜索的文档须要彻底匹配词项且位置对应,关键字"牛奶 新但愿"被解析成了"牛奶 新 但愿",尽管它与"新但愿牛奶"词项匹配但位置没有对应,因此并不能搜索出任何结果。同理,此时若是咱们插入"新但愿的牛奶"数据时,不管是搜索"新但愿牛奶"仍是"牛奶新但愿"均不能搜索出"新但愿的牛奶"结果,前者的关键字是由于词项没有彻底匹配,后者的关键字是由于词项和位置没有彻底匹配。
因此match_phrase
也没有达到完美的效果。
match_phrase_prefix
,短语前缀查询,相似MySQL中的like "新但愿%"
,它大致上和match_phrase_prefix
一致,也是须要知足文档数据和搜索关键字在词项和位置上保持一致,一样若是搜索"牛奶新但愿"也不会出现任何结果。它也并无达到咱们想要的结果。
前面两种查询中虽然能经过"新但愿牛奶"搜索到咱们想要的结果,可是对于"牛奶 新但愿"却无能为力。接下来的这种查询方式能"完美"的达到咱们想要的效果。
先来看最低匹配度的查询示例:
POST http://localhost:9200/ware_index/ware/_search { "query": { "match": { "title": { "query": "新但愿牛奶", "minimum_should_match": "80%" } } } }
minimum_should_match
即最低匹配度。"80%"表明什么意思呢?仍是要从关键字"新但愿牛奶"被解析成哪几个词项提及,前面说到"新但愿牛奶"被解析成"新 但愿 牛奶"三个词项,若是经过match
搜索,则含有"新"的数据一样出如今搜索结果中。"80%"的含义则是3个词项必须至少匹配80% * 3 = 2.4个词项才会出如今搜索结果中,向下取整为2,即搜索的数据中须要至少包含2个词项。显然,"春秋上新短袖"只有1个词项,不知足最低匹配度2个词项的要求,故不会出如今搜索结果中。
一样,若是搜索"牛奶 新但愿"也是上述的结果,它并非短语匹配,因此并不会要求词项所匹配的位置相同。
能够推出,若是"minimum_should_match":"100%"
也就是要求彻底匹配,此时要求数据中包含全部的词项,这样会出现较少的搜索结果;若是"minimun_should_match:0"
此时并不表明一个词项均可以不包含,而是只须要有一个词项就能出如今搜索结果,实际上就是默认的match
搜索,这样会出现较多的搜索结果。
找到一个合适的值,就能有一个较好的体验,根据二八原则,以及实践代表,设置为"80%"能知足大部分场景,既不会多出无用的搜索结果,也不会少。
基于Java客户端(上),本文再也不赘述如何建立一个Spring Data ElasticSearch工程,也再也不作过多文字叙述。更多的请必定配合源码使用,源码地址https://github.com/yu-linfeng/elasticsearch6.x_tutorial/tree/master/code/spring-data-elasticsearch,具体代码目录在complex
包。
本章请必定结合代码重点关注如何如何经过Java API进行父子文档的数据插入,以及查询。
父子文档在ES中存储的格式其实是以键值对方式存在,例如在定义映射Mapping时,咱们将子文档定义为:
{ ...... "level":{ "type":"join", "relations":{ "superior":"staff", "staff":"junior" } } ...... }
在写入一条数据时:
{ ...... "level":{ "name":"staff", "parent":"1" } ...... }
对于于Java实体,咱们能够把level
字段设置为Map<String, Object>
类型。关键注意的是,在使用Spring Data ElasticSearch时,咱们不能直接调用sava
或者saveAll
方法。ES规定父子文档必须属于同一分片,也就是说在写入子文档时,须要定义routing
参数。下面是代码节选:
BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); bulkRequestBuilder.add(client.prepareIndex("company", "employee", employeePO.getId()).setRouting(routing).setSource(mapper.writeValueAsString(employeePO), XContentType.JSON)).execute().actionGet();
必定参考源码一块儿使用。
ES实在是一个很是强大的搜索引擎。能力有限,实在不能将全部的Java API一一举例讲解,若是你在编写代码时,遇到困难也请联系做者邮箱hellobug at outlook.com,或者经过公众号coderbuff,解答得了的必定解答,解答不了的一块儿解答。
关注公众号:CoderBuff,回复“es”获取《ElasticSearch6.x实战教程》完整版PDF。