巨坑:ElasticSearch 无法解析序列化的 GeoPoint 字段

Posted by agentd on 04-16,2020

往 ElasticSearch 写数据的时候报了个奇怪的错误:

Exception in thread "main" ElasticsearchParseException[field must be either lat/lon or geohash]

查看 ES 索引发现 location 字段是 geo_point 类型的,我在 Java 代码里对应字段使用的是 ES 提供的 GeoPoint 字段,感觉序列化之后写入 ES 不应该报错。

翻了 ES 的文档,geo_point 类型支持的数据格式有四种:

PUT my_index/my_type/1
{
  "text": "Geo-point as an object",
  "location": { 
    "lat": 41.12,
    "lon": -71.34
  }
}

PUT my_index/my_type/2
{
  "text": "Geo-point as a string",
  "location": "41.12,-71.34" 
}

PUT my_index/my_type/3
{
  "text": "Geo-point as a geohash",
  "location": "drm3btev3e86" 
}

PUT my_index/my_type/4
{
  "text": "Geo-point as an array",
  "location": [ -71.34, 41.12 ] 
}

ElasticSearch 的 GeoPoint 类有两个字段 lanlon,分别是纬度和经度。
加了日志,我发现使用 Jackson 序列化之后结果有三个字段:

{
  "lat": 30.286928,
  "lon": 120.205459,
  "geohash": "wtmkr3dfnhwd"
}

ES 无法解析这种格式,结果报错了。
通过查看 GeoPoint 类的代码,发现它有个方法:

    public String getGeohash() {
        return stringEncode(lon, lat);
    }

Jackson 序列化/反序列化是根据 getset 方法设置字段的,结果序列化的时候多出了 geohash 字段,进而导致 ES 无法解析成 geo_point 类型数据。

解决方案也很简单,把序列化工具换成 Gson 就可以了。

不过 Jackson 也提供了接口兼容这种情况,可以通过自定义 GeoPoint 类的序列化和反序列化方式来实现。

public class GeoPointSerializer extends StdSerializer<GeoPoint> {
    private static final long serialVersionUID = -1262685290185555664L;

    GeoPointSerializer() {
        this(null);
    }

    GeoPointSerializer(Class<GeoPoint> t) {
        super(t);
    }

    @Override
    public void serialize(GeoPoint value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeNumberField("lat", value.getLat());
        gen.writeNumberField("lon", value.getLon());
        gen.writeEndObject();
    }
}

public class GeoPointDeserializer extends StdDeserializer<GeoPoint> {
    private static final long serialVersionUID = -5510642578760781580L;

    GeoPointDeserializer() {
        this(null);
    }

    GeoPointDeserializer(Class<GeoPoint> t) {
        super(t);
    }

    @Override
    public GeoPoint deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        ObjectCodec codec = p.getCodec();
        JsonNode node = codec.readTree(p);

        JsonNode latNode = node.get("lat");
        double lat = latNode.asDouble();

        JsonNode lonNode = node.get("lon");
        double lon = lonNode.asDouble();
        return new GeoPoint(lat, lon);
    }
}

然后把新的序列化和反序列化方式注册到 Jackson 的 ObjectMapper 就可以了。

SimpleModule deserializerModule = new SimpleModule("GeoPointDeserializer", new Version(1, 0, 0, null, null, null));
deserializerModule.addDeserializer(GeoPoint.class, new GeoPointDeserializer());
MAPPER.registerModule(deserializerModule);

SimpleModule serializerModule = new SimpleModule("GeoPointSerializer", new Version(1, 0, 0, null, null, null));
serializerModule.addSerializer(GeoPoint.class, new GeoPointSerializer());
MAPPER.registerModule(serializerModule);