зеркало из https://github.com/golang/appengine.git
appengine/search: Introduce SortOptions for search.
Change-Id: Id3f777a0619acacd3906e22132b95f22e3ca585c
This commit is contained in:
Родитель
0fd6f79681
Коммит
3d2bbb9da4
101
search/search.go
101
search/search.go
|
@ -402,6 +402,7 @@ func (x *Index) Search(c appengine.Context, query string, opts *SearchOptions) *
|
||||||
t.limit = opts.Limit
|
t.limit = opts.Limit
|
||||||
}
|
}
|
||||||
t.idsOnly = opts.IDsOnly
|
t.idsOnly = opts.IDsOnly
|
||||||
|
t.sort = opts.Sort
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
@ -420,6 +421,11 @@ func moreSearch(t *Iterator) error {
|
||||||
if t.idsOnly {
|
if t.idsOnly {
|
||||||
req.Params.KeysOnly = &t.idsOnly
|
req.Params.KeysOnly = &t.idsOnly
|
||||||
}
|
}
|
||||||
|
if t.sort != nil {
|
||||||
|
if err := sortToProto(t.sort, req.Params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if t.searchCursor != nil {
|
if t.searchCursor != nil {
|
||||||
req.Params.Cursor = t.searchCursor
|
req.Params.Cursor = t.searchCursor
|
||||||
|
@ -452,9 +458,103 @@ type SearchOptions struct {
|
||||||
// operation; no document fields are populated.
|
// operation; no document fields are populated.
|
||||||
IDsOnly bool
|
IDsOnly bool
|
||||||
|
|
||||||
|
// Sort controls the ordering of search results.
|
||||||
|
Sort *SortOptions
|
||||||
|
|
||||||
// TODO: cursor, offset, maybe others.
|
// TODO: cursor, offset, maybe others.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SortOptions control the ordering and scoring of search results.
|
||||||
|
type SortOptions struct {
|
||||||
|
// Expressions is a slice of expressions representing a multi-dimensional
|
||||||
|
// sort.
|
||||||
|
Expressions []SortExpression
|
||||||
|
|
||||||
|
// Scorer, when specified, will cause the documents to be scored according to
|
||||||
|
// search term frequency.
|
||||||
|
Scorer Scorer
|
||||||
|
|
||||||
|
// Limit is the maximum number of objects to score and/or sort. Limit cannot
|
||||||
|
// be more than 10,000. The zero value indicates a default limit.
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortExpression defines a single dimension for sorting a document.
|
||||||
|
type SortExpression struct {
|
||||||
|
// Expr is evaluated to providing a sorting value for each document.
|
||||||
|
// See https://developers.google.com/appengine/docs/go/search/options for
|
||||||
|
// the supported expression syntax.
|
||||||
|
Expr string
|
||||||
|
|
||||||
|
// Reverse causes the documents to be sorted in ascending order.
|
||||||
|
Reverse bool
|
||||||
|
|
||||||
|
// The default value to use when no field is present or the expresion
|
||||||
|
// cannot be calculated for a document. For text sorts, Default must
|
||||||
|
// be of type string; for numeric sorts, float64.
|
||||||
|
Default interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Scorer defines how a document is scored.
|
||||||
|
type Scorer interface {
|
||||||
|
toProto(*pb.ScorerSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
type enumScorer struct {
|
||||||
|
enum pb.ScorerSpec_Scorer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e enumScorer) toProto(spec *pb.ScorerSpec) {
|
||||||
|
spec.Scorer = e.enum.Enum()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// MatchScorer assigns a score based on term frequency in a document.
|
||||||
|
MatchScorer Scorer = enumScorer{pb.ScorerSpec_MATCH_SCORER}
|
||||||
|
|
||||||
|
// RescoringMatchScorer assigns a score based on the quality of the query
|
||||||
|
// match. It is similar to a MatchScorer but uses a more complex scoring
|
||||||
|
// algorithm based on match term frequency and other factors like field type.
|
||||||
|
// Please be aware that this algorithm is continually refined and can change
|
||||||
|
// over time without notice. This means that the ordering of search results
|
||||||
|
// that use this scorer can also change without notice.
|
||||||
|
RescoringMatchScorer Scorer = enumScorer{pb.ScorerSpec_RESCORING_MATCH_SCORER}
|
||||||
|
)
|
||||||
|
|
||||||
|
func sortToProto(sort *SortOptions, params *pb.SearchParams) error {
|
||||||
|
for _, e := range sort.Expressions {
|
||||||
|
spec := &pb.SortSpec{
|
||||||
|
SortExpression: proto.String(e.Expr),
|
||||||
|
}
|
||||||
|
if e.Reverse {
|
||||||
|
spec.SortDescending = proto.Bool(false)
|
||||||
|
}
|
||||||
|
if e.Default != nil {
|
||||||
|
switch d := e.Default.(type) {
|
||||||
|
case float64:
|
||||||
|
spec.DefaultValueNumeric = &d
|
||||||
|
case string:
|
||||||
|
spec.DefaultValueText = &d
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("search: invalid Default type %T for expression %q", d, e.Expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.SortSpec = append(params.SortSpec, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := &pb.ScorerSpec{}
|
||||||
|
if sort.Limit > 0 {
|
||||||
|
spec.Limit = proto.Int32(int32(sort.Limit))
|
||||||
|
params.ScorerSpec = spec
|
||||||
|
}
|
||||||
|
if sort.Scorer != nil {
|
||||||
|
sort.Scorer.toProto(spec)
|
||||||
|
params.ScorerSpec = spec
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Iterator is the result of searching an index for a query or listing an
|
// Iterator is the result of searching an index for a query or listing an
|
||||||
// index.
|
// index.
|
||||||
type Iterator struct {
|
type Iterator struct {
|
||||||
|
@ -469,6 +569,7 @@ type Iterator struct {
|
||||||
searchRes []*pb.SearchResult
|
searchRes []*pb.SearchResult
|
||||||
searchQuery string
|
searchQuery string
|
||||||
searchCursor *string
|
searchCursor *string
|
||||||
|
sort *SortOptions
|
||||||
|
|
||||||
more func(*Iterator) error
|
more func(*Iterator) error
|
||||||
|
|
||||||
|
|
|
@ -477,3 +477,83 @@ func TestPutBadStatus(t *testing.T) {
|
||||||
t.Fatalf("Put: got %v error, want %q", err, wantErr)
|
t.Fatalf("Put: got %v error, want %q", err, wantErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSortOptions(t *testing.T) {
|
||||||
|
index, err := Open("Doc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err from Open: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noErr := errors.New("") // sentinel error when there isn't one…
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
sort *SortOptions
|
||||||
|
wantSort []*pb.SortSpec
|
||||||
|
wantScorer *pb.ScorerSpec
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "No SortOptions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Basic",
|
||||||
|
sort: &SortOptions{
|
||||||
|
Expressions: []SortExpression{
|
||||||
|
{Expr: "dog"},
|
||||||
|
{Expr: "cat", Reverse: true},
|
||||||
|
{Expr: "gopher", Default: "blue"},
|
||||||
|
{Expr: "fish", Default: 2.0},
|
||||||
|
},
|
||||||
|
Limit: 42,
|
||||||
|
Scorer: MatchScorer,
|
||||||
|
},
|
||||||
|
wantSort: []*pb.SortSpec{
|
||||||
|
{SortExpression: proto.String("dog")},
|
||||||
|
{SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)},
|
||||||
|
{SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")},
|
||||||
|
{SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)},
|
||||||
|
},
|
||||||
|
wantScorer: &pb.ScorerSpec{
|
||||||
|
Limit: proto.Int32(42),
|
||||||
|
Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Bad expression default",
|
||||||
|
sort: &SortOptions{
|
||||||
|
Expressions: []SortExpression{
|
||||||
|
{Expr: "dog", Default: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: `search: invalid Default type bool for expression "dog"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "RescoringMatchScorer",
|
||||||
|
sort: &SortOptions{Scorer: RescoringMatchScorer},
|
||||||
|
wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
|
||||||
|
params := req.Params
|
||||||
|
if !reflect.DeepEqual(params.SortSpec, tt.wantSort) {
|
||||||
|
t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) {
|
||||||
|
t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer)
|
||||||
|
}
|
||||||
|
return noErr // Always return some error to prevent response parsing.
|
||||||
|
})
|
||||||
|
|
||||||
|
it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort})
|
||||||
|
_, err := it.Next(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("%s: err==nil; should not happen", tt.desc)
|
||||||
|
}
|
||||||
|
if err.Error() != tt.wantErr {
|
||||||
|
t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче