2020-03-21

Hibernate Search - Mapping geo-point

Hibernate Search provides a variety of spatial features such as a distance predicate and a distance sort. These features require that spatial coordinates are indexed. More precisely, it requires that a geo-point, i.e. a latitude and longitude in the geographic coordinate system, are indexed.

Geo-points are a bit of an exception, because there isn’t any type in the standard Java library to represent them. For that reason, Hibernate Search defines its own interface, org.hibernate.search.engine.spatial.GeoPoint. Since your model probably uses a different type to represent geo-points, mapping geo-points requires some extra steps.

Two options are available:

If your geo-points are represented by a dedicated, immutable type, simply use @GenericField and the GeoPoint interface, as explained here.

For every other case, use the more complex (but more powerful) @GeoPointBinding, as explained here.

Using @GenericField and the GeoPoint interface

When geo-points are represented in your entity model by a dedicated, immutable type, you can simply make that type implement the GeoPoint interface, and use simple property/field mapping with @GenericField:

Mapping spatial coordinates by implementing GeoPoint

@Embeddable
public class MyCoordinates implements GeoPoint { 

    @Basic
    private Double latitude;

    @Basic
    private Double longitude;

    protected MyCoordinates() {
        // For Hibernate ORM
    }

    public MyCoordinates(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    @Override
    public double getLatitude() { 
        return latitude;
    }

    @Override
    public double getLongitude() {
        return longitude;
    }
}
@Entity
@Indexed
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Embedded
    @GenericField 
    private MyCoordinates placeOfBirth;

    public Author() {
    }

    // Getters and setters
    // ...

}
Model the geo-point as an embeddable implementing GeoPoint. A custom type with a corresponding Hibernate ORM UserType would work as well.
The geo-point type must be immutable: it does not declare any setter.
Apply the @GenericField annotation to the placeOfBirth property holding the coordinates. An index field named placeOfBirth will be added to the index. Options generally used on @GenericField can be used here as well.
The geo-point type must be immutable, i.e. the latitude and longitude of a given instance may never change.

This is a core assumption of @GenericField and generally all @*Field annotations: changes to the coordinates will be ignored and will not trigger reindexing as one would expect.

If the type holding your coordinates is mutable, do not use @GenericField and refer to Using @GeoPointBinding, @Latitude and @Longitude instead.

If your geo-point type is immutable, but extending the GeoPoint interface is not an option, you can also use a custom value bridge converting between the custom geo-point type and GeoPoint. GeoPoint offers static methods to quickly build a GeoPoint instance.

Using @GeoPointBinding, @Latitude and @Longitude

For cases where coordinates are stored in a mutable object, the solution is the @GeoPointBinding annotation. Combined with the @Latitude and @Longitude annotation, it can map the coordinates of any type that declares a latitude and longitude of type double:

Mapping spatial coordinates using @GeoPointBinding

@Entity
@Indexed
@GeoPointBinding(fieldName = "placeOfBirth") 
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Latitude 
    private Double placeOfBirthLatitude;

    @Longitude 
    private Double placeOfBirthLongitude;

    public Author() {
    }

    // Getters and setters
    // ...

}
Apply the @GeoPointBinding annotation to the type, setting fieldName to the name of the index field.
Apply @Latitude to the property holding the latitude. It must be of double or Double type.
Apply @Longitude to the property holding the longitude. It must be of double or Double type.
The @GeoPointBinding annotation may also be applied to a property, in which case the @Latitude and @Longitude must be applied to properties of the property’s type:

 Mapping spatial coordinates using @GeoPointBinding on a property

@Embeddable
public class MyCoordinates { 

    @Basic
    @Latitude 
    private Double latitude;

    @Basic
    @Longitude 
    private Double longitude;

    protected MyCoordinates() {
        // For Hibernate ORM
    }

    public MyCoordinates(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(Double latitude) { 
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(Double longitude) {
        this.longitude = longitude;
    }
}
@Entity
@Indexed
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Embedded
    @GeoPointBinding 
    private MyCoordinates placeOfBirth;

    public Author() {
    }

    // Getters and setters
    // ...

}
Model the geo-point as an embeddable. An entity would work as well.
In the geo-point type, apply @Latitude to the property holding the latitude.
In the geo-point type, apply @Longitude to the property holding the longitude.
The geo-point type may safely declare setters (it can be mutable).
Apply the @GeoPointBinding annotation to the property. Setting fieldName to the name of the index field is possible, but optional: the property name will be used by default.
It is possible to handle multiple sets of coordinates by applying the annotations multiple times and setting the markerSet attribute to a unique value:

Mapping spatial coordinates using @GeoPointBinding on a property

@Entity
@Indexed
@GeoPointBinding(fieldName = "placeOfBirth", markerSet = "birth") 
@GeoPointBinding(fieldName = "placeOfDeath", markerSet = "death") 
public class Author {

    @Id
    @GeneratedValue
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Latitude(markerSet = "birth") 
    private Double placeOfBirthLatitude;

    @Longitude(markerSet = "birth") 
    private Double placeOfBirthLongitude;

    @Latitude(markerSet = "death") 
    private Double placeOfDeathLatitude;

    @Longitude(markerSet = "death") 
    private Double placeOfDeathLongitude;

    public Author() {
    }

    // Getters and setters
    // ...

}
Apply the @GeoPointBinding annotation to the type, setting fieldName to the name of the index field, and markerSet to a unique value.
Apply the @GeoPointBinding annotation to the type a second time, setting fieldName to the name of the index field (different from the first one), and markerSet to a unique value (different from the first one).
Apply @Latitude to the property holding the latitude for the first geo-point field. Set the markerSet attribute to the same value as the corresponding @GeoPointBinding annotation.
Apply @Longitude to the property holding the longitude for the first geo-point field. Set the markerSet attribute to the same value as the corresponding @GeoPointBinding annotation.
Apply @Latitude to the property holding the latitude for the second geo-point field. Set the markerSet attribute to the same value as the corresponding @GeoPointBinding annotation.
Apply @Longitude to the property holding the longitude for the second geo-point field. Set the markerSet attribute to the same value as the corresponding @GeoPointBinding annotation.

No comments:

Post a Comment