Skip to content

(dsl): Support Geo bounding box query #651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* text=auto eol=lf
sbt linguist-vendored
65 changes: 65 additions & 0 deletions docs/overview/queries/elastic_query_geo_bounding_box.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
id: elastic_query_geo_bounding_box
title: "Geo-bounding-box Query"
---

The `GeoBoundingBox` query matches documents containing geo points that fall within a defined rectangular bounding box, specified by the `top-left` and `bottom-right` corners.

To use the `GeoBoundingBox` query, import the following:
```scala
import zio.elasticsearch.query.GeoBoundingBoxQuery
import zio.elasticsearch.ElasticQuery._
```

You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `GeoBoundingBoxQuery` query using typed document fields like this:
```scala
val query: GeoBoundingBoxQuery[Document] =
geoBoundingBoxQuery(
field = Document.location,
topLeft = GeoPoint(40.73, -74.1),
bottomRight = GeoPoint(40.01, -71.12)
)
```

You can create a `GeoBoundingBox` query using the `geoBoundingBoxQuery` method with GeoPoints for the `top-left` and `bottom-right` corners:
```scala
val query: GeoBoundingBoxQuery[Any] =
geoBoundingBoxQuery(
field = "location",
topLeft = GeoPoint(40.73, -74.1),
bottomRight = GeoPoint(40.01, -71.12)
)
```

If you want to `boost` the relevance score of the query, you can use the `boost` method:
```scala
val queryWithBoost =
geoBoundingBoxQuery("location", GeoPoint(40.73, -74.1), GeoPoint(40.01, -71.12))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named parameters.

.boost(1.5)
```

To ignore unmapped fields (fields that do not exist in the mapping), use the ignoreUnmapped method:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To ignore unmapped fields (fields that do not exist in the mapping), use the ignoreUnmapped method:
To ignore unmapped fields (fields that do not exist in the mapping), use the `ignoreUnmapped` method:

```scala

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit empty line.

val queryWithIgnoreUnmapped =
geoBoundingBoxQuery("location", GeoPoint(40.73, -74.1), GeoPoint(40.01, -71.12))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named parameters.

.ignoreUnmapped(true)

```
To give the query a name for identification in the response, use the name method:
```scala
val queryWithName =
geoBoundingBoxQuery("location", GeoPoint(40.73, -74.1), GeoPoint(40.01, -71.12))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named parameters.

.name("myGeoBoxQuery")
```

To specify how invalid geo coordinates are handled, use the validationMethod method:
```scala
import zio.elasticsearch.query.ValidationMethod

val queryWithValidationMethod =
geoBoundingBoxQuery("location", GeoPoint(40.73, -74.1), GeoPoint(40.01, -71.12))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use named parameters.

.validationMethod(ValidationMethod.IgnoreMalformed)
```

You can find more information about `Geo-bounding-box` query [here](https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-geo-bounding-box-query).**
Original file line number Diff line number Diff line change
Expand Up @@ -2696,6 +2696,46 @@ object HttpExecutorSpec extends IntegrationSpec {
}
}
),
suite("geo-bounding-box query")(
test("using geo-bounding-box query") {
checkOnce(genTestDocument) { document =>
val indexDefinition =
"""
|{
| "mappings": {
| "properties": {
| "geoPointField": {
| "type": "geo_point"
| }
| }
| }
|}
|""".stripMargin

for {
_ <- Executor.execute(ElasticRequest.createIndex(firstSearchIndex, indexDefinition))
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
_ <- Executor.execute(
ElasticRequest.create[TestDocument](firstSearchIndex, document).refreshTrue
)
result <- Executor
.execute(
ElasticRequest.search(
firstSearchIndex,
ElasticQuery.geoBoundingBoxQuery(
"geoPointField",
topLeft =
GeoPoint(document.geoPointField.lat + 0.1, document.geoPointField.lon - 0.1),
bottomRight =
GeoPoint(document.geoPointField.lat - 0.1, document.geoPointField.lon + 0.1)
)
Comment on lines +2725 to +2731
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please name first parameter too ("geoPointField").

)
)
.documentAs[TestDocument]
} yield assert(result)(equalTo(Chunk(document)))
}
} @@ after(Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie)
),
suite("geo-distance query")(
test("using geo-distance query") {
checkOnce(genTestDocument) { document =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,64 @@ object ElasticQuery {
final def fuzzy(field: String, value: String): FuzzyQuery[Any] =
Fuzzy(field = field, value = value, fuzziness = None, maxExpansions = None, prefixLength = None)

/**
* Constructs a type-safe instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] using the specified parameters.
*
* @param field
* the type-safe GeoPoint field for which the bounding box query is specified
* @param topLeft
* the geo-point representing the top-left corner of the bounding box
* @param bottomRight
* the geo-point representing the bottom-right corner of the bounding box
* @tparam S
* the type of document on which the query is defined
* @return
* an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] that represents the `geo_bounding_box` query to be
* performed.
*/
final def geoBoundingBoxQuery[S](
field: Field[S, GeoPoint],
topLeft: GeoPoint,
bottomRight: GeoPoint
): GeoBoundingBoxQuery[S] =
GeoBoundingBox(
field = field.toString,
topLeft = topLeft,
bottomRight = bottomRight,
boost = None,
ignoreUnmapped = None,
queryName = None,
validationMethod = None
)

/**
* Constructs an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] using the specified parameters.
*
* @param field
* the name of the GeoPoint field for which the bounding box query is specified
* @param topLeft
* the geo-point representing the top-left corner of the bounding box
* @param bottomRight
* the geo-point representing the bottom-right corner of the bounding box
* @return
* an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] that represents the `geo_bounding_box` query to be
* performed.
*/
final def geoBoundingBoxQuery(
field: String,
topLeft: GeoPoint,
bottomRight: GeoPoint
): GeoBoundingBoxQuery[Any] =
GeoBoundingBox(
field = field,
topLeft = topLeft,
bottomRight = bottomRight,
boost = None,
ignoreUnmapped = None,
queryName = None,
validationMethod = None
)

/**
* Constructs a type-safe instance of [[zio.elasticsearch.query.GeoDistanceQuery]] using the specified parameters.
*
Expand Down
111 changes: 111 additions & 0 deletions modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package zio.elasticsearch.query
import zio.Chunk
import zio.elasticsearch.ElasticPrimitive._
import zio.elasticsearch.Field
import zio.elasticsearch.data.GeoPoint
import zio.elasticsearch.query.options._
import zio.elasticsearch.query.sort.options.HasFormat
import zio.json.ast.Json
Expand Down Expand Up @@ -497,6 +498,116 @@ private[elasticsearch] final case class Fuzzy[S](
}
}

sealed trait GeoBoundingBoxQuery[S] extends ElasticQuery[S] {

/**
* Sets the `boost` parameter for the [[zio.elasticsearch.query.GeoBoundingBoxQuery]]. Boosts the relevance score of
* the query.
*
* @param value
* the boost factor as a [[Double]]. Values greater than 1.0 increase relevance, values between 0 and 1.0 decrease
* it.
* @return
* an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] enriched with the `boost` parameter.
*/
def boost(value: Double): GeoBoundingBoxQuery[S]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we extend HasBoost?


/**
* Sets the `ignoreUnmapped` parameter for the [[zio.elasticsearch.query.GeoBoundingBoxQuery]]. Determines how to
* handle unmapped fields.
*
* @param value
* - true: unmapped fields are ignored and the query returns no matches for them
* - false: query will throw an exception if the field is unmapped
* @return
* an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] enriched with the `ignoreUnmapped` parameter.
*/
def ignoreUnmapped(value: Boolean = false): GeoBoundingBoxQuery[S]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use ignoreUnmapped without params.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, why shouldn't we use already existing HasIgnoreUnmapped?


/**
* Sets the `queryName` parameter for the [[zio.elasticsearch.query.GeoBoundingBoxQuery]]. Represents the optional
* name used to identify the query.
*
* @param value
* the [[String]] name used to tag and identify this query in responses
* @return
* an instance of [[zio.elasticsearch.query.GeoBoundingBoxQuery]] enriched with the `queryName` parameter.
*/
def name(value: String): GeoBoundingBoxQuery[S]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have this in GeoDistanceQuery, you should extract this as HasQueryName and use this option in both queries.


/**
* Sets the `validationMethod` parameter for the [[zio.elasticsearch.query.GeoBoundingBoxQuery]]. Defines handling of
* incorrect coordinates.
*
* @param value
* defines how to handle invalid latitude and longitude:
* - [[zio.elasticsearch.query.ValidationMethod.Strict]]: Default method
* - [[zio.elasticsearch.query.ValidationMethod.IgnoreMalformed]]: Accepts geo points with invalid latitude or
* longitude
* - [[zio.elasticsearch.query.ValidationMethod.Coerce]]: Additionally try and infer correct coordinates
* @return
* an instance of [[zio.elasticsearch.query.GeoDistanceQuery]] enriched with the `validationMethod` parameter.
*/
def validationMethod(value: ValidationMethod): GeoBoundingBoxQuery[S]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this one.

}

private[elasticsearch] final case class GeoBoundingBox[S](
field: String,
topLeft: GeoPoint,
bottomRight: GeoPoint,
Comment on lines +556 to +557
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reorder these two.

boost: Option[Double] = None,
ignoreUnmapped: Option[Boolean] = None,
queryName: Option[String] = None,
validationMethod: Option[ValidationMethod] = None
) extends GeoBoundingBoxQuery[S] { self =>

def boost(value: Double): GeoBoundingBoxQuery[S] =
self.copy(boost = Some(value))

def ignoreUnmapped(value: Boolean = false): GeoBoundingBoxQuery[S] =
self.copy(ignoreUnmapped = Some(value))

def name(value: String): GeoBoundingBoxQuery[S] =
self.copy(queryName = Some(value))

def validationMethod(value: ValidationMethod): GeoBoundingBoxQuery[S] =
self.copy(validationMethod = Some(value))

private[elasticsearch] override def toJson(fieldPath: Option[String]): Json =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit override.

Obj(
"geo_bounding_box" -> Obj(
Chunk(
Some(
field -> Obj(
Chunk(
Some(
"top_left" -> Obj(
Chunk(
Some("lat" -> topLeft.lat.toJson),
Some("lon" -> topLeft.lon.toJson)
).flatten: _*
)
),
Some(
"bottom_right" -> Obj(
Chunk(
Some("lat" -> bottomRight.lat.toJson),
Some("lon" -> bottomRight.lon.toJson)
).flatten: _*
)
)
).flatten: _*
)
),
boost.map("boost" -> _.toJson),
ignoreUnmapped.map("ignore_unmapped" -> _.toJson),
queryName.map("_name" -> _.toJson),
validationMethod.map("validation_method" -> _.toString.toJson)
).flatten: _*
)
)
}

sealed trait GeoDistanceQuery[S] extends ElasticQuery[S] {

/**
Expand Down
Loading