0%

elasticsearch中terms聚合结果json序列化处理

场景

terms聚合的结果直接返给前端处理,如果bucket的key为字符串时,在mvc层jackson进行json序列化处理会报类型转换错误。

原因分析

terms聚合的结果中有keyAsNumber字段,是将桶key转为number类型,但是当key为字符串类型的数据时,是无法转为numebr类型的。

jackson在序列化的时候,当terms聚合的key为字符串类型时,则会调用ParsedStringTerms类来转换处理字段,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class ParsedStringTerms extends ParsedTerms {

@Override
public String getType() {
return StringTerms.NAME;
}

private static ObjectParser<ParsedStringTerms, Void> PARSER =
new ObjectParser<>(ParsedStringTerms.class.getSimpleName(), true, ParsedStringTerms::new);
static {
declareParsedTermsFields(PARSER, ParsedBucket::fromXContent);
}

public static ParsedStringTerms fromXContent(XContentParser parser, String name) throws IOException {
ParsedStringTerms aggregation = PARSER.parse(parser, null);
aggregation.setName(name);
return aggregation;
}

public static class ParsedBucket extends ParsedTerms.ParsedBucket {

private BytesRef key;

@Override
public Object getKey() {
return getKeyAsString();
}

@Override
public String getKeyAsString() {
String keyAsString = super.getKeyAsString();
if (keyAsString != null) {
return keyAsString;
}
if (key != null) {
return key.utf8ToString();
}
return null;
}

public Number getKeyAsNumber() {
if (key != null) {
return Double.parseDouble(key.utf8ToString());
}
return null;
}

@Override
protected XContentBuilder keyToXContent(XContentBuilder builder) throws IOException {
return builder.field(CommonFields.KEY.getPreferredName(), getKey());
}

static ParsedBucket fromXContent(XContentParser parser) throws IOException {
return parseTermsBucketXContent(parser, ParsedBucket::new, (p, bucket) -> {
CharBuffer cb = p.charBufferOrNull();
if (cb == null) {
bucket.key = null;
} else {
bucket.key = new BytesRef(cb);
}
});
}
}
}

可以看见它在对bucket做处理的时候,会调用getKey,getKeyAsString,getKeyAsNumber方法处理相关字段,其中getKeyAsNumber是直接将key(BytesRef类型)处理为Number型,直接将字符串的字节强行转换为number,必然是行不通的。

1
2
3
4
5
6
public Number getKeyAsNumber() {
if (key != null) {
return Double.parseDouble(key.utf8ToString());
}
return null;
}

解决方案

从上述分析看来,要解决此问题需要从序列化入手,让其在json序列化的时候直接过滤keyAsNumber字段。

首先想到的是通过重写ParsedStringTerms,让es遇到terms聚合的key是字符串时,即处理聚合结果为StringTerms的时候,用自定义的parsed方式,但是此路行不通,因为在使用RestHighLevelClientAPI的时候,其限制了我们不能定制自己的parsed方式。

所以只能在外部解决,这里的解决方案是控制jackson在序列化的时候,对ParsedStringTerms.ParsedBucket类做特殊处理,也就是忽略keyAsNumber字段。

ParsedStringTerms.ParsedBucket类自定义序列化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ParsedStringTermsBucketSerializer extends StdSerializer<ParsedStringTerms.ParsedBucket> {

public ParsedStringTermsBucketSerializer(Class<ParsedStringTerms.ParsedBucket> t) {
super(t);
}

@Override
public void serialize(ParsedStringTerms.ParsedBucket value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeObjectField("aggregations", value.getAggregations());
gen.writeObjectField("key", value.getKey());
gen.writeStringField("keyAsString", value.getKeyAsString());
gen.writeNumberField("docCount", value.getDocCount());
gen.writeEndObject();
}
}

将自定义的序列化方式设置到jackson的mapper中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class ObjectMapperConfigure {
@Bean
public ObjectMapper objectMapper() {

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(simpleModule());
return objectMapper;
}

private SimpleModule simpleModule() {
ParsedStringTermsBucketSerializer serializer = new ParsedStringTermsBucketSerializer(ParsedStringTerms.ParsedBucket.class);
SimpleModule module = new SimpleModule();
module.addSerializer(serializer);
return module;
}
}