Skip to content

Commit

Permalink
HSEARCH-3676 Document mapping of geo-points
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Sep 13, 2019
1 parent 874a7e0 commit 732908a
Show file tree
Hide file tree
Showing 11 changed files with 870 additions and 3 deletions.
147 changes: 144 additions & 3 deletions documentation/src/main/asciidoc/mapper-orm-mapping.asciidoc
Expand Up @@ -1018,12 +1018,153 @@ and not to each author,
the `extraction` attribute is used to disable container extraction.
====

[[mapper-orm-spatial-types]]
== Mapping spatial types
[[mapper-orm-geopoint]]
== Mapping geo-point types
// Search 5 anchors backward compatibility
[[spatial]]

include::todo-placeholder.asciidoc[]
=== Basics

Hibernate Search provides a variety of spatial features
such as <<search-dsl-predicate-spatial-within,a distance predicate>>
and <<search-dsl-sort-distance,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 <<mapper-orm-geopoint-genericfield, here>>.
* For every other case, use the more complex (but more powerful) `@GeoPointBinding`,
as explained <<mapper-orm-geopoint-geopointbinding, here>>.

[[mapper-orm-geopoint-genericfield]]
=== Using `@GenericField` and the `GeoPoint` interface
// Search 5 anchors backward compatibility
[[spatial-coordinatesinterface]]

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 <<mapper-orm-directfieldmapping,property/field mapping>> with `@GenericField`:

.Mapping spatial coordinates by implementing `GeoPoint`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/genericfield/MyCoordinates.java[tags=include]
----
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/genericfield/Author.java[tags=include;!getters-setters]
----
<1> Model the geo-point as an embeddable implementing `GeoPoint`.
A custom type with a corresponding Hibernate ORM `UserType` would work as well.
<2> The geo-point type *must be immutable*: it does not declare any setter.
<3> 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.
====

[WARNING]
====
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 <<mapper-orm-geopoint-geopointbinding>> instead.
====

[TIP]
====
If your geo-point type is immutable, but extending the `GeoPoint` interface is not an option,
you can also use a custom <<mapper-orm-bridge-valuebridge,value bridge>>
converting between the custom geo-point type and `GeoPoint`.
`GeoPoint` offers static methods to quickly build a `GeoPoint` instance.
====

[[mapper-orm-geopoint-geopointbinding]]
=== Using `@GeoPointBinding`, `@Latitude` and `@Longitude`
// Search 5 anchors backward compatibility
[[spatial-indexing-range]]
[[spatial-indexing-spatialHash]]

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`
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/geopointbinding/type/Author.java[tags=include;!getters-setters]
----
<1> Apply the `@GeoPointBinding` annotation to the type,
setting `fieldName` to the name of the index field.
<2> Apply `@Latitude` to the property holding the latitude. It must be of `double` or `Double` type.
<3> 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
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/geopointbinding/property/MyCoordinates.java[tags=include]
----
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/geopointbinding/property/Author.java[tags=include;!getters-setters]
----
<1> Model the geo-point as an embeddable.
An entity would work as well.
<2> In the geo-point type, apply `@Latitude` to the property holding the latitude.
<3> In the geo-point type, apply `@Longitude` to the property holding the longitude.
<4> The geo-point type may safely declare setters (it can be mutable).
<5> 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:

[[spatial-multiplecoordinates]]
.Mapping spatial coordinates using `@GeoPointBinding` on a property
====
[source, JAVA, indent=0, subs="+callouts"]
----
include::{sourcedir}/org/hibernate/search/documentation/mapper/orm/spatial/geopointbinding/multiple/Author.java[tags=include;!getters-setters]
----
<1> Apply the `@GeoPointBinding` annotation to the type,
setting `fieldName` to the name of the index field, and `markerSet` to a unique value.
<2> 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).
<3> 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.
<4> 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.
<5> 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.
<6> 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.
====

[[mapper-orm-reindexing]]
== Tuning automatic reindexing
Expand Down
@@ -0,0 +1,66 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.spatial.genericfield;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;

//tag::include[]
@Entity
@Indexed
public class Author {

@Id
@GeneratedValue
private Integer id;

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

@Embedded
@GenericField // <3>
private MyCoordinates placeOfBirth;

public Author() {
}

// Getters and setters
// ...

//tag::getters-setters[]
public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public MyCoordinates getPlaceOfBirth() {
return placeOfBirth;
}

public void setPlaceOfBirth(MyCoordinates placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}
//end::getters-setters[]
}
//end::include[]
@@ -0,0 +1,78 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.spatial.genericfield;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import javax.persistence.EntityManagerFactory;

import org.hibernate.search.documentation.testsupport.BackendConfigurations;
import org.hibernate.search.engine.spatial.DistanceUnit;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingSynchronizationStrategyName;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.hibernate.search.util.impl.integrationtest.common.rule.BackendConfiguration;
import org.hibernate.search.util.impl.integrationtest.orm.OrmSetupHelper;
import org.hibernate.search.util.impl.integrationtest.orm.OrmUtils;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class GeoPointGenericFieldIT {

@Parameterized.Parameters(name = "{0}")
public static Object[] backendConfigurations() {
return BackendConfigurations.simple().toArray();
}

@Rule
public OrmSetupHelper setupHelper;

private EntityManagerFactory entityManagerFactory;

public GeoPointGenericFieldIT(BackendConfiguration backendConfiguration) {
this.setupHelper = OrmSetupHelper.withSingleBackend( backendConfiguration );
}

@Before
public void setup() {
entityManagerFactory = setupHelper.start()
.withProperty(
HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY,
AutomaticIndexingSynchronizationStrategyName.SEARCHABLE
)
.setup( Author.class, MyCoordinates.class );
}

@Test
public void smoke() {
OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
Author author = new Author();
author.setName( "Isaac Asimov" );
author.setPlaceOfBirth( new MyCoordinates( 53.976177, 32.158627 ) );

entityManager.persist( author );
} );

OrmUtils.withinJPATransaction( entityManagerFactory, entityManager -> {
SearchSession searchSession = Search.session( entityManager );

List<Author> result = searchSession.search( Author.class )
.predicate( f -> f.spatial().within().onField( "placeOfBirth" )
.circle( 53.970000, 32.150000, 50, DistanceUnit.KILOMETERS ) )
.fetchHits();
assertThat( result ).hasSize( 1 );
} );
}

}
@@ -0,0 +1,43 @@
/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.documentation.mapper.orm.spatial.genericfield;

import javax.persistence.Basic;
import javax.persistence.Embeddable;

import org.hibernate.search.engine.spatial.GeoPoint;

//tag::include[]
@Embeddable
public class MyCoordinates implements GeoPoint { // <1>

@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() { // <2>
return latitude;
}

@Override
public double getLongitude() {
return longitude;
}
}
//end::include[]

0 comments on commit 732908a

Please sign in to comment.