1// Copyright (c) 2017 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 15package searcher 16 17import ( 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) 23 24func NewGeoPointDistanceSearcher(indexReader index.IndexReader, centerLon, 25 centerLat, dist float64, field string, boost float64, 26 options search.SearcherOptions) (search.Searcher, error) { 27 // compute bounding box containing the circle 28 topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, err := 29 geo.RectFromPointDistance(centerLon, centerLat, dist) 30 if err != nil { 31 return nil, err 32 } 33 34 // build a searcher for the box 35 boxSearcher, err := boxSearcher(indexReader, 36 topLeftLon, topLeftLat, bottomRightLon, bottomRightLat, 37 field, boost, options, false) 38 if err != nil { 39 return nil, err 40 } 41 42 dvReader, err := indexReader.DocValueReader([]string{field}) 43 if err != nil { 44 return nil, err 45 } 46 47 // wrap it in a filtering searcher which checks the actual distance 48 return NewFilteringSearcher(boxSearcher, 49 buildDistFilter(dvReader, field, centerLon, centerLat, dist)), nil 50} 51 52// boxSearcher builds a searcher for the described bounding box 53// if the desired box crosses the dateline, it is automatically split into 54// two boxes joined through a disjunction searcher 55func boxSearcher(indexReader index.IndexReader, 56 topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64, 57 field string, boost float64, options search.SearcherOptions, checkBoundaries bool) ( 58 search.Searcher, error) { 59 if bottomRightLon < topLeftLon { 60 // cross date line, rewrite as two parts 61 62 leftSearcher, err := NewGeoBoundingBoxSearcher(indexReader, 63 -180, bottomRightLat, bottomRightLon, topLeftLat, 64 field, boost, options, checkBoundaries) 65 if err != nil { 66 return nil, err 67 } 68 rightSearcher, err := NewGeoBoundingBoxSearcher(indexReader, 69 topLeftLon, bottomRightLat, 180, topLeftLat, field, boost, options, 70 checkBoundaries) 71 if err != nil { 72 _ = leftSearcher.Close() 73 return nil, err 74 } 75 76 boxSearcher, err := NewDisjunctionSearcher(indexReader, 77 []search.Searcher{leftSearcher, rightSearcher}, 0, options) 78 if err != nil { 79 _ = leftSearcher.Close() 80 _ = rightSearcher.Close() 81 return nil, err 82 } 83 return boxSearcher, nil 84 } 85 86 // build geoboundingbox searcher for that bounding box 87 boxSearcher, err := NewGeoBoundingBoxSearcher(indexReader, 88 topLeftLon, bottomRightLat, bottomRightLon, topLeftLat, field, boost, 89 options, checkBoundaries) 90 if err != nil { 91 return nil, err 92 } 93 return boxSearcher, nil 94} 95 96func buildDistFilter(dvReader index.DocValueReader, field string, 97 centerLon, centerLat, maxDist float64) FilterFunc { 98 return func(d *search.DocumentMatch) bool { 99 // check geo matches against all numeric type terms indexed 100 var lons, lats []float64 101 var found bool 102 103 err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) { 104 // only consider the values which are shifted 0 105 prefixCoded := numeric.PrefixCoded(term) 106 shift, err := prefixCoded.Shift() 107 if err == nil && shift == 0 { 108 i64, err := prefixCoded.Int64() 109 if err == nil { 110 lons = append(lons, geo.MortonUnhashLon(uint64(i64))) 111 lats = append(lats, geo.MortonUnhashLat(uint64(i64))) 112 found = true 113 } 114 } 115 }) 116 if err == nil && found { 117 for i := range lons { 118 dist := geo.Haversin(lons[i], lats[i], centerLon, centerLat) 119 if dist <= maxDist/1000 { 120 return true 121 } 122 } 123 } 124 return false 125 } 126} 127