mirror of https://github.com/go-gitea/gitea
Improve issue search (#2387)
* Improve issue indexer * Fix new issue sqlite bug * Different test indexer paths for each db * Add integration indexer paths to make cleanpull/2429/merge
parent
52e11b24bf
commit
b0f7457d9e
@ -0,0 +1,143 @@ |
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package indexer |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"code.gitea.io/gitea/modules/log" |
||||
"code.gitea.io/gitea/modules/setting" |
||||
|
||||
"github.com/blevesearch/bleve" |
||||
"github.com/blevesearch/bleve/analysis/analyzer/custom" |
||||
"github.com/blevesearch/bleve/analysis/token/lowercase" |
||||
"github.com/blevesearch/bleve/analysis/token/unicodenorm" |
||||
"github.com/blevesearch/bleve/analysis/tokenizer/unicode" |
||||
) |
||||
|
||||
// issueIndexer (thread-safe) index for searching issues
|
||||
var issueIndexer bleve.Index |
||||
|
||||
// IssueIndexerData data stored in the issue indexer
|
||||
type IssueIndexerData struct { |
||||
RepoID int64 |
||||
Title string |
||||
Content string |
||||
Comments []string |
||||
} |
||||
|
||||
// IssueIndexerUpdate an update to the issue indexer
|
||||
type IssueIndexerUpdate struct { |
||||
IssueID int64 |
||||
Data *IssueIndexerData |
||||
} |
||||
|
||||
const issueIndexerAnalyzer = "issueIndexer" |
||||
|
||||
// InitIssueIndexer initialize issue indexer
|
||||
func InitIssueIndexer(populateIndexer func() error) { |
||||
_, err := os.Stat(setting.Indexer.IssuePath) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
if err = createIssueIndexer(); err != nil { |
||||
log.Fatal(4, "CreateIssuesIndexer: %v", err) |
||||
} |
||||
if err = populateIndexer(); err != nil { |
||||
log.Fatal(4, "PopulateIssuesIndex: %v", err) |
||||
} |
||||
} else { |
||||
log.Fatal(4, "InitIssuesIndexer: %v", err) |
||||
} |
||||
} else { |
||||
issueIndexer, err = bleve.Open(setting.Indexer.IssuePath) |
||||
if err != nil { |
||||
log.Error(4, "Unable to open issues indexer (%s)."+ |
||||
" If the error is due to incompatible versions, try deleting the indexer files;"+ |
||||
" gitea will recreate them with the appropriate version the next time it runs."+ |
||||
" Deleting the indexer files will not result in loss of data.", |
||||
setting.Indexer.IssuePath) |
||||
log.Fatal(4, "InitIssuesIndexer, open index: %v", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// createIssueIndexer create an issue indexer if one does not already exist
|
||||
func createIssueIndexer() error { |
||||
mapping := bleve.NewIndexMapping() |
||||
docMapping := bleve.NewDocumentMapping() |
||||
|
||||
docMapping.AddFieldMappingsAt("RepoID", bleve.NewNumericFieldMapping()) |
||||
|
||||
textFieldMapping := bleve.NewTextFieldMapping() |
||||
docMapping.AddFieldMappingsAt("Title", textFieldMapping) |
||||
docMapping.AddFieldMappingsAt("Content", textFieldMapping) |
||||
docMapping.AddFieldMappingsAt("Comments", textFieldMapping) |
||||
|
||||
const unicodeNormNFC = "unicodeNormNFC" |
||||
if err := mapping.AddCustomTokenFilter(unicodeNormNFC, map[string]interface{}{ |
||||
"type": unicodenorm.Name, |
||||
"form": unicodenorm.NFC, |
||||
}); err != nil { |
||||
return err |
||||
} else if err = mapping.AddCustomAnalyzer(issueIndexerAnalyzer, map[string]interface{}{ |
||||
"type": custom.Name, |
||||
"char_filters": []string{}, |
||||
"tokenizer": unicode.Name, |
||||
"token_filters": []string{unicodeNormNFC, lowercase.Name}, |
||||
}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
mapping.DefaultAnalyzer = issueIndexerAnalyzer |
||||
mapping.AddDocumentMapping("issues", docMapping) |
||||
|
||||
var err error |
||||
issueIndexer, err = bleve.New(setting.Indexer.IssuePath, mapping) |
||||
return err |
||||
} |
||||
|
||||
// UpdateIssue update the issue indexer
|
||||
func UpdateIssue(update IssueIndexerUpdate) error { |
||||
return issueIndexer.Index(indexerID(update.IssueID), update.Data) |
||||
} |
||||
|
||||
// BatchUpdateIssues perform a batch update of the issue indexer
|
||||
func BatchUpdateIssues(updates ...IssueIndexerUpdate) error { |
||||
batch := issueIndexer.NewBatch() |
||||
for _, update := range updates { |
||||
err := batch.Index(indexerID(update.IssueID), update.Data) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return issueIndexer.Batch(batch) |
||||
} |
||||
|
||||
// SearchIssuesByKeyword searches for issues by given conditions.
|
||||
// Returns the matching issue IDs
|
||||
func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) { |
||||
indexerQuery := bleve.NewConjunctionQuery( |
||||
numericEqualityQuery(repoID, "RepoID"), |
||||
bleve.NewDisjunctionQuery( |
||||
newMatchPhraseQuery(keyword, "Title", issueIndexerAnalyzer), |
||||
newMatchPhraseQuery(keyword, "Content", issueIndexerAnalyzer), |
||||
newMatchPhraseQuery(keyword, "Comments", issueIndexerAnalyzer), |
||||
)) |
||||
search := bleve.NewSearchRequestOptions(indexerQuery, 2147483647, 0, false) |
||||
|
||||
result, err := issueIndexer.Search(search) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
issueIDs := make([]int64, len(result.Hits)) |
||||
for i, hit := range result.Hits { |
||||
issueIDs[i], err = idOfIndexerID(hit.ID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
return issueIDs, nil |
||||
} |
@ -0,0 +1,145 @@ |
||||
// Copyright (c) 2014 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package custom |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/analysis" |
||||
"github.com/blevesearch/bleve/registry" |
||||
) |
||||
|
||||
const Name = "custom" |
||||
|
||||
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) { |
||||
|
||||
var err error |
||||
var charFilters []analysis.CharFilter |
||||
charFiltersValue, ok := config["char_filters"] |
||||
if ok { |
||||
switch charFiltersValue := charFiltersValue.(type) { |
||||
case []string: |
||||
charFilters, err = getCharFilters(charFiltersValue, cache) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
case []interface{}: |
||||
charFiltersNames, err := convertInterfaceSliceToStringSlice(charFiltersValue, "char filter") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
charFilters, err = getCharFilters(charFiltersNames, cache) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
default: |
||||
return nil, fmt.Errorf("unsupported type for char_filters, must be slice") |
||||
} |
||||
} |
||||
|
||||
var tokenizerName string |
||||
tokenizerValue, ok := config["tokenizer"] |
||||
if ok { |
||||
tokenizerName, ok = tokenizerValue.(string) |
||||
if !ok { |
||||
return nil, fmt.Errorf("must specify tokenizer as string") |
||||
} |
||||
} else { |
||||
return nil, fmt.Errorf("must specify tokenizer") |
||||
} |
||||
|
||||
tokenizer, err := cache.TokenizerNamed(tokenizerName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var tokenFilters []analysis.TokenFilter |
||||
tokenFiltersValue, ok := config["token_filters"] |
||||
if ok { |
||||
switch tokenFiltersValue := tokenFiltersValue.(type) { |
||||
case []string: |
||||
tokenFilters, err = getTokenFilters(tokenFiltersValue, cache) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
case []interface{}: |
||||
tokenFiltersNames, err := convertInterfaceSliceToStringSlice(tokenFiltersValue, "token filter") |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tokenFilters, err = getTokenFilters(tokenFiltersNames, cache) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
default: |
||||
return nil, fmt.Errorf("unsupported type for token_filters, must be slice") |
||||
} |
||||
} |
||||
|
||||
rv := analysis.Analyzer{ |
||||
Tokenizer: tokenizer, |
||||
} |
||||
if charFilters != nil { |
||||
rv.CharFilters = charFilters |
||||
} |
||||
if tokenFilters != nil { |
||||
rv.TokenFilters = tokenFilters |
||||
} |
||||
return &rv, nil |
||||
} |
||||
|
||||
func init() { |
||||
registry.RegisterAnalyzer(Name, AnalyzerConstructor) |
||||
} |
||||
|
||||
func getCharFilters(charFilterNames []string, cache *registry.Cache) ([]analysis.CharFilter, error) { |
||||
charFilters := make([]analysis.CharFilter, len(charFilterNames)) |
||||
for i, charFilterName := range charFilterNames { |
||||
charFilter, err := cache.CharFilterNamed(charFilterName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
charFilters[i] = charFilter |
||||
} |
||||
|
||||
return charFilters, nil |
||||
} |
||||
|
||||
func getTokenFilters(tokenFilterNames []string, cache *registry.Cache) ([]analysis.TokenFilter, error) { |
||||
tokenFilters := make([]analysis.TokenFilter, len(tokenFilterNames)) |
||||
for i, tokenFilterName := range tokenFilterNames { |
||||
tokenFilter, err := cache.TokenFilterNamed(tokenFilterName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
tokenFilters[i] = tokenFilter |
||||
} |
||||
|
||||
return tokenFilters, nil |
||||
} |
||||
|
||||
func convertInterfaceSliceToStringSlice(interfaceSlice []interface{}, objType string) ([]string, error) { |
||||
stringSlice := make([]string, len(interfaceSlice)) |
||||
for i, interfaceObj := range interfaceSlice { |
||||
stringObj, ok := interfaceObj.(string) |
||||
if ok { |
||||
stringSlice[i] = stringObj |
||||
} else { |
||||
return nil, fmt.Errorf(objType + " name must be a string") |
||||
} |
||||
} |
||||
|
||||
return stringSlice, nil |
||||
} |
@ -1,46 +0,0 @@ |
||||
// Copyright (c) 2014 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package simple |
||||
|
||||
import ( |
||||
"github.com/blevesearch/bleve/analysis" |
||||
"github.com/blevesearch/bleve/analysis/token/lowercase" |
||||
"github.com/blevesearch/bleve/analysis/tokenizer/letter" |
||||
"github.com/blevesearch/bleve/registry" |
||||
) |
||||
|
||||
const Name = "simple" |
||||
|
||||
func AnalyzerConstructor(config map[string]interface{}, cache *registry.Cache) (*analysis.Analyzer, error) { |
||||
tokenizer, err := cache.TokenizerNamed(letter.Name) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
toLowerFilter, err := cache.TokenFilterNamed(lowercase.Name) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rv := analysis.Analyzer{ |
||||
Tokenizer: tokenizer, |
||||
TokenFilters: []analysis.TokenFilter{ |
||||
toLowerFilter, |
||||
}, |
||||
} |
||||
return &rv, nil |
||||
} |
||||
|
||||
func init() { |
||||
registry.RegisterAnalyzer(Name, AnalyzerConstructor) |
||||
} |
@ -0,0 +1,79 @@ |
||||
// Copyright (c) 2014 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package unicodenorm |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/analysis" |
||||
"github.com/blevesearch/bleve/registry" |
||||
"golang.org/x/text/unicode/norm" |
||||
) |
||||
|
||||
const Name = "normalize_unicode" |
||||
|
||||
const NFC = "nfc" |
||||
const NFD = "nfd" |
||||
const NFKC = "nfkc" |
||||
const NFKD = "nfkd" |
||||
|
||||
var forms = map[string]norm.Form{ |
||||
NFC: norm.NFC, |
||||
NFD: norm.NFD, |
||||
NFKC: norm.NFKC, |
||||
NFKD: norm.NFKD, |
||||
} |
||||
|
||||
type UnicodeNormalizeFilter struct { |
||||
form norm.Form |
||||
} |
||||
|
||||
func NewUnicodeNormalizeFilter(formName string) (*UnicodeNormalizeFilter, error) { |
||||
form, ok := forms[formName] |
||||
if !ok { |
||||
return nil, fmt.Errorf("no form named %s", formName) |
||||
} |
||||
return &UnicodeNormalizeFilter{ |
||||
form: form, |
||||
}, nil |
||||
} |
||||
|
||||
func MustNewUnicodeNormalizeFilter(formName string) *UnicodeNormalizeFilter { |
||||
filter, err := NewUnicodeNormalizeFilter(formName) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return filter |
||||
} |
||||
|
||||
func (s *UnicodeNormalizeFilter) Filter(input analysis.TokenStream) analysis.TokenStream { |
||||
for _, token := range input { |
||||
token.Term = s.form.Bytes(token.Term) |
||||
} |
||||
return input |
||||
} |
||||
|
||||
func UnicodeNormalizeFilterConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.TokenFilter, error) { |
||||
formVal, ok := config["form"].(string) |
||||
if !ok { |
||||
return nil, fmt.Errorf("must specify form") |
||||
} |
||||
form := formVal |
||||
return NewUnicodeNormalizeFilter(form) |
||||
} |
||||
|
||||
func init() { |
||||
registry.RegisterTokenFilter(Name, UnicodeNormalizeFilterConstructor) |
||||
} |
@ -1,76 +0,0 @@ |
||||
// Copyright (c) 2016 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package character |
||||
|
||||
import ( |
||||
"unicode/utf8" |
||||
|
||||
"github.com/blevesearch/bleve/analysis" |
||||
) |
||||
|
||||
type IsTokenRune func(r rune) bool |
||||
|
||||
type CharacterTokenizer struct { |
||||
isTokenRun IsTokenRune |
||||
} |
||||
|
||||
func NewCharacterTokenizer(f IsTokenRune) *CharacterTokenizer { |
||||
return &CharacterTokenizer{ |
||||
isTokenRun: f, |
||||
} |
||||
} |
||||
|
||||
func (c *CharacterTokenizer) Tokenize(input []byte) analysis.TokenStream { |
||||
|
||||
rv := make(analysis.TokenStream, 0, 1024) |
||||
|
||||
offset := 0 |
||||
start := 0 |
||||
end := 0 |
||||
count := 0 |
||||
for currRune, size := utf8.DecodeRune(input[offset:]); currRune != utf8.RuneError; currRune, size = utf8.DecodeRune(input[offset:]) { |
||||
isToken := c.isTokenRun(currRune) |
||||
if isToken { |
||||
end = offset + size |
||||
} else { |
||||
if end-start > 0 { |
||||
// build token
|
||||
rv = append(rv, &analysis.Token{ |
||||
Term: input[start:end], |
||||
Start: start, |
||||
End: end, |
||||
Position: count + 1, |
||||
Type: analysis.AlphaNumeric, |
||||
}) |
||||
count++ |
||||
} |
||||
start = offset + size |
||||
end = start |
||||
} |
||||
offset += size |
||||
} |
||||
// if we ended in the middle of a token, finish it
|
||||
if end-start > 0 { |
||||
// build token
|
||||
rv = append(rv, &analysis.Token{ |
||||
Term: input[start:end], |
||||
Start: start, |
||||
End: end, |
||||
Position: count + 1, |
||||
Type: analysis.AlphaNumeric, |
||||
}) |
||||
} |
||||
return rv |
||||
} |
@ -1,33 +0,0 @@ |
||||
// Copyright (c) 2016 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package letter |
||||
|
||||
import ( |
||||
"unicode" |
||||
|
||||
"github.com/blevesearch/bleve/analysis" |
||||
"github.com/blevesearch/bleve/analysis/tokenizer/character" |
||||
"github.com/blevesearch/bleve/registry" |
||||
) |
||||
|
||||
const Name = "letter" |
||||
|
||||
func TokenizerConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.Tokenizer, error) { |
||||
return character.NewCharacterTokenizer(unicode.IsLetter), nil |
||||
} |
||||
|
||||
func init() { |
||||
registry.RegisterTokenizer(Name, TokenizerConstructor) |
||||
} |
@ -1,23 +0,0 @@ |
||||
// Copyright (c) 2014 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build appengine appenginevm
|
||||
|
||||
package bleve |
||||
|
||||
// in the appengine environment we cannot support disk based indexes
|
||||
// so we do no extra configuration in this method
|
||||
func initDisk() { |
||||
|
||||
} |
@ -0,0 +1,137 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package document |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/analysis" |
||||
"github.com/blevesearch/bleve/geo" |
||||
"github.com/blevesearch/bleve/numeric" |
||||
) |
||||
|
||||
var GeoPrecisionStep uint = 9 |
||||
|
||||
type GeoPointField struct { |
||||
name string |
||||
arrayPositions []uint64 |
||||
options IndexingOptions |
||||
value numeric.PrefixCoded |
||||
numPlainTextBytes uint64 |
||||
} |
||||
|
||||
func (n *GeoPointField) Name() string { |
||||
return n.name |
||||
} |
||||
|
||||
func (n *GeoPointField) ArrayPositions() []uint64 { |
||||
return n.arrayPositions |
||||
} |
||||
|
||||
func (n *GeoPointField) Options() IndexingOptions { |
||||
return n.options |
||||
} |
||||
|
||||
func (n *GeoPointField) Analyze() (int, analysis.TokenFrequencies) { |
||||
tokens := make(analysis.TokenStream, 0) |
||||
tokens = append(tokens, &analysis.Token{ |
||||
Start: 0, |
||||
End: len(n.value), |
||||
Term: n.value, |
||||
Position: 1, |
||||
Type: analysis.Numeric, |
||||
}) |
||||
|
||||
original, err := n.value.Int64() |
||||
if err == nil { |
||||
|
||||
shift := GeoPrecisionStep |
||||
for shift < 64 { |
||||
shiftEncoded, err := numeric.NewPrefixCodedInt64(original, shift) |
||||
if err != nil { |
||||
break |
||||
} |
||||
token := analysis.Token{ |
||||
Start: 0, |
||||
End: len(shiftEncoded), |
||||
Term: shiftEncoded, |
||||
Position: 1, |
||||
Type: analysis.Numeric, |
||||
} |
||||
tokens = append(tokens, &token) |
||||
shift += GeoPrecisionStep |
||||
} |
||||
} |
||||
|
||||
fieldLength := len(tokens) |
||||
tokenFreqs := analysis.TokenFrequency(tokens, n.arrayPositions, n.options.IncludeTermVectors()) |
||||
return fieldLength, tokenFreqs |
||||
} |
||||
|
||||
func (n *GeoPointField) Value() []byte { |
||||
return n.value |
||||
} |
||||
|
||||
func (n *GeoPointField) Lon() (float64, error) { |
||||
i64, err := n.value.Int64() |
||||
if err != nil { |
||||
return 0.0, err |
||||
} |
||||
return geo.MortonUnhashLon(uint64(i64)), nil |
||||
} |
||||
|
||||
func (n *GeoPointField) Lat() (float64, error) { |
||||
i64, err := n.value.Int64() |
||||
if err != nil { |
||||
return 0.0, err |
||||
} |
||||
return geo.MortonUnhashLat(uint64(i64)), nil |
||||
} |
||||
|
||||
func (n *GeoPointField) GoString() string { |
||||
return fmt.Sprintf("&document.GeoPointField{Name:%s, Options: %s, Value: %s}", n.name, n.options, n.value) |
||||
} |
||||
|
||||
func (n *GeoPointField) NumPlainTextBytes() uint64 { |
||||
return n.numPlainTextBytes |
||||
} |
||||
|
||||
func NewGeoPointFieldFromBytes(name string, arrayPositions []uint64, value []byte) *GeoPointField { |
||||
return &GeoPointField{ |
||||
name: name, |
||||
arrayPositions: arrayPositions, |
||||
value: value, |
||||
options: DefaultNumericIndexingOptions, |
||||
numPlainTextBytes: uint64(len(value)), |
||||
} |
||||
} |
||||
|
||||
func NewGeoPointField(name string, arrayPositions []uint64, lon, lat float64) *GeoPointField { |
||||
return NewGeoPointFieldWithIndexingOptions(name, arrayPositions, lon, lat, DefaultNumericIndexingOptions) |
||||
} |
||||
|
||||
func NewGeoPointFieldWithIndexingOptions(name string, arrayPositions []uint64, lon, lat float64, options IndexingOptions) *GeoPointField { |
||||
mhash := geo.MortonHash(lon, lat) |
||||
prefixCoded := numeric.MustNewPrefixCodedInt64(int64(mhash), 0) |
||||
return &GeoPointField{ |
||||
name: name, |
||||
arrayPositions: arrayPositions, |
||||
value: prefixCoded, |
||||
options: options, |
||||
// not correct, just a place holder until we revisit how fields are
|
||||
// represented and can fix this better
|
||||
numPlainTextBytes: uint64(8), |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
# geo support in bleve |
||||
|
||||
First, all of this geo code is a Go adaptation of the [Lucene 5.3.2 sandbox geo support](https://lucene.apache.org/core/5_3_2/sandbox/org/apache/lucene/util/package-summary.html). |
||||
|
||||
## Notes |
||||
|
||||
- All of the APIs will use float64 for lon/lat values. |
||||
- When describing a point in function arguments or return values, we always use the order lon, lat. |
||||
- High level APIs will use TopLeft and BottomRight to describe bounding boxes. This may not map cleanly to min/max lon/lat when crossing the dateline. The lower level APIs will use min/max lon/lat and require the higher-level code to split boxes accordingly. |
@ -0,0 +1,170 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package geo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
|
||||
"github.com/blevesearch/bleve/numeric" |
||||
) |
||||
|
||||
// GeoBits is the number of bits used for a single geo point
|
||||
// Currently this is 32bits for lon and 32bits for lat
|
||||
var GeoBits uint = 32 |
||||
|
||||
var minLon = -180.0 |
||||
var minLat = -90.0 |
||||
var maxLon = 180.0 |
||||
var maxLat = 90.0 |
||||
var minLonRad = minLon * degreesToRadian |
||||
var minLatRad = minLat * degreesToRadian |
||||
var maxLonRad = maxLon * degreesToRadian |
||||
var maxLatRad = maxLat * degreesToRadian |
||||
var geoTolerance = 1E-6 |
||||
var lonScale = float64((uint64(0x1)<<GeoBits)-1) / 360.0 |
||||
var latScale = float64((uint64(0x1)<<GeoBits)-1) / 180.0 |
||||
|
||||
// MortonHash computes the morton hash value for the provided geo point
|
||||
// This point is ordered as lon, lat.
|
||||
func MortonHash(lon, lat float64) uint64 { |
||||
return numeric.Interleave(scaleLon(lon), scaleLat(lat)) |
||||
} |
||||
|
||||
func scaleLon(lon float64) uint64 { |
||||
rv := uint64((lon - minLon) * lonScale) |
||||
return rv |
||||
} |
||||
|
||||
func scaleLat(lat float64) uint64 { |
||||
rv := uint64((lat - minLat) * latScale) |
||||
return rv |
||||
} |
||||
|
||||
// MortonUnhashLon extracts the longitude value from the provided morton hash.
|
||||
func MortonUnhashLon(hash uint64) float64 { |
||||
return unscaleLon(numeric.Deinterleave(hash)) |
||||
} |
||||
|
||||
// MortonUnhashLat extracts the latitude value from the provided morton hash.
|
||||
func MortonUnhashLat(hash uint64) float64 { |
||||
return unscaleLat(numeric.Deinterleave(hash >> 1)) |
||||
} |
||||
|
||||
func unscaleLon(lon uint64) float64 { |
||||
return (float64(lon) / lonScale) + minLon |
||||
} |
||||
|
||||
func unscaleLat(lat uint64) float64 { |
||||
return (float64(lat) / latScale) + minLat |
||||
} |
||||
|
||||
// compareGeo will compare two float values and see if they are the same
|
||||
// taking into consideration a known geo tolerance.
|
||||
func compareGeo(a, b float64) float64 { |
||||
compare := a - b |
||||
if math.Abs(compare) <= geoTolerance { |
||||
return 0 |
||||
} |
||||
return compare |
||||
} |
||||
|
||||
// RectIntersects checks whether rectangles a and b intersect
|
||||
func RectIntersects(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { |
||||
return !(aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) |
||||
} |
||||
|
||||
// RectWithin checks whether box a is within box b
|
||||
func RectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY float64) bool { |
||||
rv := !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY) |
||||
return rv |
||||
} |
||||
|
||||
// BoundingBoxContains checks whether the lon/lat point is within the box
|
||||
func BoundingBoxContains(lon, lat, minLon, minLat, maxLon, maxLat float64) bool { |
||||
return compareGeo(lon, minLon) >= 0 && compareGeo(lon, maxLon) <= 0 && |
||||
compareGeo(lat, minLat) >= 0 && compareGeo(lat, maxLat) <= 0 |
||||
} |
||||
|
||||
const degreesToRadian = math.Pi / 180 |
||||
const radiansToDegrees = 180 / math.Pi |
||||
|
||||
// DegreesToRadians converts an angle in degrees to radians
|
||||
func DegreesToRadians(d float64) float64 { |
||||
return d * degreesToRadian |
||||
} |
||||
|
||||
// RadiansToDegrees converts an angle in radians to degress
|
||||
func RadiansToDegrees(r float64) float64 { |
||||
return r * radiansToDegrees |
||||
} |
||||
|
||||
var earthMeanRadiusMeters = 6371008.7714 |
||||
|
||||
func RectFromPointDistance(lon, lat, dist float64) (float64, float64, float64, float64, error) { |
||||
err := checkLongitude(lon) |
||||
if err != nil { |
||||
return 0, 0, 0, 0, err |
||||
} |
||||
err = checkLatitude(lat) |
||||
if err != nil { |
||||
return 0, 0, 0, 0, err |
||||
} |
||||
radLon := DegreesToRadians(lon) |
||||
radLat := DegreesToRadians(lat) |
||||
radDistance := (dist + 7e-2) / earthMeanRadiusMeters |
||||
|
||||
minLatL := radLat - radDistance |
||||
maxLatL := radLat + radDistance |
||||
|
||||
var minLonL, maxLonL float64 |
||||
if minLatL > minLatRad && maxLatL < maxLatRad { |
||||
deltaLon := asin(sin(radDistance) / cos(radLat)) |
||||
minLonL = radLon - deltaLon |
||||
if minLonL < minLonRad { |
||||
minLonL += 2 * math.Pi |
||||
} |
||||
maxLonL = radLon + deltaLon |
||||
if maxLonL > maxLonRad { |
||||
maxLonL -= 2 * math.Pi |
||||
} |
||||
} else { |
||||
// pole is inside distance
|
||||
minLatL = math.Max(minLatL, minLatRad) |
||||
maxLatL = math.Min(maxLatL, maxLatRad) |
||||
minLonL = minLonRad |
||||
maxLonL = maxLonRad |
||||
} |
||||
|
||||
return RadiansToDegrees(minLonL), |
||||
RadiansToDegrees(maxLatL), |
||||
RadiansToDegrees(maxLonL), |
||||
RadiansToDegrees(minLatL), |
||||
nil |
||||
} |
||||
|
||||
func checkLatitude(latitude float64) error { |
||||
if math.IsNaN(latitude) || latitude < minLat || latitude > maxLat { |
||||
return fmt.Errorf("invalid latitude %f; must be between %f and %f", latitude, minLat, maxLat) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func checkLongitude(longitude float64) error { |
||||
if math.IsNaN(longitude) || longitude < minLon || longitude > maxLon { |
||||
return fmt.Errorf("invalid longitude %f; must be between %f and %f", longitude, minLon, maxLon) |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,98 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package geo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
type distanceUnit struct { |
||||
conv float64 |
||||
suffixes []string |
||||
} |
||||
|
||||
var inch = distanceUnit{0.0254, []string{"in", "inch"}} |
||||
var yard = distanceUnit{0.9144, []string{"yd", "yards"}} |
||||
var feet = distanceUnit{0.3048, []string{"ft", "feet"}} |
||||
var kilom = distanceUnit{1000, []string{"km", "kilometers"}} |
||||
var nauticalm = distanceUnit{1852.0, []string{"nm", "nauticalmiles"}} |
||||
var millim = distanceUnit{0.001, []string{"mm", "millimeters"}} |
||||
var centim = distanceUnit{0.01, []string{"cm", "centimeters"}} |
||||
var miles = distanceUnit{1609.344, []string{"mi", "miles"}} |
||||
var meters = distanceUnit{1, []string{"m", "meters"}} |
||||
|
||||
var distanceUnits = []*distanceUnit{ |
||||
&inch, &yard, &feet, &kilom, &nauticalm, &millim, ¢im, &miles, &meters, |
||||
} |
||||
|
||||
// ParseDistance attempts to parse a distance string and return distance in
|
||||
// meters. Example formats supported:
|
||||
// "5in" "5inch" "7yd" "7yards" "9ft" "9feet" "11km" "11kilometers"
|
||||
// "3nm" "3nauticalmiles" "13mm" "13millimeters" "15cm" "15centimeters"
|
||||
// "17mi" "17miles" "19m" "19meters"
|
||||
// If the unit cannot be determined, the entire string is parsed and the
|
||||
// unit of meters is assumed.
|
||||
// If the number portion cannot be parsed, 0 and the parse error are returned.
|
||||
func ParseDistance(d string) (float64, error) { |
||||
for _, unit := range distanceUnits { |
||||
for _, unitSuffix := range unit.suffixes { |
||||
if strings.HasSuffix(d, unitSuffix) { |
||||
parsedNum, err := strconv.ParseFloat(d[0:len(d)-len(unitSuffix)], 64) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
return parsedNum * unit.conv, nil |
||||
} |
||||
} |
||||
} |
||||
// no unit matched, try assuming meters?
|
||||
parsedNum, err := strconv.ParseFloat(d, 64) |
||||
if err != nil { |
||||
return 0, err |
||||
} |
||||
return parsedNum, nil |
||||
} |
||||
|
||||
// ParseDistanceUnit attempts to parse a distance unit and return the
|
||||
// multiplier for converting this to meters. If the unit cannot be parsed
|
||||
// then 0 and the error message is returned.
|
||||
func ParseDistanceUnit(u string) (float64, error) { |
||||
for _, unit := range distanceUnits { |
||||
for _, unitSuffix := range unit.suffixes { |
||||
if u == unitSuffix { |
||||
return unit.conv, nil |
||||
} |
||||
} |
||||
} |
||||
return 0, fmt.Errorf("unknown distance unit: %s", u) |
||||
} |
||||
|
||||
// Haversin computes the distance between two points.
|
||||
// This implemenation uses the sloppy math implemenations which trade off
|
||||
// accuracy for performance. The distance returned is in kilometers.
|
||||
func Haversin(lon1, lat1, lon2, lat2 float64) float64 { |
||||
x1 := lat1 * degreesToRadian |
||||
x2 := lat2 * degreesToRadian |
||||
h1 := 1 - cos(x1-x2) |
||||
h2 := 1 - cos((lon1-lon2)*degreesToRadian) |
||||
h := (h1 + cos(x1)*cos(x2)*h2) / 2 |
||||
avgLat := (x1 + x2) / 2 |
||||
diameter := earthDiameter(avgLat) |
||||
|
||||
return diameter * asin(math.Min(1, math.Sqrt(h))) |
||||
} |
@ -0,0 +1,140 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package geo |
||||
|
||||
import ( |
||||
"reflect" |
||||
"strings" |
||||
) |
||||
|
||||
// ExtractGeoPoint takes an arbitrary interface{} and tries it's best to
|
||||
// interpret it is as geo point. Supported formats:
|
||||
// Container:
|
||||
// slice length 2 (GeoJSON)
|
||||
// first element lon, second element lat
|
||||
// map[string]interface{}
|
||||
// exact keys lat and lon or lng
|
||||
// struct
|
||||
// w/exported fields case-insensitive match on lat and lon or lng
|
||||
// struct
|
||||
// satisfying Later and Loner or Lnger interfaces
|
||||
//
|
||||
// in all cases values must be some sort of numeric-like thing: int/uint/float
|
||||
func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) { |
||||
var foundLon, foundLat bool |
||||
|
||||
thingVal := reflect.ValueOf(thing) |
||||
thingTyp := thingVal.Type() |
||||
|
||||
// is it a slice
|
||||
if thingVal.IsValid() && thingVal.Kind() == reflect.Slice { |
||||
// must be length 2
|
||||
if thingVal.Len() == 2 { |
||||
first := thingVal.Index(0) |
||||
if first.CanInterface() { |
||||
firstVal := first.Interface() |
||||
lon, foundLon = extractNumericVal(firstVal) |
||||
} |
||||
second := thingVal.Index(1) |
||||
if second.CanInterface() { |
||||
secondVal := second.Interface() |
||||
lat, foundLat = extractNumericVal(secondVal) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// is it a map
|
||||
if l, ok := thing.(map[string]interface{}); ok { |
||||
if lval, ok := l["lon"]; ok { |
||||
lon, foundLon = extractNumericVal(lval) |
||||
} else if lval, ok := l["lng"]; ok { |
||||
lon, foundLon = extractNumericVal(lval) |
||||
} |
||||
if lval, ok := l["lat"]; ok { |
||||
lat, foundLat = extractNumericVal(lval) |
||||
} |
||||
} |
||||
|
||||
// now try reflection on struct fields
|
||||
if thingVal.IsValid() && thingVal.Kind() == reflect.Struct { |
||||
for i := 0; i < thingVal.NumField(); i++ { |
||||
fieldName := thingTyp.Field(i).Name |
||||
if strings.HasPrefix(strings.ToLower(fieldName), "lon") { |
||||
if thingVal.Field(i).CanInterface() { |
||||
fieldVal := thingVal.Field(i).Interface() |
||||
lon, foundLon = extractNumericVal(fieldVal) |
||||
} |
||||
} |
||||
if strings.HasPrefix(strings.ToLower(fieldName), "lng") { |
||||
if thingVal.Field(i).CanInterface() { |
||||
fieldVal := thingVal.Field(i).Interface() |
||||
lon, foundLon = extractNumericVal(fieldVal) |
||||
} |
||||
} |
||||
if strings.HasPrefix(strings.ToLower(fieldName), "lat") { |
||||
if thingVal.Field(i).CanInterface() { |
||||
fieldVal := thingVal.Field(i).Interface() |
||||
lat, foundLat = extractNumericVal(fieldVal) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// last hope, some interfaces
|
||||
// lon
|
||||
if l, ok := thing.(loner); ok { |
||||
lon = l.Lon() |
||||
foundLon = true |
||||
} else if l, ok := thing.(lnger); ok { |
||||
lon = l.Lng() |
||||
foundLon = true |
||||
} |
||||
// lat
|
||||
if l, ok := thing.(later); ok { |
||||
lat = l.Lat() |
||||
foundLat = true |
||||
} |
||||
|
||||
return lon, lat, foundLon && foundLat |
||||
} |
||||
|
||||
// extract numeric value (if possible) and returns a float64
|
||||
func extractNumericVal(v interface{}) (float64, bool) { |
||||
val := reflect.ValueOf(v) |
||||
typ := val.Type() |
||||
switch typ.Kind() { |
||||
case reflect.Float32, reflect.Float64: |
||||
return val.Float(), true |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return float64(val.Int()), true |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
return float64(val.Uint()), true |
||||
} |
||||
|
||||
return 0, false |
||||
} |
||||
|
||||
// various support interfaces which can be used to find lat/lon
|
||||
type loner interface { |
||||
Lon() float64 |
||||
} |
||||
|
||||
type later interface { |
||||
Lat() float64 |
||||
} |
||||
|
||||
type lnger interface { |
||||
Lng() float64 |
||||
} |
@ -0,0 +1,212 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package geo |
||||
|
||||
import ( |
||||
"math" |
||||
) |
||||
|
||||
var earthDiameterPerLatitude []float64 |
||||
var sinTab []float64 |
||||
var cosTab []float64 |
||||
var asinTab []float64 |
||||
var asinDer1DivF1Tab []float64 |
||||
var asinDer2DivF2Tab []float64 |
||||
var asinDer3DivF3Tab []float64 |
||||
var asinDer4DivF4Tab []float64 |
||||
|
||||
const radiusTabsSize = (1 << 10) + 1 |
||||
const radiusDelta = (math.Pi / 2) / (radiusTabsSize - 1) |
||||
const radiusIndexer = 1 / radiusDelta |
||||
const sinCosTabsSize = (1 << 11) + 1 |
||||
const asinTabsSize = (1 << 13) + 1 |
||||
const oneDivF2 = 1 / 2.0 |
||||
const oneDivF3 = 1 / 6.0 |
||||
const oneDivF4 = 1 / 24.0 |
||||
|
||||
// 1.57079632673412561417e+00 first 33 bits of pi/2
|
||||
var pio2Hi = math.Float64frombits(0x3FF921FB54400000) |
||||
|
||||
// 6.07710050650619224932e-11 pi/2 - PIO2_HI
|
||||
var pio2Lo = math.Float64frombits(0x3DD0B4611A626331) |
||||
|
||||
var asinPio2Hi = math.Float64frombits(0x3FF921FB54442D18) // 1.57079632679489655800e+00
|
||||
var asinPio2Lo = math.Float64frombits(0x3C91A62633145C07) // 6.12323399573676603587e-17
|
||||
var asinPs0 = math.Float64frombits(0x3fc5555555555555) // 1.66666666666666657415e-01
|
||||
var asinPs1 = math.Float64frombits(0xbfd4d61203eb6f7d) // -3.25565818622400915405e-01
|
||||
var asinPs2 = math.Float64frombits(0x3fc9c1550e884455) // 2.01212532134862925881e-01
|
||||
var asinPs3 = math.Float64frombits(0xbfa48228b5688f3b) // -4.00555345006794114027e-02
|
||||
var asinPs4 = math.Float64frombits(0x3f49efe07501b288) // 7.91534994289814532176e-04
|
||||
var asinPs5 = math.Float64frombits(0x3f023de10dfdf709) // 3.47933107596021167570e-05
|
||||
var asinQs1 = math.Float64frombits(0xc0033a271c8a2d4b) // -2.40339491173441421878e+00
|
||||
var asinQs2 = math.Float64frombits(0x40002ae59c598ac8) // 2.02094576023350569471e+00
|
||||
var asinQs3 = math.Float64frombits(0xbfe6066c1b8d0159) // -6.88283971605453293030e-01
|
||||
var asinQs4 = math.Float64frombits(0x3fb3b8c5b12e9282) // 7.70381505559019352791e-02
|
||||
|
||||
var twoPiHi = 4 * pio2Hi |
||||
var twoPiLo = 4 * pio2Lo |
||||
var sinCosDeltaHi = twoPiHi/sinCosTabsSize - 1 |
||||
var sinCosDeltaLo = twoPiLo/sinCosTabsSize - 1 |
||||
var sinCosIndexer = 1 / (sinCosDeltaHi + sinCosDeltaLo) |
||||
var sinCosMaxValueForIntModulo = ((math.MaxInt64 >> 9) / sinCosIndexer) * 0.99 |
||||
var asinMaxValueForTabs = math.Sin(73.0 * degreesToRadian) |
||||
|
||||
var asinDelta = asinMaxValueForTabs / (asinTabsSize - 1) |
||||
var asinIndexer = 1 / asinDelta |
||||
|
||||
func init() { |
||||
// initializes the tables used for the sloppy math functions
|
||||
|
||||
// sin and cos
|
||||
sinTab = make([]float64, sinCosTabsSize) |
||||
cosTab = make([]float64, sinCosTabsSize) |
||||
sinCosPiIndex := (sinCosTabsSize - 1) / 2 |
||||
sinCosPiMul2Index := 2 * sinCosPiIndex |
||||
sinCosPiMul05Index := sinCosPiIndex / 2 |
||||
sinCosPiMul15Index := 3 * sinCosPiIndex / 2 |
||||
for i := 0; i < sinCosTabsSize; i++ { |
||||
// angle: in [0,2*PI].
|
||||
angle := float64(i)*sinCosDeltaHi + float64(i)*sinCosDeltaLo |
||||
sinAngle := math.Sin(angle) |
||||
cosAngle := math.Cos(angle) |
||||
// For indexes corresponding to null cosine or sine, we make sure the value is zero
|
||||
// and not an epsilon. This allows for a much better accuracy for results close to zero.
|
||||
if i == sinCosPiIndex { |
||||
sinAngle = 0.0 |
||||
} else if i == sinCosPiMul2Index { |
||||
sinAngle = 0.0 |
||||
} else if i == sinCosPiMul05Index { |
||||
sinAngle = 0.0 |
||||
} else if i == sinCosPiMul15Index { |
||||
sinAngle = 0.0 |
||||
} |
||||
sinTab[i] = sinAngle |
||||
cosTab[i] = cosAngle |
||||
} |
||||
|
||||
// asin
|
||||
asinTab = make([]float64, asinTabsSize) |
||||
asinDer1DivF1Tab = make([]float64, asinTabsSize) |
||||
asinDer2DivF2Tab = make([]float64, asinTabsSize) |
||||
asinDer3DivF3Tab = make([]float64, asinTabsSize) |
||||
asinDer4DivF4Tab = make([]float64, asinTabsSize) |
||||
for i := 0; i < asinTabsSize; i++ { |
||||
// x: in [0,ASIN_MAX_VALUE_FOR_TABS].
|
||||
x := float64(i) * asinDelta |
||||
asinTab[i] = math.Asin(x) |
||||
oneMinusXSqInv := 1.0 / (1 - x*x) |
||||
oneMinusXSqInv05 := math.Sqrt(oneMinusXSqInv) |
||||
oneMinusXSqInv15 := oneMinusXSqInv05 * oneMinusXSqInv |
||||
oneMinusXSqInv25 := oneMinusXSqInv15 * oneMinusXSqInv |
||||
oneMinusXSqInv35 := oneMinusXSqInv25 * oneMinusXSqInv |
||||
asinDer1DivF1Tab[i] = oneMinusXSqInv05 |
||||
asinDer2DivF2Tab[i] = (x * oneMinusXSqInv15) * oneDivF2 |
||||
asinDer3DivF3Tab[i] = ((1 + 2*x*x) * oneMinusXSqInv25) * oneDivF3 |
||||
asinDer4DivF4Tab[i] = ((5 + 2*x*(2+x*(5-2*x))) * oneMinusXSqInv35) * oneDivF4 |
||||
} |
||||
|
||||
// earth radius
|
||||
a := 6378137.0 |
||||
b := 6356752.31420 |
||||
a2 := a * a |
||||
b2 := b * b |
||||
earthDiameterPerLatitude = make([]float64, radiusTabsSize) |
||||
earthDiameterPerLatitude[0] = 2.0 * a / 1000 |
||||
earthDiameterPerLatitude[radiusTabsSize-1] = 2.0 * b / 1000 |
||||
for i := 1; i < radiusTabsSize-1; i++ { |
||||
lat := math.Pi * float64(i) / (2*radiusTabsSize - 1) |
||||
one := math.Pow(a2*math.Cos(lat), 2) |
||||
two := math.Pow(b2*math.Sin(lat), 2) |
||||
three := math.Pow(float64(a)*math.Cos(lat), 2) |
||||
four := math.Pow(b*math.Sin(lat), 2) |
||||
radius := math.Sqrt((one + two) / (three + four)) |
||||
earthDiameterPerLatitude[i] = 2 * radius / 1000 |
||||
} |
||||
} |
||||
|
||||
// earthDiameter returns an estimation of the earth's diameter at the specified
|
||||
// latitude in kilometers
|
||||
func earthDiameter(lat float64) float64 { |
||||
index := math.Mod(math.Abs(lat)*radiusIndexer+0.5, float64(len(earthDiameterPerLatitude))) |
||||
if math.IsNaN(index) { |
||||
return 0 |
||||
} |
||||
return earthDiameterPerLatitude[int(index)] |
||||
} |
||||
|
||||
var pio2 = math.Pi / 2 |
||||
|
||||
func sin(a float64) float64 { |
||||
return cos(a - pio2) |
||||
} |
||||
|
||||
// cos is a sloppy math (faster) implementation of math.Cos
|
||||
func cos(a float64) float64 { |
||||
if a < 0.0 { |
||||
a = -a |
||||
} |
||||
if a > sinCosMaxValueForIntModulo { |
||||
return math.Cos(a) |
||||
} |
||||
// index: possibly outside tables range.
|
||||
index := int(a*sinCosIndexer + 0.5) |
||||
delta := (a - float64(index)*sinCosDeltaHi) - float64(index)*sinCosDeltaLo |
||||
// Making sure index is within tables range.
|
||||
// Last value of each table is the same than first, so we ignore it (tabs size minus one) for modulo.
|
||||
index &= (sinCosTabsSize - 2) // index % (SIN_COS_TABS_SIZE-1)
|
||||
indexCos := cosTab[index] |
||||
indexSin := sinTab[index] |
||||
return indexCos + delta*(-indexSin+delta*(-indexCos*oneDivF2+delta*(indexSin*oneDivF3+delta*indexCos*oneDivF4))) |
||||
} |
||||
|
||||
// asin is a sloppy math (faster) implementation of math.Asin
|
||||
func asin(a float64) float64 { |
||||
var negateResult bool |
||||
if a < 0 { |
||||
a = -a |
||||
negateResult = true |
||||
} |
||||
if a <= asinMaxValueForTabs { |
||||
index := int(a*asinIndexer + 0.5) |
||||
delta := a - float64(index)*asinDelta |
||||
result := asinTab[index] + delta*(asinDer1DivF1Tab[index]+delta*(asinDer2DivF2Tab[index]+delta*(asinDer3DivF3Tab[index]+delta*asinDer4DivF4Tab[index]))) |
||||
if negateResult { |
||||
return -result |
||||
} |
||||
return result |
||||
} |
||||
// value > ASIN_MAX_VALUE_FOR_TABS, or value is NaN
|
||||
// This part is derived from fdlibm.
|
||||
if a < 1 { |
||||
t := (1.0 - a) * 0.5 |
||||
p := t * (asinPs0 + t*(asinPs1+t*(asinPs2+t*(asinPs3+t*(asinPs4+t+asinPs5))))) |
||||
q := 1.0 + t*(asinQs1+t*(asinQs2+t*(asinQs3+t*asinQs4))) |
||||
s := math.Sqrt(t) |
||||
z := s + s*(p/q) |
||||
result := asinPio2Hi - ((z + z) - asinPio2Lo) |
||||
if negateResult { |
||||
return -result |
||||
} |
||||
return result |
||||
} |
||||
// value >= 1.0, or value is NaN
|
||||
if a == 1.0 { |
||||
if negateResult { |
||||
return -math.Pi / 2 |
||||
} |
||||
return math.Pi / 2 |
||||
} |
||||
return math.NaN() |
||||
} |
@ -0,0 +1,43 @@ |
||||
package numeric |
||||
|
||||
var interleaveMagic = []uint64{ |
||||
0x5555555555555555, |
||||
0x3333333333333333, |
||||
0x0F0F0F0F0F0F0F0F, |
||||
0x00FF00FF00FF00FF, |
||||
0x0000FFFF0000FFFF, |
||||
0x00000000FFFFFFFF, |
||||
0xAAAAAAAAAAAAAAAA, |
||||
} |
||||
|
||||
var interleaveShift = []uint{1, 2, 4, 8, 16} |
||||
|
||||
// Interleave the first 32 bits of each uint64
|
||||
// apdated from org.apache.lucene.util.BitUtil
|
||||
// whcih was adapted from:
|
||||
// http://graphics.stanford.edu/~seander/bithacks.html#InterleaveBMN
|
||||
func Interleave(v1, v2 uint64) uint64 { |
||||
v1 = (v1 | (v1 << interleaveShift[4])) & interleaveMagic[4] |
||||
v1 = (v1 | (v1 << interleaveShift[3])) & interleaveMagic[3] |
||||
v1 = (v1 | (v1 << interleaveShift[2])) & interleaveMagic[2] |
||||
v1 = (v1 | (v1 << interleaveShift[1])) & interleaveMagic[1] |
||||
v1 = (v1 | (v1 << interleaveShift[0])) & interleaveMagic[0] |
||||
v2 = (v2 | (v2 << interleaveShift[4])) & interleaveMagic[4] |
||||
v2 = (v2 | (v2 << interleaveShift[3])) & interleaveMagic[3] |
||||
v2 = (v2 | (v2 << interleaveShift[2])) & interleaveMagic[2] |
||||
v2 = (v2 | (v2 << interleaveShift[1])) & interleaveMagic[1] |
||||
v2 = (v2 | (v2 << interleaveShift[0])) & interleaveMagic[0] |
||||
return (v2 << 1) | v1 |
||||
} |
||||
|
||||
// Deinterleave the 32-bit value starting at position 0
|
||||
// to get the other 32-bit value, shift it by 1 first
|
||||
func Deinterleave(b uint64) uint64 { |
||||
b &= interleaveMagic[0] |
||||
b = (b ^ (b >> interleaveShift[0])) & interleaveMagic[1] |
||||
b = (b ^ (b >> interleaveShift[1])) & interleaveMagic[2] |
||||
b = (b ^ (b >> interleaveShift[2])) & interleaveMagic[3] |
||||
b = (b ^ (b >> interleaveShift[3])) & interleaveMagic[4] |
||||
b = (b ^ (b >> interleaveShift[4])) & interleaveMagic[5] |
||||
return b |
||||
} |
@ -0,0 +1,113 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package query |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/geo" |
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/mapping" |
||||
"github.com/blevesearch/bleve/search" |
||||
"github.com/blevesearch/bleve/search/searcher" |
||||
) |
||||
|
||||
type GeoBoundingBoxQuery struct { |
||||
TopLeft []float64 `json:"top_left,omitempty"` |
||||
BottomRight []float64 `json:"bottom_right,omitempty"` |
||||
FieldVal string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
} |
||||
|
||||
func NewGeoBoundingBoxQuery(topLeftLon, topLeftLat, bottomRightLon, bottomRightLat float64) *GeoBoundingBoxQuery { |
||||
return &GeoBoundingBoxQuery{ |
||||
TopLeft: []float64{topLeftLon, topLeftLat}, |
||||
BottomRight: []float64{bottomRightLon, bottomRightLat}, |
||||
} |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) SetBoost(b float64) { |
||||
boost := Boost(b) |
||||
q.BoostVal = &boost |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) Boost() float64 { |
||||
return q.BoostVal.Value() |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) SetField(f string) { |
||||
q.FieldVal = f |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) Field() string { |
||||
return q.FieldVal |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { |
||||
field := q.FieldVal |
||||
if q.FieldVal == "" { |
||||
field = m.DefaultSearchField() |
||||
} |
||||
|
||||
if q.BottomRight[0] < q.TopLeft[0] { |
||||
// cross date line, rewrite as two parts
|
||||
|
||||
leftSearcher, err := searcher.NewGeoBoundingBoxSearcher(i, -180, q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rightSearcher, err := searcher.NewGeoBoundingBoxSearcher(i, q.TopLeft[0], q.BottomRight[1], 180, q.TopLeft[1], field, q.BoostVal.Value(), options, true) |
||||
if err != nil { |
||||
_ = leftSearcher.Close() |
||||
return nil, err |
||||
} |
||||
|
||||
return searcher.NewDisjunctionSearcher(i, []search.Searcher{leftSearcher, rightSearcher}, 0, options) |
||||
} |
||||
|
||||
return searcher.NewGeoBoundingBoxSearcher(i, q.TopLeft[0], q.BottomRight[1], q.BottomRight[0], q.TopLeft[1], field, q.BoostVal.Value(), options, true) |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
func (q *GeoBoundingBoxQuery) UnmarshalJSON(data []byte) error { |
||||
tmp := struct { |
||||
TopLeft interface{} `json:"top_left,omitempty"` |
||||
BottomRight interface{} `json:"bottom_right,omitempty"` |
||||
FieldVal string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
}{} |
||||
err := json.Unmarshal(data, &tmp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// now use our generic point parsing code from the geo package
|
||||
lon, lat, found := geo.ExtractGeoPoint(tmp.TopLeft) |
||||
if !found { |
||||
return fmt.Errorf("geo location top_left not in a valid format") |
||||
} |
||||
q.TopLeft = []float64{lon, lat} |
||||
lon, lat, found = geo.ExtractGeoPoint(tmp.BottomRight) |
||||
if !found { |
||||
return fmt.Errorf("geo location bottom_right not in a valid format") |
||||
} |
||||
q.BottomRight = []float64{lon, lat} |
||||
q.FieldVal = tmp.FieldVal |
||||
q.BoostVal = tmp.BoostVal |
||||
return nil |
||||
} |
@ -0,0 +1,100 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package query |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/geo" |
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/mapping" |
||||
"github.com/blevesearch/bleve/search" |
||||
"github.com/blevesearch/bleve/search/searcher" |
||||
) |
||||
|
||||
type GeoDistanceQuery struct { |
||||
Location []float64 `json:"location,omitempty"` |
||||
Distance string `json:"distance,omitempty"` |
||||
FieldVal string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
} |
||||
|
||||
func NewGeoDistanceQuery(lon, lat float64, distance string) *GeoDistanceQuery { |
||||
return &GeoDistanceQuery{ |
||||
Location: []float64{lon, lat}, |
||||
Distance: distance, |
||||
} |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) SetBoost(b float64) { |
||||
boost := Boost(b) |
||||
q.BoostVal = &boost |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) Boost() float64 { |
||||
return q.BoostVal.Value() |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) SetField(f string) { |
||||
q.FieldVal = f |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) Field() string { |
||||
return q.FieldVal |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, |
||||
options search.SearcherOptions) (search.Searcher, error) { |
||||
field := q.FieldVal |
||||
if q.FieldVal == "" { |
||||
field = m.DefaultSearchField() |
||||
} |
||||
|
||||
dist, err := geo.ParseDistance(q.Distance) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return searcher.NewGeoPointDistanceSearcher(i, q.Location[0], q.Location[1], |
||||
dist, field, q.BoostVal.Value(), options) |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) Validate() error { |
||||
return nil |
||||
} |
||||
|
||||
func (q *GeoDistanceQuery) UnmarshalJSON(data []byte) error { |
||||
tmp := struct { |
||||
Location interface{} `json:"location,omitempty"` |
||||
Distance string `json:"distance,omitempty"` |
||||
FieldVal string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
}{} |
||||
err := json.Unmarshal(data, &tmp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
// now use our generic point parsing code from the geo package
|
||||
lon, lat, found := geo.ExtractGeoPoint(tmp.Location) |
||||
if !found { |
||||
return fmt.Errorf("geo location not in a valid format") |
||||
} |
||||
q.Location = []float64{lon, lat} |
||||
q.Distance = tmp.Distance |
||||
q.FieldVal = tmp.FieldVal |
||||
q.BoostVal = tmp.BoostVal |
||||
return nil |
||||
} |
@ -0,0 +1,80 @@ |
||||
// Copyright (c) 2014 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package query |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/mapping" |
||||
"github.com/blevesearch/bleve/search" |
||||
"github.com/blevesearch/bleve/search/searcher" |
||||
) |
||||
|
||||
type MultiPhraseQuery struct { |
||||
Terms [][]string `json:"terms"` |
||||
Field string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
} |
||||
|
||||
// NewMultiPhraseQuery creates a new Query for finding
|
||||
// term phrases in the index.
|
||||
// It is like PhraseQuery, but each position in the
|
||||
// phrase may be satisfied by a list of terms
|
||||
// as opposed to just one.
|
||||
// At least one of the terms must exist in the correct
|
||||
// order, at the correct index offsets, in the
|
||||
// specified field. Queried field must have been indexed with
|
||||
// IncludeTermVectors set to true.
|
||||
func NewMultiPhraseQuery(terms [][]string, field string) *MultiPhraseQuery { |
||||
return &MultiPhraseQuery{ |
||||
Terms: terms, |
||||
Field: field, |
||||
} |
||||
} |
||||
|
||||
func (q *MultiPhraseQuery) SetBoost(b float64) { |
||||
boost := Boost(b) |
||||
q.BoostVal = &boost |
||||
} |
||||
|
||||
func (q *MultiPhraseQuery) Boost() float64 { |
||||
return q.BoostVal.Value() |
||||
} |
||||
|
||||
func (q *MultiPhraseQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { |
||||
return searcher.NewMultiPhraseSearcher(i, q.Terms, q.Field, options) |
||||
} |
||||
|
||||
func (q *MultiPhraseQuery) Validate() error { |
||||
if len(q.Terms) < 1 { |
||||
return fmt.Errorf("phrase query must contain at least one term") |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (q *MultiPhraseQuery) UnmarshalJSON(data []byte) error { |
||||
type _mphraseQuery MultiPhraseQuery |
||||
tmp := _mphraseQuery{} |
||||
err := json.Unmarshal(data, &tmp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
q.Terms = tmp.Terms |
||||
q.Field = tmp.Field |
||||
q.BoostVal = tmp.BoostVal |
||||
return nil |
||||
} |
@ -0,0 +1,95 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package query |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/mapping" |
||||
"github.com/blevesearch/bleve/search" |
||||
"github.com/blevesearch/bleve/search/searcher" |
||||
) |
||||
|
||||
type TermRangeQuery struct { |
||||
Min string `json:"min,omitempty"` |
||||
Max string `json:"max,omitempty"` |
||||
InclusiveMin *bool `json:"inclusive_min,omitempty"` |
||||
InclusiveMax *bool `json:"inclusive_max,omitempty"` |
||||
FieldVal string `json:"field,omitempty"` |
||||
BoostVal *Boost `json:"boost,omitempty"` |
||||
} |
||||
|
||||
// NewTermRangeQuery creates a new Query for ranges
|
||||
// of text term values.
|
||||
// Either, but not both endpoints can be nil.
|
||||
// The minimum value is inclusive.
|
||||
// The maximum value is exclusive.
|
||||
func NewTermRangeQuery(min, max string) *TermRangeQuery { |
||||
return NewTermRangeInclusiveQuery(min, max, nil, nil) |
||||
} |
||||
|
||||
// NewTermRangeInclusiveQuery creates a new Query for ranges
|
||||
// of numeric values.
|
||||
// Either, but not both endpoints can be nil.
|
||||
// Control endpoint inclusion with inclusiveMin, inclusiveMax.
|
||||
func NewTermRangeInclusiveQuery(min, max string, minInclusive, maxInclusive *bool) *TermRangeQuery { |
||||
return &TermRangeQuery{ |
||||
Min: min, |
||||
Max: max, |
||||
InclusiveMin: minInclusive, |
||||
InclusiveMax: maxInclusive, |
||||
} |
||||
} |
||||
|
||||
func (q *TermRangeQuery) SetBoost(b float64) { |
||||
boost := Boost(b) |
||||
q.BoostVal = &boost |
||||
} |
||||
|
||||
func (q *TermRangeQuery) Boost() float64 { |
||||
return q.BoostVal.Value() |
||||
} |
||||
|
||||
func (q *TermRangeQuery) SetField(f string) { |
||||
q.FieldVal = f |
||||
} |
||||
|
||||
func (q *TermRangeQuery) Field() string { |
||||
return q.FieldVal |
||||
} |
||||
|
||||
func (q *TermRangeQuery) Searcher(i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) { |
||||
field := q.FieldVal |
||||
if q.FieldVal == "" { |
||||
field = m.DefaultSearchField() |
||||
} |
||||
var minTerm []byte |
||||
if q.Min != "" { |
||||
minTerm = []byte(q.Min) |
||||
} |
||||
var maxTerm []byte |
||||
if q.Max != "" { |
||||
maxTerm = []byte(q.Max) |
||||
} |
||||
return searcher.NewTermRangeSearcher(i, minTerm, maxTerm, q.InclusiveMin, q.InclusiveMax, field, q.BoostVal.Value(), options) |
||||
} |
||||
|
||||
func (q *TermRangeQuery) Validate() error { |
||||
if q.Min == "" && q.Min == q.Max { |
||||
return fmt.Errorf("term range query must specify min or max") |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,88 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package searcher |
||||
|
||||
import ( |
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/search" |
||||
) |
||||
|
||||
// FilterFunc defines a function which can filter documents
|
||||
// returning true means keep the document
|
||||
// returning false means do not keep the document
|
||||
type FilterFunc func(d *search.DocumentMatch) bool |
||||
|
||||
// FilteringSearcher wraps any other searcher, but checks any Next/Advance
|
||||
// call against the supplied FilterFunc
|
||||
type FilteringSearcher struct { |
||||
child search.Searcher |
||||
accept FilterFunc |
||||
} |
||||
|
||||
func NewFilteringSearcher(s search.Searcher, filter FilterFunc) *FilteringSearcher { |
||||
return &FilteringSearcher{ |
||||
child: s, |
||||
accept: filter, |
||||
} |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) { |
||||
next, err := f.child.Next(ctx) |
||||
for next != nil && err == nil { |
||||
if f.accept(next) { |
||||
return next, nil |
||||
} |
||||
next, err = f.child.Next(ctx) |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) { |
||||
adv, err := f.child.Advance(ctx, ID) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if adv == nil { |
||||
return nil, nil |
||||
} |
||||
if f.accept(adv) { |
||||
return adv, nil |
||||
} |
||||
return f.Next(ctx) |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Close() error { |
||||
return f.child.Close() |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Weight() float64 { |
||||
return f.child.Weight() |
||||
} |
||||
|
||||
func (f *FilteringSearcher) SetQueryNorm(n float64) { |
||||
f.child.SetQueryNorm(n) |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Count() uint64 { |
||||
return f.child.Count() |
||||
} |
||||
|
||||
func (f *FilteringSearcher) Min() int { |
||||
return f.child.Min() |
||||
} |
||||
|
||||
func (f *FilteringSearcher) DocumentMatchPoolSize() int { |
||||
return f.child.DocumentMatchPoolSize() |
||||
} |
@ -0,0 +1,173 @@ |
||||
// Copyright (c) 2017 Couchbase, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package searcher |
||||
|
||||
import ( |
||||
"github.com/blevesearch/bleve/document" |
||||
"github.com/blevesearch/bleve/geo" |
||||
"github.com/blevesearch/bleve/index" |
||||
"github.com/blevesearch/bleve/numeric" |
||||
"github.com/blevesearch/bleve/search" |
||||
) |
||||
|
||||
func NewGeoBoundingBoxSearcher(indexReader index.IndexReader, minLon, minLat, |
||||
maxLon, maxLat float64, field string, boost float64, |
||||
options search.SearcherOptions, checkBoundaries bool) ( |
||||
search.Searcher, error) { |
||||
|
||||
// track list of opened searchers, for cleanup on early exit
|
||||
var openedSearchers []search.Searcher |
||||
cleanupOpenedSearchers := func() { |
||||
for _, s := range openedSearchers { |
||||
_ = s.Close() |
||||
} |
||||
} |
||||
|
||||
// do math to produce list of terms needed for this search
|
||||
onBoundaryTerms, notOnBoundaryTerms := ComputeGeoRange(0, (geo.GeoBits<<1)-1, |
||||
minLon, minLat, maxLon, maxLat, checkBoundaries) |
||||
|
||||
var onBoundarySearcher search.Searcher |
||||
if len(onBoundaryTerms) > 0 { |
||||
rawOnBoundarySearcher, err := NewMultiTermSearcherBytes(indexReader, |
||||
onBoundaryTerms, field, boost, options, false) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
// add filter to check points near the boundary
|
||||
onBoundarySearcher = NewFilteringSearcher(rawOnBoundarySearcher, |
||||
buildRectFilter(indexReader, field, minLon, minLat, maxLon, maxLat)) |
||||
openedSearchers = append(openedSearchers, onBoundarySearcher) |
||||
} |
||||
|
||||
var notOnBoundarySearcher search.Searcher |
||||
if len(notOnBoundaryTerms) > 0 { |
||||
var err error |
||||
notOnBoundarySearcher, err = NewMultiTermSearcherBytes(indexReader, |
||||
notOnBoundaryTerms, field, boost, options, false) |
||||
if err != nil { |
||||
cleanupOpenedSearchers() |
||||
return nil, err |
||||
} |
||||
openedSearchers = append(openedSearchers, notOnBoundarySearcher) |
||||
} |
||||
|
||||
if onBoundarySearcher != nil && notOnBoundarySearcher != nil { |
||||
rv, err := NewDisjunctionSearcher(indexReader, |
||||
[]search.Searcher{ |
||||
onBoundarySearcher, |
||||
notOnBoundarySearcher, |
||||
}, |
||||
0, options) |
||||
if err != nil { |
||||
cleanupOpenedSearchers() |
||||
return nil, err |
||||
} |
||||
return rv, nil |
||||
} else if onBoundarySearcher != nil { |
||||
return onBoundarySearcher, nil |
||||
} else if notOnBoundarySearcher != nil { |
||||
return notOnBoundarySearcher, nil |
||||
} |
||||
|
||||
return NewMatchNoneSearcher(indexReader) |
||||
} |
||||
|
||||
var geoMaxShift = document.GeoPrecisionStep * 4 |
||||
var geoDetailLevel = ((geo.GeoBits << 1) - geoMaxShift) / 2 |
||||
|
||||
func ComputeGeoRange(term uint64, shift uint, |
||||
sminLon, sminLat, smaxLon, smaxLat float64, |
||||
checkBoundaries bool) ( |
||||
onBoundary [][]byte, notOnBoundary [][]byte) { |
||||
split := term | uint64(0x1)<<shift |
||||
var upperMax uint64 |
||||
if shift < 63 { |
||||
upperMax = term | ((uint64(1) << (shift + 1)) - 1) |
||||
} else { |
||||
upperMax = 0xffffffffffffffff |
||||
} |
||||
lowerMax := split - 1 |
||||
onBoundary, notOnBoundary = relateAndRecurse(term, lowerMax, shift, |
||||
sminLon, sminLat, smaxLon, smaxLat, checkBoundaries) |
||||
plusOnBoundary, plusNotOnBoundary := relateAndRecurse(split, upperMax, shift, |
||||
sminLon, sminLat, smaxLon, smaxLat, checkBoundaries) |
||||
onBoundary = append(onBoundary, plusOnBoundary...) |
||||
notOnBoundary = append(notOnBoundary, plusNotOnBoundary...) |
||||
return |
||||
} |
||||
|
||||
func relateAndRecurse(start, end uint64, res uint, |
||||
sminLon, sminLat, smaxLon, smaxLat float64, |
||||
checkBoundaries bool) ( |
||||
onBoundary [][]byte, notOnBoundary [][]byte) { |
||||
minLon := geo.MortonUnhashLon(start) |
||||
minLat := geo.MortonUnhashLat(start) |
||||
maxLon := geo.MortonUnhashLon(end) |
||||
maxLat := geo.MortonUnhashLat(end) |
||||
|
||||
level := ((geo.GeoBits << 1) - res) >> 1 |
||||
|
||||
within := res%document.GeoPrecisionStep == 0 && |
||||
geo.RectWithin(minLon, minLat, maxLon, maxLat, |
||||
sminLon, sminLat, smaxLon, smaxLat) |
||||
if within || (level == geoDetailLevel && |
||||
geo.RectIntersects(minLon, minLat, maxLon, maxLat, |
||||
sminLon, sminLat, smaxLon, smaxLat)) { |
||||
if !within && checkBoundaries { |
||||
return [][]byte{ |
||||
numeric.MustNewPrefixCodedInt64(int64(start), res), |
||||
}, nil |
||||
} |
||||
return nil, |
||||
[][]byte{ |
||||
numeric.MustNewPrefixCodedInt64(int64(start), res), |
||||
} |
||||
} else if level < geoDetailLevel && |
||||
geo.RectIntersects(minLon, minLat, maxLon, maxLat, |
||||
sminLon, sminLat, smaxLon, smaxLat) { |
||||
return ComputeGeoRange(start, res-1, sminLon, sminLat, smaxLon, smaxLat, |
||||
checkBoundaries) |
||||
} |
||||
return nil, nil |
||||
} |
||||
|
||||
func buildRectFilter(indexReader index.IndexReader, field string, |
||||
minLon, minLat, maxLon, maxLat float64) FilterFunc { |
||||
return func(d *search.DocumentMatch) bool { |
||||
var lon, lat float64 |
||||
var found bool |
||||
err := indexReader.DocumentVisitFieldTerms(d.IndexInternalID, |
||||
[]string{field}, func(field string, term []byte) { |
||||
// only consider the values which are shifted 0
|
||||
prefixCoded := numeric.PrefixCoded(term) |
||||
shift, err := prefixCoded.Shift() |
||||
if err == nil && shift == 0 { |
||||
var i64 int64 |
||||
i64, err = prefixCoded.Int64() |
||||
if err == nil { |
||||
lon = geo.MortonUnhashLon(uint64(i64)) |
||||
lat = geo.MortonUnhashLat(uint64(i64)) |
||||
found = true |
||||
} |
||||
} |
||||
}) |
||||
if err == nil && found { |
||||
return geo.BoundingBoxContains(lon, lat, |
||||
minLon, minLat, maxLon, maxLat) |
||||
} |
||||
return false |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue