背景

我司有一套开源使用规范,衰退期的软件或版本需要升级到GA版本。我们ES服务端是6.8.x的,根据ES官方推荐版本,spring data elasticsearch使用的是3.2.x,配套的spring boot版本为2.2.x.

我们当前使用的版本已经比较老了,我们需要将spring boot升级到2.6.x,并将spring data elasticsearch升级到4.3.x。
因为高版本spring data elasticsearch的API有较大的改动,我们代码中用到API已经被删掉了,整改工作量非常大,因此决定先升级spring boot到2.6.x,spring data elasticsearch还是沿用老的版本3.2.x。等下个版本在预留工作量处理spring data elasticsearch升级问题。
采用上述方案后,发现项目启动的时候报错mapper [xx] of different type, current_type [text], merged_type [keyword]。

定位思路

首先找到问题代码,我们在DAO层Bean的初始化方法中会根据Entity(使用@Document及@Field定义的VO)去创建索引并设置mapping,这样每新增索引或者有新增字段,都不用手工去更新ES。而且也不用担心索引已经存在或者mapping已经被设置导致调用报错,方法内部会处理这些情况。
根据报错信息,定位是putMapping报错。

    @PostConstructpublic void init() {elasticsearchTemplate.createIndex(XX.class);elasticsearchTemplate.putMapping(XX.class);}

整体定位过程入下

1.确认是否有elasticsearch相关的包的版本是否有变化

首先想到是否包的变化导致的,因为spring boot已经升级到2.6.x,我们是重新定义dependency manager来引用老版本的spring data elasticsearch。除非是某个包的版本发生了变化,不然不可能一样的代码,跑出来的效果不一样。

结果

初步扫了一眼spring data elasticsearch以及elasticsearch本身版本都是ok的,因此决定还是老老实实的追踪开源源码找出根因。

2.确认一下生成出来的mapping和服务端的到底有啥差异

根据putMapping源码在下面代码中打断点观察,到底根据Entity生成出来的mapping与服务端的有啥差异
org.springframework.data.elasticsearch.core.ElasticsearchTemplate#putMapping(java.lang.Class)

 @Overridepublic <T> boolean putMapping(Class<T> clazz) {# 在buildMapping中打断点,观察buildMapping的返回值return putMapping(clazz, buildMapping(clazz));}

实体结构定义如下:

class AA {@Field(type= FieldType.Keyword)private String a;... ...省略其他无问题的字段private List<List<XXVo>> xx;
}

对比发现确实有差异,本地生成XX字段在mapping中的定义如下

{"AA": {"properties": {"a": {"type": "keyword","index": true},"xx": {"type": "object","properties": {"xx": {"type": "keyword"}}}}}
}

而服务端的定义却如下

{"AA": {"properties": {"a": {"type": "keyword","index": true},"xx": {"type": "object","properties": {"xx": {"type": "text","fields": {"keyword": {"type": "keyword","ignore_above": 256}}}}}}}
}

为了解释为啥同样的东西之前为啥没问题,我们把所有东西都回退,然后在相同的地方打断点,观察生成的mapping。
mapping如下:

{"AA": {"properties": {"a": {"type": "keyword","index": true},"xx": {"type": "object"}}}
}

到此能够证明报错确实是本次升级导致的,只不过相关逻辑中有变化的包未被发现而已。因此决定打断点,看是那部分的逻辑变化导致mapping生成不一致了。
PS:
观察老版本代码生成出来的mapping,发现部分字段(List<XX>)能够正常递归解析,而本字段(List<List<XX>>)不行,盲猜低版本的工具只递归解析一层。

3.探究升级前后是那一部分逻辑变化导致的

buildMapping最终由MappingBuilder#buildPropertyMapping函数完成

 String buildPropertyMapping(Class<?> clazz) throws IOException {ElasticsearchPersistentEntity<?> entity = elasticsearchConverter.getMappingContext().getRequiredPersistentEntity(clazz);XContentBuilder builder = jsonBuilder().startObject().startObject(entity.getIndexType());... ...# 主逻辑在此mapEntity(builder, entity, true, "", false, FieldType.Auto, null);builder.endObject() // FIELD_PROPERTIES.endObject() // indexType.endObject() // root object.close();return builder.getOutputStream().toString();}
 private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity entity, boolean isRootObject,String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType,@Nullable Field parentFieldAnnotation) throws IOException {... ...# 遍历每个属性,构建Mappingentity.doWithProperties((PropertyHandler<ElasticsearchPersistentProperty>) property -> {try {buildPropertyMapping(builder, isRootObject, property);} }if (writeNestedProperties) {builder.endObject().endObject();}}
    private boolean isNestedOrObjectProperty(ElasticsearchPersistentProperty property) {Field fieldAnnotation = property.findAnnotation(Field.class);return fieldAnnotation != null&& (FieldType.Nested == fieldAnnotation.type() || FieldType.Object == fieldAnnotation.type());}private void buildPropertyMapping(XContentBuilder builder, boolean isRootObject,ElasticsearchPersistentProperty property) throws IOException {... ...boolean isNestedOrObjectProperty = isNestedOrObjectProperty(property);Field fieldAnnotation = property.findAnnotation(Field.class);# 差异点主要在property.isEntity()# 此段逻辑主要是判断当前字段是否是对象(Entity),如果是的话,则递归去解析字段构建mapping# if中去掉了无关条件表达式if (property.isEntity() && hasRelevantAnnotation(property)) {Iterator<? extends TypeInformation<?>> iterator = property.getPersistentEntityTypes().iterator();ElasticsearchPersistentEntity<?> persistentEntity = iterator.hasNext()? elasticsearchConverter.getMappingContext().getPersistentEntity(iterator.next()): null;mapEntity(builder, persistentEntity, false, property.getFieldName(), isNestedOrObjectProperty,fieldAnnotation.type(), fieldAnnotation);if (isNestedOrObjectProperty) {return;}}... ...代码有省略}

property.isEntity()最终调用的是org.springframework.data.mapping.model.AbstractPersistentProperty中的isEntity方法,该类是spring-data-common,对比发现该jar包版本确实发生了变化。所以升级前后虽然spring data elasticsearch版本没变,但仍然产生了问题。

org.springframework.data.mapping.model.AbstractPersistentProperty新老版本的差异

  1. 老版本2.3.9
    从代码中可看到该逻辑只解析一层,获取到ActualType后,过滤掉Map、Collection。因此List<List<XX>>这种嵌套两层及以上的泛型,isEntity()计算后未false,因此不会递归解析底层数据结构了。
    # 构造函数public AbstractPersistentProperty(){this.entityTypeInformation = Lazy.of(() -> Optional.ofNullable(information.getActualType())//.filter(it -> !simpleTypeHolder.isSimpleType(it.getType()))//.filter(it -> !it.isCollectionLike())//.filter(it -> !it.isMap()));... ...}@Overridepublic boolean isEntity() {return !isTransient() && entityTypeInformation.get().isPresent();}
  1. 新版本2.6.4
    从代码中可以看到该逻辑会递归分析直到找到最底层的数据结构。因此List<List<XX>>计算isEntity()后为true,能够递归分析底层数据结构。
#构造函数
public AbstractPersistentProperty(){this.entityTypeInformation = Lazy.of(() -> detectEntityTypes(simpleTypeHolder));... ...
}private Set<TypeInformation<?>> detectEntityTypes(SimpleTypeHolder simpleTypes) {TypeInformation<?> typeToStartWith = getAssociationTargetTypeInformation();typeToStartWith = typeToStartWith == null ? information : typeToStartWith;Set<TypeInformation<?>> result = detectEntityTypes(typeToStartWith);return result.stream().filter(it -> !simpleTypes.isSimpleType(it.getType())).filter(it -> !it.getType().equals(ASSOCIATION_TYPE)).collect(Collectors.toSet());
}
private Set<TypeInformation<?>> detectEntityTypes(@Nullable TypeInformation<?> source) {Set<TypeInformation<?>> result = new HashSet<>();if (source.isMap()) {result.addAll(detectEntityTypes(source.getComponentType()));}TypeInformation<?> actualType = source.getActualType();# source不等于actualType,说明是泛型类,需要递归去解析底层类型if (source.equals(actualType)) {result.add(source);} else {result.addAll(detectEntityTypes(actualType));}return result;
}

疑问-为啥@Field(type = FieldType.Keyword)的字段,服务端的mapping被解析成了

{

 “type” : “text”,

  “fields” : {

    “keyword” : {

    “type” : “keyword”,

    “ignore_above” : 256

    }

  }

}

经过google发现一片跟该问题比较相关的文章https://www.elastic.co/cn/blog/strings-are-dead-long-live-strings
文章大意是
随着 Elasticsearch 5.0 的发布临近,是时候介绍这个即将发布的版本的发布亮点之一:删除string类型。这种变化的背景是我们认为string类型令人困惑:Elasticsearch 有两种非常不同的方式来搜索字符串。您可以搜索整个值,我们通常将其称为keyword搜索,也可以搜索单个分词,我们通常将其称为全文(full-text)搜索。如果您熟悉 Elasticsearch,就会知道前者的字符串应映射为 not_analyzed string,而后者应映射为analyzed string。

但是,对于这两个非常不同的场景使用相同的字段类型这一事实会导致问题,因为某些选项仅对其中一个用例有意义。例如, position_increment_gap 对于 not_analyzed string没有什么意义,并且在analyzed string的情况下,ignore_above 是适用于整个值还是适用于单个分词并不明显(如果您想知道:它确实适用于整个值,限制单个令牌可以与限制令牌过滤器一起应用)。

为避免这些问题,字符串字段已拆分为两种新类型:文本,应该用于全文搜索,以及关键字,应该用于关键字搜索。

在我们拆分类型的同时,我们决定更改字符串字段的默认动态映射。开始使用 Elasticsearch 时,一个常见的挫折是您必须重新索引才能聚合整个字段值。例如,假设您正在索引具有城市字段的文档。在此字段上进行聚合将为 new 和 york 提供不同的计数,而不是对 New York 进行单一计数,这通常是预期的行为。不幸的是,解决这个问题需要重新索引该字段,以便索引具有正确的结构来回答这个问题。

为了让事情变得更好,Elasticsearch 决定借用一个最初源于 Logstash 的想法:默认情况下,字符串现在将被映射为文本和关键字。例如,如果您索引以下简单文档:

{"foo": "bar"
}

然后将创建以下动态映射

{"foo": {"type" "text","fields": {"keyword": {"type": "keyword","ignore_above": 256}}}
}

由此可见,因为老版本的spring-data-common只支持解析一层,所以List<List<XX>>这种新增的字段,无法解析到底层结构,代码中动态更新mapping的机制不生效,最终由服务端动态生成映射。而且生成出来的结构也符合上面引用文章中的分析。

ES报错处理-mapper [xx.xx] of different type, current_type [text], merged_type [keyword]相关推荐

  1. ElasticSearch mapper [brandName] of different type, current_type [text], merged_type [keyword]

    错误信息:  Constructor threw exception; nested exception is java.lang.IllegalArgumentException: mapper [ ...

  2. 报错: java.lang.IllegalArgumentException: mapper [categoryName] of different type, current_type [text]

    Elasticsearch报错: 非法参数异常 Caused by: java.lang.IllegalArgumentException: mapper [categoryName] of diff ...

  3. mysql报错:Duplicate entry ‘xx‘ for key ‘PRIMARY‘ 解决可行方案。已解决

    mysql报错:Duplicate entry 'xx' for key 'xxux1' 解决可行方案.已解决 web开发多次遇到Duplicate entry 'xx' for key 'PRIMA ...

  4. ES报错:Connection reset by peer 解决经历

    http://nicethemes.cn/news/txtlist_i28391v.html 这次来分享一下ES报错:java.io.IOException: Connection reset by ...

  5. 【Kibana】es 报错 all shards failed: [search_phase_execution_exception] all shards failed

    1.背景 访问kibana报错,all shards failed {"message":"all shards failed: [search_phase_execut ...

  6. 报错Field Mapper in xxx.xxxServiceImpl required a bean of type ‘dao.xxxMapper‘ that could not be found

    报错Field Mapper in xxx.xxxServiceImpl required a bean of type 'dao.xxxMapper' that could not be found ...

  7. MyBatis报错:org.apache.ibatis.binding.BindingException: Type interface com.smbms.dao.provider.Provider

    在Java使用MyBatis框架开发时,遇到报错:org.apache.ibatis.binding.BindingException: Type interface com.smbms.dao.pr ...

  8. 运行报错:java.io.IOException: invalid constant type: 15

    为什么80%的码农都做不了架构师?>>>    jdk,tomcat更新到jdk1.8与 tomcat8 运行报错:java.io.IOException: invalid cons ...

  9. darknet编译报错 error: ‘__fatBinC_Wrapper_t’ does not name a type

    git clone darknet项目,进行make -j8编译,发现报错error: '__fatBinC_Wrapper_t' does not name a type. 发生这种错误的原因在于c ...

  10. 编译PX4时,报错error ‘i‘ does not name a type __ULong i[2];解决方法

    编译PX4时,报错error: 'i' does not name a type __ULong i[2];解决方法 在编译PX4的时候,会遇到报错: /usr/include/newlib/math ...

最新文章

  1. js弹出一段html,html js 弹出层
  2. python约瑟夫环问题给十个学生编号报到3者出列_趣味算法--约瑟夫环问题(示例代码)...
  3. 现在学java还是python好_该学Java还是Python?
  4. 【总结整理】关于切图
  5. spring10: 引用类型的自动注入
  6. 【Python】一文搞懂Pandas数据排序
  7. oracle里的AUE是什么意思,oracle 创建表空间步骤代码
  8. 如何恢复在 PyCharm 中误删的整个项目文件
  9. linux下面的挂载点讲解
  10. 如何获取Java用户输入?
  11. 如何利用迅雷下载百度云获取实际下载地址
  12. 7.8 Cound 练习
  13. 寿险精算实验一——编制生命表换算表
  14. Failed to obtain JDBC Connection
  15. 宠物经济:吃、用、病、葬都是生意
  16. 安卓应用 .9.png类型启动图/背景图
  17. 停车场设计软件测试,停车场车位视频检测系统设计
  18. OpenStack和Open Source MANO:NFV部署两大支柱
  19. 【计算机网络】:1-基本知识
  20. 电子时钟万年历+51单片机+1602液晶屏+DS1302+DS18B20+按键

热门文章

  1. 周立功专访:周立功和他的团队已经找到了属于自己的道路
  2. interface Ethernet 0/0/0 和interface GigabitEthernet 1/0/0
  3. 详解无刷直流电机的工作原理
  4. ready等方法 微信小程序_微信小程序初步入坑指南
  5. java文件下载文件损坏_java上传并下载以及解压zip文件有时会报文件被损坏错误分析以及解决...
  6. Typora数学符号如何表示
  7. c语言 计算机 只用if,C语言if语句的用法
  8. android keep倒计时,Android仿Keep运动休息倒计时圆形控件
  9. Audacious实现cue、ape音乐支持,GBK支持、cue乱码完美解决
  10. 网络爬虫-抓取酷航机票信息