123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906 |
- // Copyright 2012 Google Inc. All rights reserved.
- // Use of this source code is governed by the Apache 2.0
- // license that can be found in the LICENSE file.
- package search
- import (
- "errors"
- "fmt"
- "reflect"
- "strings"
- "testing"
- "time"
- "github.com/golang/protobuf/proto"
- "google.golang.org/appengine"
- "google.golang.org/appengine/internal/aetesting"
- pb "google.golang.org/appengine/internal/search"
- )
- type TestDoc struct {
- String string
- Atom Atom
- HTML HTML
- Float float64
- Location appengine.GeoPoint
- Time time.Time
- }
- type FieldListWithMeta struct {
- Fields FieldList
- Meta *DocumentMetadata
- }
- func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error {
- f.Meta = meta
- return f.Fields.Load(fields, nil)
- }
- func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) {
- fields, _, err := f.Fields.Save()
- return fields, f.Meta, err
- }
- // Assert that FieldListWithMeta satisfies FieldLoadSaver
- var _ FieldLoadSaver = &FieldListWithMeta{}
- var (
- float = 3.14159
- floatOut = "3.14159e+00"
- latitude = 37.3894
- longitude = 122.0819
- testGeo = appengine.GeoPoint{latitude, longitude}
- testString = "foo<b>bar"
- testTime = time.Unix(1337324400, 0)
- testTimeOut = "1337324400000"
- searchMeta = &DocumentMetadata{
- Rank: 42,
- }
- searchDoc = TestDoc{
- String: testString,
- Atom: Atom(testString),
- HTML: HTML(testString),
- Float: float,
- Location: testGeo,
- Time: testTime,
- }
- searchFields = FieldList{
- Field{Name: "String", Value: testString},
- Field{Name: "Atom", Value: Atom(testString)},
- Field{Name: "HTML", Value: HTML(testString)},
- Field{Name: "Float", Value: float},
- Field{Name: "Location", Value: testGeo},
- Field{Name: "Time", Value: testTime},
- }
- // searchFieldsWithLang is a copy of the searchFields with the Language field
- // set on text/HTML Fields.
- searchFieldsWithLang = FieldList{}
- protoFields = []*pb.Field{
- newStringValueField("String", testString, pb.FieldValue_TEXT),
- newStringValueField("Atom", testString, pb.FieldValue_ATOM),
- newStringValueField("HTML", testString, pb.FieldValue_HTML),
- newStringValueField("Float", floatOut, pb.FieldValue_NUMBER),
- {
- Name: proto.String("Location"),
- Value: &pb.FieldValue{
- Geo: &pb.FieldValue_Geo{
- Lat: proto.Float64(latitude),
- Lng: proto.Float64(longitude),
- },
- Type: pb.FieldValue_GEO.Enum(),
- },
- },
- newStringValueField("Time", testTimeOut, pb.FieldValue_DATE),
- }
- )
- func init() {
- for _, f := range searchFields {
- if f.Name == "String" || f.Name == "HTML" {
- f.Language = "en"
- }
- searchFieldsWithLang = append(searchFieldsWithLang, f)
- }
- }
- func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field {
- return &pb.Field{
- Name: proto.String(name),
- Value: &pb.FieldValue{
- StringValue: proto.String(value),
- Type: valueType.Enum(),
- },
- }
- }
- func TestValidIndexNameOrDocID(t *testing.T) {
- testCases := []struct {
- s string
- want bool
- }{
- {"", true},
- {"!", false},
- {"$", true},
- {"!bad", false},
- {"good!", true},
- {"alsoGood", true},
- {"has spaces", false},
- {"is_inva\xffid_UTF-8", false},
- {"is_non-ASCïI", false},
- {"underscores_are_ok", true},
- }
- for _, tc := range testCases {
- if got := validIndexNameOrDocID(tc.s); got != tc.want {
- t.Errorf("%q: got %v, want %v", tc.s, got, tc.want)
- }
- }
- }
- func TestLoadDoc(t *testing.T) {
- got, want := TestDoc{}, searchDoc
- if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
- t.Fatalf("loadDoc: %v", err)
- }
- if got != want {
- t.Errorf("loadDoc: got %v, wanted %v", got, want)
- }
- }
- func TestSaveDoc(t *testing.T) {
- got, err := saveDoc(&searchDoc)
- if err != nil {
- t.Fatalf("saveDoc: %v", err)
- }
- want := protoFields
- if !reflect.DeepEqual(got.Field, want) {
- t.Errorf("\ngot %v\nwant %v", got, want)
- }
- }
- func TestLoadFieldList(t *testing.T) {
- var got FieldList
- want := searchFieldsWithLang
- if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
- t.Fatalf("loadDoc: %v", err)
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("\ngot %v\nwant %v", got, want)
- }
- }
- func TestLangFields(t *testing.T) {
- fl := &FieldList{
- {Name: "Foo", Value: "I am English", Language: "en"},
- {Name: "Bar", Value: "私は日本人だ", Language: "jp"},
- }
- var got FieldList
- doc, err := saveDoc(fl)
- if err != nil {
- t.Fatalf("saveDoc: %v", err)
- }
- if err := loadDoc(&got, doc, nil); err != nil {
- t.Fatalf("loadDoc: %v", err)
- }
- if want := fl; !reflect.DeepEqual(&got, want) {
- t.Errorf("got %v\nwant %v", got, want)
- }
- }
- func TestSaveFieldList(t *testing.T) {
- got, err := saveDoc(&searchFields)
- if err != nil {
- t.Fatalf("saveDoc: %v", err)
- }
- want := protoFields
- if !reflect.DeepEqual(got.Field, want) {
- t.Errorf("\ngot %v\nwant %v", got, want)
- }
- }
- func TestLoadFieldAndExprList(t *testing.T) {
- var got, want FieldList
- for i, f := range searchFieldsWithLang {
- f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions".
- want = append(want, f)
- }
- doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:]
- if err := loadDoc(&got, doc, expr); err != nil {
- t.Fatalf("loadDoc: %v", err)
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("got %v\nwant %v", got, want)
- }
- }
- func TestLoadMeta(t *testing.T) {
- var got FieldListWithMeta
- want := FieldListWithMeta{
- Meta: searchMeta,
- Fields: searchFieldsWithLang,
- }
- doc := &pb.Document{
- Field: protoFields,
- OrderId: proto.Int32(42),
- }
- if err := loadDoc(&got, doc, nil); err != nil {
- t.Fatalf("loadDoc: %v", err)
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("\ngot %v\nwant %v", got, want)
- }
- }
- func TestSaveMeta(t *testing.T) {
- got, err := saveDoc(&FieldListWithMeta{
- Meta: searchMeta,
- Fields: searchFields,
- })
- if err != nil {
- t.Fatalf("saveDoc: %v", err)
- }
- want := &pb.Document{
- Field: protoFields,
- OrderId: proto.Int32(42),
- }
- if !proto.Equal(got, want) {
- t.Errorf("\ngot %v\nwant %v", got, want)
- }
- }
- func TestValidFieldNames(t *testing.T) {
- testCases := []struct {
- name string
- valid bool
- }{
- {"Normal", true},
- {"Also_OK_123", true},
- {"Not so great", false},
- {"lower_case", true},
- {"Exclaim!", false},
- {"Hello세상아 안녕", false},
- {"", false},
- {"Hεllo", false},
- {strings.Repeat("A", 500), true},
- {strings.Repeat("A", 501), false},
- }
- for _, tc := range testCases {
- _, err := saveDoc(&FieldList{
- Field{Name: tc.name, Value: "val"},
- })
- if err != nil && !strings.Contains(err.Error(), "invalid field name") {
- t.Errorf("unexpected err %q for field name %q", err, tc.name)
- }
- if (err == nil) != tc.valid {
- t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err)
- }
- }
- }
- func TestValidLangs(t *testing.T) {
- testCases := []struct {
- field Field
- valid bool
- }{
- {Field{Name: "Foo", Value: "String", Language: ""}, true},
- {Field{Name: "Foo", Value: "String", Language: "en"}, true},
- {Field{Name: "Foo", Value: "String", Language: "aussie"}, false},
- {Field{Name: "Foo", Value: "String", Language: "12"}, false},
- {Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true},
- {Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false},
- {Field{Name: "Foo", Value: 42, Language: "en"}, false},
- }
- for _, tt := range testCases {
- _, err := saveDoc(&FieldList{tt.field})
- if err == nil != tt.valid {
- t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid)
- }
- }
- }
- func TestDuplicateFields(t *testing.T) {
- testCases := []struct {
- desc string
- fields FieldList
- errMsg string // Non-empty if we expect an error
- }{
- {
- desc: "multi string",
- fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}},
- },
- {
- desc: "multi atom",
- fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}},
- },
- {
- desc: "mixed",
- fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}},
- },
- {
- desc: "multi time",
- fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}},
- errMsg: `duplicate time field "FieldA"`,
- },
- {
- desc: "multi num",
- fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}},
- errMsg: `duplicate numeric field "FieldA"`,
- },
- }
- for _, tc := range testCases {
- _, err := saveDoc(&tc.fields)
- if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) {
- t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg)
- }
- }
- }
- func TestLoadErrFieldMismatch(t *testing.T) {
- testCases := []struct {
- desc string
- dst interface{}
- src []*pb.Field
- err error
- }{
- {
- desc: "missing",
- dst: &struct{ One string }{},
- src: []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)},
- err: &ErrFieldMismatch{
- FieldName: "Two",
- Reason: "no such struct field",
- },
- },
- {
- desc: "wrong type",
- dst: &struct{ Num float64 }{},
- src: []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)},
- err: &ErrFieldMismatch{
- FieldName: "Num",
- Reason: "type mismatch: float64 for string data",
- },
- },
- {
- desc: "unsettable",
- dst: &struct{ lower string }{},
- src: []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)},
- err: &ErrFieldMismatch{
- FieldName: "lower",
- Reason: "cannot set struct field",
- },
- },
- }
- for _, tc := range testCases {
- err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil)
- if !reflect.DeepEqual(err, tc.err) {
- t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err)
- }
- }
- }
- func TestLimit(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error {
- limit := 20 // Default per page.
- if req.Params.Limit != nil {
- limit = int(*req.Params.Limit)
- }
- res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()}
- res.MatchedCount = proto.Int64(int64(limit))
- for i := 0; i < limit; i++ {
- res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}})
- res.Cursor = proto.String("moreresults")
- }
- return nil
- })
- const maxDocs = 500 // Limit maximum number of docs.
- testCases := []struct {
- limit, want int
- }{
- {limit: 0, want: maxDocs},
- {limit: 42, want: 42},
- {limit: 100, want: 100},
- {limit: 1000, want: maxDocs},
- }
- for _, tt := range testCases {
- it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true})
- count := 0
- for ; count < maxDocs; count++ {
- _, err := it.Next(nil)
- if err == Done {
- break
- }
- if err != nil {
- t.Fatalf("err after %d: %v", count, err)
- }
- }
- if count != tt.want {
- t.Errorf("got %d results, expected %d", count, tt.want)
- }
- }
- }
- func TestPut(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
- expectedIn := &pb.IndexDocumentRequest{
- Params: &pb.IndexDocumentParams{
- Document: []*pb.Document{
- {Field: protoFields, OrderId: proto.Int32(42)},
- },
- IndexSpec: &pb.IndexSpec{
- Name: proto.String("Doc"),
- },
- },
- }
- if !proto.Equal(in, expectedIn) {
- return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn)
- }
- *out = pb.IndexDocumentResponse{
- Status: []*pb.RequestStatus{
- {Code: pb.SearchServiceError_OK.Enum()},
- },
- DocId: []string{
- "doc_id",
- },
- }
- return nil
- })
- id, err := index.Put(c, "", &FieldListWithMeta{
- Meta: searchMeta,
- Fields: searchFields,
- })
- if err != nil {
- t.Fatal(err)
- }
- if want := "doc_id"; id != want {
- t.Errorf("Got doc ID %q, want %q", id, want)
- }
- }
- func TestPutAutoOrderID(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
- if len(in.Params.GetDocument()) < 1 {
- return fmt.Errorf("expected at least one Document, got %v", in)
- }
- got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds())
- if d := got - want; -5 > d || d > 5 {
- return fmt.Errorf("got OrderId %d, want near %d", got, want)
- }
- *out = pb.IndexDocumentResponse{
- Status: []*pb.RequestStatus{
- {Code: pb.SearchServiceError_OK.Enum()},
- },
- DocId: []string{
- "doc_id",
- },
- }
- return nil
- })
- if _, err := index.Put(c, "", &searchFields); err != nil {
- t.Fatal(err)
- }
- }
- func TestPutBadStatus(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
- *out = pb.IndexDocumentResponse{
- Status: []*pb.RequestStatus{
- {
- Code: pb.SearchServiceError_INVALID_REQUEST.Enum(),
- ErrorDetail: proto.String("insufficient gophers"),
- },
- },
- }
- return nil
- })
- wantErr := "search: INVALID_REQUEST: insufficient gophers"
- if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != 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 err to return to prevent sending request.
- 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)
- }
- }
- }
- func TestFieldSpec(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- errFoo := errors.New("foo") // sentinel error when there isn't one.
- testCases := []struct {
- desc string
- opts *SearchOptions
- want *pb.FieldSpec
- }{
- {
- desc: "No options",
- want: &pb.FieldSpec{},
- },
- {
- desc: "Fields",
- opts: &SearchOptions{
- Fields: []string{"one", "two"},
- },
- want: &pb.FieldSpec{
- Name: []string{"one", "two"},
- },
- },
- {
- desc: "Expressions",
- opts: &SearchOptions{
- Expressions: []FieldExpression{
- {Name: "one", Expr: "price * quantity"},
- {Name: "two", Expr: "min(daily_use, 10) * rate"},
- },
- },
- want: &pb.FieldSpec{
- Expression: []*pb.FieldSpec_Expression{
- {Name: proto.String("one"), Expression: proto.String("price * quantity")},
- {Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")},
- },
- },
- },
- }
- for _, tt := range testCases {
- c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
- params := req.Params
- if !reflect.DeepEqual(params.FieldSpec, tt.want) {
- t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want)
- }
- return errFoo // Always return some error to prevent response parsing.
- })
- it := index.Search(c, "gopher", tt.opts)
- if _, err := it.Next(nil); err != errFoo {
- t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo)
- }
- }
- }
- func TestBasicSearchOpts(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- noErr := errors.New("") // Sentinel err to return to prevent sending request.
- testCases := []struct {
- desc string
- facetOpts []FacetSearchOption
- cursor Cursor
- offset int
- want *pb.SearchParams
- wantErr string
- }{
- {
- desc: "No options",
- want: &pb.SearchParams{},
- },
- {
- desc: "Default auto discovery",
- facetOpts: []FacetSearchOption{
- AutoFacetDiscovery(0, 0),
- },
- want: &pb.SearchParams{
- AutoDiscoverFacetCount: proto.Int32(10),
- },
- },
- {
- desc: "Auto discovery",
- facetOpts: []FacetSearchOption{
- AutoFacetDiscovery(7, 12),
- },
- want: &pb.SearchParams{
- AutoDiscoverFacetCount: proto.Int32(7),
- FacetAutoDetectParam: &pb.FacetAutoDetectParam{
- ValueLimit: proto.Int32(12),
- },
- },
- },
- {
- desc: "Param Depth",
- facetOpts: []FacetSearchOption{
- AutoFacetDiscovery(7, 12),
- },
- want: &pb.SearchParams{
- AutoDiscoverFacetCount: proto.Int32(7),
- FacetAutoDetectParam: &pb.FacetAutoDetectParam{
- ValueLimit: proto.Int32(12),
- },
- },
- },
- {
- desc: "Doc depth",
- facetOpts: []FacetSearchOption{
- FacetDocumentDepth(123),
- },
- want: &pb.SearchParams{
- FacetDepth: proto.Int32(123),
- },
- },
- {
- desc: "Facet discovery",
- facetOpts: []FacetSearchOption{
- FacetDiscovery("colour"),
- FacetDiscovery("size", Atom("M"), Atom("L")),
- FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)),
- },
- want: &pb.SearchParams{
- IncludeFacet: []*pb.FacetRequest{
- {Name: proto.String("colour")},
- {Name: proto.String("size"), Params: &pb.FacetRequestParam{
- ValueConstraint: []string{"M", "L"},
- }},
- {Name: proto.String("price"), Params: &pb.FacetRequestParam{
- Range: []*pb.FacetRange{
- {End: proto.String("7e+00")},
- {Start: proto.String("7e+00"), End: proto.String("1.4e+01")},
- {Start: proto.String("1.4e+01")},
- },
- }},
- },
- },
- },
- {
- desc: "Facet discovery - bad value",
- facetOpts: []FacetSearchOption{
- FacetDiscovery("colour", true),
- },
- wantErr: "bad FacetSearchOption: unsupported value type bool",
- },
- {
- desc: "Facet discovery - mix value types",
- facetOpts: []FacetSearchOption{
- FacetDiscovery("colour", Atom("blue"), AtLeast(7)),
- },
- wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range",
- },
- {
- desc: "Facet discovery - invalid range",
- facetOpts: []FacetSearchOption{
- FacetDiscovery("colour", Range{negInf, posInf}),
- },
- wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite",
- },
- {
- desc: "Cursor",
- cursor: Cursor("mycursor"),
- want: &pb.SearchParams{
- Cursor: proto.String("mycursor"),
- },
- },
- {
- desc: "Offset",
- offset: 121,
- want: &pb.SearchParams{
- Offset: proto.Int32(121),
- },
- },
- {
- desc: "Cursor and Offset set",
- cursor: Cursor("mycursor"),
- offset: 121,
- wantErr: "at most one of Cursor and Offset may be specified",
- },
- }
- for _, tt := range testCases {
- c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
- if tt.want == nil {
- t.Errorf("%s: expected call to fail", tt.desc)
- return nil
- }
- // Set default fields.
- tt.want.Query = proto.String("gopher")
- tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")}
- tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum()
- tt.want.FieldSpec = &pb.FieldSpec{}
- if got := req.Params; !reflect.DeepEqual(got, tt.want) {
- t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want)
- }
- return noErr // Always return some error to prevent response parsing.
- })
- it := index.Search(c, "gopher", &SearchOptions{
- Facets: tt.facetOpts,
- Cursor: tt.cursor,
- Offset: tt.offset,
- })
- _, 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)
- }
- }
- }
- func TestFacetRefinements(t *testing.T) {
- index, err := Open("Doc")
- if err != nil {
- t.Fatalf("err from Open: %v", err)
- }
- noErr := errors.New("") // Sentinel err to return to prevent sending request.
- testCases := []struct {
- desc string
- refine []Facet
- want []*pb.FacetRefinement
- wantErr string
- }{
- {
- desc: "No refinements",
- },
- {
- desc: "Basic",
- refine: []Facet{
- {Name: "fur", Value: Atom("fluffy")},
- {Name: "age", Value: LessThan(123)},
- {Name: "age", Value: AtLeast(0)},
- {Name: "legs", Value: Range{Start: 3, End: 5}},
- },
- want: []*pb.FacetRefinement{
- {Name: proto.String("fur"), Value: proto.String("fluffy")},
- {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}},
- {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}},
- {Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}},
- },
- },
- {
- desc: "Infinite range",
- refine: []Facet{
- {Name: "age", Value: Range{Start: negInf, End: posInf}},
- },
- wantErr: `search: refinement for facet "age": either Start or End must be finite`,
- },
- {
- desc: "Bad End value in range",
- refine: []Facet{
- {Name: "age", Value: LessThan(2147483648)},
- },
- wantErr: `search: refinement for facet "age": invalid value for End`,
- },
- {
- desc: "Bad Start value in range",
- refine: []Facet{
- {Name: "age", Value: AtLeast(-2147483649)},
- },
- wantErr: `search: refinement for facet "age": invalid value for Start`,
- },
- {
- desc: "Unknown value type",
- refine: []Facet{
- {Name: "age", Value: "you can't use strings!"},
- },
- wantErr: `search: unsupported refinement for facet "age" of type string`,
- },
- }
- for _, tt := range testCases {
- c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
- if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) {
- t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want)
- }
- return noErr // Always return some error to prevent response parsing.
- })
- it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine})
- _, 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)
- }
- }
- }
|