Skip to content

Commit

Permalink
Add Geo hash and WKT to GeoBoundingBoxQuery
Browse files Browse the repository at this point in the history
The `GeoBoundingBoxQuery` now allows to pass not only lat/lon and
`GeoPoints`, but also Geo hashes and WKT values. Furthermore, we added
the missing fields for validation method and the flag to indicate
whether to ignore unmapped fields.

See https://www.elastic.co/guide/en/elasticsearch/reference/7.14/query-dsl-geo-bounding-box-query.html
for details.

Close #1530
  • Loading branch information
olivere committed Sep 16, 2021
1 parent e7c6a74 commit ef30a99
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 46 deletions.
123 changes: 91 additions & 32 deletions search_queries_geo_bounding_box.go
Expand Up @@ -4,21 +4,22 @@

package elastic

import "errors"

// GeoBoundingBoxQuery allows to filter hits based on a point location using
// a bounding box.
//
// For more details, see:
// https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-geo-bounding-box-query.html
type GeoBoundingBoxQuery struct {
name string
top *float64
left *float64
bottom *float64
right *float64
typ string
queryName string
name string
topLeft interface{} // can be a GeoPoint, a GeoHash (string), or a lat/lon pair as float64
topRight interface{}
bottomRight interface{} // can be a GeoPoint, a GeoHash (string), or a lat/lon pair as float64
bottomLeft interface{}
wkt interface{}
typ string
validationMethod string
ignoreUnmapped *bool
queryName string
}

// NewGeoBoundingBoxQuery creates and initializes a new GeoBoundingBoxQuery.
Expand All @@ -28,53 +29,104 @@ func NewGeoBoundingBoxQuery(name string) *GeoBoundingBoxQuery {
}
}

// TopLeft position from longitude (left) and latitude (top).
func (q *GeoBoundingBoxQuery) TopLeft(top, left float64) *GeoBoundingBoxQuery {
q.top = &top
q.left = &left
q.topLeft = []float64{left, top}
return q
}

// TopLeftFromGeoPoint from a GeoPoint.
func (q *GeoBoundingBoxQuery) TopLeftFromGeoPoint(point *GeoPoint) *GeoBoundingBoxQuery {
return q.TopLeft(point.Lat, point.Lon)
}

// TopLeftFromGeoHash from a Geo hash.
func (q *GeoBoundingBoxQuery) TopLeftFromGeoHash(topLeft string) *GeoBoundingBoxQuery {
q.topLeft = topLeft
return q
}

// BottomRight position from longitude (right) and latitude (bottom).
func (q *GeoBoundingBoxQuery) BottomRight(bottom, right float64) *GeoBoundingBoxQuery {
q.bottom = &bottom
q.right = &right
q.bottomRight = []float64{right, bottom}
return q
}

// BottomRightFromGeoPoint from a GeoPoint.
func (q *GeoBoundingBoxQuery) BottomRightFromGeoPoint(point *GeoPoint) *GeoBoundingBoxQuery {
return q.BottomRight(point.Lat, point.Lon)
}

// BottomRightFromGeoHash from a Geo hash.
func (q *GeoBoundingBoxQuery) BottomRightFromGeoHash(bottomRight string) *GeoBoundingBoxQuery {
q.bottomRight = bottomRight
return q
}

// BottomLeft position from longitude (left) and latitude (bottom).
func (q *GeoBoundingBoxQuery) BottomLeft(bottom, left float64) *GeoBoundingBoxQuery {
q.bottom = &bottom
q.left = &left
q.bottomLeft = []float64{bottom, left}
return q
}

// BottomLeftFromGeoPoint from a GeoPoint.
func (q *GeoBoundingBoxQuery) BottomLeftFromGeoPoint(point *GeoPoint) *GeoBoundingBoxQuery {
return q.BottomLeft(point.Lat, point.Lon)
}

// BottomLeftFromGeoHash from a Geo hash.
func (q *GeoBoundingBoxQuery) BottomLeftFromGeoHash(bottomLeft string) *GeoBoundingBoxQuery {
q.bottomLeft = bottomLeft
return q
}

// TopRight position from longitude (right) and latitude (top).
func (q *GeoBoundingBoxQuery) TopRight(top, right float64) *GeoBoundingBoxQuery {
q.top = &top
q.right = &right
q.topRight = []float64{right, top}
return q
}

// TopRightFromGeoPoint from a GeoPoint.
func (q *GeoBoundingBoxQuery) TopRightFromGeoPoint(point *GeoPoint) *GeoBoundingBoxQuery {
return q.TopRight(point.Lat, point.Lon)
}

// TopRightFromGeoHash from a Geo hash.
func (q *GeoBoundingBoxQuery) TopRightFromGeoHash(topRight string) *GeoBoundingBoxQuery {
q.topRight = topRight
return q
}

// WKT initializes the bounding box from Well-Known Text (WKT),
// e.g. "BBOX (-74.1, -71.12, 40.73, 40.01)".
func (q *GeoBoundingBoxQuery) WKT(wkt interface{}) *GeoBoundingBoxQuery {
q.wkt = wkt
return q
}

// Type sets the type of executing the geo bounding box. It can be either
// memory or indexed. It defaults to memory.
func (q *GeoBoundingBoxQuery) Type(typ string) *GeoBoundingBoxQuery {
q.typ = typ
return q
}

// ValidationMethod accepts IGNORE_MALFORMED, COERCE, and STRICT (default).
// IGNORE_MALFORMED accepts geo points with invalid lat/lon.
// COERCE tries to infer the correct lat/lon.
func (q *GeoBoundingBoxQuery) ValidationMethod(method string) *GeoBoundingBoxQuery {
q.validationMethod = method
return q
}

// IgnoreUnmapped indicates whether to ignore unmapped fields (and run a
// MatchNoDocsQuery in place of this).
func (q *GeoBoundingBoxQuery) IgnoreUnmapped(ignoreUnmapped bool) *GeoBoundingBoxQuery {
q.ignoreUnmapped = &ignoreUnmapped
return q
}

// QueryName gives the query a name. It is used for caching.
func (q *GeoBoundingBoxQuery) QueryName(queryName string) *GeoBoundingBoxQuery {
q.queryName = queryName
return q
Expand All @@ -88,31 +140,38 @@ func (q *GeoBoundingBoxQuery) Source() (interface{}, error) {
// }
// }

if q.top == nil {
return nil, errors.New("geo_bounding_box requires top latitude to be set")
}
if q.bottom == nil {
return nil, errors.New("geo_bounding_box requires bottom latitude to be set")
}
if q.right == nil {
return nil, errors.New("geo_bounding_box requires right longitude to be set")
}
if q.left == nil {
return nil, errors.New("geo_bounding_box requires left longitude to be set")
}

source := make(map[string]interface{})
params := make(map[string]interface{})
source["geo_bounding_box"] = params

box := make(map[string]interface{})
box["top_left"] = []float64{*q.left, *q.top}
box["bottom_right"] = []float64{*q.right, *q.bottom}
if q.wkt != nil {
box["wkt"] = q.wkt
} else {
if q.topLeft != nil {
box["top_left"] = q.topLeft
}
if q.topRight != nil {
box["top_right"] = q.topRight
}
if q.bottomLeft != nil {
box["bottom_left"] = q.bottomLeft
}
if q.bottomRight != nil {
box["bottom_right"] = q.bottomRight
}
}
params[q.name] = box

if q.typ != "" {
params["type"] = q.typ
}
if q.validationMethod != "" {
params["validation_method"] = q.validationMethod
}
if q.ignoreUnmapped != nil {
params["ignore_unmapped"] = *q.ignoreUnmapped
}
if q.queryName != "" {
params["_name"] = q.queryName
}
Expand Down
91 changes: 77 additions & 14 deletions search_queries_geo_bounding_box_test.go
Expand Up @@ -9,20 +9,6 @@ import (
"testing"
)

func TestGeoBoundingBoxQueryIncomplete(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.TopLeft(40.73, -74.1)
// no bottom and no right here
q = q.Type("memory")
src, err := q.Source()
if err == nil {
t.Fatal("expected error")
}
if src != nil {
t.Fatal("expected empty source")
}
}

func TestGeoBoundingBoxQuery(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.TopLeft(40.73, -74.1)
Expand Down Expand Up @@ -61,3 +47,80 @@ func TestGeoBoundingBoxQueryWithGeoPoint(t *testing.T) {
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
}
}

func TestGeoBoundingBoxQueryWithGeoHash(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.TopLeftFromGeoHash("dr5r9ydj2y73")
q = q.BottomRightFromGeoHash("drj7teegpus6")
src, err := q.Source()
if err != nil {
t.Fatal(err)
}
data, err := json.Marshal(src)
if err != nil {
t.Fatalf("marshaling to JSON failed: %v", err)
}
got := string(data)
expected := `{"geo_bounding_box":{"pin.location":{"bottom_right":"drj7teegpus6","top_left":"dr5r9ydj2y73"}}}`
if got != expected {
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
}
}

func TestGeoBoundingBoxQueryWithWKT(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.WKT("BBOX (-74.1, -71.12, 40.73, 40.01)")
src, err := q.Source()
if err != nil {
t.Fatal(err)
}
data, err := json.Marshal(src)
if err != nil {
t.Fatalf("marshaling to JSON failed: %v", err)
}
got := string(data)
expected := `{"geo_bounding_box":{"pin.location":{"wkt":"BBOX (-74.1, -71.12, 40.73, 40.01)"}}}`
if got != expected {
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
}
}

func TestGeoBoundingBoxQueryWithMixed(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.TopLeftFromGeoPoint(GeoPointFromLatLon(40.73, -74.1))
q = q.BottomRightFromGeoHash("drj7teegpus6")
src, err := q.Source()
if err != nil {
t.Fatal(err)
}
data, err := json.Marshal(src)
if err != nil {
t.Fatalf("marshaling to JSON failed: %v", err)
}
got := string(data)
expected := `{"geo_bounding_box":{"pin.location":{"bottom_right":"drj7teegpus6","top_left":[-74.1,40.73]}}}`
if got != expected {
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
}
}

func TestGeoBoundingBoxQueryWithParameters(t *testing.T) {
q := NewGeoBoundingBoxQuery("pin.location")
q = q.TopLeftFromGeoHash("dr5r9ydj2y73")
q = q.BottomRightFromGeoHash("drj7teegpus6")
q = q.ValidationMethod("IGNORE_MALFORMED")
q = q.IgnoreUnmapped((true))
src, err := q.Source()
if err != nil {
t.Fatal(err)
}
data, err := json.Marshal(src)
if err != nil {
t.Fatalf("marshaling to JSON failed: %v", err)
}
got := string(data)
expected := `{"geo_bounding_box":{"ignore_unmapped":true,"pin.location":{"bottom_right":"drj7teegpus6","top_left":"dr5r9ydj2y73"},"validation_method":"IGNORE_MALFORMED"}}`
if got != expected {
t.Errorf("expected\n%s\n,got:\n%s", expected, got)
}
}

0 comments on commit ef30a99

Please sign in to comment.