search_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  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. package search
  5. import (
  6. "errors"
  7. "fmt"
  8. "reflect"
  9. "strings"
  10. "testing"
  11. "time"
  12. "github.com/golang/protobuf/proto"
  13. "google.golang.org/appengine"
  14. "google.golang.org/appengine/internal/aetesting"
  15. pb "google.golang.org/appengine/internal/search"
  16. )
  17. type TestDoc struct {
  18. String string
  19. Atom Atom
  20. HTML HTML
  21. Float float64
  22. Location appengine.GeoPoint
  23. Time time.Time
  24. }
  25. type FieldListWithMeta struct {
  26. Fields FieldList
  27. Meta *DocumentMetadata
  28. }
  29. func (f *FieldListWithMeta) Load(fields []Field, meta *DocumentMetadata) error {
  30. f.Meta = meta
  31. return f.Fields.Load(fields, nil)
  32. }
  33. func (f *FieldListWithMeta) Save() ([]Field, *DocumentMetadata, error) {
  34. fields, _, err := f.Fields.Save()
  35. return fields, f.Meta, err
  36. }
  37. // Assert that FieldListWithMeta satisfies FieldLoadSaver
  38. var _ FieldLoadSaver = &FieldListWithMeta{}
  39. var (
  40. float = 3.14159
  41. floatOut = "3.14159e+00"
  42. latitude = 37.3894
  43. longitude = 122.0819
  44. testGeo = appengine.GeoPoint{latitude, longitude}
  45. testString = "foo<b>bar"
  46. testTime = time.Unix(1337324400, 0)
  47. testTimeOut = "1337324400000"
  48. searchMeta = &DocumentMetadata{
  49. Rank: 42,
  50. }
  51. searchDoc = TestDoc{
  52. String: testString,
  53. Atom: Atom(testString),
  54. HTML: HTML(testString),
  55. Float: float,
  56. Location: testGeo,
  57. Time: testTime,
  58. }
  59. searchFields = FieldList{
  60. Field{Name: "String", Value: testString},
  61. Field{Name: "Atom", Value: Atom(testString)},
  62. Field{Name: "HTML", Value: HTML(testString)},
  63. Field{Name: "Float", Value: float},
  64. Field{Name: "Location", Value: testGeo},
  65. Field{Name: "Time", Value: testTime},
  66. }
  67. // searchFieldsWithLang is a copy of the searchFields with the Language field
  68. // set on text/HTML Fields.
  69. searchFieldsWithLang = FieldList{}
  70. protoFields = []*pb.Field{
  71. newStringValueField("String", testString, pb.FieldValue_TEXT),
  72. newStringValueField("Atom", testString, pb.FieldValue_ATOM),
  73. newStringValueField("HTML", testString, pb.FieldValue_HTML),
  74. newStringValueField("Float", floatOut, pb.FieldValue_NUMBER),
  75. {
  76. Name: proto.String("Location"),
  77. Value: &pb.FieldValue{
  78. Geo: &pb.FieldValue_Geo{
  79. Lat: proto.Float64(latitude),
  80. Lng: proto.Float64(longitude),
  81. },
  82. Type: pb.FieldValue_GEO.Enum(),
  83. },
  84. },
  85. newStringValueField("Time", testTimeOut, pb.FieldValue_DATE),
  86. }
  87. )
  88. func init() {
  89. for _, f := range searchFields {
  90. if f.Name == "String" || f.Name == "HTML" {
  91. f.Language = "en"
  92. }
  93. searchFieldsWithLang = append(searchFieldsWithLang, f)
  94. }
  95. }
  96. func newStringValueField(name, value string, valueType pb.FieldValue_ContentType) *pb.Field {
  97. return &pb.Field{
  98. Name: proto.String(name),
  99. Value: &pb.FieldValue{
  100. StringValue: proto.String(value),
  101. Type: valueType.Enum(),
  102. },
  103. }
  104. }
  105. func TestValidIndexNameOrDocID(t *testing.T) {
  106. testCases := []struct {
  107. s string
  108. want bool
  109. }{
  110. {"", true},
  111. {"!", false},
  112. {"$", true},
  113. {"!bad", false},
  114. {"good!", true},
  115. {"alsoGood", true},
  116. {"has spaces", false},
  117. {"is_inva\xffid_UTF-8", false},
  118. {"is_non-ASCïI", false},
  119. {"underscores_are_ok", true},
  120. }
  121. for _, tc := range testCases {
  122. if got := validIndexNameOrDocID(tc.s); got != tc.want {
  123. t.Errorf("%q: got %v, want %v", tc.s, got, tc.want)
  124. }
  125. }
  126. }
  127. func TestLoadDoc(t *testing.T) {
  128. got, want := TestDoc{}, searchDoc
  129. if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
  130. t.Fatalf("loadDoc: %v", err)
  131. }
  132. if got != want {
  133. t.Errorf("loadDoc: got %v, wanted %v", got, want)
  134. }
  135. }
  136. func TestSaveDoc(t *testing.T) {
  137. got, err := saveDoc(&searchDoc)
  138. if err != nil {
  139. t.Fatalf("saveDoc: %v", err)
  140. }
  141. want := protoFields
  142. if !reflect.DeepEqual(got.Field, want) {
  143. t.Errorf("\ngot %v\nwant %v", got, want)
  144. }
  145. }
  146. func TestLoadFieldList(t *testing.T) {
  147. var got FieldList
  148. want := searchFieldsWithLang
  149. if err := loadDoc(&got, &pb.Document{Field: protoFields}, nil); err != nil {
  150. t.Fatalf("loadDoc: %v", err)
  151. }
  152. if !reflect.DeepEqual(got, want) {
  153. t.Errorf("\ngot %v\nwant %v", got, want)
  154. }
  155. }
  156. func TestLangFields(t *testing.T) {
  157. fl := &FieldList{
  158. {Name: "Foo", Value: "I am English", Language: "en"},
  159. {Name: "Bar", Value: "私は日本人だ", Language: "jp"},
  160. }
  161. var got FieldList
  162. doc, err := saveDoc(fl)
  163. if err != nil {
  164. t.Fatalf("saveDoc: %v", err)
  165. }
  166. if err := loadDoc(&got, doc, nil); err != nil {
  167. t.Fatalf("loadDoc: %v", err)
  168. }
  169. if want := fl; !reflect.DeepEqual(&got, want) {
  170. t.Errorf("got %v\nwant %v", got, want)
  171. }
  172. }
  173. func TestSaveFieldList(t *testing.T) {
  174. got, err := saveDoc(&searchFields)
  175. if err != nil {
  176. t.Fatalf("saveDoc: %v", err)
  177. }
  178. want := protoFields
  179. if !reflect.DeepEqual(got.Field, want) {
  180. t.Errorf("\ngot %v\nwant %v", got, want)
  181. }
  182. }
  183. func TestLoadFieldAndExprList(t *testing.T) {
  184. var got, want FieldList
  185. for i, f := range searchFieldsWithLang {
  186. f.Derived = (i >= 2) // First 2 elements are "fields", next are "expressions".
  187. want = append(want, f)
  188. }
  189. doc, expr := &pb.Document{Field: protoFields[:2]}, protoFields[2:]
  190. if err := loadDoc(&got, doc, expr); err != nil {
  191. t.Fatalf("loadDoc: %v", err)
  192. }
  193. if !reflect.DeepEqual(got, want) {
  194. t.Errorf("got %v\nwant %v", got, want)
  195. }
  196. }
  197. func TestLoadMeta(t *testing.T) {
  198. var got FieldListWithMeta
  199. want := FieldListWithMeta{
  200. Meta: searchMeta,
  201. Fields: searchFieldsWithLang,
  202. }
  203. doc := &pb.Document{
  204. Field: protoFields,
  205. OrderId: proto.Int32(42),
  206. }
  207. if err := loadDoc(&got, doc, nil); err != nil {
  208. t.Fatalf("loadDoc: %v", err)
  209. }
  210. if !reflect.DeepEqual(got, want) {
  211. t.Errorf("\ngot %v\nwant %v", got, want)
  212. }
  213. }
  214. func TestSaveMeta(t *testing.T) {
  215. got, err := saveDoc(&FieldListWithMeta{
  216. Meta: searchMeta,
  217. Fields: searchFields,
  218. })
  219. if err != nil {
  220. t.Fatalf("saveDoc: %v", err)
  221. }
  222. want := &pb.Document{
  223. Field: protoFields,
  224. OrderId: proto.Int32(42),
  225. }
  226. if !proto.Equal(got, want) {
  227. t.Errorf("\ngot %v\nwant %v", got, want)
  228. }
  229. }
  230. func TestValidFieldNames(t *testing.T) {
  231. testCases := []struct {
  232. name string
  233. valid bool
  234. }{
  235. {"Normal", true},
  236. {"Also_OK_123", true},
  237. {"Not so great", false},
  238. {"lower_case", true},
  239. {"Exclaim!", false},
  240. {"Hello세상아 안녕", false},
  241. {"", false},
  242. {"Hεllo", false},
  243. {strings.Repeat("A", 500), true},
  244. {strings.Repeat("A", 501), false},
  245. }
  246. for _, tc := range testCases {
  247. _, err := saveDoc(&FieldList{
  248. Field{Name: tc.name, Value: "val"},
  249. })
  250. if err != nil && !strings.Contains(err.Error(), "invalid field name") {
  251. t.Errorf("unexpected err %q for field name %q", err, tc.name)
  252. }
  253. if (err == nil) != tc.valid {
  254. t.Errorf("field %q: expected valid %t, received err %v", tc.name, tc.valid, err)
  255. }
  256. }
  257. }
  258. func TestValidLangs(t *testing.T) {
  259. testCases := []struct {
  260. field Field
  261. valid bool
  262. }{
  263. {Field{Name: "Foo", Value: "String", Language: ""}, true},
  264. {Field{Name: "Foo", Value: "String", Language: "en"}, true},
  265. {Field{Name: "Foo", Value: "String", Language: "aussie"}, false},
  266. {Field{Name: "Foo", Value: "String", Language: "12"}, false},
  267. {Field{Name: "Foo", Value: HTML("String"), Language: "en"}, true},
  268. {Field{Name: "Foo", Value: Atom("String"), Language: "en"}, false},
  269. {Field{Name: "Foo", Value: 42, Language: "en"}, false},
  270. }
  271. for _, tt := range testCases {
  272. _, err := saveDoc(&FieldList{tt.field})
  273. if err == nil != tt.valid {
  274. t.Errorf("Field %v, got error %v, wanted valid %t", tt.field, err, tt.valid)
  275. }
  276. }
  277. }
  278. func TestDuplicateFields(t *testing.T) {
  279. testCases := []struct {
  280. desc string
  281. fields FieldList
  282. errMsg string // Non-empty if we expect an error
  283. }{
  284. {
  285. desc: "multi string",
  286. fields: FieldList{{Name: "FieldA", Value: "val1"}, {Name: "FieldA", Value: "val2"}, {Name: "FieldA", Value: "val3"}},
  287. },
  288. {
  289. desc: "multi atom",
  290. fields: FieldList{{Name: "FieldA", Value: Atom("val1")}, {Name: "FieldA", Value: Atom("val2")}, {Name: "FieldA", Value: Atom("val3")}},
  291. },
  292. {
  293. desc: "mixed",
  294. fields: FieldList{{Name: "FieldA", Value: testString}, {Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: float}},
  295. },
  296. {
  297. desc: "multi time",
  298. fields: FieldList{{Name: "FieldA", Value: testTime}, {Name: "FieldA", Value: testTime}},
  299. errMsg: `duplicate time field "FieldA"`,
  300. },
  301. {
  302. desc: "multi num",
  303. fields: FieldList{{Name: "FieldA", Value: float}, {Name: "FieldA", Value: float}},
  304. errMsg: `duplicate numeric field "FieldA"`,
  305. },
  306. }
  307. for _, tc := range testCases {
  308. _, err := saveDoc(&tc.fields)
  309. if (err == nil) != (tc.errMsg == "") || (err != nil && !strings.Contains(err.Error(), tc.errMsg)) {
  310. t.Errorf("%s: got err %v, wanted %q", tc.desc, err, tc.errMsg)
  311. }
  312. }
  313. }
  314. func TestLoadErrFieldMismatch(t *testing.T) {
  315. testCases := []struct {
  316. desc string
  317. dst interface{}
  318. src []*pb.Field
  319. err error
  320. }{
  321. {
  322. desc: "missing",
  323. dst: &struct{ One string }{},
  324. src: []*pb.Field{newStringValueField("Two", "woop!", pb.FieldValue_TEXT)},
  325. err: &ErrFieldMismatch{
  326. FieldName: "Two",
  327. Reason: "no such struct field",
  328. },
  329. },
  330. {
  331. desc: "wrong type",
  332. dst: &struct{ Num float64 }{},
  333. src: []*pb.Field{newStringValueField("Num", "woop!", pb.FieldValue_TEXT)},
  334. err: &ErrFieldMismatch{
  335. FieldName: "Num",
  336. Reason: "type mismatch: float64 for string data",
  337. },
  338. },
  339. {
  340. desc: "unsettable",
  341. dst: &struct{ lower string }{},
  342. src: []*pb.Field{newStringValueField("lower", "woop!", pb.FieldValue_TEXT)},
  343. err: &ErrFieldMismatch{
  344. FieldName: "lower",
  345. Reason: "cannot set struct field",
  346. },
  347. },
  348. }
  349. for _, tc := range testCases {
  350. err := loadDoc(tc.dst, &pb.Document{Field: tc.src}, nil)
  351. if !reflect.DeepEqual(err, tc.err) {
  352. t.Errorf("%s, got err %v, wanted %v", tc.desc, err, tc.err)
  353. }
  354. }
  355. }
  356. func TestLimit(t *testing.T) {
  357. index, err := Open("Doc")
  358. if err != nil {
  359. t.Fatalf("err from Open: %v", err)
  360. }
  361. c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, res *pb.SearchResponse) error {
  362. limit := 20 // Default per page.
  363. if req.Params.Limit != nil {
  364. limit = int(*req.Params.Limit)
  365. }
  366. res.Status = &pb.RequestStatus{Code: pb.SearchServiceError_OK.Enum()}
  367. res.MatchedCount = proto.Int64(int64(limit))
  368. for i := 0; i < limit; i++ {
  369. res.Result = append(res.Result, &pb.SearchResult{Document: &pb.Document{}})
  370. res.Cursor = proto.String("moreresults")
  371. }
  372. return nil
  373. })
  374. const maxDocs = 500 // Limit maximum number of docs.
  375. testCases := []struct {
  376. limit, want int
  377. }{
  378. {limit: 0, want: maxDocs},
  379. {limit: 42, want: 42},
  380. {limit: 100, want: 100},
  381. {limit: 1000, want: maxDocs},
  382. }
  383. for _, tt := range testCases {
  384. it := index.Search(c, "gopher", &SearchOptions{Limit: tt.limit, IDsOnly: true})
  385. count := 0
  386. for ; count < maxDocs; count++ {
  387. _, err := it.Next(nil)
  388. if err == Done {
  389. break
  390. }
  391. if err != nil {
  392. t.Fatalf("err after %d: %v", count, err)
  393. }
  394. }
  395. if count != tt.want {
  396. t.Errorf("got %d results, expected %d", count, tt.want)
  397. }
  398. }
  399. }
  400. func TestPut(t *testing.T) {
  401. index, err := Open("Doc")
  402. if err != nil {
  403. t.Fatalf("err from Open: %v", err)
  404. }
  405. c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
  406. expectedIn := &pb.IndexDocumentRequest{
  407. Params: &pb.IndexDocumentParams{
  408. Document: []*pb.Document{
  409. {Field: protoFields, OrderId: proto.Int32(42)},
  410. },
  411. IndexSpec: &pb.IndexSpec{
  412. Name: proto.String("Doc"),
  413. },
  414. },
  415. }
  416. if !proto.Equal(in, expectedIn) {
  417. return fmt.Errorf("unsupported argument:\ngot %v\nwant %v", in, expectedIn)
  418. }
  419. *out = pb.IndexDocumentResponse{
  420. Status: []*pb.RequestStatus{
  421. {Code: pb.SearchServiceError_OK.Enum()},
  422. },
  423. DocId: []string{
  424. "doc_id",
  425. },
  426. }
  427. return nil
  428. })
  429. id, err := index.Put(c, "", &FieldListWithMeta{
  430. Meta: searchMeta,
  431. Fields: searchFields,
  432. })
  433. if err != nil {
  434. t.Fatal(err)
  435. }
  436. if want := "doc_id"; id != want {
  437. t.Errorf("Got doc ID %q, want %q", id, want)
  438. }
  439. }
  440. func TestPutAutoOrderID(t *testing.T) {
  441. index, err := Open("Doc")
  442. if err != nil {
  443. t.Fatalf("err from Open: %v", err)
  444. }
  445. c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(in *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
  446. if len(in.Params.GetDocument()) < 1 {
  447. return fmt.Errorf("expected at least one Document, got %v", in)
  448. }
  449. got, want := in.Params.Document[0].GetOrderId(), int32(time.Since(orderIDEpoch).Seconds())
  450. if d := got - want; -5 > d || d > 5 {
  451. return fmt.Errorf("got OrderId %d, want near %d", got, want)
  452. }
  453. *out = pb.IndexDocumentResponse{
  454. Status: []*pb.RequestStatus{
  455. {Code: pb.SearchServiceError_OK.Enum()},
  456. },
  457. DocId: []string{
  458. "doc_id",
  459. },
  460. }
  461. return nil
  462. })
  463. if _, err := index.Put(c, "", &searchFields); err != nil {
  464. t.Fatal(err)
  465. }
  466. }
  467. func TestPutBadStatus(t *testing.T) {
  468. index, err := Open("Doc")
  469. if err != nil {
  470. t.Fatalf("err from Open: %v", err)
  471. }
  472. c := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(_ *pb.IndexDocumentRequest, out *pb.IndexDocumentResponse) error {
  473. *out = pb.IndexDocumentResponse{
  474. Status: []*pb.RequestStatus{
  475. {
  476. Code: pb.SearchServiceError_INVALID_REQUEST.Enum(),
  477. ErrorDetail: proto.String("insufficient gophers"),
  478. },
  479. },
  480. }
  481. return nil
  482. })
  483. wantErr := "search: INVALID_REQUEST: insufficient gophers"
  484. if _, err := index.Put(c, "", &searchFields); err == nil || err.Error() != wantErr {
  485. t.Fatalf("Put: got %v error, want %q", err, wantErr)
  486. }
  487. }
  488. func TestSortOptions(t *testing.T) {
  489. index, err := Open("Doc")
  490. if err != nil {
  491. t.Fatalf("err from Open: %v", err)
  492. }
  493. noErr := errors.New("") // Sentinel err to return to prevent sending request.
  494. testCases := []struct {
  495. desc string
  496. sort *SortOptions
  497. wantSort []*pb.SortSpec
  498. wantScorer *pb.ScorerSpec
  499. wantErr string
  500. }{
  501. {
  502. desc: "No SortOptions",
  503. },
  504. {
  505. desc: "Basic",
  506. sort: &SortOptions{
  507. Expressions: []SortExpression{
  508. {Expr: "dog"},
  509. {Expr: "cat", Reverse: true},
  510. {Expr: "gopher", Default: "blue"},
  511. {Expr: "fish", Default: 2.0},
  512. },
  513. Limit: 42,
  514. Scorer: MatchScorer,
  515. },
  516. wantSort: []*pb.SortSpec{
  517. {SortExpression: proto.String("dog")},
  518. {SortExpression: proto.String("cat"), SortDescending: proto.Bool(false)},
  519. {SortExpression: proto.String("gopher"), DefaultValueText: proto.String("blue")},
  520. {SortExpression: proto.String("fish"), DefaultValueNumeric: proto.Float64(2)},
  521. },
  522. wantScorer: &pb.ScorerSpec{
  523. Limit: proto.Int32(42),
  524. Scorer: pb.ScorerSpec_MATCH_SCORER.Enum(),
  525. },
  526. },
  527. {
  528. desc: "Bad expression default",
  529. sort: &SortOptions{
  530. Expressions: []SortExpression{
  531. {Expr: "dog", Default: true},
  532. },
  533. },
  534. wantErr: `search: invalid Default type bool for expression "dog"`,
  535. },
  536. {
  537. desc: "RescoringMatchScorer",
  538. sort: &SortOptions{Scorer: RescoringMatchScorer},
  539. wantScorer: &pb.ScorerSpec{Scorer: pb.ScorerSpec_RESCORING_MATCH_SCORER.Enum()},
  540. },
  541. }
  542. for _, tt := range testCases {
  543. c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
  544. params := req.Params
  545. if !reflect.DeepEqual(params.SortSpec, tt.wantSort) {
  546. t.Errorf("%s: params.SortSpec=%v; want %v", tt.desc, params.SortSpec, tt.wantSort)
  547. }
  548. if !reflect.DeepEqual(params.ScorerSpec, tt.wantScorer) {
  549. t.Errorf("%s: params.ScorerSpec=%v; want %v", tt.desc, params.ScorerSpec, tt.wantScorer)
  550. }
  551. return noErr // Always return some error to prevent response parsing.
  552. })
  553. it := index.Search(c, "gopher", &SearchOptions{Sort: tt.sort})
  554. _, err := it.Next(nil)
  555. if err == nil {
  556. t.Fatalf("%s: err==nil; should not happen", tt.desc)
  557. }
  558. if err.Error() != tt.wantErr {
  559. t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
  560. }
  561. }
  562. }
  563. func TestFieldSpec(t *testing.T) {
  564. index, err := Open("Doc")
  565. if err != nil {
  566. t.Fatalf("err from Open: %v", err)
  567. }
  568. errFoo := errors.New("foo") // sentinel error when there isn't one.
  569. testCases := []struct {
  570. desc string
  571. opts *SearchOptions
  572. want *pb.FieldSpec
  573. }{
  574. {
  575. desc: "No options",
  576. want: &pb.FieldSpec{},
  577. },
  578. {
  579. desc: "Fields",
  580. opts: &SearchOptions{
  581. Fields: []string{"one", "two"},
  582. },
  583. want: &pb.FieldSpec{
  584. Name: []string{"one", "two"},
  585. },
  586. },
  587. {
  588. desc: "Expressions",
  589. opts: &SearchOptions{
  590. Expressions: []FieldExpression{
  591. {Name: "one", Expr: "price * quantity"},
  592. {Name: "two", Expr: "min(daily_use, 10) * rate"},
  593. },
  594. },
  595. want: &pb.FieldSpec{
  596. Expression: []*pb.FieldSpec_Expression{
  597. {Name: proto.String("one"), Expression: proto.String("price * quantity")},
  598. {Name: proto.String("two"), Expression: proto.String("min(daily_use, 10) * rate")},
  599. },
  600. },
  601. },
  602. }
  603. for _, tt := range testCases {
  604. c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
  605. params := req.Params
  606. if !reflect.DeepEqual(params.FieldSpec, tt.want) {
  607. t.Errorf("%s: params.FieldSpec=%v; want %v", tt.desc, params.FieldSpec, tt.want)
  608. }
  609. return errFoo // Always return some error to prevent response parsing.
  610. })
  611. it := index.Search(c, "gopher", tt.opts)
  612. if _, err := it.Next(nil); err != errFoo {
  613. t.Fatalf("%s: got error %v; want %v", tt.desc, err, errFoo)
  614. }
  615. }
  616. }
  617. func TestBasicSearchOpts(t *testing.T) {
  618. index, err := Open("Doc")
  619. if err != nil {
  620. t.Fatalf("err from Open: %v", err)
  621. }
  622. noErr := errors.New("") // Sentinel err to return to prevent sending request.
  623. testCases := []struct {
  624. desc string
  625. facetOpts []FacetSearchOption
  626. cursor Cursor
  627. offset int
  628. want *pb.SearchParams
  629. wantErr string
  630. }{
  631. {
  632. desc: "No options",
  633. want: &pb.SearchParams{},
  634. },
  635. {
  636. desc: "Default auto discovery",
  637. facetOpts: []FacetSearchOption{
  638. AutoFacetDiscovery(0, 0),
  639. },
  640. want: &pb.SearchParams{
  641. AutoDiscoverFacetCount: proto.Int32(10),
  642. },
  643. },
  644. {
  645. desc: "Auto discovery",
  646. facetOpts: []FacetSearchOption{
  647. AutoFacetDiscovery(7, 12),
  648. },
  649. want: &pb.SearchParams{
  650. AutoDiscoverFacetCount: proto.Int32(7),
  651. FacetAutoDetectParam: &pb.FacetAutoDetectParam{
  652. ValueLimit: proto.Int32(12),
  653. },
  654. },
  655. },
  656. {
  657. desc: "Param Depth",
  658. facetOpts: []FacetSearchOption{
  659. AutoFacetDiscovery(7, 12),
  660. },
  661. want: &pb.SearchParams{
  662. AutoDiscoverFacetCount: proto.Int32(7),
  663. FacetAutoDetectParam: &pb.FacetAutoDetectParam{
  664. ValueLimit: proto.Int32(12),
  665. },
  666. },
  667. },
  668. {
  669. desc: "Doc depth",
  670. facetOpts: []FacetSearchOption{
  671. FacetDocumentDepth(123),
  672. },
  673. want: &pb.SearchParams{
  674. FacetDepth: proto.Int32(123),
  675. },
  676. },
  677. {
  678. desc: "Facet discovery",
  679. facetOpts: []FacetSearchOption{
  680. FacetDiscovery("colour"),
  681. FacetDiscovery("size", Atom("M"), Atom("L")),
  682. FacetDiscovery("price", LessThan(7), Range{7, 14}, AtLeast(14)),
  683. },
  684. want: &pb.SearchParams{
  685. IncludeFacet: []*pb.FacetRequest{
  686. {Name: proto.String("colour")},
  687. {Name: proto.String("size"), Params: &pb.FacetRequestParam{
  688. ValueConstraint: []string{"M", "L"},
  689. }},
  690. {Name: proto.String("price"), Params: &pb.FacetRequestParam{
  691. Range: []*pb.FacetRange{
  692. {End: proto.String("7e+00")},
  693. {Start: proto.String("7e+00"), End: proto.String("1.4e+01")},
  694. {Start: proto.String("1.4e+01")},
  695. },
  696. }},
  697. },
  698. },
  699. },
  700. {
  701. desc: "Facet discovery - bad value",
  702. facetOpts: []FacetSearchOption{
  703. FacetDiscovery("colour", true),
  704. },
  705. wantErr: "bad FacetSearchOption: unsupported value type bool",
  706. },
  707. {
  708. desc: "Facet discovery - mix value types",
  709. facetOpts: []FacetSearchOption{
  710. FacetDiscovery("colour", Atom("blue"), AtLeast(7)),
  711. },
  712. wantErr: "bad FacetSearchOption: values must all be Atom, or must all be Range",
  713. },
  714. {
  715. desc: "Facet discovery - invalid range",
  716. facetOpts: []FacetSearchOption{
  717. FacetDiscovery("colour", Range{negInf, posInf}),
  718. },
  719. wantErr: "bad FacetSearchOption: invalid range: either Start or End must be finite",
  720. },
  721. {
  722. desc: "Cursor",
  723. cursor: Cursor("mycursor"),
  724. want: &pb.SearchParams{
  725. Cursor: proto.String("mycursor"),
  726. },
  727. },
  728. {
  729. desc: "Offset",
  730. offset: 121,
  731. want: &pb.SearchParams{
  732. Offset: proto.Int32(121),
  733. },
  734. },
  735. {
  736. desc: "Cursor and Offset set",
  737. cursor: Cursor("mycursor"),
  738. offset: 121,
  739. wantErr: "at most one of Cursor and Offset may be specified",
  740. },
  741. }
  742. for _, tt := range testCases {
  743. c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
  744. if tt.want == nil {
  745. t.Errorf("%s: expected call to fail", tt.desc)
  746. return nil
  747. }
  748. // Set default fields.
  749. tt.want.Query = proto.String("gopher")
  750. tt.want.IndexSpec = &pb.IndexSpec{Name: proto.String("Doc")}
  751. tt.want.CursorType = pb.SearchParams_PER_RESULT.Enum()
  752. tt.want.FieldSpec = &pb.FieldSpec{}
  753. if got := req.Params; !reflect.DeepEqual(got, tt.want) {
  754. t.Errorf("%s: params=%v; want %v", tt.desc, got, tt.want)
  755. }
  756. return noErr // Always return some error to prevent response parsing.
  757. })
  758. it := index.Search(c, "gopher", &SearchOptions{
  759. Facets: tt.facetOpts,
  760. Cursor: tt.cursor,
  761. Offset: tt.offset,
  762. })
  763. _, err := it.Next(nil)
  764. if err == nil {
  765. t.Fatalf("%s: err==nil; should not happen", tt.desc)
  766. }
  767. if err.Error() != tt.wantErr {
  768. t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
  769. }
  770. }
  771. }
  772. func TestFacetRefinements(t *testing.T) {
  773. index, err := Open("Doc")
  774. if err != nil {
  775. t.Fatalf("err from Open: %v", err)
  776. }
  777. noErr := errors.New("") // Sentinel err to return to prevent sending request.
  778. testCases := []struct {
  779. desc string
  780. refine []Facet
  781. want []*pb.FacetRefinement
  782. wantErr string
  783. }{
  784. {
  785. desc: "No refinements",
  786. },
  787. {
  788. desc: "Basic",
  789. refine: []Facet{
  790. {Name: "fur", Value: Atom("fluffy")},
  791. {Name: "age", Value: LessThan(123)},
  792. {Name: "age", Value: AtLeast(0)},
  793. {Name: "legs", Value: Range{Start: 3, End: 5}},
  794. },
  795. want: []*pb.FacetRefinement{
  796. {Name: proto.String("fur"), Value: proto.String("fluffy")},
  797. {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{End: proto.String("1.23e+02")}},
  798. {Name: proto.String("age"), Range: &pb.FacetRefinement_Range{Start: proto.String("0e+00")}},
  799. {Name: proto.String("legs"), Range: &pb.FacetRefinement_Range{Start: proto.String("3e+00"), End: proto.String("5e+00")}},
  800. },
  801. },
  802. {
  803. desc: "Infinite range",
  804. refine: []Facet{
  805. {Name: "age", Value: Range{Start: negInf, End: posInf}},
  806. },
  807. wantErr: `search: refinement for facet "age": either Start or End must be finite`,
  808. },
  809. {
  810. desc: "Bad End value in range",
  811. refine: []Facet{
  812. {Name: "age", Value: LessThan(2147483648)},
  813. },
  814. wantErr: `search: refinement for facet "age": invalid value for End`,
  815. },
  816. {
  817. desc: "Bad Start value in range",
  818. refine: []Facet{
  819. {Name: "age", Value: AtLeast(-2147483649)},
  820. },
  821. wantErr: `search: refinement for facet "age": invalid value for Start`,
  822. },
  823. {
  824. desc: "Unknown value type",
  825. refine: []Facet{
  826. {Name: "age", Value: "you can't use strings!"},
  827. },
  828. wantErr: `search: unsupported refinement for facet "age" of type string`,
  829. },
  830. }
  831. for _, tt := range testCases {
  832. c := aetesting.FakeSingleContext(t, "search", "Search", func(req *pb.SearchRequest, _ *pb.SearchResponse) error {
  833. if got := req.Params.FacetRefinement; !reflect.DeepEqual(got, tt.want) {
  834. t.Errorf("%s: params.FacetRefinement=%v; want %v", tt.desc, got, tt.want)
  835. }
  836. return noErr // Always return some error to prevent response parsing.
  837. })
  838. it := index.Search(c, "gopher", &SearchOptions{Refinements: tt.refine})
  839. _, err := it.Next(nil)
  840. if err == nil {
  841. t.Fatalf("%s: err==nil; should not happen", tt.desc)
  842. }
  843. if err.Error() != tt.wantErr {
  844. t.Errorf("%s: got error %q, want %q", tt.desc, err, tt.wantErr)
  845. }
  846. }
  847. }