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

背景

我司有一套开源使用规范,衰退期的软件或版本需要升级到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报错。

    @PostConstruct
    public 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)

	@Override
	public <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 {
        ... ...
        # 遍历每个属性,构建Mapping
		entity.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()));
		... ...
    }
	@Override
	public 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的机制不生效,最终由服务端动态生成映射。而且生成出来的结构也符合上面引用文章中的分析。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值