Skip to content

Commit bd14714

Browse files
author
Sreekanth Sivasankaran
authored
Merge pull request #1286 from sreekanth-cb/bounded_polygon
Support for bounded polygon query
2 parents 1626176 + 39c1fb6 commit bd14714

File tree

10 files changed

+678
-5
lines changed

10 files changed

+678
-5
lines changed

geo/geo.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ var geoTolerance = 1E-6
3737
var lonScale = float64((uint64(0x1)<<GeoBits)-1) / 360.0
3838
var latScale = float64((uint64(0x1)<<GeoBits)-1) / 180.0
3939

40+
// Point represents a geo point.
41+
type Point struct {
42+
Lon float64
43+
Lat float64
44+
}
45+
4046
// MortonHash computes the morton hash value for the provided geo point
4147
// This point is ordered as lon, lat.
4248
func MortonHash(lon, lat float64) uint64 {
@@ -168,3 +174,35 @@ func checkLongitude(longitude float64) error {
168174
}
169175
return nil
170176
}
177+
178+
func BoundingRectangleForPolygon(polygon []Point) (
179+
float64, float64, float64, float64, error) {
180+
err := checkLongitude(polygon[0].Lon)
181+
if err != nil {
182+
return 0, 0, 0, 0, err
183+
}
184+
err = checkLatitude(polygon[0].Lat)
185+
if err != nil {
186+
return 0, 0, 0, 0, err
187+
}
188+
maxY, minY := polygon[0].Lat, polygon[0].Lat
189+
maxX, minX := polygon[0].Lon, polygon[0].Lon
190+
for i := 1; i < len(polygon); i++ {
191+
err := checkLongitude(polygon[i].Lon)
192+
if err != nil {
193+
return 0, 0, 0, 0, err
194+
}
195+
err = checkLatitude(polygon[i].Lat)
196+
if err != nil {
197+
return 0, 0, 0, 0, err
198+
}
199+
200+
maxY = math.Max(maxY, polygon[i].Lat)
201+
minY = math.Min(minY, polygon[i].Lat)
202+
203+
maxX = math.Max(maxX, polygon[i].Lon)
204+
minX = math.Min(minX, polygon[i].Lon)
205+
}
206+
207+
return minX, maxY, maxX, minY, nil
208+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) 2019 Couchbase, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package query
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
21+
"github.com/blevesearch/bleve/geo"
22+
"github.com/blevesearch/bleve/index"
23+
"github.com/blevesearch/bleve/mapping"
24+
"github.com/blevesearch/bleve/search"
25+
"github.com/blevesearch/bleve/search/searcher"
26+
)
27+
28+
type GeoBoundingPolygonQuery struct {
29+
Points []geo.Point `json:"polygon_points"`
30+
FieldVal string `json:"field,omitempty"`
31+
BoostVal *Boost `json:"boost,omitempty"`
32+
}
33+
34+
func NewGeoBoundingPolygonQuery(points []geo.Point) *GeoBoundingPolygonQuery {
35+
return &GeoBoundingPolygonQuery{
36+
Points: points}
37+
}
38+
39+
func (q *GeoBoundingPolygonQuery) SetBoost(b float64) {
40+
boost := Boost(b)
41+
q.BoostVal = &boost
42+
}
43+
44+
func (q *GeoBoundingPolygonQuery) Boost() float64 {
45+
return q.BoostVal.Value()
46+
}
47+
48+
func (q *GeoBoundingPolygonQuery) SetField(f string) {
49+
q.FieldVal = f
50+
}
51+
52+
func (q *GeoBoundingPolygonQuery) Field() string {
53+
return q.FieldVal
54+
}
55+
56+
func (q *GeoBoundingPolygonQuery) Searcher(i index.IndexReader,
57+
m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
58+
field := q.FieldVal
59+
if q.FieldVal == "" {
60+
field = m.DefaultSearchField()
61+
}
62+
63+
return searcher.NewGeoBoundedPolygonSearcher(i, q.Points, field, q.BoostVal.Value(), options)
64+
}
65+
66+
func (q *GeoBoundingPolygonQuery) Validate() error {
67+
return nil
68+
}
69+
70+
func (q *GeoBoundingPolygonQuery) UnmarshalJSON(data []byte) error {
71+
tmp := struct {
72+
Points []interface{} `json:"polygon_points"`
73+
FieldVal string `json:"field,omitempty"`
74+
BoostVal *Boost `json:"boost,omitempty"`
75+
}{}
76+
err := json.Unmarshal(data, &tmp)
77+
if err != nil {
78+
return err
79+
}
80+
81+
q.Points = make([]geo.Point, 0, len(tmp.Points))
82+
for _, i := range tmp.Points {
83+
// now use our generic point parsing code from the geo package
84+
lon, lat, found := geo.ExtractGeoPoint(i)
85+
if !found {
86+
return fmt.Errorf("geo polygon point: %v is not in a valid format", i)
87+
}
88+
q.Points = append(q.Points, geo.Point{Lon: lon, Lat: lat})
89+
}
90+
91+
q.FieldVal = tmp.FieldVal
92+
q.BoostVal = tmp.BoostVal
93+
return nil
94+
}

search/query/query.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ func ParseQuery(input []byte) (Query, error) {
273273
}
274274
return &rv, nil
275275
}
276+
_, hasPoints := tmp["polygon_points"]
277+
if hasPoints {
278+
var rv GeoBoundingPolygonQuery
279+
err := json.Unmarshal(input, &rv)
280+
if err != nil {
281+
return nil, err
282+
}
283+
return &rv, nil
284+
}
276285
return nil, fmt.Errorf("unknown query type")
277286
}
278287

search/searcher/search_geopointdistance.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
3434
// build a searcher for the box
3535
boxSearcher, err := boxSearcher(indexReader,
3636
topLeftLon, topLeftLat, bottomRightLon, bottomRightLat,
37-
field, boost, options)
37+
field, boost, options, false)
3838
if err != nil {
3939
return nil, err
4040
}
@@ -54,19 +54,20 @@ func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon,
5454
// two boxes joined through a disjunction searcher
5555
func boxSearcher(indexReader index.IndexReader,
5656
topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64,
57-
field string, boost float64, options search.SearcherOptions) (
57+
field string, boost float64, options search.SearcherOptions, checkBoundaries bool) (
5858
search.Searcher, error) {
5959
if bottomRightLon < topLeftLon {
6060
// cross date line, rewrite as two parts
6161

6262
leftSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
6363
-180, bottomRightLat, bottomRightLon, topLeftLat,
64-
field, boost, options, false)
64+
field, boost, options, checkBoundaries)
6565
if err != nil {
6666
return nil, err
6767
}
6868
rightSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
69-
topLeftLon, bottomRightLat, 180, topLeftLat, field, boost, options, false)
69+
topLeftLon, bottomRightLat, 180, topLeftLat, field, boost, options,
70+
checkBoundaries)
7071
if err != nil {
7172
_ = leftSearcher.Close()
7273
return nil, err
@@ -85,7 +86,7 @@ func boxSearcher(indexReader index.IndexReader,
8586
// build geoboundinggox searcher for that bounding box
8687
boxSearcher, err := NewGeoBoundingBoxSearcher(indexReader,
8788
topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, field, boost,
88-
options, false)
89+
options, checkBoundaries)
8990
if err != nil {
9091
return nil, err
9192
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2019 Couchbase, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package searcher
16+
17+
import (
18+
"github.com/blevesearch/bleve/geo"
19+
"github.com/blevesearch/bleve/index"
20+
"github.com/blevesearch/bleve/numeric"
21+
"github.com/blevesearch/bleve/search"
22+
"math"
23+
)
24+
25+
func NewGeoBoundedPolygonSearcher(indexReader index.IndexReader,
26+
polygon []geo.Point, field string, boost float64,
27+
options search.SearcherOptions) (search.Searcher, error) {
28+
29+
// compute the bounding box enclosing the polygon
30+
topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err :=
31+
geo.BoundingRectangleForPolygon(polygon)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
// build a searcher for the bounding box on the polygon
37+
boxSearcher, err := boxSearcher(indexReader,
38+
topLeftLon, topLeftLat, bottomRightLon, bottomRightLat,
39+
field, boost, options, true)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
dvReader, err := indexReader.DocValueReader([]string{field})
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
// wrap it in a filtering searcher that checks for the polygon inclusivity
50+
return NewFilteringSearcher(boxSearcher,
51+
buildPolygonFilter(dvReader, field, polygon)), nil
52+
}
53+
54+
const float64EqualityThreshold = 1e-6
55+
56+
func almostEqual(a, b float64) bool {
57+
return math.Abs(a-b) <= float64EqualityThreshold
58+
}
59+
60+
// buildPolygonFilter returns true if the point lies inside the
61+
// polygon. It is based on the ray-casting technique as referred
62+
// here: https://wrf.ecse.rpi.edu/nikola/pubdetails/pnpoly.html
63+
func buildPolygonFilter(dvReader index.DocValueReader, field string,
64+
polygon []geo.Point) FilterFunc {
65+
return func(d *search.DocumentMatch) bool {
66+
var lon, lat float64
67+
var found bool
68+
69+
err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) {
70+
// only consider the values which are shifted 0
71+
prefixCoded := numeric.PrefixCoded(term)
72+
shift, err := prefixCoded.Shift()
73+
if err == nil && shift == 0 {
74+
i64, err := prefixCoded.Int64()
75+
if err == nil {
76+
lon = geo.MortonUnhashLon(uint64(i64))
77+
lat = geo.MortonUnhashLat(uint64(i64))
78+
found = true
79+
}
80+
}
81+
})
82+
83+
// Note: this approach works for points which are strictly inside
84+
// the polygon. ie it might fail for certain points on the polygon boundaries.
85+
if err == nil && found {
86+
nVertices := len(polygon)
87+
var inside bool
88+
// check for a direct vertex match
89+
if almostEqual(polygon[0].Lat, lat) &&
90+
almostEqual(polygon[0].Lon, lon) {
91+
return true
92+
}
93+
94+
for i := 1; i < nVertices; i++ {
95+
if almostEqual(polygon[i].Lat, lat) &&
96+
almostEqual(polygon[i].Lon, lon) {
97+
return true
98+
}
99+
if (polygon[i].Lat > lat) != (polygon[i-1].Lat > lat) &&
100+
lon < (polygon[i-1].Lon-polygon[i].Lon)*(lat-polygon[i].Lat)/
101+
(polygon[i-1].Lat-polygon[i].Lat)+polygon[i].Lon {
102+
inside = !inside
103+
}
104+
}
105+
return inside
106+
107+
}
108+
return false
109+
}
110+
}

0 commit comments

Comments
 (0)