DATAES-931 - Add query support for geo shape queries.

Original PR: #542
This commit is contained in:
Peter-Josef Meisch 2020-10-21 23:05:18 +02:00 committed by GitHub
parent 7198a02a00
commit da20cc1684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 554 additions and 220 deletions

View File

@ -4,10 +4,12 @@
[[new-features.4-1-0]]
== New in Spring Data Elasticsearch 4.1
* Upgrade to Elasticsearch 7.9.2
* Improved API for alias management
* Introduction of `ReactiveIndexOperations` for index management
* Index templates support
* Uses Spring 5.3.
* Upgrade to Elasticsearch 7.9.2.
* Improved API for alias management.
* Introduction of `ReactiveIndexOperations` for index management.
* Index templates support.
* Support for Geo-shape data with GeoJson.
[[new-features.4-0-0]]
== New in Spring Data Elasticsearch 4.0

View File

@ -3,17 +3,19 @@
Spring Data Elasticsearch Object Mapping is the process that maps a Java object - the domain entity - into the JSON representation that is stored in Elasticsearch and back.
Earlier versions of Spring Data Elasticsearch used a Jackson based conversion, Spring Data Elasticsearch 3.2.x introduced the <<elasticsearch.mapping.meta-model>>. As of version 4.0 only the Meta Object Mapping is used, the Jackson based mapper is not available anymore and the `MappingElasticsearchConverter` is used.
Earlier versions of Spring Data Elasticsearch used a Jackson based conversion, Spring Data Elasticsearch 3.2.x introduced the <<elasticsearch.mapping.meta-model>>.
As of version 4.0 only the Meta Object Mapping is used, the Jackson based mapper is not available anymore and the `MappingElasticsearchConverter` is used.
The main reasons for the removal of the Jackson based mapper are:
* Custom mappings of fields needed to be done with annotations like `@JsonFormat` or `@JsonInclude`. This often caused problems when the same object was used in different JSON based datastores or sent over a JSON based API.
* Custom field types and formats also need to be stored into the Elasticsearch index mappings. The Jackson based annotations did not fully provide all the information that is necessary to represent the types of Elasticsearch.
* Custom mappings of fields needed to be done with annotations like `@JsonFormat` or `@JsonInclude`.
This often caused problems when the same object was used in different JSON based datastores or sent over a JSON based API.
* Custom field types and formats also need to be stored into the Elasticsearch index mappings.
The Jackson based annotations did not fully provide all the information that is necessary to represent the types of Elasticsearch.
* Fields must be mapped not only when converting from and to entities, but also in query argument, returned data and on other places.
Using the `MappingElasticsearchConverter` now covers all these cases.
[[elasticsearch.mapping.meta-model]]
== Meta Model Object Mapping
@ -23,33 +25,48 @@ This allows to register `Converter` instances for specific domain type mapping.
[[elasticsearch.mapping.meta-model.annotations]]
=== Mapping Annotation Overview
The `MappingElasticsearchConverter` uses metadata to drive the mapping of objects to documents. The metadata is taken from the entity's properties which can be annotated.
The `MappingElasticsearchConverter` uses metadata to drive the mapping of objects to documents.
The metadata is taken from the entity's properties which can be annotated.
The following annotations are available:
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. The most important attributes are:
** `indexName`: the name of the index to store this entity in. This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
** `type`: [line-through]#the mapping type. If not set, the lowercased simple name of the class is used.# (deprecated since version 4.0)
* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database.
The most important attributes are:
** `indexName`: the name of the index to store this entity in.
This can contain a SpEL template expression like `"log-#{T(java.time.LocalDate).now().toString()}"`
** `type`: [line-through]#the mapping type.
If not set, the lowercased simple name of the class is used.# (deprecated since version 4.0)
** `shards`: the number of shards for the index.
** `replicas`: the number of replicas for the index.
** `refreshIntervall`: Refresh interval for the index. Used for index creation. Default value is _"1s"_.
** `indexStoreType`: Index storage type for the index. Used for index creation. Default value is _"fs"_.
** `createIndex`: flag whether to create an index on repository bootstrapping. Default value is _true_. See <<elasticsearch.repositories.autocreation>>
** `versionType`: Configuration of version management. Default value is _EXTERNAL_.
** `refreshIntervall`: Refresh interval for the index.
Used for index creation.
Default value is _"1s"_.
** `indexStoreType`: Index storage type for the index.
Used for index creation.
Default value is _"fs"_.
** `createIndex`: flag whether to create an index on repository bootstrapping.
Default value is _true_.
See <<elasticsearch.repositories.autocreation>>
** `versionType`: Configuration of version management.
Default value is _EXTERNAL_.
* `@Id`: Applied at the field level to mark the field used for identity purpose.
* `@Transient`: By default all fields are mapped to the document when it is stored or retrieved, this annotation excludes the field.
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document.
* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database.
Constructor arguments are mapped by name to the key values in the retrieved Document.
* `@Field`: Applied at the field level and defines properties of the field, most of the attributes map to the respective https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html[Elasticsearch Mapping] definitions (the following list is not complete, check the annotation Javadoc for a complete reference):
** `name`: The name of the field as it will be represented in the Elasticsearch document, if not set, the Java field name is used.
** `type`: the field type, can be one of _Text, Keyword, Long, Integer, Short, Byte, Double, Float, Half_Float, Scaled_Float, Date, Date_Nanos, Boolean, Binary, Integer_Range, Float_Range, Long_Range, Double_Range, Date_Range, Ip_Range, Object, Nested, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type_. See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html[Elasticsearch Mapping Types]
** `type`: the field type, can be one of _Text, Keyword, Long, Integer, Short, Byte, Double, Float, Half_Float, Scaled_Float, Date, Date_Nanos, Boolean, Binary, Integer_Range, Float_Range, Long_Range, Double_Range, Date_Range, Ip_Range, Object, Nested, Ip, TokenCount, Percolator, Flattened, Search_As_You_Type_.
See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html[Elasticsearch Mapping Types]
** `format` and `pattern` definitions for the _Date_ type. `format` must be defined for date types.
** `store`: Flag whether the original field value should be store in Elasticsearch, default value is _false_.
** `analyzer`, `searchAnalyzer`, `normalizer` for specifying custom analyzers and normalizer.
* `@GeoPoint`: marks a field as _geo_point_ datatype. Can be omitted if the field is an instance of the `GeoPoint` class.
* `@GeoPoint`: marks a field as _geo_point_ datatype.
Can be omitted if the field is an instance of the `GeoPoint` class.
NOTE: Properties that derive from `TemporalAccessor` must either have a `@Field` annotation of type `FieldType.Date` or a custom converter must be registered for this type. +
If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_. This is due to a https://www.elastic.co/guide/en/elasticsearch/reference/current/migrate-to-java-time.html#java-time-migration-incompatible-date-formats[change in Elasticsearch 7].
If you are using a custom date format, you need to use _uuuu_ for the year instead of _yyyy_.
This is due to a https://www.elastic.co/guide/en/elasticsearch/reference/current/migrate-to-java-time.html#java-time-migration-incompatible-date-formats[change in Elasticsearch 7].
The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.
@ -72,6 +89,7 @@ public class Person { <1>
String lastname;
}
----
[source,json]
----
{
@ -84,10 +102,10 @@ public class Person { <1>
<1> By default the domain types class name is used for the type hint.
====
Type hints can be configured to hold custom information. Use the `@TypeAlias` annotation to do so.
Type hints can be configured to hold custom information.
Use the `@TypeAlias` annotation to do so.
NOTE: Make sure to add types with `@TypeAlias` to the initial entity set (`AbstractElasticsearchConfiguration#getInitialEntitySet`)
to already have entity information available when first reading data from the store.
NOTE: Make sure to add types with `@TypeAlias` to the initial entity set (`AbstractElasticsearchConfiguration#getInitialEntitySet`) to already have entity information available when first reading data from the store.
.Type Hints with Alias
====
@ -100,6 +118,7 @@ public class Person {
// ...
}
----
[source,json]
----
{
@ -126,6 +145,7 @@ public class Address {
Point location;
}
----
[source,json]
----
{
@ -138,8 +158,9 @@ public class Address {
==== GeoJson Types
Spring Data Elasticsearch supports the GeoJson types by providing an interface `GeoJson` and implementations for the different geometries. They are mapped to Elasticsearch
documents according to the GeoJson specification. The corresponding properties of the entity are specified as `geo_shape` when the index mappings is written.
Spring Data Elasticsearch supports the GeoJson types by providing an interface `GeoJson` and implementations for the different geometries.
They are mapped to Elasticsearch documents according to the GeoJson specification.
The corresponding properties of the entity are specified in the index mappings as `geo_shape` when the index mappings is written. (check the https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html[Elasticsearch documentation] as well)
.GeoJson types
====
@ -151,6 +172,7 @@ public class Address {
GeoJsonPoint location;
}
----
[source,json]
----
{
@ -190,6 +212,7 @@ public class Person {
}
----
[source,json]
----
{
@ -217,6 +240,7 @@ public class Person {
}
----
[source,json]
----
{
@ -283,6 +307,7 @@ public class Config extends AbstractElasticsearchConfiguration {
}
}
----
[source,json]
----
{

View File

@ -8,12 +8,14 @@ The Elasticsearch module supports all basic query building feature as string que
=== Declared queries
Deriving the query from the method name is not always sufficient and/or may result in unreadable method names. In this case one might make use of the `@Query` annotation (see <<elasticsearch.query-methods.at-query>> ).
Deriving the query from the method name is not always sufficient and/or may result in unreadable method names.
In this case one might make use of the `@Query` annotation (see <<elasticsearch.query-methods.at-query>> ).
[[elasticsearch.query-methods.criterions]]
== Query creation
Generally the query creation mechanism for Elasticsearch works as described in <<repositories.query-methods>>. Here's a short example of what a Elasticsearch query method translates into:
Generally the query creation mechanism for Elasticsearch works as described in <<repositories.query-methods>>.
Here's a short example of what a Elasticsearch query method translates into:
.Query creation from method names
====
@ -43,7 +45,7 @@ The method name above will be translated into the following Elasticsearch json q
A list of supported keywords for Elasticsearch is shown below.
[cols="1,2,3", options="header"]
[cols="1,2,3",options="header"]
.Supported keywords inside method names
|===
| Keyword
@ -55,10 +57,10 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "price" ] } }
]
}
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "price" ] } }
]
}
}}`
| `Or`
@ -66,10 +68,10 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"should" : [
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "price" ] } }
]
}
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } },
{ "query_string" : { "query" : "?", "fields" : [ "price" ] } }
]
}
}}`
| `Is`
@ -77,9 +79,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } }
]
}
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } }
]
}
}}`
| `Not`
@ -87,9 +89,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must_not" : [
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } }
]
}
{ "query_string" : { "query" : "?", "fields" : [ "name" ] } }
]
}
}}`
| `Between`
@ -97,9 +99,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : ?, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
}}`
| `LessThan`
@ -107,9 +109,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } }
]
}
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : false } } }
]
}
}}`
| `LessThanEqual`
@ -117,9 +119,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
}}`
| `GreaterThan`
@ -127,9 +129,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : false, "include_upper" : true } } }
]
}
}}`
@ -138,9 +140,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } }
]
}
}}`
| `Before`
@ -148,9 +150,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : null, "to" : ?, "include_lower" : true, "include_upper" : true } } }
]
}
}}`
| `After`
@ -158,9 +160,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } }
]
}
{"range" : {"price" : {"from" : ?, "to" : null, "include_lower" : true, "include_upper" : true } } }
]
}
}}`
| `Like`
@ -168,9 +170,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
{ "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
}}`
| `StartingWith`
@ -178,9 +180,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
{ "query_string" : { "query" : "?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
}}`
| `EndingWith`
@ -188,9 +190,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
{ "query_string" : { "query" : "*?", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
}}`
| `Contains/Containing`
@ -198,9 +200,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "\*?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
{ "query_string" : { "query" : "\*?*", "fields" : [ "name" ] }, "analyze_wildcard": true }
]
}
}}`
| `In` (when annotated as FieldType.Keyword)
@ -208,13 +210,13 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"bool" : {"must" : [
{"terms" : {"name" : ["?","?"]}}
]
}
}
]
}
{"bool" : {"must" : [
{"terms" : {"name" : ["?","?"]}}
]
}
}
]
}
}}`
@ -227,13 +229,13 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{"bool" : {"must_not" : [
{"terms" : {"name" : ["?","?"]}}
]
}
}
]
}
{"bool" : {"must_not" : [
{"terms" : {"name" : ["?","?"]}}
]
}
}
]
}
}}`
| `NotIn`
@ -249,9 +251,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "true", "fields" : [ "available" ] } }
]
}
{ "query_string" : { "query" : "true", "fields" : [ "available" ] } }
]
}
}}`
| `False`
@ -259,9 +261,9 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "false", "fields" : [ "available" ] } }
]
}
{ "query_string" : { "query" : "false", "fields" : [ "available" ] } }
]
}
}}`
| `OrderBy`
@ -269,14 +271,17 @@ A list of supported keywords for Elasticsearch is shown below.
| `{ "query" : {
"bool" : {
"must" : [
{ "query_string" : { "query" : "true", "fields" : [ "available" ] } }
]
}
{ "query_string" : { "query" : "true", "fields" : [ "available" ] } }
]
}
}, "sort":[{"name":{"order":"desc"}}]
}`
|===
NOTE: Methods names to build Geo-shape queries taking `GeoJson` parameters are not supported.
Use `ElasticsearchOperations` with `CriteriaQuery` in a custom repository implementation if you need to have such a function in a repository.
== Method return types
Repository methods can be defined to have the following return types for returning multiple Elements:
@ -300,7 +305,10 @@ interface BookRepository extends ElasticsearchRepository<Book, String> {
Page<Book> findByName(String name,Pageable pageable);
}
----
The String that is set as the annotation argument must be a valid Elasticsearch JSON query. It will be sent to Easticsearch as value of the query element; if for example the function is called with the parameter _John_, it would produce the following query body:
The String that is set as the annotation argument must be a valid Elasticsearch JSON query.
It will be sent to Easticsearch as value of the query element; if for example the function is called with the parameter _John_, it would produce the following query body:
[source,json]
----
{

View File

@ -30,6 +30,7 @@ import org.elasticsearch.index.query.GeoDistanceQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.geo.Box;
@ -102,75 +103,105 @@ class CriteriaFilterProcessor {
QueryBuilder filter = null;
switch (key) {
case WITHIN: {
GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery(fieldName);
case WITHIN:
Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values.");
Object[] valArray = (Object[]) value;
Assert.noNullElements(valArray, "Geo distance filter takes 2 not null elements array as parameter.");
Assert.isTrue(valArray.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
Assert.isTrue(valArray[0] instanceof GeoPoint || valArray[0] instanceof String || valArray[0] instanceof Point,
"First element of a geo distance filter must be a GeoPoint, a Point or a text");
Assert.isTrue(valArray[1] instanceof String || valArray[1] instanceof Distance,
"Second element of a geo distance filter must be a text or a Distance");
StringBuilder dist = new StringBuilder();
if (valArray[1] instanceof Distance) {
extractDistanceString((Distance) valArray[1], dist);
} else {
dist.append((String) valArray[1]);
}
if (valArray[0] instanceof GeoPoint) {
GeoPoint loc = (GeoPoint) valArray[0];
geoDistanceQueryBuilder.point(loc.getLat(), loc.getLon()).distance(dist.toString())
.geoDistance(GeoDistance.PLANE);
} else if (valArray[0] instanceof Point) {
GeoPoint loc = GeoPoint.fromPoint((Point) valArray[0]);
geoDistanceQueryBuilder.point(loc.getLat(), loc.getLon()).distance(dist.toString())
.geoDistance(GeoDistance.PLANE);
} else {
String loc = (String) valArray[0];
if (loc.contains(",")) {
String[] c = loc.split(",");
geoDistanceQueryBuilder.point(Double.parseDouble(c[0]), Double.parseDouble(c[1])).distance(dist.toString())
.geoDistance(GeoDistance.PLANE);
} else {
geoDistanceQueryBuilder.geohash(loc).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
}
}
filter = geoDistanceQueryBuilder;
filter = withinQuery(fieldName, (Object[]) value);
break;
}
case BBOX: {
filter = QueryBuilders.geoBoundingBoxQuery(fieldName);
case BBOX:
Assert.isTrue(value instanceof Object[],
"Value of a boundedBy filter should be an array of one or two values.");
Object[] valArray = (Object[]) value;
Assert.noNullElements(valArray, "Geo boundedBy filter takes a not null element array as parameter.");
if (valArray.length == 1) {
// GeoEnvelop
oneParameterBBox((GeoBoundingBoxQueryBuilder) filter, valArray[0]);
} else if (valArray.length == 2) {
// 2x GeoPoint
// 2x text
twoParameterBBox((GeoBoundingBoxQueryBuilder) filter, valArray);
} else {
throw new IllegalArgumentException(
"Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
}
filter = boundingBoxQuery(fieldName, (Object[]) value);
break;
case GEO_INTERSECTS:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_INTERSECTS filter must be a GeoJson object");
filter = geoJsonQuery(fieldName, (GeoJson<?>) value, "intersects");
break;
case GEO_IS_DISJOINT:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_IS_DISJOINT filter must be a GeoJson object");
filter = geoJsonQuery(fieldName, (GeoJson<?>) value, "disjoint");
break;
case GEO_WITHIN:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_WITHIN filter must be a GeoJson object");
filter = geoJsonQuery(fieldName, (GeoJson<?>) value, "within");
break;
case GEO_CONTAINS:
Assert.isTrue(value instanceof GeoJson<?>, "value of a GEO_CONTAINS filter must be a GeoJson object");
filter = geoJsonQuery(fieldName, (GeoJson<?>) value, "contains");
break;
}
return filter;
}
private QueryBuilder withinQuery(String fieldName, Object[] valArray) {
GeoDistanceQueryBuilder filter = QueryBuilders.geoDistanceQuery(fieldName);
Assert.noNullElements(valArray, "Geo distance filter takes 2 not null elements array as parameter.");
Assert.isTrue(valArray.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
Assert.isTrue(valArray[0] instanceof GeoPoint || valArray[0] instanceof String || valArray[0] instanceof Point,
"First element of a geo distance filter must be a GeoPoint, a Point or a text");
Assert.isTrue(valArray[1] instanceof String || valArray[1] instanceof Distance,
"Second element of a geo distance filter must be a text or a Distance");
StringBuilder dist = new StringBuilder();
if (valArray[1] instanceof Distance) {
extractDistanceString((Distance) valArray[1], dist);
} else {
dist.append((String) valArray[1]);
}
if (valArray[0] instanceof GeoPoint) {
GeoPoint loc = (GeoPoint) valArray[0];
filter.point(loc.getLat(), loc.getLon()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
} else if (valArray[0] instanceof Point) {
GeoPoint loc = GeoPoint.fromPoint((Point) valArray[0]);
filter.point(loc.getLat(), loc.getLon()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
} else {
String loc = (String) valArray[0];
if (loc.contains(",")) {
String[] c = loc.split(",");
filter.point(Double.parseDouble(c[0]), Double.parseDouble(c[1])).distance(dist.toString())
.geoDistance(GeoDistance.PLANE);
} else {
filter.geohash(loc).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
}
}
return filter;
}
private QueryBuilder boundingBoxQuery(String fieldName, Object[] valArray) {
Assert.noNullElements(valArray, "Geo boundedBy filter takes a not null element array as parameter.");
GeoBoundingBoxQueryBuilder filter = QueryBuilders.geoBoundingBoxQuery(fieldName);
if (valArray.length == 1) {
// GeoEnvelop
oneParameterBBox(filter, valArray[0]);
} else if (valArray.length == 2) {
// 2x GeoPoint
// 2x text
twoParameterBBox(filter, valArray);
} else {
throw new IllegalArgumentException(
"Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
}
return filter;
}
private QueryBuilder geoJsonQuery(String fieldName, GeoJson<?> geoJson, String relation) {
return QueryBuilders.wrapperQuery(buildJsonQuery(fieldName, geoJson, relation));
}
private String buildJsonQuery(String fieldName, GeoJson<?> geoJson, String relation) {
return "{\"geo_shape\": {\"" + fieldName + "\": {\"shape\": " + geoJson.toJson() + ", \"relation\": \"" + relation
+ "\"}}}";
}
/**
* extract the distance string from a {@link org.springframework.data.geo.Distance} object.
*

View File

@ -1300,7 +1300,7 @@ class RequestFactory {
if (query instanceof NativeSearchQuery) {
NativeSearchQuery nativeSearchQuery = (NativeSearchQuery) query;
List<SortBuilder> sorts = nativeSearchQuery.getElasticsearchSorts();
List<SortBuilder<?>> sorts = nativeSearchQuery.getElasticsearchSorts();
if (sorts != null) {
sorts.forEach(sourceBuilder::sort);
}
@ -1316,7 +1316,7 @@ class RequestFactory {
if (query instanceof NativeSearchQuery) {
NativeSearchQuery nativeSearchQuery = (NativeSearchQuery) query;
List<SortBuilder> sorts = nativeSearchQuery.getElasticsearchSorts();
List<SortBuilder<?>> sorts = nativeSearchQuery.getElasticsearchSorts();
if (sorts != null) {
sorts.forEach(searchRequestBuilder::addSort);
}
@ -1514,6 +1514,7 @@ class RequestFactory {
// endregion
// region helper functions
@Nullable
private QueryBuilder getQuery(Query query) {
QueryBuilder elasticsearchQuery;

View File

@ -46,7 +46,7 @@ import org.springframework.util.NumberUtils;
* @author Peter-Josef Meisch
* @since 3.2
*/
class GeoConverters {
public class GeoConverters {
static Collection<Converter<?, ?>> getConvertersToRegister() {
@ -67,7 +67,7 @@ class GeoConverters {
* {@link Converter} to write a {@link Point} to {@link Map} using {@code lat/long} properties.
*/
@WritingConverter
enum PointToMapConverter implements Converter<Point, Map<String, Object>> {
public enum PointToMapConverter implements Converter<Point, Map<String, Object>> {
INSTANCE;
@ -85,7 +85,7 @@ class GeoConverters {
* {@link Converter} to read a {@link Point} from {@link Map} using {@code lat/long} properties.
*/
@ReadingConverter
enum MapToPointConverter implements Converter<Map<String, Object>, Point> {
public enum MapToPointConverter implements Converter<Map<String, Object>, Point> {
INSTANCE;
@ -104,7 +104,7 @@ class GeoConverters {
* {@link Converter} to write a {@link GeoPoint} to {@link Map} using {@code lat/long} properties.
*/
@WritingConverter
enum GeoPointToMapConverter implements Converter<GeoPoint, Map<String, Object>> {
public enum GeoPointToMapConverter implements Converter<GeoPoint, Map<String, Object>> {
INSTANCE;
@ -119,7 +119,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoPointConverter implements Converter<Map<String, Object>, GeoPoint> {
public enum MapToGeoPointConverter implements Converter<Map<String, Object>, GeoPoint> {
INSTANCE;
@ -135,7 +135,7 @@ class GeoConverters {
// region GeoJson
@WritingConverter
enum GeoJsonToMapConverter implements Converter<GeoJson<? extends Iterable<?>>, Map<String, Object>> {
public enum GeoJsonToMapConverter implements Converter<GeoJson<? extends Iterable<?>>, Map<String, Object>> {
INSTANCE;
@ -162,7 +162,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonConverter implements Converter<Map<String, Object>, GeoJson<? extends Iterable<?>>> {
public enum MapToGeoJsonConverter implements Converter<Map<String, Object>, GeoJson<? extends Iterable<?>>> {
INSTANCE;
@ -195,7 +195,7 @@ class GeoConverters {
// region GeoJsonPoint
@WritingConverter
enum GeoJsonPointToMapConverter implements Converter<GeoJsonPoint, Map<String, Object>> {
public enum GeoJsonPointToMapConverter implements Converter<GeoJsonPoint, Map<String, Object>> {
INSTANCE;
@ -209,7 +209,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonPointConverter implements Converter<Map<String, Object>, GeoJsonPoint> {
public enum MapToGeoJsonPointConverter implements Converter<Map<String, Object>, GeoJsonPoint> {
INSTANCE;
@ -233,7 +233,7 @@ class GeoConverters {
// region GeoJsonMultiPoint
@WritingConverter
enum GeoJsonMultiPointToMapConverter implements Converter<GeoJsonMultiPoint, Map<String, Object>> {
public enum GeoJsonMultiPointToMapConverter implements Converter<GeoJsonMultiPoint, Map<String, Object>> {
INSTANCE;
@ -247,7 +247,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonMultiPointConverter implements Converter<Map<String, Object>, GeoJsonMultiPoint> {
public enum MapToGeoJsonMultiPointConverter implements Converter<Map<String, Object>, GeoJsonMultiPoint> {
INSTANCE;
@ -268,7 +268,7 @@ class GeoConverters {
// region GeoJsonLineString
@WritingConverter
enum GeoJsonLineStringToMapConverter implements Converter<GeoJsonLineString, Map<String, Object>> {
public enum GeoJsonLineStringToMapConverter implements Converter<GeoJsonLineString, Map<String, Object>> {
INSTANCE;
@ -282,7 +282,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonLineStringConverter implements Converter<Map<String, Object>, GeoJsonLineString> {
public enum MapToGeoJsonLineStringConverter implements Converter<Map<String, Object>, GeoJsonLineString> {
INSTANCE;
@ -303,7 +303,7 @@ class GeoConverters {
// region GeoJsonMultiLineString
@WritingConverter
enum GeoJsonMultiLineStringToMapConverter implements Converter<GeoJsonMultiLineString, Map<String, Object>> {
public enum GeoJsonMultiLineStringToMapConverter implements Converter<GeoJsonMultiLineString, Map<String, Object>> {
INSTANCE;
@ -314,7 +314,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonMultiLineStringConverter implements Converter<Map<String, Object>, GeoJsonMultiLineString> {
public enum MapToGeoJsonMultiLineStringConverter implements Converter<Map<String, Object>, GeoJsonMultiLineString> {
INSTANCE;
@ -331,7 +331,7 @@ class GeoConverters {
// region GeoJsonPolygon
@WritingConverter
enum GeoJsonPolygonToMapConverter implements Converter<GeoJsonPolygon, Map<String, Object>> {
public enum GeoJsonPolygonToMapConverter implements Converter<GeoJsonPolygon, Map<String, Object>> {
INSTANCE;
@ -342,7 +342,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonPolygonConverter implements Converter<Map<String, Object>, GeoJsonPolygon> {
public enum MapToGeoJsonPolygonConverter implements Converter<Map<String, Object>, GeoJsonPolygon> {
INSTANCE;
@ -364,7 +364,7 @@ class GeoConverters {
// region GeoJsonMultiPolygon
@WritingConverter
enum GeoJsonMultiPolygonToMapConverter implements Converter<GeoJsonMultiPolygon, Map<String, Object>> {
public enum GeoJsonMultiPolygonToMapConverter implements Converter<GeoJsonMultiPolygon, Map<String, Object>> {
INSTANCE;
@ -386,7 +386,7 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonMultiPolygonConverter implements Converter<Map<String, Object>, GeoJsonMultiPolygon> {
public enum MapToGeoJsonMultiPolygonConverter implements Converter<Map<String, Object>, GeoJsonMultiPolygon> {
INSTANCE;
@ -413,7 +413,8 @@ class GeoConverters {
// region GeoJsonGeometryCollection
@WritingConverter
enum GeoJsonGeometryCollectionToMapConverter implements Converter<GeoJsonGeometryCollection, Map<String, Object>> {
public enum GeoJsonGeometryCollectionToMapConverter
implements Converter<GeoJsonGeometryCollection, Map<String, Object>> {
INSTANCE;
@ -431,7 +432,8 @@ class GeoConverters {
}
@ReadingConverter
enum MapToGeoJsonGeometryCollectionConverter implements Converter<Map<String, Object>, GeoJsonGeometryCollection> {
public enum MapToGeoJsonGeometryCollectionConverter
implements Converter<Map<String, Object>, GeoJsonGeometryCollection> {
INSTANCE;

View File

@ -24,7 +24,7 @@ import java.util.function.IntSupplier;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import org.springframework.data.elasticsearch.ElasticsearchException;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -83,7 +83,7 @@ public interface Document extends Map<String, Object> {
try {
return new MapDocument(MapDocument.OBJECT_MAPPER.readerFor(Map.class).readValue(json));
} catch (IOException e) {
throw new ElasticsearchException("Cannot parse JSON", e);
throw new ConversionException("Cannot parse JSON", e);
}
}

View File

@ -15,9 +15,13 @@
*/
package org.springframework.data.elasticsearch.core.geo;
import org.springframework.data.elasticsearch.core.convert.ConversionException;
import org.springframework.data.elasticsearch.core.convert.GeoConverters;
import org.springframework.data.elasticsearch.core.document.Document;
/**
* Interface definition for structures defined in <a href="https://geojson.org/>GeoJSON</a> format.
* copied from Spring Data Mongodb
* Interface definition for structures defined in <a href="https://geojson.org/>GeoJSON</a> format. copied from Spring
* Data Mongodb
*
* @author Christoph Strobl
* @since 1.7
@ -28,7 +32,8 @@ public interface GeoJson<T extends Iterable<?>> {
* String value representing the type of the {@link GeoJson} object.
*
* @return will never be {@literal null}.
* @see <a href="https://geojson.org/geojson-spec.html#geojson-objects">https://geojson.org/geojson-spec.html#geojson-objects</a>
* @see <a href=
* "https://geojson.org/geojson-spec.html#geojson-objects">https://geojson.org/geojson-spec.html#geojson-objects</a>
*/
String getType();
@ -37,7 +42,24 @@ public interface GeoJson<T extends Iterable<?>> {
* determined by {@link #getType()} of geometry.
*
* @return will never be {@literal null}.
* @see <a href="https://geojson.org/geojson-spec.html#geometry-objects">https://geojson.org/geojson-spec.html#geometry-objects</a>
* @see <a href=
* "https://geojson.org/geojson-spec.html#geometry-objects">https://geojson.org/geojson-spec.html#geometry-objects</a>
*/
T getCoordinates();
/**
* @param json the JSON string to parse
* @return the parsed {@link GeoJson} object
* @throws ConversionException on parse erros
*/
static GeoJson<?> of(String json) {
return GeoConverters.MapToGeoJsonConverter.INSTANCE.convert(Document.parse(json));
}
/**
* @return a JSON representation of this object
*/
default String toJson() {
return Document.from(GeoConverters.GeoJsonToMapConverter.INSTANCE.convert(this)).toJson();
}
}

View File

@ -25,6 +25,7 @@ import java.util.Set;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
@ -38,7 +39,7 @@ import org.springframework.util.StringUtils;
* easily chain together multiple criteria. <br/>
* <br/>
* A Criteria references a field and has {@link CriteriaEntry} sets for query and filter context. When building the
* query, the entries from the criteriaentries are combined in a bool must query (if more than one.<br/>
* query, the entries from the criteria entries are combined in a bool must query (if more than one.<br/>
* <br/>
* A Criteria also has a {@link CriteriaChain} which is used to build a collection of Criteria with the fluent API. The
* value of {@link #isAnd()} and {@link #isOr()} describes whether the queries built from the criteria chain should be
@ -54,7 +55,7 @@ public class Criteria {
public static final String CRITERIA_VALUE_SEPARATOR = " ";
/** @deprecated since 4.1, use {@link #CRITERIA_VALUE_SEPARATOR} */
public static final String CRITERIA_VALUE_SEPERATOR = CRITERIA_VALUE_SEPARATOR;
@SuppressWarnings("SpellCheckingInspection") public static final String CRITERIA_VALUE_SEPERATOR = CRITERIA_VALUE_SEPARATOR;
private @Nullable Field field;
private float boost = Float.NaN;
@ -730,6 +731,59 @@ public class Criteria {
return this;
}
/**
* Adds a new filter CriteriaEntry for GEO_INTERSECTS.
*
* @param geoShape the GeoJson shape
* @return this object
*/
public Criteria intersects(GeoJson<?> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_INTERSECTS, geoShape));
return this;
}
/**
* Adds a new filter CriteriaEntry for GEO_IS_DISJOINT.
*
* @param geoShape the GeoJson shape
* @return this object
*/
public Criteria isDisjoint(GeoJson<?> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_IS_DISJOINT, geoShape));
return this;
}
/**
* Adds a new filter CriteriaEntry for GEO_WITHIN.
*
* @param geoShape the GeoJson shape
* @return this object
*/
public Criteria within(GeoJson<?> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_WITHIN, geoShape));
return this;
}
/**
* Adds a new filter CriteriaEntry for GEO_CONTAINS.
*
* @param geoShape the GeoJson shape
* @return this object
*/
public Criteria contains(GeoJson<?> geoShape) {
Assert.notNull(geoShape, "geoShape must not be null");
filterCriteriaEntries.add(new CriteriaEntry(OperationKey.GEO_CONTAINS, geoShape));
return this;
}
// endregion
// region helper functions
@ -868,7 +922,23 @@ public class Criteria {
/**
* @since 4.0
*/
EXISTS //
EXISTS, //
/**
* @since 4.1
*/
GEO_INTERSECTS, //
/**
* @since 4.1
*/
GEO_IS_DISJOINT, //
/**
* @since 4.1
*/
GEO_WITHIN, //
/**
* @since 4.1
*/
GEO_CONTAINS
}
/**

View File

@ -27,7 +27,10 @@ import org.elasticsearch.search.sort.SortBuilder;
import org.springframework.lang.Nullable;
/**
* NativeSearchQuery
* A query created from Elasticsearch QueryBuilder instances. Note: the filter constructor parameter is used to create a
* post_filter
* {@see https://www.elastic.co/guide/en/elasticsearch/reference/7.9/filter-search-results.html#post-filter}, if a
* filter is needed that filters before aggregations are build, it must be included in the query constructor parameter.
*
* @author Rizwan Idrees
* @author Mohsin Husen
@ -38,36 +41,37 @@ import org.springframework.lang.Nullable;
*/
public class NativeSearchQuery extends AbstractQuery {
private QueryBuilder query;
@Nullable private final QueryBuilder query;
@Nullable private QueryBuilder filter;
@Nullable private List<SortBuilder> sorts;
@Nullable private List<SortBuilder<?>> sorts;
private final List<ScriptField> scriptFields = new ArrayList<>();
@Nullable private CollapseBuilder collapseBuilder;
@Nullable private List<AbstractAggregationBuilder> aggregations;
@Nullable private List<AbstractAggregationBuilder<?>> aggregations;
@Nullable private HighlightBuilder highlightBuilder;
@Nullable private HighlightBuilder.Field[] highlightFields;
@Nullable private List<IndexBoost> indicesBoost;
public NativeSearchQuery(QueryBuilder query) {
public NativeSearchQuery(@Nullable QueryBuilder query) {
this.query = query;
}
public NativeSearchQuery(QueryBuilder query, QueryBuilder filter) {
public NativeSearchQuery(@Nullable QueryBuilder query, @Nullable QueryBuilder filter) {
this.query = query;
this.filter = filter;
}
public NativeSearchQuery(QueryBuilder query, QueryBuilder filter, List<SortBuilder> sorts) {
public NativeSearchQuery(@Nullable QueryBuilder query, @Nullable QueryBuilder filter,
@Nullable List<SortBuilder<?>> sorts) {
this.query = query;
this.filter = filter;
this.sorts = sorts;
}
public NativeSearchQuery(QueryBuilder query, QueryBuilder filter, List<SortBuilder> sorts,
HighlightBuilder.Field[] highlightFields) {
public NativeSearchQuery(@Nullable QueryBuilder query, @Nullable QueryBuilder filter,
@Nullable List<SortBuilder<?>> sorts, @Nullable HighlightBuilder.Field[] highlightFields) {
this.query = query;
this.filter = filter;
@ -75,16 +79,18 @@ public class NativeSearchQuery extends AbstractQuery {
this.highlightFields = highlightFields;
}
public NativeSearchQuery(QueryBuilder query, QueryBuilder filter, List<SortBuilder> sorts,
HighlightBuilder highlighBuilder, HighlightBuilder.Field[] highlightFields) {
public NativeSearchQuery(@Nullable QueryBuilder query, @Nullable QueryBuilder filter,
@Nullable List<SortBuilder<?>> sorts, @Nullable HighlightBuilder highlightBuilder,
@Nullable HighlightBuilder.Field[] highlightFields) {
this.query = query;
this.filter = filter;
this.sorts = sorts;
this.highlightBuilder = highlighBuilder;
this.highlightBuilder = highlightBuilder;
this.highlightFields = highlightFields;
}
@Nullable
public QueryBuilder getQuery() {
return query;
}
@ -95,7 +101,7 @@ public class NativeSearchQuery extends AbstractQuery {
}
@Nullable
public List<SortBuilder> getElasticsearchSorts() {
public List<SortBuilder<?>> getElasticsearchSorts() {
return sorts;
}
@ -131,11 +137,11 @@ public class NativeSearchQuery extends AbstractQuery {
}
@Nullable
public List<AbstractAggregationBuilder> getAggregations() {
public List<AbstractAggregationBuilder<?>> getAggregations() {
return aggregations;
}
public void addAggregation(AbstractAggregationBuilder aggregationBuilder) {
public void addAggregation(AbstractAggregationBuilder<?> aggregationBuilder) {
if (aggregations == null) {
aggregations = new ArrayList<>();
@ -144,7 +150,7 @@ public class NativeSearchQuery extends AbstractQuery {
aggregations.add(aggregationBuilder);
}
public void setAggregations(List<AbstractAggregationBuilder> aggregations) {
public void setAggregations(List<AbstractAggregationBuilder<?>> aggregations) {
this.aggregations = aggregations;
}

View File

@ -49,9 +49,9 @@ public class NativeSearchQueryBuilder {
@Nullable private QueryBuilder queryBuilder;
@Nullable private QueryBuilder filterBuilder;
private List<ScriptField> scriptFields = new ArrayList<>();
private List<SortBuilder> sortBuilders = new ArrayList<>();
private List<AbstractAggregationBuilder> aggregationBuilders = new ArrayList<>();
private final List<ScriptField> scriptFields = new ArrayList<>();
private final List<SortBuilder<?>> sortBuilders = new ArrayList<>();
private final List<AbstractAggregationBuilder<?>> aggregationBuilders = new ArrayList<>();
@Nullable private HighlightBuilder highlightBuilder;
@Nullable private HighlightBuilder.Field[] highlightFields;
private Pageable pageable = Pageable.unpaged();
@ -77,7 +77,7 @@ public class NativeSearchQueryBuilder {
return this;
}
public NativeSearchQueryBuilder withSort(SortBuilder sortBuilder) {
public NativeSearchQueryBuilder withSort(SortBuilder<?> sortBuilder) {
this.sortBuilders.add(sortBuilder);
return this;
}
@ -92,7 +92,7 @@ public class NativeSearchQueryBuilder {
return this;
}
public NativeSearchQueryBuilder addAggregation(AbstractAggregationBuilder aggregationBuilder) {
public NativeSearchQueryBuilder addAggregation(AbstractAggregationBuilder<?> aggregationBuilder) {
this.aggregationBuilders.add(aggregationBuilder);
return this;
}
@ -134,7 +134,7 @@ public class NativeSearchQueryBuilder {
/**
* @param trackScores whether to track scores.
* @return
* @return this object
* @since 3.1
*/
public NativeSearchQueryBuilder withTrackScores(boolean trackScores) {
@ -168,6 +168,7 @@ public class NativeSearchQueryBuilder {
}
public NativeSearchQuery build() {
NativeSearchQuery nativeSearchQuery = new NativeSearchQuery(queryBuilder, filterBuilder, sortBuilders,
highlightBuilder, highlightFields);

View File

@ -18,17 +18,24 @@ package org.springframework.data.elasticsearch.core;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import java.time.LocalDate;
import java.util.Base64;
import java.util.Collections;
import java.util.Objects;
import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.core.convert.GeoConverters;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.geo.GeoJson;
import org.springframework.data.elasticsearch.core.geo.GeoJsonPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
@ -38,13 +45,14 @@ import org.springframework.lang.Nullable;
* Tests for the mapping of {@link CriteriaQuery} by a
* {@link org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter}. In the same package as
* {@link CriteriaQueryProcessor} as this is needed to get the String representation to assert.
*
*
* @author Peter-Josef Meisch
*/
public class CriteriaQueryMappingTests {
public class CriteriaQueryMappingUnitTests {
MappingElasticsearchConverter mappingElasticsearchConverter;
// region setup
@BeforeEach
void setUp() {
SimpleElasticsearchMappingContext mappingContext = new SimpleElasticsearchMappingContext();
@ -55,8 +63,11 @@ public class CriteriaQueryMappingTests {
mappingElasticsearchConverter.afterPropertiesSet();
}
// endregion
@Test // DATAES-716
// region tests
@Test
// DATAES-716
void shouldMapNamesAndConvertValuesInCriteriaQuery() throws JSONException {
// use POJO properties and types in the query building
@ -98,7 +109,8 @@ public class CriteriaQueryMappingTests {
assertEquals(expected, queryString, false);
}
@Test // DATAES-706
@Test
// DATAES-706
void shouldMapNamesAndValuesInSubCriteriaQuery() throws JSONException {
CriteriaQuery criteriaQuery = new CriteriaQuery( //
@ -151,6 +163,36 @@ public class CriteriaQueryMappingTests {
assertEquals(expected, queryString, false);
}
@Test // DATAES-931
@DisplayName("should map names in GeoJson query")
void shouldMapNamesInGeoJsonQuery() throws JSONException {
GeoJsonPoint geoJsonPoint = GeoJsonPoint.of(1.2, 3.4);
CriteriaQuery criteriaQuery = new CriteriaQuery(new Criteria("geoShapeField").intersects(geoJsonPoint));
String base64Query = getBase64EncodedGeoShapeQuery(geoJsonPoint, "geo-shape-field", "intersects");
String expected = "{\n" + //
" \"wrapper\": {\n" + //
" \"query\": \"" + base64Query + "\"\n" + //
" }\n" + //
"}\n"; //
mappingElasticsearchConverter.updateQuery(criteriaQuery, GeoShapeEntity.class);
String queryString = new CriteriaFilterProcessor().createFilter(criteriaQuery.getCriteria()).toString();
assertEquals(expected, queryString, false);
}
private String getBase64EncodedGeoShapeQuery(GeoJson<?> geoJson, String elasticFieldName, String relation) {
return Base64.getEncoder()
.encodeToString(("{\"geo_shape\": {\""
+ elasticFieldName + "\": {\"shape\": " + Document
.from(Objects.requireNonNull(GeoConverters.GeoJsonToMapConverter.INSTANCE.convert(geoJson))).toJson()
+ ", \"relation\": \"" + relation + "\"}}}").getBytes());
}
// endregion
// region test entities
static class Person {
@Nullable @Id String id;
@Nullable @Field(name = "first-name") String firstName;
@ -158,4 +200,9 @@ public class CriteriaQueryMappingTests {
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.custom,
pattern = "dd.MM.uuuu") LocalDate birthDate;
}
static class GeoShapeEntity {
@Nullable @Field(name = "geo-shape-field") GeoJson<?> geoShapeField;
}
// endregion
}

View File

@ -24,7 +24,7 @@ import org.springframework.data.elasticsearch.core.query.Criteria;
/**
* @author Peter-Josef Meisch
*/
class CriteriaQueryProcessorTests {
class CriteriaQueryProcessorUnitTests {
private final CriteriaQueryProcessor queryProcessor = new CriteriaQueryProcessor();

View File

@ -17,6 +17,9 @@ package org.springframework.data.elasticsearch.core.geo;
import static org.assertj.core.api.Assertions.*;
import lombok.Builder;
import lombok.Data;
import java.util.Arrays;
import org.junit.jupiter.api.AfterEach;
@ -24,8 +27,14 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchRestTemplateConfiguration;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
import org.springframework.data.geo.Point;
@ -43,19 +52,73 @@ class GeoJsonIntegrationTest {
private IndexOperations indexOps;
// region data
private final GeoJsonPolygon geoShape10To20 = GeoJsonPolygon.of( //
new Point(10, 10), //
new Point(20, 10), //
new Point(20, 20), //
new Point(10, 20), //
new Point(10, 10));
private final Area area10To20 = Area.builder().id("area10To20").area(geoShape10To20) /**/.build();
private final GeoJsonPolygon geoShape5To35 = GeoJsonPolygon.of( //
new Point(5, 5), //
new Point(35, 5), //
new Point(35, 35), //
new Point(5, 35), //
new Point(5, 5));
private final Area area5To35 = Area.builder().id("area5To35").area(geoShape5To35).build();
private final GeoJsonPolygon geoShape15To25 = GeoJsonPolygon.of( //
new Point(15, 15), //
new Point(25, 15), //
new Point(25, 25), //
new Point(15, 25), //
new Point(15, 15));
private final Area area15To25 = Area.builder().id("area15To25").area(geoShape15To25).build();
private final GeoJsonPolygon geoShape30To40 = GeoJsonPolygon.of( //
new Point(30, 30), //
new Point(40, 30), //
new Point(40, 40), //
new Point(30, 40), //
new Point(30, 30));
private final Area area30To40 = Area.builder().id("area30To40").area(geoShape30To40).build();
private final GeoJsonPolygon geoShape32To37 = GeoJsonPolygon.of( //
new Point(32, 32), //
new Point(37, 32), //
new Point(37, 37), //
new Point(32, 37), //
new Point(32, 32));
private final Area area32To37 = Area.builder().id("area32To37").area(geoShape30To40).build();
// endregion
// region setup
@BeforeEach
void setUp() {
indexOps = operations.indexOps(GeoJsonEntity.class);
indexOps.delete();
indexOps.create();
indexOps.putMapping();
IndexOperations indexOpsArea = operations.indexOps(Area.class);
indexOpsArea.delete();
indexOpsArea.create();
indexOpsArea.putMapping();
operations.save(Arrays.asList(area10To20, area30To40));
indexOpsArea.refresh();
}
@AfterEach
void tearDown() {
indexOps.delete();
}
// endregion
// region tests
@Test // DATAES-930
@DisplayName("should write and read an entity with GeoJson properties")
void shouldWriteAndReadAnEntityWithGeoJsonProperties() {
@ -99,4 +162,60 @@ class GeoJsonIntegrationTest {
assertThat(result).isEqualTo(entity);
}
@Test // DATAES-931
@DisplayName("should find intersecting objects with Criteria query")
void shouldFindIntersectingObjectsWithCriteriaQuery() {
CriteriaQuery query = new CriteriaQuery(new Criteria("area").intersects(geoShape15To25));
SearchHits<Area> searchHits = operations.search(query, Area.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("area10To20");
}
@Test // DATAES-931
@DisplayName("should find disjoint objects with Criteria query")
void shouldFindDisjointObjectsWithCriteriaQuery() {
CriteriaQuery query = new CriteriaQuery(new Criteria("area").isDisjoint(geoShape15To25));
SearchHits<Area> searchHits = operations.search(query, Area.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("area30To40");
}
@Test // DATAES-931
@DisplayName("should find within objects with Criteria query")
void shouldFindWithinObjectsWithCriteriaQuery() {
CriteriaQuery query = new CriteriaQuery(new Criteria("area").within(geoShape5To35));
SearchHits<Area> searchHits = operations.search(query, Area.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("area10To20");
}
@Test // DATAES-931
@DisplayName("should find contains objects with Criteria query")
void shouldFindContainsObjectsWithCriteriaQuery() {
CriteriaQuery query = new CriteriaQuery(new Criteria("area").contains(geoShape32To37));
SearchHits<Area> searchHits = operations.search(query, Area.class);
assertThat(searchHits.getTotalHits()).isEqualTo(1L);
assertThat(searchHits.getSearchHit(0).getId()).isEqualTo("area30To40");
}
// endregion
// region test classes
@Data
@Builder
@Document(indexName = "areas")
static class Area {
@Id private String id;
@Field(name = "the_area") private GeoJsonPolygon area;
}
// endregion
}

View File

@ -57,8 +57,8 @@ import org.springframework.test.context.ContextConfiguration;
* @author James Bodkin
*/
@SpringIntegrationTest
@ContextConfiguration(classes = { CriteriaQueryTests.Config.class })
public class CriteriaQueryTests {
@ContextConfiguration(classes = { CriteriaQueryIntegrationTests.Config.class })
public class CriteriaQueryIntegrationTests {
@Configuration
@Import({ ElasticsearchRestTemplateConfiguration.class })

View File

@ -24,8 +24,8 @@ import org.springframework.test.context.ContextConfiguration;
/**
* @author Peter-Josef Meisch
*/
@ContextConfiguration(classes = { CriteriaQueryTransportTests.Config.class })
public class CriteriaQueryTransportTests extends CriteriaQueryTests {
@ContextConfiguration(classes = { CriteriaQueryTransportIntegrationTests.Config.class })
public class CriteriaQueryTransportIntegrationTests extends CriteriaQueryIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
@EnableElasticsearchRepositories(considerNestedRepositories = true)