introduce new query TermRange
The term range query is not often used in full-text queries, but can be useful when filtering on keyword indexed text terms in the index. The JSON syntax to do a TermRange query is the same as for NumericRange, but the min/max values must be string and not float64.
This commit is contained in:
parent
4d00d863af
commit
1eba5541f2
17
query.go
17
query.go
|
@ -139,6 +139,23 @@ func NewNumericRangeInclusiveQuery(min, max *float64, minInclusive, maxInclusive
|
||||||
return query.NewNumericRangeInclusiveQuery(min, max, minInclusive, maxInclusive)
|
return query.NewNumericRangeInclusiveQuery(min, max, minInclusive, maxInclusive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTermRangeQuery creates a new Query for ranges
|
||||||
|
// of text terms.
|
||||||
|
// Either, but not both endpoints can be "".
|
||||||
|
// The minimum value is inclusive.
|
||||||
|
// The maximum value is exclusive.
|
||||||
|
func NewTermRangeQuery(min, max string) *query.TermRangeQuery {
|
||||||
|
return query.NewTermRangeQuery(min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTermRangeInclusiveQuery creates a new Query for ranges
|
||||||
|
// of text terms.
|
||||||
|
// Either, but not both endpoints can be "".
|
||||||
|
// Control endpoint inclusion with inclusiveMin, inclusiveMax.
|
||||||
|
func NewTermRangeInclusiveQuery(min, max string, minInclusive, maxInclusive *bool) *query.TermRangeQuery {
|
||||||
|
return query.NewTermRangeInclusiveQuery(min, max, minInclusive, maxInclusive)
|
||||||
|
}
|
||||||
|
|
||||||
// NewPhraseQuery creates a new Query for finding
|
// NewPhraseQuery creates a new Query for finding
|
||||||
// exact term phrases in the index.
|
// exact term phrases in the index.
|
||||||
// The provided terms must exist in the correct
|
// The provided terms must exist in the correct
|
||||||
|
|
|
@ -161,8 +161,8 @@ func ParseQuery(input []byte) (Query, error) {
|
||||||
}
|
}
|
||||||
return &rv, nil
|
return &rv, nil
|
||||||
}
|
}
|
||||||
_, hasMin := tmp["min"]
|
_, hasMin := tmp["min"].(float64)
|
||||||
_, hasMax := tmp["max"]
|
_, hasMax := tmp["max"].(float64)
|
||||||
if hasMin || hasMax {
|
if hasMin || hasMax {
|
||||||
var rv NumericRangeQuery
|
var rv NumericRangeQuery
|
||||||
err := json.Unmarshal(input, &rv)
|
err := json.Unmarshal(input, &rv)
|
||||||
|
@ -171,6 +171,16 @@ func ParseQuery(input []byte) (Query, error) {
|
||||||
}
|
}
|
||||||
return &rv, nil
|
return &rv, nil
|
||||||
}
|
}
|
||||||
|
_, hasMinStr := tmp["min"].(string)
|
||||||
|
_, hasMaxStr := tmp["max"].(string)
|
||||||
|
if hasMinStr || hasMaxStr {
|
||||||
|
var rv TermRangeQuery
|
||||||
|
err := json.Unmarshal(input, &rv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rv, nil
|
||||||
|
}
|
||||||
_, hasStart := tmp["start"]
|
_, hasStart := tmp["start"]
|
||||||
_, hasEnd := tmp["end"]
|
_, hasEnd := tmp["end"]
|
||||||
if hasStart || hasEnd {
|
if hasStart || hasEnd {
|
||||||
|
|
|
@ -25,6 +25,8 @@ import (
|
||||||
|
|
||||||
var minNum = 5.1
|
var minNum = 5.1
|
||||||
var maxNum = 7.1
|
var maxNum = 7.1
|
||||||
|
var minTerm = "bob"
|
||||||
|
var maxTerm = "cat"
|
||||||
var startDateStr = "2011-01-01T00:00:00Z"
|
var startDateStr = "2011-01-01T00:00:00Z"
|
||||||
var endDateStr = "2012-01-01T00:00:00Z"
|
var endDateStr = "2012-01-01T00:00:00Z"
|
||||||
var startDate time.Time
|
var startDate time.Time
|
||||||
|
@ -142,6 +144,14 @@ func TestParseQuery(t *testing.T) {
|
||||||
return q
|
return q
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`{"min":"bob","max":"cat","field":"desc"}`),
|
||||||
|
output: func() Query {
|
||||||
|
q := NewTermRangeQuery(minTerm, maxTerm)
|
||||||
|
q.SetField("desc")
|
||||||
|
return q
|
||||||
|
}(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`),
|
input: []byte(`{"start":"` + startDateStr + `","end":"` + endDateStr + `","field":"desc"}`),
|
||||||
output: func() Query {
|
output: func() Query {
|
||||||
|
|
|
@ -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,75 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTermRangeSearcher(indexReader index.IndexReader,
|
||||||
|
min, max []byte, inclusiveMin, inclusiveMax *bool, field string,
|
||||||
|
boost float64, options search.SearcherOptions) (search.Searcher, error) {
|
||||||
|
|
||||||
|
if inclusiveMin == nil {
|
||||||
|
defaultInclusiveMin := true
|
||||||
|
inclusiveMin = &defaultInclusiveMin
|
||||||
|
}
|
||||||
|
if inclusiveMax == nil {
|
||||||
|
defaultInclusiveMax := false
|
||||||
|
inclusiveMax = &defaultInclusiveMax
|
||||||
|
}
|
||||||
|
|
||||||
|
if min == nil {
|
||||||
|
min = []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeMax := max
|
||||||
|
if rangeMax != nil {
|
||||||
|
// the term dictionary range end has an unfortunate implementation
|
||||||
|
rangeMax = append(rangeMax, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the terms with this prefix
|
||||||
|
fieldDict, err := indexReader.FieldDictRange(field, min, rangeMax)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var terms []string
|
||||||
|
tfd, err := fieldDict.Next()
|
||||||
|
for err == nil && tfd != nil {
|
||||||
|
terms = append(terms, tfd.Term)
|
||||||
|
tfd, err = fieldDict.Next()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(terms) < 1 {
|
||||||
|
return NewMatchNoneSearcher(indexReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*inclusiveMin && min != nil && string(min) == terms[0] {
|
||||||
|
terms = terms[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if our term list included the max, it would be the last item
|
||||||
|
if !*inclusiveMax && max != nil && string(max) == terms[len(terms)-1] {
|
||||||
|
terms = terms[:len(terms)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewMultiTermSearcher(indexReader, terms, field, boost, options)
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
// 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 (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/blevesearch/bleve/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTermRangeSearch(t *testing.T) {
|
||||||
|
|
||||||
|
twoDocIndexReader, err := twoDocIndex.Reader()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := twoDocIndexReader.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
min []byte
|
||||||
|
max []byte
|
||||||
|
inclusiveMin bool
|
||||||
|
inclusiveMax bool
|
||||||
|
field string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: []byte("marty"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: []byte("ravi"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1", "4"},
|
||||||
|
},
|
||||||
|
// inclusive max false should exclude ravi
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: []byte("ravi"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: false,
|
||||||
|
want: []string{"1"},
|
||||||
|
},
|
||||||
|
// inclusive max false should remove last/only item
|
||||||
|
{
|
||||||
|
min: []byte("martz"),
|
||||||
|
max: []byte("ravi"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: false,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
// inclusive min false should remove marty
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: []byte("ravi"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: false,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"4"},
|
||||||
|
},
|
||||||
|
// inclusive min false should remove first/only item
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: []byte("rav"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: false,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
// max nil sees everyting after marty
|
||||||
|
{
|
||||||
|
min: []byte("marty"),
|
||||||
|
max: nil,
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1", "2", "4"},
|
||||||
|
},
|
||||||
|
// min nil sees everyting before ravi
|
||||||
|
{
|
||||||
|
min: nil,
|
||||||
|
max: []byte("ravi"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
// min and max nil sees everything
|
||||||
|
{
|
||||||
|
min: nil,
|
||||||
|
max: nil,
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1", "2", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
// min and max nil sees everything, even with inclusiveMin false
|
||||||
|
{
|
||||||
|
min: nil,
|
||||||
|
max: nil,
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: false,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: []string{"1", "2", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
// min and max nil sees everything, even with inclusiveMax false
|
||||||
|
{
|
||||||
|
min: nil,
|
||||||
|
max: nil,
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: false,
|
||||||
|
want: []string{"1", "2", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
// min and max nil sees everything, even with both false
|
||||||
|
{
|
||||||
|
min: nil,
|
||||||
|
max: nil,
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: false,
|
||||||
|
inclusiveMax: false,
|
||||||
|
want: []string{"1", "2", "3", "4", "5"},
|
||||||
|
},
|
||||||
|
// min and max non-nil, but match 0 terms
|
||||||
|
{
|
||||||
|
min: []byte("martz"),
|
||||||
|
max: []byte("rav"),
|
||||||
|
field: "name",
|
||||||
|
inclusiveMin: true,
|
||||||
|
inclusiveMax: true,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
|
||||||
|
searcher, err := NewTermRangeSearcher(twoDocIndexReader, test.min, test.max,
|
||||||
|
&test.inclusiveMin, &test.inclusiveMax, test.field, 1.0, search.SearcherOptions{Explain: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var got []string
|
||||||
|
ctx := &search.SearchContext{
|
||||||
|
DocumentMatchPool: search.NewDocumentMatchPool(
|
||||||
|
searcher.DocumentMatchPoolSize(), 0),
|
||||||
|
}
|
||||||
|
next, err := searcher.Next(ctx)
|
||||||
|
i := 0
|
||||||
|
for err == nil && next != nil {
|
||||||
|
got = append(got, string(next.IndexInternalID))
|
||||||
|
ctx.DocumentMatchPool.Put(next)
|
||||||
|
next, err = searcher.Next(ctx)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error iterating searcher: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, test.want) {
|
||||||
|
t.Errorf("expected: %v, got %v for test %#v", test.want, got, test)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -801,5 +801,28 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"comment": "test term range",
|
||||||
|
"search": {
|
||||||
|
"from": 0,
|
||||||
|
"size": 10,
|
||||||
|
"query": {
|
||||||
|
"field": "title",
|
||||||
|
"max": "miz",
|
||||||
|
"min": "mis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"total_hits": 2,
|
||||||
|
"hits": [
|
||||||
|
{
|
||||||
|
"id": "a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue