描述:主要使用ES对大量数据进行关联分析,由于各种原因,使用ES进行关联分析时(即使用ES的agg),需要重新组织下数据,形成父子文档,如分析个人数据,需要将个人数据(网购订单、通话记录、话费记录、房产、失信执行人等等、、、)整理成树形结构存入ES,在开发过程中遇到了各种坑,在这里总结下,希望能帮到大家,文末会附上java代码的工具类,覆盖多种情况,满足学习和开发。
前提:java6+、ElasticSearch5.5.0+、kibana5.5.0+。
以下是遇到的坑:
1、在ES中除了组织成父子文档外,需要分析的字段也可能是一个大PO,这时需要将这个字段的类型设为nested,但是ES默认的一个索引中的nested字段数量有限制,需要手动设置:
PUT aggstest5/_settings
{
"index": {
"mapping": {
"nested_fields": {
"limit": "100"
}
}
}
}
2、由于数据量较大,但是ES提供的from\size方式查询默认最大查询数量有限,最多多少条来着??所以有需求一次取上千上万条数据需要手动设置一下(请考虑实际内存):
PUT _settings
{
"index": {
"max_result_window": "50000"
}
}
3、常常需要只清空某个索引(库)里的某个type(表)的数据,而不是清除整个索引:
POST aggstest/type1/_delete_by_query?conflicts=proceed
{
"query": {
"match_all": {}
}
}
4、添加子文档时,记得加上routing:
PUT aggstest/parenttype1/1
{
"a":1
}
PUT aggstest/sontype1/1?routing=1
{
"b":1
}
接下来是些与业务逻辑相关的坑了
5、时间类型的数据,ES聚合中提供data_range和range,但是有些不太够用,比如分析通话记录时,需要用到yyyy-MM-dd HH:mm:ss格式中的时(mm),或者用到yyyy-MM,或者其他组合,但由于es中时间始终是值类型(long),尽管es包装了下变成date类型,但是有些场景我们更想使用terms的聚合或简单的24小时制的range聚合。那么这里ES也有处理方案,
方案1:即script,可以用来自定义对数据的处理方法,比如计算用户年龄,由于数据中只有出生日期,就需要计算:
{
"aggtest":{
"user": {
"age":{
"lang":"painless",
"script":"params.curYear - doc['ubYear'].value",
"params":[
{
"curYear":2018
}
]
}
}
}
}
方案2:es是基于luence的,所以也可以使用luence的一些语法,官方网址:https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-expression.html
方案3:是不是觉得好麻烦,超级蛋疼,最简单的山寨方法,把日期类型的字段直接拆开,入库的时候多存几个字段,yyyy当成一个数字类型的字段,同理yyyy-MM存一个字符串,HH存一个数字,这样就简单多了,只要不是需求变态,基本够用了,不管是range还是terms都能用。
6、由于关联数据较多经常跨type取数据,将某一个人的信息全部取出来,形成一个大po,例如,现在有用户表user,子表运营商信息mobileinfo,运营商信息的子表通话记录callrecordsd,子表公积金信息funinfos,我要同时把这四张表都查出来:
GET aggstest/user/_search
{
"query": {
"bool": {
"should": [
{
"has_child": {
"type": "fundinfos",
"query": {
"match_all": {}
},
"inner_hits":{}
}
},
{
"has_child": {
"type": "mobileinfo",
"query": {
"bool": {
"should": [
{
"has_child": {
"type": "callrecords",
"query": {"match_all": {}},
"inner_hits":{}
}
}
]
}
},
"inner_hits":{}
}
}
]
}
}
}
关键在于inner_hits和has_children,其中inner_hits也可以设置from,size!
7、使用agg时,由于关联数据较多经常跨type分析(单表聚合不存在此问题),但是ES仅提供由父到子的聚合,而不提供子到父和子到子的聚合查询,例如,现在有用户表user,子表京东账户jingdonguser,京东账户的子表订单记录jingdongorder,
现在查询每个年龄段的京东总消费是多少
GET aggstest5/user/_search
{
"size": 0,
"aggs": {
"ages": {
"histogram": {
"field": "ubYear", //生日年
"interval": 1 //步长1年
},
"aggs": {
"jingdong": {
"children": {
"type": "jingdonguser" //子表京东账户
},
"aggs": {
"order": {
"children": {
"type": "jingdongorder" //京东账户子表京东订单
},
"aggs": {
"NAME": {
"sum": {
"field": "payAmount" //对订单金额求和
}
}
}
}
}
}
}
}
}
}
结果是没问题的,但是发现没有,关联关系是通过children操作一层一层往下的,而且es在agg中并不提供parent操作,造成的结果就是,在现有的数据结构中,我不能查询不同订单金额范围是男的多还是女的多,因为我是从订单开始聚合分组,无法关联到parent中的男女信息。
方案1:改es的源码,因为既然有children,那么必定parent是可以实现的,只是由于es设计方案的多方考虑舍弃了这种操作(效率肯定很低)。
方案2:方案1太扎心,那就没办法了,又想要这种功能,现有数据结构又不支持,那就只有自己在内存中实现了,单表聚合和父到子聚合通过ES实现,子到子和子到父自己写方法实现。
接下来就是坑7中方案2遇到的一些问题,其实坑7中的方案2又是另一种解决方法了,又可以延伸出很多东西,java实现的分组聚合:
8、在上述方案2中,需要取大po,而坑6的解决方法有局限性,那就是es提供的from,size的局限不能一次查大量数据,也不适合全量遍历数据,但是我们现在需要取大量的数据和全量遍历,在不考虑内存和分布式的情况下,我单机一次从es中取十万百万条数据使用需要用到es提供的scroll方式读取数据,全量遍历数据也可以使用scroll方式,但是scroll方式貌似不保证准确性:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
附上上述采坑的java代码:下载代码
PS:代码中,elasticsearchImpl是增删查改的工具,esindexmapping是把po映射到es的工具,
po关系描述需要用到注解,且必须有一个String id作主键,示例如下:
@ClassMeta(name="个人用户")
public class User {
@FieldMeta(name="主键")
private String id;
@FieldMeta(name="姓名")
private String userName;
@FieldMeta(name="性别")
private String userGender;
@FieldMeta(name="民族")
private String userNation;
...
//关联数据
//公积金
@NotMapping //表示es中并没有这个字段,只用来作关系描述
@Child //是user的儿子
@RelationshipOne2One //一对一的关系,user只有一个fundinfos
private FundInfo fundinfos;
@NotMapping@Child@RelationshipOne2Oneprivate Jingdong jingdonguser;@NotMapping@Child@RelationshipOne2Many //一对多的关系,user可能有多个税务违法信息private List<TaxPerson> taxperson;@NotMapping@Child@RelationshipOne2Many//一对多的关系,user可能有多个失信执行人信息private List<DishonestPerson> dishonestperson;...}
@ClassMeta(name = "公积金账户信息")
public class FundInfo {
@FieldMeta(name="主键")
private String id;
//个人姓名
@FieldMeta(name="个人姓名")
private String fundName;
//登录账号
@FieldMeta(name="登录账号")
private String fundAccount;
@FieldMeta(name="证件号码")
private String cardNum;
@FieldMeta(name="证件类型")
private String cardType;
@FieldMeta(name="性别")
private String sex;
@FieldMeta(name="婚姻状况")
private String marriage;
@FieldMeta(name="公积金卡号")
private String fundNum;
@FieldMeta(name="缴费基数")
private Float payBase;
@NotMapping //es中并没有这个字段
@Child
@RelationshipOne2Many //公积金账户信息有多个缴费信息
private List<FundPayInfo> fundpayinfos;
@NotMapping
@Parent //公积金账户的父亲是用户信息
@RelationshipOne2One //一个公积金只对应一个user
private User user;
}