1// Copyright 2012 Google Inc. All rights reserved. 2// Use of this source code is governed by the Apache 2.0 3// license that can be found in the LICENSE file. 4 5package search 6 7import ( 8 "errors" 9 "fmt" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/golang/protobuf/proto" 16 17 "google.golang.org/appengine" 18 "google.golang.org/appengine/internal/aetesting" 19 pb "google.golang.org/appengine/internal/search" 20) 21 22type TestDoc struct { 23 String string 24 Atom Atom 25 HTML HTML 26 Float float64 27 Location appengine.GeoPoint 28 Time time.Time 29} 30 31type FieldListWithMeta struct { 32 Fields FieldList 33 Meta *DocumentMetadata 34} 35 36func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error { 37 f.Meta = meta 38 return f.Fields.Load(fields, nil) 39} 40 41func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) { 42 fields, _, err := f.Fields.Save() 43 return fields, f.Meta, err 44} 45 46// Assert that FieldListWithMeta satisfies FieldLoadSaver 47var _ FieldLoadSaver = &FieldListWithMeta{} 48 49var ( 50 float = 3.14159 51 floatOut = "3.14159e+00" 52 latitude = 37.3894 53 longitude = 122.0819 54 testGeo = appengine.GeoPoint{latitude, longitude} 55 testString = "foo<b>bar" 56 testTime = time.Unix(1337324400, 0) 57 testTimeOut = "1337324400000" 58 searchMeta = &DocumentMetadata{ 59 Rank: 42, 60 } 61 searchDoc = TestDoc{ 62 String: testString, 63 Atom: Atom(testString), 64 HTML: HTML(testString), 65 Float: float, 66 Location: testGeo, 67 Time: testTime, 68 } 69 searchFields = FieldList{ 70 Field{Name: "String", Value: testString}, 71 Field{Name: "Atom", Value: Atom(testString)}, 72 Field{Name: "HTML", Value: HTML(testString)}, 73 Field{Name: "Float", Value: float}, 74 Field{Name: "Location", Value: testGeo}, 75 Field{Name: "Time", Value: testTime}, 76 } 77 // searchFieldsWithLang is a copy of the searchFields with the Language field 78 // set on text/HTML Fields. 79 searchFieldsWithLang = FieldList{} 80 protoFields = []*pb.Field{ 81 newStringValueField("String", testString, pb.FieldValue_TEXT), 82 newStringValueField("Atom", testString, pb.FieldValue_ATOM), 83 newStringValueField("HTML", testString, pb.FieldValue_HTML), 84 newStringValueField("Float", floatOut, pb.FieldValue_NUMBER), 85 { 86 Name: proto.String("Location"), 87 Value: &pb.FieldValue{ 88 Geo: &pb.FieldValue_Geo{ 89 Lat: proto.Float64(latitude), 90 Lng: proto.Float64(longitude), 91 }, 92 Type: pb.FieldValue_GEO.Enum(), 93 }, 94 }, 95 newStringValueField("Time", testTimeOut, pb.FieldValue_DATE), 96 } 97) 98 99func init() { 100 for _, f := range searchFields { 101 if f.Name == "String" || f.Name == "HTML" { 102 f.Language = "en" 103 } 104 searchFieldsWithLang = append(searchFieldsWithLang, f) 105 } 106} 107 108func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field { 109 return &pb.Field{ 110 Name: proto.String(name), 111 Value: &pb.FieldValue{ 112 StringValue: proto.String(value), 113 Type: valueType.Enum(), 114 }, 115 } 116} 117 118func newFacet(name, value string, valueType pb.FacetValue_ContentType) *pb.Facet { 119 return &pb.Facet{ 120 Name: proto.String(name), 121 Value: &pb.FacetValue{ 122 StringValue: proto.String(value), 123 Type: valueType.Enum(), 124 }, 125 } 126} 127 128func TestValidIndexNameOrDocID(t *testing.T) { 129 testCases := []struct { 130 s string 131 want bool 132 }{ 133 {"", true}, 134 {"!", false}, 135 {"$", true}, 136 {"!bad", false}, 137 {"good!", true}, 138 {"alsoGood", true}, 139 {"has spaces", false}, 140 {"is_inva\xffid_UTF-8", false}, 141 {"is_non-ASCïI", false}, 142 {"underscores_are_ok", true}, 143 } 144 for _, tc := range testCases { 145 if got := validIndexNameOrDocID(tc.s); got != tc.want { 146 t.Errorf("%q: got %v, want %v", tc.s, got, tc.want) 147 } 148 } 149} 150 151func TestLoadDoc(t *testing.T) { 152 got, want := TestDoc{}, searchDoc 153 if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil { 154 t.Fatalf("loadDoc: %v", err) 155 } 156 if got != want { 157 t.Errorf("loadDoc: got %v, wanted %v", got, want) 158 } 159} 160 161func TestSaveDoc(t *testing.T) { 162 got, err := saveDoc(&searchDoc) 163 if err != nil { 164 t.Fatalf("saveDoc: %v", err) 165 } 166 want := protoFields 167 if !reflect.DeepEqual(got.Field, want) { 168 t.Errorf("\ngot %v\nwant %v", got, want) 169 } 170} 171 172func TestSaveDocUsesDefaultedRankIfNotSpecified(t *testing.T) { 173 got, err := saveDoc(&searchDoc) 174 if err != nil { 175 t.Fatalf("saveDoc: %v", err) 176 } 177 orderIdSource := got.GetOrderIdSource() 178 if orderIdSource != pb.Document_DEFAULTED { 179 t.Errorf("OrderIdSource: got %v, wanted DEFAULTED", orderIdSource) 180 } 181} 182 183func TestLoadFieldList(t *testing.T) { 184 var got FieldList 185 want := searchFieldsWithLang 186 if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil { 187 t.Fatalf("loadDoc: %v", err) 188 } 189 if !reflect.DeepEqual(got, want) { 190 t.Errorf("\ngot %v\nwant %v", got, want) 191 } 192} 193 194func TestLangFields(t *testing.T) { 195 fl := &FieldList{ 196 {Name: "Foo", Value: "I am English", Language: "en"}, 197 {Name: "Bar", Value: "私は日本人だ", Language: "ja"}, 198 } 199 var got FieldList 200 doc, err := saveDoc(fl) 201 if err != nil { 202 t.Fatalf("saveDoc: %v", err) 203 } 204 if err := loadDoc(&got, doc, nil); err != nil { 205 t.Fatalf("loadDoc: %v", err) 206 } 207 if want := fl; !reflect.DeepEqual(&got, want) { 208 t.Errorf("got %v\nwant %v", got, want) 209 } 210} 211 212func TestSaveFieldList(t *testing.T) { 213 got, err := saveDoc(&searchFields) 214 if err != nil { 215 t.Fatalf("saveDoc: %v", err) 216 } 217 want := protoFields 218 if !reflect.DeepEqual(got.Field, want) { 219 t.Errorf("\ngot %v\nwant %v", got, want) 220 } 221} 222 223func TestLoadFieldAndExprList(t *testing.T) { 224 var got, want FieldList 225 for i, f := range searchFieldsWithLang { 226 f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions". 227 want = append(want, f) 228 } 229 doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:] 230 if err := loadDoc(&got, doc, expr); err != nil { 231 t.Fatalf("loadDoc: %v", err) 232 } 233 if !reflect.DeepEqual(got, want) { 234 t.Errorf("got %v\nwant %v", got, want) 235 } 236} 237 238func TestLoadMeta(t *testing.T) { 239 var got FieldListWithMeta 240 want := FieldListWithMeta{ 241 Meta: searchMeta, 242 Fields: searchFieldsWithLang, 243 } 244 doc := &pb.Document{ 245 Field: protoFields, 246 OrderId: proto.Int32(42), 247 OrderIdSource: pb.Document_SUPPLIED.Enum(), 248 } 249 if err := loadDoc(&got, doc, nil); err != nil { 250 t.Fatalf("loadDoc: %v", err) 251 } 252 if !reflect.DeepEqual(got, want) { 253 t.Errorf("\ngot %v\nwant %v", got, want) 254 } 255} 256 257func TestSaveMeta(t *testing.T) { 258 got, err := saveDoc(&FieldListWithMeta{ 259 Meta: searchMeta, 260 Fields: searchFields, 261 }) 262 if err != nil { 263 t.Fatalf("saveDoc: %v", err) 264 } 265 want := &pb.Document{ 266 Field: protoFields, 267 OrderId: proto.Int32(42), 268 OrderIdSource: pb.Document_SUPPLIED.Enum(), 269 } 270 if !proto.Equal(got, want) { 271 t.Errorf("\ngot %v\nwant %v", got, want) 272 } 273} 274 275func TestSaveMetaWithDefaultedRank(t *testing.T) { 276 metaWithoutRank := &DocumentMetadata{ 277 Rank: 0, 278 } 279 got, err := saveDoc(&FieldListWithMeta{ 280 Meta: metaWithoutRank, 281 Fields: searchFields, 282 }) 283 if err != nil { 284 t.Fatalf("saveDoc: %v", err) 285 } 286 want := &pb.Document{ 287 Field: protoFields, 288 OrderId: got.OrderId, 289 OrderIdSource: pb.Document_DEFAULTED.Enum(), 290 } 291 if !proto.Equal(got, want) { 292 t.Errorf("\ngot %v\nwant %v", got, want) 293 } 294} 295 296func TestSaveWithoutMetaUsesDefaultedRank(t *testing.T) { 297 got, err := saveDoc(&FieldListWithMeta{ 298 Fields: searchFields, 299 }) 300 if err != nil { 301 t.Fatalf("saveDoc: %v", err) 302 } 303 want := &pb.Document{ 304 Field: protoFields, 305 OrderId: got.OrderId, 306 OrderIdSource: pb.Document_DEFAULTED.Enum(), 307 } 308 if !proto.Equal(got, want) { 309 t.Errorf("\ngot %v\nwant %v", got, want) 310 } 311} 312 313func TestLoadSaveWithStruct(t *testing.T) { 314 type gopher struct { 315 Name string 316 Info string `search:"about"` 317 Legs float64 `search:",facet"` 318 Fuzz Atom `search:"Fur,facet"` 319 } 320 321 doc := gopher{"Gopher", "Likes slide rules.", 4, Atom("furry")} 322 pb := &pb.Document{ 323 Field: []*pb.Field{ 324 newStringValueField("Name", "Gopher", pb.FieldValue_TEXT), 325 newStringValueField("about", "Likes slide rules.", pb.FieldValue_TEXT), 326 }, 327 Facet: []*pb.Facet{ 328 newFacet("Legs", "4e+00", pb.FacetValue_NUMBER), 329 newFacet("Fur", "furry", pb.FacetValue_ATOM), 330 }, 331 } 332 333 var gotDoc gopher 334 if err := loadDoc(&gotDoc, pb, nil); err != nil { 335 t.Fatalf("loadDoc: %v", err) 336 } 337 if !reflect.DeepEqual(gotDoc, doc) { 338 t.Errorf("loading doc\ngot %v\nwant %v", gotDoc, doc) 339 } 340 341 gotPB, err := saveDoc(&doc) 342 if err != nil { 343 t.Fatalf("saveDoc: %v", err) 344 } 345 gotPB.OrderId = nil // Don't test: it's time dependent. 346 gotPB.OrderIdSource = nil // Don't test because it's contingent on OrderId. 347 if !proto.Equal(gotPB, pb) { 348 t.Errorf("saving doc\ngot %v\nwant %v", gotPB, pb) 349 } 350} 351 352func TestValidFieldNames(t *testing.T) { 353 testCases := []struct { 354 name string 355 valid bool 356 }{ 357 {"Normal", true}, 358 {"Also_OK_123", true}, 359 {"Not so great", false}, 360 {"lower_case", true}, 361 {"Exclaim!", false}, 362 {"Hello세상아 안녕", false}, 363 {"", false}, 364 {"Hεllo", false}, 365 {strings.Repeat("A", 500), true}, 366 {strings.Repeat("A", 501), false}, 367 } 368 369 for _, tc := range testCases { 370 _, err := saveDoc(&FieldList{ 371 Field{Name: tc.name, Value: "val"}, 372 }) 373 if err != nil && !strings.Contains(err.Error(), "invalid field name") { 374 t.Errorf("unexpected err %q for field name %q", err, tc.name) 375 } 376 if (err == nil) != tc.valid { 377 t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err) 378 } 379 } 380} 381 382func TestValidLangs(t *testing.T) { 383 testCases := []struct { 384 field Field 385 valid bool 386 }{ 387 {Field{Name: "Foo", Value: "String", Language: ""}, true}, 388 {Field{Name: "Foo", Value: "String", Language: "en"}, true}, 389 {Field{Name: "Foo", Value: "String", Language: "aussie"}, false}, 390 {Field{Name: "Foo", Value: "String", Language: "12"}, false}, 391 {Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true}, 392 {Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false}, 393 {Field{Name: "Foo", Value: 42, Language: "en"}, false}, 394 } 395 396 for _, tt := range testCases { 397 _, err := saveDoc(&FieldList{tt.field}) 398 if err == nil != tt.valid { 399 t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid) 400 } 401 } 402} 403 404func TestDuplicateFields(t *testing.T) { 405 testCases := []struct { 406 desc string 407 fields FieldList 408 errMsg string // Non-empty if we expect an error 409 }{ 410 { 411 desc: "multi string", 412 fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}}, 413 }, 414 { 415 desc: "multi atom", 416 fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}}, 417 }, 418 { 419 desc: "mixed", 420 fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}}, 421 }, 422 { 423 desc: "multi time", 424 fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}}, 425 errMsg: `duplicate time field "FieldA"`, 426 }, 427 { 428 desc: "multi num", 429 fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}}, 430 errMsg: `duplicate numeric field "FieldA"`, 431 }, 432 } 433 for _, tc := range testCases { 434 _, err := saveDoc(&tc.fields) 435 if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) { 436 t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg) 437 } 438 } 439} 440 441func TestLoadErrFieldMismatch(t *testing.T) { 442 testCases := []struct { 443 desc string 444 dst interface{} 445 src []*pb.Field 446 err error 447 }{ 448 { 449 desc: "missing", 450 dst: &struct{ One string }{}, 451 src: []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)}, 452 err: &ErrFieldMismatch{ 453 FieldName: "Two", 454 Reason: "no such struct field", 455 }, 456 }, 457 { 458 desc: "wrong type", 459 dst: &struct{ Num float64 }{}, 460 src: []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)}, 461 err: &ErrFieldMismatch{ 462 FieldName: "Num", 463 Reason: "type mismatch: float64 for string data", 464 }, 465 }, 466 { 467 desc: "unsettable", 468 dst: &struct{ lower string }{}, 469 src: []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)}, 470 err: &ErrFieldMismatch{ 471 FieldName: "lower", 472 Reason: "cannot set struct field", 473 }, 474 }, 475 } 476 for _, tc := range testCases { 477 err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil) 478 if !reflect.DeepEqual(err, tc.err) { 479 t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err) 480 } 481 } 482} 483 484func TestLimit(t *testing.T) { 485 index, err := Open("Doc") 486 if err != nil { 487 t.Fatalf("err from Open: %v", err) 488 } 489 c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error { 490 limit := 20 // Default per page. 491 if req.Params.Limit != nil { 492 limit = int(*req.Params.Limit) 493 } 494 res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()} 495 res.MatchedCount = proto.Int64(int64(limit)) 496 for i := 0; i < limit; i++ { 497 res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}}) 498 res.Cursor = proto.String("moreresults") 499 } 500 return nil 501 }) 502 503 const maxDocs = 500 // Limit maximum number of docs. 504 testCases := []struct { 505 limit, want int 506 }{ 507 {limit: 0, want: maxDocs}, 508 {limit: 42, want: 42}, 509 {limit: 100, want: 100}, 510 {limit: 1000, want: maxDocs}, 511 } 512 513 for _, tt := range testCases { 514 it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true}) 515 count := 0 516 for ; count < maxDocs; count++ { 517 _, err := it.Next(nil) 518 if err == Done { 519 break 520 } 521 if err != nil { 522 t.Fatalf("err after %d: %v", count, err) 523 } 524 } 525 if count != tt.want { 526 t.Errorf("got %d results, expected %d", count, tt.want) 527 } 528 } 529} 530 531func TestPut(t *testing.T) { 532 index, err := Open("Doc") 533 if err != nil { 534 t.Fatalf("err from Open: %v", err) 535 } 536 537 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 538 expectedIn := &pb.IndexDocumentRequest{ 539 Params: &pb.IndexDocumentParams{ 540 Document: []*pb.Document{ 541 {Field: protoFields, OrderId: proto.Int32(42), OrderIdSource: pb.Document_SUPPLIED.Enum()}, 542 }, 543 IndexSpec: &pb.IndexSpec{ 544 Name: proto.String("Doc"), 545 }, 546 }, 547 } 548 if !proto.Equal(in, expectedIn) { 549 return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) 550 } 551 *out = pb.IndexDocumentResponse{ 552 Status: []*pb.RequestStatus{ 553 {Code: pb.SearchServiceError_OK.Enum()}, 554 }, 555 DocId: []string{ 556 "doc_id", 557 }, 558 } 559 return nil 560 }) 561 562 id, err := index.Put(c, "", &FieldListWithMeta{ 563 Meta: searchMeta, 564 Fields: searchFields, 565 }) 566 if err != nil { 567 t.Fatal(err) 568 } 569 if want := "doc_id"; id != want { 570 t.Errorf("Got doc ID %q, want %q", id, want) 571 } 572} 573 574func TestPutAutoOrderID(t *testing.T) { 575 index, err := Open("Doc") 576 if err != nil { 577 t.Fatalf("err from Open: %v", err) 578 } 579 580 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 581 if len(in.Params.GetDocument()) < 1 { 582 return fmt.Errorf("expected at least one Document, got %v", in) 583 } 584 got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds()) 585 if d := got - want; -5 > d || d > 5 { 586 return fmt.Errorf("got OrderId %d, want near %d", got, want) 587 } 588 *out = pb.IndexDocumentResponse{ 589 Status: []*pb.RequestStatus{ 590 {Code: pb.SearchServiceError_OK.Enum()}, 591 }, 592 DocId: []string{ 593 "doc_id", 594 }, 595 } 596 return nil 597 }) 598 599 if _, err := index.Put(c, "", &searchFields); err != nil { 600 t.Fatal(err) 601 } 602} 603 604func TestPutBadStatus(t *testing.T) { 605 index, err := Open("Doc") 606 if err != nil { 607 t.Fatalf("err from Open: %v", err) 608 } 609 610 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 611 *out = pb.IndexDocumentResponse{ 612 Status: []*pb.RequestStatus{ 613 { 614 Code: pb.SearchServiceError_INVALID_REQUEST.Enum(), 615 ErrorDetail: proto.String("insufficient gophers"), 616 }, 617 }, 618 } 619 return nil 620 }) 621 622 wantErr := "search: INVALID_REQUEST: insufficient gophers" 623 if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != wantErr { 624 t.Fatalf("Put: got %v error, want %q", err, wantErr) 625 } 626} 627 628func TestPutMultiNilIDSlice(t *testing.T) { 629 index, err := Open("Doc") 630 if err != nil { 631 t.Fatalf("err from Open: %v", err) 632 } 633 634 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 635 if len(in.Params.GetDocument()) < 1 { 636 return fmt.Errorf("got %v, want at least 1 document", in) 637 } 638 got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds()) 639 if d := got - want; -5 > d || d > 5 { 640 return fmt.Errorf("got OrderId %d, want near %d", got, want) 641 } 642 *out = pb.IndexDocumentResponse{ 643 Status: []*pb.RequestStatus{ 644 {Code: pb.SearchServiceError_OK.Enum()}, 645 }, 646 DocId: []string{ 647 "doc_id", 648 }, 649 } 650 return nil 651 }) 652 653 if _, err := index.PutMulti(c, nil, []interface{}{&searchFields}); err != nil { 654 t.Fatal(err) 655 } 656} 657 658func TestPutMultiError(t *testing.T) { 659 index, err := Open("Doc") 660 if err != nil { 661 t.Fatalf("err from Open: %v", err) 662 } 663 664 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 665 *out = pb.IndexDocumentResponse{ 666 Status: []*pb.RequestStatus{ 667 {Code: pb.SearchServiceError_OK.Enum()}, 668 {Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")}, 669 }, 670 DocId: []string{ 671 "id1", 672 "", 673 }, 674 } 675 return nil 676 }) 677 678 switch _, err := index.PutMulti(c, nil, []interface{}{&searchFields, &searchFields}); { 679 case err == nil: 680 t.Fatalf("got nil, want error") 681 case err.(appengine.MultiError)[0] != nil: 682 t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0]) 683 case err.(appengine.MultiError)[1] == nil: 684 t.Fatalf("got nil, want not-nill MultiError[1]") 685 } 686} 687 688func TestPutMultiWrongNumberOfIDs(t *testing.T) { 689 index, err := Open("Doc") 690 if err != nil { 691 t.Fatalf("err from Open: %v", err) 692 } 693 694 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 695 return nil 696 }) 697 698 if _, err := index.PutMulti(c, []string{"a"}, []interface{}{&searchFields, &searchFields}); err == nil { 699 t.Fatal("got success, want error") 700 } 701} 702 703func TestPutMultiTooManyDocs(t *testing.T) { 704 index, err := Open("Doc") 705 if err != nil { 706 t.Fatalf("err from Open: %v", err) 707 } 708 709 c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error { 710 return nil 711 }) 712 713 srcs := make([]interface{}, 201) 714 for i, _ := range srcs { 715 srcs[i] = &searchFields 716 } 717 718 if _, err := index.PutMulti(c, nil, srcs); err != ErrTooManyDocuments { 719 t.Fatalf("got %v, want ErrTooManyDocuments", err) 720 } 721} 722 723func TestSortOptions(t *testing.T) { 724 index, err := Open("Doc") 725 if err != nil { 726 t.Fatalf("err from Open: %v", err) 727 } 728 729 noErr := errors.New("") // Sentinel err to return to prevent sending request. 730 731 testCases := []struct { 732 desc string 733 sort *SortOptions 734 wantSort []*pb.SortSpec 735 wantScorer *pb.ScorerSpec 736 wantErr string 737 }{ 738 { 739 desc: "No SortOptions", 740 }, 741 { 742 desc: "Basic", 743 sort: &SortOptions{ 744 Expressions: []SortExpression{ 745 {Expr: "dog"}, 746 {Expr: "cat", Reverse: true}, 747 {Expr: "gopher", Default: "blue"}, 748 {Expr: "fish", Default: 2.0}, 749 }, 750 Limit: 42, 751 Scorer: MatchScorer, 752 }, 753 wantSort: []*pb.SortSpec{ 754 {SortExpression: proto.String("dog")}, 755 {SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)}, 756 {SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")}, 757 {SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)}, 758 }, 759 wantScorer: &pb.ScorerSpec{ 760 Limit: proto.Int32(42), 761 Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(), 762 }, 763 }, 764 { 765 desc: "Bad expression default", 766 sort: &SortOptions{ 767 Expressions: []SortExpression{ 768 {Expr: "dog", Default: true}, 769 }, 770 }, 771 wantErr: `search: invalid Default type bool for expression "dog"`, 772 }, 773 { 774 desc: "RescoringMatchScorer", 775 sort: &SortOptions{Scorer: RescoringMatchScorer}, 776 wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()}, 777 }, 778 } 779 780 for _, tt := range testCases { 781 c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { 782 params := req.Params 783 if !reflect.DeepEqual(params.SortSpec, tt.wantSort) { 784 t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort) 785 } 786 if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) { 787 t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer) 788 } 789 return noErr // Always return some error to prevent response parsing. 790 }) 791 792 it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort}) 793 _, err := it.Next(nil) 794 if err == nil { 795 t.Fatalf("%s: err==nil; should not happen", tt.desc) 796 } 797 if err.Error() != tt.wantErr { 798 t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) 799 } 800 } 801} 802 803func TestFieldSpec(t *testing.T) { 804 index, err := Open("Doc") 805 if err != nil { 806 t.Fatalf("err from Open: %v", err) 807 } 808 809 errFoo := errors.New("foo") // sentinel error when there isn't one. 810 811 testCases := []struct { 812 desc string 813 opts *SearchOptions 814 want *pb.FieldSpec 815 }{ 816 { 817 desc: "No options", 818 want: &pb.FieldSpec{}, 819 }, 820 { 821 desc: "Fields", 822 opts: &SearchOptions{ 823 Fields: []string{"one", "two"}, 824 }, 825 want: &pb.FieldSpec{ 826 Name: []string{"one", "two"}, 827 }, 828 }, 829 { 830 desc: "Expressions", 831 opts: &SearchOptions{ 832 Expressions: []FieldExpression{ 833 {Name: "one", Expr: "price * quantity"}, 834 {Name: "two", Expr: "min(daily_use, 10) * rate"}, 835 }, 836 }, 837 want: &pb.FieldSpec{ 838 Expression: []*pb.FieldSpec_Expression{ 839 {Name: proto.String("one"), Expression: proto.String("price * quantity")}, 840 {Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")}, 841 }, 842 }, 843 }, 844 } 845 846 for _, tt := range testCases { 847 c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { 848 params := req.Params 849 if !reflect.DeepEqual(params.FieldSpec, tt.want) { 850 t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want) 851 } 852 return errFoo // Always return some error to prevent response parsing. 853 }) 854 855 it := index.Search(c, "gopher", tt.opts) 856 if _, err := it.Next(nil); err != errFoo { 857 t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo) 858 } 859 } 860} 861 862func TestBasicSearchOpts(t *testing.T) { 863 index, err := Open("Doc") 864 if err != nil { 865 t.Fatalf("err from Open: %v", err) 866 } 867 868 noErr := errors.New("") // Sentinel err to return to prevent sending request. 869 870 testCases := []struct { 871 desc string 872 facetOpts []FacetSearchOption 873 cursor Cursor 874 offset int 875 countAccuracy int 876 want *pb.SearchParams 877 wantErr string 878 }{ 879 { 880 desc: "No options", 881 want: &pb.SearchParams{}, 882 }, 883 { 884 desc: "Default auto discovery", 885 facetOpts: []FacetSearchOption{ 886 AutoFacetDiscovery(0, 0), 887 }, 888 want: &pb.SearchParams{ 889 AutoDiscoverFacetCount: proto.Int32(10), 890 }, 891 }, 892 { 893 desc: "Auto discovery", 894 facetOpts: []FacetSearchOption{ 895 AutoFacetDiscovery(7, 12), 896 }, 897 want: &pb.SearchParams{ 898 AutoDiscoverFacetCount: proto.Int32(7), 899 FacetAutoDetectParam: &pb.FacetAutoDetectParam{ 900 ValueLimit: proto.Int32(12), 901 }, 902 }, 903 }, 904 { 905 desc: "Param Depth", 906 facetOpts: []FacetSearchOption{ 907 AutoFacetDiscovery(7, 12), 908 }, 909 want: &pb.SearchParams{ 910 AutoDiscoverFacetCount: proto.Int32(7), 911 FacetAutoDetectParam: &pb.FacetAutoDetectParam{ 912 ValueLimit: proto.Int32(12), 913 }, 914 }, 915 }, 916 { 917 desc: "Doc depth", 918 facetOpts: []FacetSearchOption{ 919 FacetDocumentDepth(123), 920 }, 921 want: &pb.SearchParams{ 922 FacetDepth: proto.Int32(123), 923 }, 924 }, 925 { 926 desc: "Facet discovery", 927 facetOpts: []FacetSearchOption{ 928 FacetDiscovery("colour"), 929 FacetDiscovery("size", Atom("M"), Atom("L")), 930 FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)), 931 }, 932 want: &pb.SearchParams{ 933 IncludeFacet: []*pb.FacetRequest{ 934 {Name: proto.String("colour")}, 935 {Name: proto.String("size"), Params: &pb.FacetRequestParam{ 936 ValueConstraint: []string{"M", "L"}, 937 }}, 938 {Name: proto.String("price"), Params: &pb.FacetRequestParam{ 939 Range: []*pb.FacetRange{ 940 {End: proto.String("7e+00")}, 941 {Start: proto.String("7e+00"), End: proto.String("1.4e+01")}, 942 {Start: proto.String("1.4e+01")}, 943 }, 944 }}, 945 }, 946 }, 947 }, 948 { 949 desc: "Facet discovery - bad value", 950 facetOpts: []FacetSearchOption{ 951 FacetDiscovery("colour", true), 952 }, 953 wantErr: "bad FacetSearchOption: unsupported value type bool", 954 }, 955 { 956 desc: "Facet discovery - mix value types", 957 facetOpts: []FacetSearchOption{ 958 FacetDiscovery("colour", Atom("blue"), AtLeast(7)), 959 }, 960 wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range", 961 }, 962 { 963 desc: "Facet discovery - invalid range", 964 facetOpts: []FacetSearchOption{ 965 FacetDiscovery("colour", Range{negInf, posInf}), 966 }, 967 wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite", 968 }, 969 { 970 desc: "Cursor", 971 cursor: Cursor("mycursor"), 972 want: &pb.SearchParams{ 973 Cursor: proto.String("mycursor"), 974 }, 975 }, 976 { 977 desc: "Offset", 978 offset: 121, 979 want: &pb.SearchParams{ 980 Offset: proto.Int32(121), 981 }, 982 }, 983 { 984 desc: "Cursor and Offset set", 985 cursor: Cursor("mycursor"), 986 offset: 121, 987 wantErr: "at most one of Cursor and Offset may be specified", 988 }, 989 { 990 desc: "Count accuracy", 991 countAccuracy: 100, 992 want: &pb.SearchParams{ 993 MatchedCountAccuracy: proto.Int32(100), 994 }, 995 }, 996 } 997 998 for _, tt := range testCases { 999 c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { 1000 if tt.want == nil { 1001 t.Errorf("%s: expected call to fail", tt.desc) 1002 return nil 1003 } 1004 // Set default fields. 1005 tt.want.Query = proto.String("gopher") 1006 tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")} 1007 tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum() 1008 tt.want.FieldSpec = &pb.FieldSpec{} 1009 if got := req.Params; !reflect.DeepEqual(got, tt.want) { 1010 t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want) 1011 } 1012 return noErr // Always return some error to prevent response parsing. 1013 }) 1014 1015 it := index.Search(c, "gopher", &SearchOptions{ 1016 Facets: tt.facetOpts, 1017 Cursor: tt.cursor, 1018 Offset: tt.offset, 1019 CountAccuracy: tt.countAccuracy, 1020 }) 1021 _, err := it.Next(nil) 1022 if err == nil { 1023 t.Fatalf("%s: err==nil; should not happen", tt.desc) 1024 } 1025 if err.Error() != tt.wantErr { 1026 t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) 1027 } 1028 } 1029} 1030 1031func TestFacetRefinements(t *testing.T) { 1032 index, err := Open("Doc") 1033 if err != nil { 1034 t.Fatalf("err from Open: %v", err) 1035 } 1036 1037 noErr := errors.New("") // Sentinel err to return to prevent sending request. 1038 1039 testCases := []struct { 1040 desc string 1041 refine []Facet 1042 want []*pb.FacetRefinement 1043 wantErr string 1044 }{ 1045 { 1046 desc: "No refinements", 1047 }, 1048 { 1049 desc: "Basic", 1050 refine: []Facet{ 1051 {Name: "fur", Value: Atom("fluffy")}, 1052 {Name: "age", Value: LessThan(123)}, 1053 {Name: "age", Value: AtLeast(0)}, 1054 {Name: "legs", Value: Range{Start: 3, End: 5}}, 1055 }, 1056 want: []*pb.FacetRefinement{ 1057 {Name: proto.String("fur"), Value: proto.String("fluffy")}, 1058 {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}}, 1059 {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}}, 1060 {Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}}, 1061 }, 1062 }, 1063 { 1064 desc: "Infinite range", 1065 refine: []Facet{ 1066 {Name: "age", Value: Range{Start: negInf, End: posInf}}, 1067 }, 1068 wantErr: `search: refinement for facet "age": either Start or End must be finite`, 1069 }, 1070 { 1071 desc: "Bad End value in range", 1072 refine: []Facet{ 1073 {Name: "age", Value: LessThan(2147483648)}, 1074 }, 1075 wantErr: `search: refinement for facet "age": invalid value for End`, 1076 }, 1077 { 1078 desc: "Bad Start value in range", 1079 refine: []Facet{ 1080 {Name: "age", Value: AtLeast(-2147483649)}, 1081 }, 1082 wantErr: `search: refinement for facet "age": invalid value for Start`, 1083 }, 1084 { 1085 desc: "Unknown value type", 1086 refine: []Facet{ 1087 {Name: "age", Value: "you can't use strings!"}, 1088 }, 1089 wantErr: `search: unsupported refinement for facet "age" of type string`, 1090 }, 1091 } 1092 1093 for _, tt := range testCases { 1094 c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error { 1095 if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) { 1096 t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want) 1097 } 1098 return noErr // Always return some error to prevent response parsing. 1099 }) 1100 1101 it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine}) 1102 _, err := it.Next(nil) 1103 if err == nil { 1104 t.Fatalf("%s: err==nil; should not happen", tt.desc) 1105 } 1106 if err.Error() != tt.wantErr { 1107 t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr) 1108 } 1109 } 1110} 1111 1112func TestNamespaceResetting(t *testing.T) { 1113 namec := make(chan *string, 1) 1114 c0 := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(req *pb.IndexDocumentRequest, res *pb.IndexDocumentResponse) error { 1115 namec <- req.Params.IndexSpec.Namespace 1116 return fmt.Errorf("RPC error") 1117 }) 1118 1119 // Check that wrapping c0 in a namespace twice works correctly. 1120 c1, err := appengine.Namespace(c0, "A") 1121 if err != nil { 1122 t.Fatalf("appengine.Namespace: %v", err) 1123 } 1124 c2, err := appengine.Namespace(c1, "") // should act as the original context 1125 if err != nil { 1126 t.Fatalf("appengine.Namespace: %v", err) 1127 } 1128 1129 i := (&Index{}) 1130 1131 i.Put(c0, "something", &searchDoc) 1132 if ns := <-namec; ns != nil { 1133 t.Errorf(`Put with c0: ns = %q, want nil`, *ns) 1134 } 1135 1136 i.Put(c1, "something", &searchDoc) 1137 if ns := <-namec; ns == nil { 1138 t.Error(`Put with c1: ns = nil, want "A"`) 1139 } else if *ns != "A" { 1140 t.Errorf(`Put with c1: ns = %q, want "A"`, *ns) 1141 } 1142 1143 i.Put(c2, "something", &searchDoc) 1144 if ns := <-namec; ns != nil { 1145 t.Errorf(`Put with c2: ns = %q, want nil`, *ns) 1146 } 1147} 1148 1149func TestDelete(t *testing.T) { 1150 index, err := Open("Doc") 1151 if err != nil { 1152 t.Fatalf("err from Open: %v", err) 1153 } 1154 1155 c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { 1156 expectedIn := &pb.DeleteDocumentRequest{ 1157 Params: &pb.DeleteDocumentParams{ 1158 DocId: []string{"id"}, 1159 IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, 1160 }, 1161 } 1162 if !proto.Equal(in, expectedIn) { 1163 return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) 1164 } 1165 *out = pb.DeleteDocumentResponse{ 1166 Status: []*pb.RequestStatus{ 1167 {Code: pb.SearchServiceError_OK.Enum()}, 1168 }, 1169 } 1170 return nil 1171 }) 1172 1173 if err := index.Delete(c, "id"); err != nil { 1174 t.Fatal(err) 1175 } 1176} 1177 1178func TestDeleteMulti(t *testing.T) { 1179 index, err := Open("Doc") 1180 if err != nil { 1181 t.Fatalf("err from Open: %v", err) 1182 } 1183 1184 c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { 1185 expectedIn := &pb.DeleteDocumentRequest{ 1186 Params: &pb.DeleteDocumentParams{ 1187 DocId: []string{"id1", "id2"}, 1188 IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, 1189 }, 1190 } 1191 if !proto.Equal(in, expectedIn) { 1192 return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) 1193 } 1194 *out = pb.DeleteDocumentResponse{ 1195 Status: []*pb.RequestStatus{ 1196 {Code: pb.SearchServiceError_OK.Enum()}, 1197 {Code: pb.SearchServiceError_OK.Enum()}, 1198 }, 1199 } 1200 return nil 1201 }) 1202 1203 if err := index.DeleteMulti(c, []string{"id1", "id2"}); err != nil { 1204 t.Fatal(err) 1205 } 1206} 1207 1208func TestDeleteWrongNumberOfResults(t *testing.T) { 1209 index, err := Open("Doc") 1210 if err != nil { 1211 t.Fatalf("err from Open: %v", err) 1212 } 1213 1214 c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { 1215 expectedIn := &pb.DeleteDocumentRequest{ 1216 Params: &pb.DeleteDocumentParams{ 1217 DocId: []string{"id1", "id2"}, 1218 IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, 1219 }, 1220 } 1221 if !proto.Equal(in, expectedIn) { 1222 return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) 1223 } 1224 *out = pb.DeleteDocumentResponse{ 1225 Status: []*pb.RequestStatus{ 1226 {Code: pb.SearchServiceError_OK.Enum()}, 1227 }, 1228 } 1229 return nil 1230 }) 1231 1232 if err := index.DeleteMulti(c, []string{"id1", "id2"}); err == nil { 1233 t.Fatalf("got nil, want error") 1234 } 1235} 1236 1237func TestDeleteMultiError(t *testing.T) { 1238 index, err := Open("Doc") 1239 if err != nil { 1240 t.Fatalf("err from Open: %v", err) 1241 } 1242 1243 c := aetesting.FakeSingleContext(t, "search", "DeleteDocument", func(in *pb.DeleteDocumentRequest, out *pb.DeleteDocumentResponse) error { 1244 expectedIn := &pb.DeleteDocumentRequest{ 1245 Params: &pb.DeleteDocumentParams{ 1246 DocId: []string{"id1", "id2"}, 1247 IndexSpec: &pb.IndexSpec{Name: proto.String("Doc")}, 1248 }, 1249 } 1250 if !proto.Equal(in, expectedIn) { 1251 return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn) 1252 } 1253 *out = pb.DeleteDocumentResponse{ 1254 Status: []*pb.RequestStatus{ 1255 {Code: pb.SearchServiceError_OK.Enum()}, 1256 {Code: pb.SearchServiceError_PERMISSION_DENIED.Enum(), ErrorDetail: proto.String("foo")}, 1257 }, 1258 } 1259 return nil 1260 }) 1261 1262 switch err := index.DeleteMulti(c, []string{"id1", "id2"}); { 1263 case err == nil: 1264 t.Fatalf("got nil, want error") 1265 case err.(appengine.MultiError)[0] != nil: 1266 t.Fatalf("got %v, want nil MultiError[0]", err.(appengine.MultiError)[0]) 1267 case err.(appengine.MultiError)[1] == nil: 1268 t.Fatalf("got nil, want not-nill MultiError[1]") 1269 } 1270} 1271