Browse Source

move geospatial and full text to query ops

labs/antora
Christoph Strobl 2 years ago
parent
commit
e2326618a1
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 5
      src/main/antora/modules/ROOT/nav.adoc
  2. 268
      src/main/antora/modules/ROOT/pages/mongodb/mongo-query/geo-json.adoc
  3. 147
      src/main/antora/modules/ROOT/pages/mongodb/mongo-query/geospatial.adoc
  4. 39
      src/main/antora/modules/ROOT/pages/mongodb/mongo-query/template-distinct.adoc
  5. 12
      src/main/antora/modules/ROOT/pages/mongodb/mongo-query/template-querying.adoc
  6. 75
      src/main/antora/modules/ROOT/pages/mongodb/mongo-query/textsearch.adoc
  7. 497
      src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc

5
src/main/antora/modules/ROOT/nav.adoc

@ -11,12 +11,7 @@ @@ -11,12 +11,7 @@
*** xref:mongodb/template-config.adoc[]
*** xref:mongodb/template-crud-operations.adoc[]
*** xref:mongodb/template-query-operations.adoc[]
**** xref:mongodb/mongo-query/template.adoc[]
**** xref:mongodb/mongo-query/template-querying.adoc[]
**** xref:mongodb/mongo-query/template-distinct.adoc[]
**** xref:mongodb/mongo-query/geospatial.adoc[]
**** xref:mongodb/mongo-query/geo-json.adoc[]
**** xref:mongodb/mongo-query/textsearch.adoc[]
**** xref:mongodb/mongo-query/collation.adoc[]
**** xref:mongodb/mongo-query/kotlin-support.adoc[]
**** xref:mongodb/mongo-query/additional-options.adoc[]

268
src/main/antora/modules/ROOT/pages/mongodb/mongo-query/geo-json.adoc

@ -1,270 +1,4 @@ @@ -1,270 +1,4 @@
[[mongo.geo-json]]
= GeoJSON Support
MongoDB supports https://geojson.org/[GeoJSON] and simple (legacy) coordinate pairs for geospatial data. Those formats can both be used for storing as well as querying data. See the https://docs.mongodb.org/manual/core/2dsphere/#geospatial-indexes-store-geojson/[MongoDB manual on GeoJSON support] to learn about requirements and restrictions.
[[mongo.geo-json.domain.classes]]
== GeoJSON Types in Domain Classes
Usage of https://geojson.org/[GeoJSON] types in domain classes is straightforward. The `org.springframework.data.mongodb.core.geo` package contains types such as `GeoJsonPoint`, `GeoJsonPolygon`, and others. These types are extend the existing `org.springframework.data.geo` types. The following example uses a `GeoJsonPoint`:
====
[source,java]
----
public class Store {
String id;
/**
* location is stored in GeoJSON format.
* {
* "type" : "Point",
* "coordinates" : [ x, y ]
* }
*/
GeoJsonPoint location;
}
----
====
[TIP]
====
If the `coordinates` of a GeoJSON object represent _latitude_ and _longitude_ pairs, the _longitude_ goes first followed by _latitude_. +
`GeoJsonPoint` therefore treats `getX()` as _longitude_ and `getY()` as _latitude_.
====
[[mongo.geo-json.query-methods]]
== GeoJSON Types in Repository Query Methods
Using GeoJSON types as repository query parameters forces usage of the `$geometry` operator when creating the query, as the following example shows:
====
[source,java]
----
public interface StoreRepository extends CrudRepository<Store, String> {
List<Store> findByLocationWithin(Polygon polygon); <1>
}
/*
* {
* "location": {
* "$geoWithin": {
* "$geometry": {
* "type": "Polygon",
* "coordinates": [
* [
* [-73.992514,40.758934],
* [-73.961138,40.760348],
* [-73.991658,40.730006],
* [-73.992514,40.758934]
* ]
* ]
* }
* }
* }
* }
*/
repo.findByLocationWithin( <2>
new GeoJsonPolygon(
new Point(-73.992514, 40.758934),
new Point(-73.961138, 40.760348),
new Point(-73.991658, 40.730006),
new Point(-73.992514, 40.758934))); <3>
/*
* {
* "location" : {
* "$geoWithin" : {
* "$polygon" : [ [-73.992514,40.758934] , [-73.961138,40.760348] , [-73.991658,40.730006] ]
* }
* }
* }
*/
repo.findByLocationWithin( <4>
new Polygon(
new Point(-73.992514, 40.758934),
new Point(-73.961138, 40.760348),
new Point(-73.991658, 40.730006)));
----
<1> Repository method definition using the commons type allows calling it with both the GeoJSON and the legacy format.
<2> Use GeoJSON type to make use of `$geometry` operator.
<3> Note that GeoJSON polygons need to define a closed ring.
<4> Use the legacy format `$polygon` operator.
====
[[mongo.geo-json.metrics]]
== Metrics and Distance calculation
Then MongoDB `$geoNear` operator allows usage of a GeoJSON Point or legacy coordinate pairs.
====
[source,java]
----
NearQuery.near(new Point(-73.99171, 40.738868))
----
[source,json]
----
{
"$geoNear": {
//...
"near": [-73.99171, 40.738868]
}
}
----
====
====
[source,java]
----
NearQuery.near(new GeoJsonPoint(-73.99171, 40.738868))
----
[source,json]
----
{
"$geoNear": {
//...
"near": { "type": "Point", "coordinates": [-73.99171, 40.738868] }
}
}
----
====
Though syntactically different the server is fine accepting both no matter what format the target Document within the collection
is using.
WARNING: There is a huge difference in the distance calculation. Using the legacy format operates
upon _Radians_ on an Earth like sphere, whereas the GeoJSON format uses _Meters_.
To avoid a serious headache make sure to set the `Metric` to the desired unit of measure which ensures the
distance to be calculated correctly.
In other words:
====
Assume you've got 5 Documents like the ones below:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a5"),
"name" : "Penn Station",
"location" : { "type" : "Point", "coordinates" : [ -73.99408, 40.75057 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796ab"),
"name" : "Momofuku Milk Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.985839, 40.731698 ] }
}
----
====
Fetching all Documents within a 400 Meter radius from `[-73.99171, 40.738868]` would look like this using
GeoJSON:
.GeoNear with GeoJSON
====
[source,json]
----
{
"$geoNear": {
"maxDistance": 400, <1>
"num": 10,
"near": { type: "Point", coordinates: [-73.99171, 40.738868] },
"spherical":true, <2>
"key": "location",
"distanceField": "distance"
}
}
----
Returning the following 3 Documents:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
"distance" : 0.0 <3>
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 69.3582262492474 <3>
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 69.3582262492474 <3>
}
----
<1> Maximum distance from center point in _Meters_.
<2> GeoJSON always operates upon a sphere.
<3> Distance from center point in _Meters_.
====
Now, when using legacy coordinate pairs one operates upon _Radians_ as discussed before. So we use `Metrics#KILOMETERS
when constructing the `$geoNear` command. The `Metric` makes sure the distance multiplier is set correctly.
.GeoNear with Legacy Coordinate Pairs
====
[source,json]
----
{
"$geoNear": {
"maxDistance": 0.0000627142377, <1>
"distanceMultiplier": 6378.137, <2>
"num": 10,
"near": [-73.99171, 40.738868],
"spherical":true, <3>
"key": "location",
"distanceField": "distance"
}
}
----
Returning the 3 Documents just like the GeoJSON variant:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
"distance" : 0.0 <4>
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 0.0693586286032982 <4>
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 0.0693586286032982 <4>
}
----
<1> Maximum distance from center point in _Radians_.
<2> The distance multiplier so we get _Kilometers_ as resulting distance.
<3> Make sure we operate on a 2d_sphere index.
<4> Distance from center point in _Kilometers_ - take it times 1000 to match _Meters_ of the GeoJSON variant.
====
TODO: add the following section somewhere
[[mongo.geo-json.jackson-modules]]
== GeoJSON Jackson Modules

147
src/main/antora/modules/ROOT/pages/mongodb/mongo-query/geospatial.adoc

@ -1,147 +0,0 @@ @@ -1,147 +0,0 @@
[[mongo.geospatial]]
= GeoSpatial Queries
MongoDB supports GeoSpatial queries through the use of operators such as `$near`, `$within`, `geoWithin`, and `$nearSphere`. Methods specific to geospatial queries are available on the `Criteria` class. There are also a few shape classes (`Box`, `Circle`, and `Point`) that are used in conjunction with geospatial related `Criteria` methods.
NOTE: Using GeoSpatial queries requires attention when used within MongoDB transactions, see xref:reference/client-session-transactions.adoc#mongo.transactions.behavior[Special behavior inside transactions].
To understand how to perform GeoSpatial queries, consider the following `Venue` class (taken from the integration tests and relying on the rich `MappingMongoConverter`):
[source,java]
----
@Document(collection="newyork")
public class Venue {
@Id
private String id;
private String name;
private double[] location;
@PersistenceConstructor
Venue(String name, double[] location) {
super();
this.name = name;
this.location = location;
}
public Venue(String name, double x, double y) {
super();
this.name = name;
this.location = new double[] { x, y };
}
public String getName() {
return name;
}
public double[] getLocation() {
return location;
}
@Override
public String toString() {
return "Venue [id=" + id + ", name=" + name + ", location="
+ Arrays.toString(location) + "]";
}
}
----
To find locations within a `Circle`, you can use the following query:
[source,java]
----
Circle circle = new Circle(-73.99171, 40.738868, 0.01);
List<Venue> venues =
template.find(new Query(Criteria.where("location").within(circle)), Venue.class);
----
To find venues within a `Circle` using spherical coordinates, you can use the following query:
[source,java]
----
Circle circle = new Circle(-73.99171, 40.738868, 0.003712240453784);
List<Venue> venues =
template.find(new Query(Criteria.where("location").withinSphere(circle)), Venue.class);
----
To find venues within a `Box`, you can use the following query:
[source,java]
----
//lower-left then upper-right
Box box = new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404));
List<Venue> venues =
template.find(new Query(Criteria.where("location").within(box)), Venue.class);
----
To find venues near a `Point`, you can use the following queries:
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(Criteria.where("location").near(point).maxDistance(0.01)), Venue.class);
----
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(Criteria.where("location").near(point).minDistance(0.01).maxDistance(100)), Venue.class);
----
To find venues near a `Point` using spherical coordinates, you can use the following query:
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(
Criteria.where("location").nearSphere(point).maxDistance(0.003712240453784)),
Venue.class);
----
[[mongo.geo-near]]
== Geo-near Queries
[WARNING]
====
*Changed in 2.2!* +
https://docs.mongodb.com/master/release-notes/4.2-compatibility/[MongoDB 4.2] removed support for the
`geoNear` command which had been previously used to run the `NearQuery`.
Spring Data MongoDB 2.2 `MongoOperations#geoNear` uses the `$geoNear` https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/[aggregation]
instead of the `geoNear` command to run a `NearQuery`.
The calculated distance (the `dis` when using a geoNear command) previously returned within a wrapper type now is embedded
into the resulting document.
If the given domain type already contains a property with that name, the calculated distance
is named `calculated-distance` with a potentially random postfix.
Target types may contain a property named after the returned distance to (additionally) read it back directly into the domain type as shown below.
[source,java]
----
GeoResults<VenueWithDisField> = template.query(Venue.class) <1>
.as(VenueWithDisField.class) <2>
.near(NearQuery.near(new GeoJsonPoint(-73.99, 40.73), KILOMETERS))
.all();
----
<1> Domain type used to identify the target collection and potential query mapping.
<2> Target type containing a `dis` field of type `Number`.
====
MongoDB supports querying the database for geo locations and calculating the distance from a given origin at the same time. With geo-near queries, you can express queries such as "find all restaurants in the surrounding 10 miles". To let you do so, `MongoOperations` provides `geoNear(…)` methods that take a `NearQuery` as an argument (as well as the already familiar entity type and collection), as shown in the following example:
[source,java]
----
Point location = new Point(-73.99171, 40.738868);
NearQuery query = NearQuery.near(location).maxDistance(new Distance(10, Metrics.MILES));
GeoResults<Restaurant> = operations.geoNear(query, Restaurant.class);
----
We use the `NearQuery` builder API to set up a query to return all `Restaurant` instances surrounding the given `Point` out to 10 miles. The `Metrics` enum used here actually implements an interface so that other metrics could be plugged into a distance as well. A `Metric` is backed by a multiplier to transform the distance value of the given metric into native distances. The sample shown here would consider the 10 to be miles. Using one of the built-in metrics (miles and kilometers) automatically triggers the spherical flag to be set on the query. If you want to avoid that, pass plain `double` values into `maxDistance(…)`. For more information, see the https://docs.spring.io/spring-data/mongodb/docs/{version}/api/index.html[JavaDoc] of `NearQuery` and `Distance`.
The geo-near operations return a `GeoResults` wrapper object that encapsulates `GeoResult` instances. Wrapping `GeoResults` allows accessing the average distance of all results. A single `GeoResult` object carries the entity found plus its distance from the origin.

39
src/main/antora/modules/ROOT/pages/mongodb/mongo-query/template-distinct.adoc

@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
[[mongo-template.query.distinct]]
= Query Distinct Values
MongoDB provides an operation to obtain distinct values for a single field by using a query from the resulting documents.
Resulting values are not required to have the same data type, nor is the feature limited to simple types.
For retrieval, the actual result type does matter for the sake of conversion and typing. The following example shows how to query for distinct values:
.Retrieving distinct values
====
[source,java]
----
template.query(Person.class) <1>
.distinct("lastname") <2>
.all(); <3>
----
<1> Query the `Person` collection.
<2> Select distinct values of the `lastname` field. The field name is mapped according to the domain types property declaration, taking potential `@Field` annotations into account.
<3> Retrieve all distinct values as a `List` of `Object` (due to no explicit result type being specified).
====
Retrieving distinct values into a `Collection` of `Object` is the most flexible way, as it tries to determine the property value of the domain type and convert results to the desired type or mapping `Document` structures.
Sometimes, when all values of the desired field are fixed to a certain type, it is more convenient to directly obtain a correctly typed `Collection`, as shown in the following example:
.Retrieving strongly typed distinct values
====
[source,java]
----
template.query(Person.class) <1>
.distinct("lastname") <2>
.as(String.class) <3>
.all(); <4>
----
<1> Query the collection of `Person`.
<2> Select distinct values of the `lastname` field. The fieldname is mapped according to the domain types property declaration, taking potential `@Field` annotations into account.
<3> Retrieved values are converted into the desired target type -- in this case, `String`. It is also possible to map the values to a more complex type if the stored field contains a document.
<4> Retrieve all distinct values as a `List` of `String`. If the type cannot be converted into the desired target type, this method throws a `DataAccessException`.
====

12
src/main/antora/modules/ROOT/pages/mongodb/mongo-query/template-querying.adoc

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
[[mongo-template.querying]]
= Methods for Querying for Documents
:page-section-summary-toc: 1
The query methods need to specify the target type `T` that is returned, and they are overloaded with an explicit collection name for queries that should operate on a collection other than the one indicated by the return type. The following query methods let you find one or more documents:
* *findAll*: Query for a list of objects of type `T` from the collection.
* *findOne*: Map the results of an ad-hoc query on the collection to a single instance of an object of the specified type.
* *findById*: Return an object of the given ID and target class.
* *find*: Map the results of an ad-hoc query on the collection to a `List` of the specified type.
* *findAndRemove*: Map the results of an ad-hoc query on the collection to a single instance of an object of the specified type. The first document that matches the query is returned and removed from the collection in the database.

75
src/main/antora/modules/ROOT/pages/mongodb/mongo-query/textsearch.adoc

@ -1,75 +0,0 @@ @@ -1,75 +0,0 @@
[[mongo.textsearch]]
= Full-text Queries
Since version 2.6 of MongoDB, you can run full-text queries by using the `$text` operator. Methods and operations specific to full-text queries are available in `TextQuery` and `TextCriteria`. When doing full text search, see the https://docs.mongodb.org/manual/reference/operator/query/text/#behavior[MongoDB reference] for its behavior and limitations.
[[full-text-search]]
== Full-text Search
Before you can actually use full-text search, you must set up the search index correctly. See xref:reference/mapping.adoc#mapping-usage-indexes.text-index[Text Index] for more detail on how to create index structures. The following example shows how to set up a full-text search:
[source,javascript]
----
db.foo.createIndex(
{
title : "text",
content : "text"
},
{
weights : {
title : 3
}
}
)
----
A query searching for `coffee cake` can be defined and run as follows:
.Full Text Query
====
[source,java]
----
Query query = TextQuery
.queryText(new TextCriteria().matchingAny("coffee", "cake"));
List<Document> page = template.find(query, Document.class);
----
====
To sort results by relevance according to the `weights` use `TextQuery.sortByScore`.
.Full Text Query - Sort by Score
====
[source,java]
----
Query query = TextQuery
.queryText(new TextCriteria().matchingAny("coffee", "cake"))
.sortByScore() <1>
.includeScore(); <2>
List<Document> page = template.find(query, Document.class);
----
<1> Use the score property for sorting results by relevance which triggers `.sort({'score': {'$meta': 'textScore'}})`.
<2> Use `TextQuery.includeScore()` to include the calculated relevance in the resulting `Document`.
====
You can exclude search terms by prefixing the term with `-` or by using `notMatching`, as shown in the following example (note that the two lines have the same effect and are thus redundant):
[source,java]
----
// search for 'coffee' and not 'cake'
TextQuery.queryText(new TextCriteria().matching("coffee").matching("-cake"));
TextQuery.queryText(new TextCriteria().matching("coffee").notMatching("cake"));
----
`TextCriteria.matching` takes the provided term as is. Therefore, you can define phrases by putting them between double quotation marks (for example, `\"coffee cake\")` or using by `TextCriteria.phrase.` The following example shows both ways of defining a phrase:
[source,java]
----
// search for phrase 'coffee cake'
TextQuery.queryText(new TextCriteria().matching("\"coffee cake\""));
TextQuery.queryText(new TextCriteria().phrase("coffee cake"));
----
You can set flags for `$caseSensitive` and `$diacriticSensitive` by using the corresponding methods on `TextCriteria`. Note that these two optional flags have been introduced in MongoDB 3.2 and are not included in the query unless explicitly set.

497
src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc

@ -225,4 +225,501 @@ template.query(Person.class) <1> @@ -225,4 +225,501 @@ template.query(Person.class) <1>
<4> Retrieve all distinct values as a `List` of `String`. If the type cannot be converted into the desired target type, this method throws a `DataAccessException`.
====
[[mongo.geospatial]]
= GeoSpatial Queries
MongoDB supports GeoSpatial queries through the use of operators such as `$near`, `$within`, `geoWithin`, and `$nearSphere`. Methods specific to geospatial queries are available on the `Criteria` class. There are also a few shape classes (`Box`, `Circle`, and `Point`) that are used in conjunction with geospatial related `Criteria` methods.
NOTE: Using GeoSpatial queries requires attention when used within MongoDB transactions, see xref:reference/client-session-transactions.adoc#mongo.transactions.behavior[Special behavior inside transactions].
To understand how to perform GeoSpatial queries, consider the following `Venue` class (taken from the integration tests and relying on the rich `MappingMongoConverter`):
[source,java]
----
@Document(collection="newyork")
public class Venue {
@Id
private String id;
private String name;
private double[] location;
@PersistenceConstructor
Venue(String name, double[] location) {
super();
this.name = name;
this.location = location;
}
public Venue(String name, double x, double y) {
super();
this.name = name;
this.location = new double[] { x, y };
}
public String getName() {
return name;
}
public double[] getLocation() {
return location;
}
@Override
public String toString() {
return "Venue [id=" + id + ", name=" + name + ", location="
+ Arrays.toString(location) + "]";
}
}
----
To find locations within a `Circle`, you can use the following query:
[source,java]
----
Circle circle = new Circle(-73.99171, 40.738868, 0.01);
List<Venue> venues =
template.find(new Query(Criteria.where("location").within(circle)), Venue.class);
----
To find venues within a `Circle` using spherical coordinates, you can use the following query:
[source,java]
----
Circle circle = new Circle(-73.99171, 40.738868, 0.003712240453784);
List<Venue> venues =
template.find(new Query(Criteria.where("location").withinSphere(circle)), Venue.class);
----
To find venues within a `Box`, you can use the following query:
[source,java]
----
//lower-left then upper-right
Box box = new Box(new Point(-73.99756, 40.73083), new Point(-73.988135, 40.741404));
List<Venue> venues =
template.find(new Query(Criteria.where("location").within(box)), Venue.class);
----
To find venues near a `Point`, you can use the following queries:
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(Criteria.where("location").near(point).maxDistance(0.01)), Venue.class);
----
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(Criteria.where("location").near(point).minDistance(0.01).maxDistance(100)), Venue.class);
----
To find venues near a `Point` using spherical coordinates, you can use the following query:
[source,java]
----
Point point = new Point(-73.99171, 40.738868);
List<Venue> venues =
template.find(new Query(
Criteria.where("location").nearSphere(point).maxDistance(0.003712240453784)),
Venue.class);
----
[[mongo.geo-near]]
== Geo-near Queries
[WARNING]
====
*Changed in 2.2!* +
https://docs.mongodb.com/master/release-notes/4.2-compatibility/[MongoDB 4.2] removed support for the
`geoNear` command which had been previously used to run the `NearQuery`.
Spring Data MongoDB 2.2 `MongoOperations#geoNear` uses the `$geoNear` https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/[aggregation]
instead of the `geoNear` command to run a `NearQuery`.
The calculated distance (the `dis` when using a geoNear command) previously returned within a wrapper type now is embedded
into the resulting document.
If the given domain type already contains a property with that name, the calculated distance
is named `calculated-distance` with a potentially random postfix.
Target types may contain a property named after the returned distance to (additionally) read it back directly into the domain type as shown below.
[source,java]
----
GeoResults<VenueWithDistanceField> = template.query(Venue.class) <1>
.as(VenueWithDistanceField.class) <2>
.near(NearQuery.near(new GeoJsonPoint(-73.99, 40.73), KILOMETERS))
.all();
----
<1> Domain type used to identify the target collection and potential query mapping.
<2> Target type containing a `dis` field of type `Number`.
====
MongoDB supports querying the database for geo locations and calculating the distance from a given origin at the same time. With geo-near queries, you can express queries such as "find all restaurants in the surrounding 10 miles". To let you do so, `MongoOperations` provides `geoNear(…)` methods that take a `NearQuery` as an argument (as well as the already familiar entity type and collection), as shown in the following example:
[source,java]
----
Point location = new Point(-73.99171, 40.738868);
NearQuery query = NearQuery.near(location).maxDistance(new Distance(10, Metrics.MILES));
GeoResults<Restaurant> = operations.geoNear(query, Restaurant.class);
----
We use the `NearQuery` builder API to set up a query to return all `Restaurant` instances surrounding the given `Point` out to 10 miles.
The `Metrics` enum used here actually implements an interface so that other metrics could be plugged into a distance as well.
A `Metric` is backed by a multiplier to transform the distance value of the given metric into native distances.
The sample shown here would consider the 10 to be miles. Using one of the built-in metrics (miles and kilometers) automatically triggers the spherical flag to be set on the query.
If you want to avoid that, pass plain `double` values into `maxDistance(…)`.
For more information, see the https://docs.spring.io/spring-data/mongodb/docs/{version}/api/index.html[JavaDoc] of `NearQuery` and `Distance`.
The geo-near operations return a `GeoResults` wrapper object that encapsulates `GeoResult` instances.
Wrapping `GeoResults` allows accessing the average distance of all results.
A single `GeoResult` object carries the entity found plus its distance from the origin.
[[mongo.geo-json]]
== GeoJSON Support
MongoDB supports https://geojson.org/[GeoJSON] and simple (legacy) coordinate pairs for geospatial data. Those formats can both be used for storing as well as querying data. See the https://docs.mongodb.org/manual/core/2dsphere/#geospatial-indexes-store-geojson/[MongoDB manual on GeoJSON support] to learn about requirements and restrictions.
[[mongo.geo-json.domain.classes]]
== GeoJSON Types in Domain Classes
Usage of https://geojson.org/[GeoJSON] types in domain classes is straightforward. The `org.springframework.data.mongodb.core.geo` package contains types such as `GeoJsonPoint`, `GeoJsonPolygon`, and others. These types are extend the existing `org.springframework.data.geo` types. The following example uses a `GeoJsonPoint`:
====
[source,java]
----
public class Store {
String id;
/**
* location is stored in GeoJSON format.
* {
* "type" : "Point",
* "coordinates" : [ x, y ]
* }
*/
GeoJsonPoint location;
}
----
====
[TIP]
====
If the `coordinates` of a GeoJSON object represent _latitude_ and _longitude_ pairs, the _longitude_ goes first followed by _latitude_. +
`GeoJsonPoint` therefore treats `getX()` as _longitude_ and `getY()` as _latitude_.
====
[[mongo.geo-json.query-methods]]
== GeoJSON Types in Repository Query Methods
Using GeoJSON types as repository query parameters forces usage of the `$geometry` operator when creating the query, as the following example shows:
====
[source,java]
----
public interface StoreRepository extends CrudRepository<Store, String> {
List<Store> findByLocationWithin(Polygon polygon); <1>
}
/*
* {
* "location": {
* "$geoWithin": {
* "$geometry": {
* "type": "Polygon",
* "coordinates": [
* [
* [-73.992514,40.758934],
* [-73.961138,40.760348],
* [-73.991658,40.730006],
* [-73.992514,40.758934]
* ]
* ]
* }
* }
* }
* }
*/
repo.findByLocationWithin( <2>
new GeoJsonPolygon(
new Point(-73.992514, 40.758934),
new Point(-73.961138, 40.760348),
new Point(-73.991658, 40.730006),
new Point(-73.992514, 40.758934))); <3>
/*
* {
* "location" : {
* "$geoWithin" : {
* "$polygon" : [ [-73.992514,40.758934] , [-73.961138,40.760348] , [-73.991658,40.730006] ]
* }
* }
* }
*/
repo.findByLocationWithin( <4>
new Polygon(
new Point(-73.992514, 40.758934),
new Point(-73.961138, 40.760348),
new Point(-73.991658, 40.730006)));
----
<1> Repository method definition using the commons type allows calling it with both the GeoJSON and the legacy format.
<2> Use GeoJSON type to make use of `$geometry` operator.
<3> Note that GeoJSON polygons need to define a closed ring.
<4> Use the legacy format `$polygon` operator.
====
[[mongo.geo-json.metrics]]
== Metrics and Distance calculation
Then MongoDB `$geoNear` operator allows usage of a GeoJSON Point or legacy coordinate pairs.
====
[source,java]
----
NearQuery.near(new Point(-73.99171, 40.738868))
----
[source,json]
----
{
"$geoNear": {
//...
"near": [-73.99171, 40.738868]
}
}
----
====
====
[source,java]
----
NearQuery.near(new GeoJsonPoint(-73.99171, 40.738868))
----
[source,json]
----
{
"$geoNear": {
//...
"near": { "type": "Point", "coordinates": [-73.99171, 40.738868] }
}
}
----
====
Though syntactically different the server is fine accepting both no matter what format the target Document within the collection
is using.
WARNING: There is a huge difference in the distance calculation. Using the legacy format operates
upon _Radians_ on an Earth like sphere, whereas the GeoJSON format uses _Meters_.
To avoid a serious headache make sure to set the `Metric` to the desired unit of measure which ensures the
distance to be calculated correctly.
In other words:
====
Assume you've got 5 Documents like the ones below:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a5"),
"name" : "Penn Station",
"location" : { "type" : "Point", "coordinates" : [ -73.99408, 40.75057 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
}
{
"_id" : ObjectId("5c10f3735d38908db52796ab"),
"name" : "Momofuku Milk Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.985839, 40.731698 ] }
}
----
====
Fetching all Documents within a 400 Meter radius from `[-73.99171, 40.738868]` would look like this using
GeoJSON:
.GeoNear with GeoJSON
====
[source,json]
----
{
"$geoNear": {
"maxDistance": 400, <1>
"num": 10,
"near": { type: "Point", coordinates: [-73.99171, 40.738868] },
"spherical":true, <2>
"key": "location",
"distanceField": "distance"
}
}
----
Returning the following 3 Documents:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
"distance" : 0.0 <3>
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 69.3582262492474 <3>
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 69.3582262492474 <3>
}
----
<1> Maximum distance from center point in _Meters_.
<2> GeoJSON always operates upon a sphere.
<3> Distance from center point in _Meters_.
====
Now, when using legacy coordinate pairs one operates upon _Radians_ as discussed before. So we use `Metrics#KILOMETERS
when constructing the `$geoNear` command. The `Metric` makes sure the distance multiplier is set correctly.
.GeoNear with Legacy Coordinate Pairs
====
[source,json]
----
{
"$geoNear": {
"maxDistance": 0.0000627142377, <1>
"distanceMultiplier": 6378.137, <2>
"num": 10,
"near": [-73.99171, 40.738868],
"spherical":true, <3>
"key": "location",
"distanceField": "distance"
}
}
----
Returning the 3 Documents just like the GeoJSON variant:
[source,json]
----
{
"_id" : ObjectId("5c10f3735d38908db52796a6"),
"name" : "10gen Office",
"location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] }
"distance" : 0.0 <4>
}
{
"_id" : ObjectId("5c10f3735d38908db52796a9"),
"name" : "City Bakery ",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 0.0693586286032982 <4>
}
{
"_id" : ObjectId("5c10f3735d38908db52796aa"),
"name" : "Splash Bar",
"location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] }
"distance" : 0.0693586286032982 <4>
}
----
<1> Maximum distance from center point in _Radians_.
<2> The distance multiplier so we get _Kilometers_ as resulting distance.
<3> Make sure we operate on a 2d_sphere index.
<4> Distance from center point in _Kilometers_ - take it times 1000 to match _Meters_ of the GeoJSON variant.
====
[[mongo.textsearch]]
== Full-text Search
Since version 2.6 of MongoDB, you can run full-text queries by using the `$text` operator. Methods and operations specific to full-text queries are available in `TextQuery` and `TextCriteria`. When doing full text search, see the https://docs.mongodb.org/manual/reference/operator/query/text/#behavior[MongoDB reference] for its behavior and limitations.
Before you can actually use full-text search, you must set up the search index correctly.
See xref:mongodb/mapping.adoc#mapping-usage-indexes.text-index[Text Index] for more detail on how to create index structures.
The following example shows how to set up a full-text search:
[source,javascript]
----
db.foo.createIndex(
{
title : "text",
content : "text"
},
{
weights : {
title : 3
}
}
)
----
A query searching for `coffee cake` can be defined and run as follows:
.Full Text Query
====
[source,java]
----
Query query = TextQuery
.queryText(new TextCriteria().matchingAny("coffee", "cake"));
List<Document> page = template.find(query, Document.class);
----
====
To sort results by relevance according to the `weights` use `TextQuery.sortByScore`.
.Full Text Query - Sort by Score
====
[source,java]
----
Query query = TextQuery
.queryText(new TextCriteria().matchingAny("coffee", "cake"))
.sortByScore() <1>
.includeScore(); <2>
List<Document> page = template.find(query, Document.class);
----
<1> Use the score property for sorting results by relevance which triggers `.sort({'score': {'$meta': 'textScore'}})`.
<2> Use `TextQuery.includeScore()` to include the calculated relevance in the resulting `Document`.
====
You can exclude search terms by prefixing the term with `-` or by using `notMatching`, as shown in the following example (note that the two lines have the same effect and are thus redundant):
[source,java]
----
// search for 'coffee' and not 'cake'
TextQuery.queryText(new TextCriteria().matching("coffee").matching("-cake"));
TextQuery.queryText(new TextCriteria().matching("coffee").notMatching("cake"));
----
`TextCriteria.matching` takes the provided term as is.
Therefore, you can define phrases by putting them between double quotation marks (for example, `\"coffee cake\")` or using by `TextCriteria.phrase.`
The following example shows both ways of defining a phrase:
[source,java]
----
// search for phrase 'coffee cake'
TextQuery.queryText(new TextCriteria().matching("\"coffee cake\""));
TextQuery.queryText(new TextCriteria().phrase("coffee cake"));
----
You can set flags for `$caseSensitive` and `$diacriticSensitive` by using the corresponding methods on `TextCriteria`.
Note that these two optional flags have been introduced in MongoDB 3.2 and are not included in the query unless explicitly set.

Loading…
Cancel
Save