Adding sort to SearchRequest.
This commit is contained in:
parent
aa3ae3d39c
commit
5164e70f6e
|
@ -384,8 +384,6 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
|
|||
return nil, ErrorIndexClosed
|
||||
}
|
||||
|
||||
collector := collectors.NewTopScorerSkipCollector(req.Size, req.From)
|
||||
|
||||
// open a reader for this search
|
||||
indexReader, err := i.i.Reader()
|
||||
if err != nil {
|
||||
|
@ -407,6 +405,9 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
|
|||
}
|
||||
}()
|
||||
|
||||
collector := collectors.NewHeapCollector(req.Size, req.From, indexReader, req.Sort)
|
||||
//collector := collectors.NewTopScorerSkipCollector(req.Size, req.From)
|
||||
|
||||
if req.Facets != nil {
|
||||
facetsBuilder := search.NewFacetsBuilder(indexReader)
|
||||
for facetName, facetRequest := range req.Facets {
|
||||
|
|
|
@ -29,6 +29,9 @@ import (
|
|||
"github.com/blevesearch/bleve/index"
|
||||
"github.com/blevesearch/bleve/index/store/null"
|
||||
"github.com/blevesearch/bleve/search"
|
||||
|
||||
"github.com/Pallinder/go-randomdata"
|
||||
"github.com/divan/num2words"
|
||||
)
|
||||
|
||||
func TestCrud(t *testing.T) {
|
||||
|
@ -715,6 +718,52 @@ func TestIndexMetadataRaceBug198(t *testing.T) {
|
|||
close(done)
|
||||
}
|
||||
|
||||
func TestSortMatchSearch(t *testing.T) {
|
||||
defer func() {
|
||||
err := os.RemoveAll("testidx")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
index, err := New("testidx", NewIndexMapping())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
doc := make(map[string]interface{})
|
||||
doc["Name"] = randomdata.SillyName()
|
||||
doc["Day"] = randomdata.Day()
|
||||
doc["Number"] = num2words.Convert(i)
|
||||
err = index.Index(fmt.Sprintf("%d", i), doc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
req := NewSearchRequest(NewMatchQuery("one"))
|
||||
req.SortBy("Day", true)
|
||||
req.SortBy("Name", true)
|
||||
req.Fields = []string{"*"}
|
||||
sr, err := index.Search(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prev := ""
|
||||
for _, hit := range sr.Hits {
|
||||
val := hit.Fields["Day"].(string)
|
||||
if prev > val {
|
||||
t.Errorf("Hits must be sorted by 'Day'. Found '%s' before '%s'", prev, val)
|
||||
}
|
||||
prev = val
|
||||
}
|
||||
err = index.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexCountMatchSearch(t *testing.T) {
|
||||
defer func() {
|
||||
err := os.RemoveAll("testidx")
|
||||
|
|
|
@ -201,6 +201,7 @@ type SearchRequest struct {
|
|||
Fields []string `json:"fields"`
|
||||
Facets FacetsRequest `json:"facets"`
|
||||
Explain bool `json:"explain"`
|
||||
Sort search.SortOrder `json:"sort"`
|
||||
}
|
||||
|
||||
func (sr *SearchRequest) Validate() error {
|
||||
|
@ -220,6 +221,10 @@ func (r *SearchRequest) AddFacet(facetName string, f *FacetRequest) {
|
|||
r.Facets[facetName] = f
|
||||
}
|
||||
|
||||
func (r *SearchRequest) SortBy(fieldName string, ascends bool) {
|
||||
r.Sort = append(r.Sort, search.SearchSort{Field: fieldName, Ascends: ascends})
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes a JSON representation of
|
||||
// a SearchRequest
|
||||
func (r *SearchRequest) UnmarshalJSON(input []byte) error {
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
package collectors
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"github.com/blevesearch/bleve/document"
|
||||
"github.com/blevesearch/bleve/index"
|
||||
"github.com/blevesearch/bleve/search"
|
||||
"golang.org/x/net/context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type collectedDoc struct {
|
||||
match search.DocumentMatch
|
||||
doc *document.Document
|
||||
}
|
||||
|
||||
type HeapCollector struct {
|
||||
size int
|
||||
skip int
|
||||
total uint64
|
||||
took time.Duration
|
||||
sort search.SortOrder
|
||||
results []*collectedDoc
|
||||
facetsBuilder *search.FacetsBuilder
|
||||
reader index.IndexReader
|
||||
}
|
||||
|
||||
func NewHeapCollector(size int, skip int, reader index.IndexReader, sort search.SortOrder) *HeapCollector {
|
||||
hc := &HeapCollector{size: size, skip: skip, reader: reader, sort: sort}
|
||||
heap.Init(hc)
|
||||
return hc
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Collect(ctx context.Context, searcher search.Searcher) error {
|
||||
startTime := time.Now()
|
||||
var err error
|
||||
var pre search.DocumentMatch // A single pre-alloc'ed, reused instance.
|
||||
var next *search.DocumentMatch
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
next, err = searcher.Next(&pre)
|
||||
}
|
||||
for err == nil && next != nil {
|
||||
if hc.total%COLLECT_CHECK_DONE_EVERY == 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
hc.collectSingle(next)
|
||||
if hc.facetsBuilder != nil {
|
||||
err = hc.facetsBuilder.Update(next)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
next, err = searcher.Next(pre.Reset())
|
||||
}
|
||||
// compute search duration
|
||||
hc.took = time.Since(startTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) collectSingle(dmIn *search.DocumentMatch) error {
|
||||
// increment total hits
|
||||
hc.total++
|
||||
single := new(collectedDoc)
|
||||
single.match = *dmIn
|
||||
var err error
|
||||
if len(hc.sort) > 0 {
|
||||
single.doc, err = hc.reader.Document(dmIn.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if hc.Len() >= hc.size {
|
||||
hc.Pop()
|
||||
}
|
||||
heap.Push(hc, single)
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) SetFacetsBuilder(facetsBuilder *search.FacetsBuilder) {
|
||||
hc.facetsBuilder = facetsBuilder
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Results() search.DocumentMatchCollection {
|
||||
rv := make(search.DocumentMatchCollection, hc.Len())
|
||||
for i := 0; hc.Len() > 0; i++ {
|
||||
doc := heap.Pop(hc).(*collectedDoc)
|
||||
rv[i] = &doc.match
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Total() uint64 {
|
||||
return hc.total
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) MaxScore() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) FacetResults() search.FacetResults {
|
||||
if hc.facetsBuilder != nil {
|
||||
return hc.facetsBuilder.Results()
|
||||
}
|
||||
return search.FacetResults{}
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Len() int {
|
||||
return len(hc.results)
|
||||
}
|
||||
|
||||
func field(doc *document.Document, field string) document.Field {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
for _, f := range doc.Fields {
|
||||
if f.Name() == field {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func textFieldCompare(i, j *document.TextField, ascends bool) (bool, bool) {
|
||||
ivalue := string(i.Value())
|
||||
jvalue := string(j.Value())
|
||||
if ivalue == jvalue {
|
||||
return true, false
|
||||
}
|
||||
if ascends {
|
||||
return false, ivalue < jvalue
|
||||
}
|
||||
return false, ivalue > jvalue
|
||||
}
|
||||
|
||||
func numericFieldCompare(i, j *document.NumericField, ascends bool) (bool, bool) {
|
||||
ivalue, _ := i.Number()
|
||||
jvalue, _ := i.Number()
|
||||
if ivalue == jvalue {
|
||||
return true, false
|
||||
}
|
||||
if ascends {
|
||||
return false, ivalue < jvalue
|
||||
}
|
||||
return false, ivalue > jvalue
|
||||
}
|
||||
|
||||
func dateTimeFieldCompare(i, j *document.DateTimeField, ascends bool) (bool, bool) {
|
||||
ivalue, _ := i.DateTime()
|
||||
jvalue, _ := i.DateTime()
|
||||
if ivalue.Equal(jvalue) {
|
||||
return true, false
|
||||
}
|
||||
if ascends {
|
||||
return false, ivalue.Before(jvalue)
|
||||
}
|
||||
return false, ivalue.After(jvalue)
|
||||
}
|
||||
|
||||
func boolFieldCompare(i, j *document.BooleanField, ascends bool) (bool, bool) {
|
||||
ivalue, _ := i.Boolean()
|
||||
jvalue, _ := i.Boolean()
|
||||
if ivalue == jvalue {
|
||||
return true, false
|
||||
}
|
||||
return false, ascends && jvalue
|
||||
}
|
||||
|
||||
//returns: equals, less
|
||||
func fieldCompare(i, j document.Field, ascends bool) (bool, bool) {
|
||||
switch ft := i.(type) {
|
||||
case *document.TextField:
|
||||
return textFieldCompare(ft, j.(*document.TextField), ascends)
|
||||
case *document.NumericField:
|
||||
return numericFieldCompare(ft, j.(*document.NumericField), ascends)
|
||||
case *document.DateTimeField:
|
||||
return dateTimeFieldCompare(ft, j.(*document.DateTimeField), ascends)
|
||||
case *document.BooleanField:
|
||||
return boolFieldCompare(ft, j.(*document.BooleanField), ascends)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Less(i, j int) bool {
|
||||
if len(hc.sort) > 0 {
|
||||
doci := hc.results[i].doc
|
||||
docj := hc.results[j].doc
|
||||
if doci != nil && docj != nil {
|
||||
for _, so := range hc.sort {
|
||||
fieldi := field(doci, so.Field)
|
||||
fieldj := field(docj, so.Field)
|
||||
equals, lt := fieldCompare(fieldi, fieldj, so.Ascends)
|
||||
if !equals {
|
||||
return lt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hc.results[i].match.Score > hc.results[j].match.Score
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Swap(i, j int) {
|
||||
hc.results[i], hc.results[j] = hc.results[j], hc.results[i]
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Push(x interface{}) {
|
||||
hc.results = append(hc.results, x.(*collectedDoc))
|
||||
}
|
||||
|
||||
func (hc *HeapCollector) Pop() interface{} {
|
||||
n := len(hc.results)
|
||||
doc := hc.results[n-1]
|
||||
hc.results = hc.results[0 : n-1]
|
||||
return doc
|
||||
}
|
|
@ -105,3 +105,10 @@ type Searcher interface {
|
|||
Count() uint64
|
||||
Min() int
|
||||
}
|
||||
|
||||
type SearchSort struct {
|
||||
Field string `json:"field"`
|
||||
Ascends bool `json:"ascends"`
|
||||
}
|
||||
|
||||
type SortOrder []SearchSort
|
||||
|
|
Loading…
Reference in New Issue