appengine/search: Introduce SortOptions for search.

Change-Id: Id3f777a0619acacd3906e22132b95f22e3ca585c
This commit is contained in:
Dave Day 2014-10-03 15:19:38 +10:00
Родитель 0fd6f79681
Коммит 3d2bbb9da4
2 изменённых файлов: 181 добавлений и 0 удалений

Просмотреть файл

@ -402,6 +402,7 @@ func (x *Index) Search(c appengine.Context, query string, opts *SearchOptions) *
t.limit = opts.Limit
}
t.idsOnly = opts.IDsOnly
t.sort = opts.Sort
}
return t
}
@ -420,6 +421,11 @@ func moreSearch(t *Iterator) error {
if 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 {
req.Params.Cursor = t.searchCursor
@ -452,9 +458,103 @@ type SearchOptions struct {
// operation; no document fields are populated.
IDsOnly bool
// Sort controls the ordering of search results.
Sort *SortOptions
// 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
// index.
type Iterator struct {
@ -469,6 +569,7 @@ type Iterator struct {
searchRes []*pb.SearchResult
searchQuery string
searchCursor *string
sort *SortOptions
more func(*Iterator) error

Просмотреть файл

@ -477,3 +477,83 @@ func TestPutBadStatus(t *testing.T) {
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)
}
}
}