initial commit
This commit is contained in:
209
vendor/google.golang.org/appengine/search/doc.go
generated
vendored
Normal file
209
vendor/google.golang.org/appengine/search/doc.go
generated
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright 2015 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 provides a client for App Engine's search service.
|
||||
|
||||
|
||||
Basic Operations
|
||||
|
||||
Indexes contain documents. Each index is identified by its name: a
|
||||
human-readable ASCII string.
|
||||
|
||||
Within an index, documents are associated with an ID, which is also
|
||||
a human-readable ASCII string. A document's contents are a mapping from
|
||||
case-sensitive field names to values. Valid types for field values are:
|
||||
- string,
|
||||
- search.Atom,
|
||||
- search.HTML,
|
||||
- time.Time (stored with millisecond precision),
|
||||
- float64 (value between -2,147,483,647 and 2,147,483,647 inclusive),
|
||||
- appengine.GeoPoint.
|
||||
|
||||
The Get and Put methods on an Index load and save a document.
|
||||
A document's contents are typically represented by a struct pointer.
|
||||
|
||||
Example code:
|
||||
|
||||
type Doc struct {
|
||||
Author string
|
||||
Comment string
|
||||
Creation time.Time
|
||||
}
|
||||
|
||||
index, err := search.Open("comments")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newID, err := index.Put(ctx, "", &Doc{
|
||||
Author: "gopher",
|
||||
Comment: "the truth of the matter",
|
||||
Creation: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
A single document can be retrieved by its ID. Pass a destination struct
|
||||
to Get to hold the resulting document.
|
||||
|
||||
var doc Doc
|
||||
err := index.Get(ctx, id, &doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Search and Listing Documents
|
||||
|
||||
Indexes have two methods for retrieving multiple documents at once: Search and
|
||||
List.
|
||||
|
||||
Searching an index for a query will result in an iterator. As with an iterator
|
||||
from package datastore, pass a destination struct to Next to decode the next
|
||||
result. Next will return Done when the iterator is exhausted.
|
||||
|
||||
for t := index.Search(ctx, "Comment:truth", nil); ; {
|
||||
var doc Doc
|
||||
id, err := t.Next(&doc)
|
||||
if err == search.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, "%s -> %#v\n", id, doc)
|
||||
}
|
||||
|
||||
Search takes a string query to determine which documents to return. The query
|
||||
can be simple, such as a single word to match, or complex. The query
|
||||
language is described at
|
||||
https://cloud.google.com/appengine/docs/go/search/query_strings
|
||||
|
||||
Search also takes an optional SearchOptions struct which gives much more
|
||||
control over how results are calculated and returned.
|
||||
|
||||
Call List to iterate over all documents in an index.
|
||||
|
||||
for t := index.List(ctx, nil); ; {
|
||||
var doc Doc
|
||||
id, err := t.Next(&doc)
|
||||
if err == search.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(w, "%s -> %#v\n", id, doc)
|
||||
}
|
||||
|
||||
|
||||
Fields and Facets
|
||||
|
||||
A document's contents can be represented by a variety of types. These are
|
||||
typically struct pointers, but they can also be represented by any type
|
||||
implementing the FieldLoadSaver interface. The FieldLoadSaver allows metadata
|
||||
to be set for the document with the DocumentMetadata type. Struct pointers are
|
||||
more strongly typed and are easier to use; FieldLoadSavers are more flexible.
|
||||
|
||||
A document's contents can be expressed in two ways: fields and facets.
|
||||
|
||||
Fields are the most common way of providing content for documents. Fields can
|
||||
store data in multiple types and can be matched in searches using query
|
||||
strings.
|
||||
|
||||
Facets provide a way to attach categorical information to a document. The only
|
||||
valid types for facets are search.Atom and float64. Facets allow search
|
||||
results to contain summaries of the categories matched in a search, and to
|
||||
restrict searches to only match against specific categories.
|
||||
|
||||
By default, for struct pointers, all of the struct fields are used as document
|
||||
fields, and the field name used is the same as on the struct (and hence must
|
||||
start with an upper case letter). Struct fields may have a
|
||||
`search:"name,options"` tag. The name must start with a letter and be
|
||||
composed only of word characters. A "-" tag name means that the field will be
|
||||
ignored. If options is "facet" then the struct field will be used as a
|
||||
document facet. If options is "" then the comma may be omitted. There are no
|
||||
other recognized options.
|
||||
|
||||
Example code:
|
||||
|
||||
// A and B are renamed to a and b.
|
||||
// A, C and I are facets.
|
||||
// D's tag is equivalent to having no tag at all (E).
|
||||
// F and G are ignored entirely by the search package.
|
||||
// I has tag information for both the search and json packages.
|
||||
type TaggedStruct struct {
|
||||
A float64 `search:"a,facet"`
|
||||
B float64 `search:"b"`
|
||||
C float64 `search:",facet"`
|
||||
D float64 `search:""`
|
||||
E float64
|
||||
F float64 `search:"-"`
|
||||
G float64 `search:"-,facet"`
|
||||
I float64 `search:",facet" json:"i"`
|
||||
}
|
||||
|
||||
|
||||
The FieldLoadSaver Interface
|
||||
|
||||
A document's contents can also be represented by any type that implements the
|
||||
FieldLoadSaver interface. This type may be a struct pointer, but it
|
||||
does not have to be. The search package will call Load when loading the
|
||||
document's contents, and Save when saving them. In addition to a slice of
|
||||
Fields, the Load and Save methods also use the DocumentMetadata type to
|
||||
provide additional information about a document (such as its Rank, or set of
|
||||
Facets). Possible uses for this interface include deriving non-stored fields,
|
||||
verifying fields or setting specific languages for string and HTML fields.
|
||||
|
||||
Example code:
|
||||
|
||||
type CustomFieldsExample struct {
|
||||
// Item's title and which language it is in.
|
||||
Title string
|
||||
Lang string
|
||||
// Mass, in grams.
|
||||
Mass int
|
||||
}
|
||||
|
||||
func (x *CustomFieldsExample) Load(fields []search.Field, meta *search.DocumentMetadata) error {
|
||||
// Load the title field, failing if any other field is found.
|
||||
for _, f := range fields {
|
||||
if f.Name != "title" {
|
||||
return fmt.Errorf("unknown field %q", f.Name)
|
||||
}
|
||||
s, ok := f.Value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported type %T for field %q", f.Value, f.Name)
|
||||
}
|
||||
x.Title = s
|
||||
x.Lang = f.Language
|
||||
}
|
||||
// Load the mass facet, failing if any other facet is found.
|
||||
for _, f := range meta.Facets {
|
||||
if f.Name != "mass" {
|
||||
return fmt.Errorf("unknown facet %q", f.Name)
|
||||
}
|
||||
m, ok := f.Value.(float64)
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported type %T for facet %q", f.Value, f.Name)
|
||||
}
|
||||
x.Mass = int(m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CustomFieldsExample) Save() ([]search.Field, *search.DocumentMetadata, error) {
|
||||
fields := []search.Field{
|
||||
{Name: "title", Value: x.Title, Language: x.Lang},
|
||||
}
|
||||
meta := &search.DocumentMetadata{
|
||||
Facets: {
|
||||
{Name: "mass", Value: float64(x.Mass)},
|
||||
},
|
||||
}
|
||||
return fields, meta, nil
|
||||
}
|
||||
*/
|
||||
package search
|
82
vendor/google.golang.org/appengine/search/field.go
generated
vendored
Normal file
82
vendor/google.golang.org/appengine/search/field.go
generated
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2014 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
|
||||
|
||||
// Field is a name/value pair. A search index's document can be loaded and
|
||||
// saved as a sequence of Fields.
|
||||
type Field struct {
|
||||
// Name is the field name. A valid field name matches /[A-Za-z][A-Za-z0-9_]*/.
|
||||
Name string
|
||||
// Value is the field value. The valid types are:
|
||||
// - string,
|
||||
// - search.Atom,
|
||||
// - search.HTML,
|
||||
// - time.Time (stored with millisecond precision),
|
||||
// - float64,
|
||||
// - GeoPoint.
|
||||
Value interface{}
|
||||
// Language is a two-letter ISO 639-1 code for the field's language,
|
||||
// defaulting to "en" if nothing is specified. It may only be specified for
|
||||
// fields of type string and search.HTML.
|
||||
Language string
|
||||
// Derived marks fields that were calculated as a result of a
|
||||
// FieldExpression provided to Search. This field is ignored when saving a
|
||||
// document.
|
||||
Derived bool
|
||||
}
|
||||
|
||||
// Facet is a name/value pair which is used to add categorical information to a
|
||||
// document.
|
||||
type Facet struct {
|
||||
// Name is the facet name. A valid facet name matches /[A-Za-z][A-Za-z0-9_]*/.
|
||||
// A facet name cannot be longer than 500 characters.
|
||||
Name string
|
||||
// Value is the facet value.
|
||||
//
|
||||
// When being used in documents (for example, in
|
||||
// DocumentMetadata.Facets), the valid types are:
|
||||
// - search.Atom,
|
||||
// - float64.
|
||||
//
|
||||
// When being used in SearchOptions.Refinements or being returned
|
||||
// in FacetResult, the valid types are:
|
||||
// - search.Atom,
|
||||
// - search.Range.
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// DocumentMetadata is a struct containing information describing a given document.
|
||||
type DocumentMetadata struct {
|
||||
// Rank is an integer specifying the order the document will be returned in
|
||||
// search results. If zero, the rank will be set to the number of seconds since
|
||||
// 2011-01-01 00:00:00 UTC when being Put into an index.
|
||||
Rank int
|
||||
// Facets is the set of facets for this document.
|
||||
Facets []Facet
|
||||
}
|
||||
|
||||
// FieldLoadSaver can be converted from and to a slice of Fields
|
||||
// with additional document metadata.
|
||||
type FieldLoadSaver interface {
|
||||
Load([]Field, *DocumentMetadata) error
|
||||
Save() ([]Field, *DocumentMetadata, error)
|
||||
}
|
||||
|
||||
// FieldList converts a []Field to implement FieldLoadSaver.
|
||||
type FieldList []Field
|
||||
|
||||
// Load loads all of the provided fields into l.
|
||||
// It does not first reset *l to an empty slice.
|
||||
func (l *FieldList) Load(f []Field, _ *DocumentMetadata) error {
|
||||
*l = append(*l, f...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save returns all of l's fields as a slice of Fields.
|
||||
func (l *FieldList) Save() ([]Field, *DocumentMetadata, error) {
|
||||
return *l, nil, nil
|
||||
}
|
||||
|
||||
var _ FieldLoadSaver = (*FieldList)(nil)
|
1112
vendor/google.golang.org/appengine/search/search.go
generated
vendored
Normal file
1112
vendor/google.golang.org/appengine/search/search.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
991
vendor/google.golang.org/appengine/search/search_test.go
generated
vendored
Normal file
991
vendor/google.golang.org/appengine/search/search_test.go
generated
vendored
Normal file
@@ -0,0 +1,991 @@
|
||||
// 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 newFacet(name, value string, valueType pb.FacetValue_ContentType) *pb.Facet {
|
||||
return &pb.Facet{
|
||||
Name: proto.String(name),
|
||||
Value: &pb.FacetValue{
|
||||
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 TestLoadSaveWithStruct(t *testing.T) {
|
||||
type gopher struct {
|
||||
Name string
|
||||
Info string `search:"about"`
|
||||
Legs float64 `search:",facet"`
|
||||
Fuzz Atom `search:"Fur,facet"`
|
||||
}
|
||||
|
||||
doc := gopher{"Gopher", "Likes slide rules.", 4, Atom("furry")}
|
||||
pb := &pb.Document{
|
||||
Field: []*pb.Field{
|
||||
newStringValueField("Name", "Gopher", pb.FieldValue_TEXT),
|
||||
newStringValueField("about", "Likes slide rules.", pb.FieldValue_TEXT),
|
||||
},
|
||||
Facet: []*pb.Facet{
|
||||
newFacet("Legs", "4e+00", pb.FacetValue_NUMBER),
|
||||
newFacet("Fur", "furry", pb.FacetValue_ATOM),
|
||||
},
|
||||
}
|
||||
|
||||
var gotDoc gopher
|
||||
if err := loadDoc(&gotDoc, pb, nil); err != nil {
|
||||
t.Fatalf("loadDoc: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotDoc, doc) {
|
||||
t.Errorf("loading doc\ngot %v\nwant %v", gotDoc, doc)
|
||||
}
|
||||
|
||||
gotPB, err := saveDoc(&doc)
|
||||
if err != nil {
|
||||
t.Fatalf("saveDoc: %v", err)
|
||||
}
|
||||
gotPB.OrderId = nil // Don't test: it's time dependent.
|
||||
if !proto.Equal(gotPB, pb) {
|
||||
t.Errorf("saving doc\ngot %v\nwant %v", gotPB, pb)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceResetting(t *testing.T) {
|
||||
namec := make(chan *string, 1)
|
||||
c0 := aetesting.FakeSingleContext(t, "search", "IndexDocument", func(req *pb.IndexDocumentRequest, res *pb.IndexDocumentResponse) error {
|
||||
namec <- req.Params.IndexSpec.Namespace
|
||||
return fmt.Errorf("RPC error")
|
||||
})
|
||||
|
||||
// Check that wrapping c0 in a namespace twice works correctly.
|
||||
c1, err := appengine.Namespace(c0, "A")
|
||||
if err != nil {
|
||||
t.Fatalf("appengine.Namespace: %v", err)
|
||||
}
|
||||
c2, err := appengine.Namespace(c1, "") // should act as the original context
|
||||
if err != nil {
|
||||
t.Fatalf("appengine.Namespace: %v", err)
|
||||
}
|
||||
|
||||
i := (&Index{})
|
||||
|
||||
i.Put(c0, "something", &searchDoc)
|
||||
if ns := <-namec; ns != nil {
|
||||
t.Errorf(`Put with c0: ns = %q, want nil`, *ns)
|
||||
}
|
||||
|
||||
i.Put(c1, "something", &searchDoc)
|
||||
if ns := <-namec; ns == nil {
|
||||
t.Error(`Put with c1: ns = nil, want "A"`)
|
||||
} else if *ns != "A" {
|
||||
t.Errorf(`Put with c1: ns = %q, want "A"`, *ns)
|
||||
}
|
||||
|
||||
i.Put(c2, "something", &searchDoc)
|
||||
if ns := <-namec; ns != nil {
|
||||
t.Errorf(`Put with c2: ns = %q, want nil`, *ns)
|
||||
}
|
||||
}
|
251
vendor/google.golang.org/appengine/search/struct.go
generated
vendored
Normal file
251
vendor/google.golang.org/appengine/search/struct.go
generated
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright 2015 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrFieldMismatch is returned when a field is to be loaded into a different
|
||||
// than the one it was stored from, or when a field is missing or unexported in
|
||||
// the destination struct.
|
||||
type ErrFieldMismatch struct {
|
||||
FieldName string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *ErrFieldMismatch) Error() string {
|
||||
return fmt.Sprintf("search: cannot load field %q: %s", e.FieldName, e.Reason)
|
||||
}
|
||||
|
||||
// ErrFacetMismatch is returned when a facet is to be loaded into a different
|
||||
// type than the one it was stored from, or when a field is missing or
|
||||
// unexported in the destination struct. StructType is the type of the struct
|
||||
// pointed to by the destination argument passed to Iterator.Next.
|
||||
type ErrFacetMismatch struct {
|
||||
StructType reflect.Type
|
||||
FacetName string
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e *ErrFacetMismatch) Error() string {
|
||||
return fmt.Sprintf("search: cannot load facet %q into a %q: %s", e.FacetName, e.StructType, e.Reason)
|
||||
}
|
||||
|
||||
// structCodec defines how to convert a given struct to/from a search document.
|
||||
type structCodec struct {
|
||||
// byIndex returns the struct tag for the i'th struct field.
|
||||
byIndex []structTag
|
||||
|
||||
// fieldByName returns the index of the struct field for the given field name.
|
||||
fieldByName map[string]int
|
||||
|
||||
// facetByName returns the index of the struct field for the given facet name,
|
||||
facetByName map[string]int
|
||||
}
|
||||
|
||||
// structTag holds a structured version of each struct field's parsed tag.
|
||||
type structTag struct {
|
||||
name string
|
||||
facet bool
|
||||
ignore bool
|
||||
}
|
||||
|
||||
var (
|
||||
codecsMu sync.RWMutex
|
||||
codecs = map[reflect.Type]*structCodec{}
|
||||
)
|
||||
|
||||
func loadCodec(t reflect.Type) (*structCodec, error) {
|
||||
codecsMu.RLock()
|
||||
codec, ok := codecs[t]
|
||||
codecsMu.RUnlock()
|
||||
if ok {
|
||||
return codec, nil
|
||||
}
|
||||
|
||||
codecsMu.Lock()
|
||||
defer codecsMu.Unlock()
|
||||
if codec, ok := codecs[t]; ok {
|
||||
return codec, nil
|
||||
}
|
||||
|
||||
codec = &structCodec{
|
||||
fieldByName: make(map[string]int),
|
||||
facetByName: make(map[string]int),
|
||||
}
|
||||
|
||||
for i, I := 0, t.NumField(); i < I; i++ {
|
||||
f := t.Field(i)
|
||||
name, opts := f.Tag.Get("search"), ""
|
||||
if i := strings.Index(name, ","); i != -1 {
|
||||
name, opts = name[:i], name[i+1:]
|
||||
}
|
||||
ignore := false
|
||||
if name == "-" {
|
||||
ignore = true
|
||||
} else if name == "" {
|
||||
name = f.Name
|
||||
} else if !validFieldName(name) {
|
||||
return nil, fmt.Errorf("search: struct tag has invalid field name: %q", name)
|
||||
}
|
||||
facet := opts == "facet"
|
||||
codec.byIndex = append(codec.byIndex, structTag{name: name, facet: facet, ignore: ignore})
|
||||
if facet {
|
||||
codec.facetByName[name] = i
|
||||
} else {
|
||||
codec.fieldByName[name] = i
|
||||
}
|
||||
}
|
||||
|
||||
codecs[t] = codec
|
||||
return codec, nil
|
||||
}
|
||||
|
||||
// structFLS adapts a struct to be a FieldLoadSaver.
|
||||
type structFLS struct {
|
||||
v reflect.Value
|
||||
codec *structCodec
|
||||
}
|
||||
|
||||
func (s structFLS) Load(fields []Field, meta *DocumentMetadata) error {
|
||||
var err error
|
||||
for _, field := range fields {
|
||||
i, ok := s.codec.fieldByName[field.Name]
|
||||
if !ok {
|
||||
// Note the error, but keep going.
|
||||
err = &ErrFieldMismatch{
|
||||
FieldName: field.Name,
|
||||
Reason: "no such struct field",
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
f := s.v.Field(i)
|
||||
if !f.CanSet() {
|
||||
// Note the error, but keep going.
|
||||
err = &ErrFieldMismatch{
|
||||
FieldName: field.Name,
|
||||
Reason: "cannot set struct field",
|
||||
}
|
||||
continue
|
||||
}
|
||||
v := reflect.ValueOf(field.Value)
|
||||
if ft, vt := f.Type(), v.Type(); ft != vt {
|
||||
err = &ErrFieldMismatch{
|
||||
FieldName: field.Name,
|
||||
Reason: fmt.Sprintf("type mismatch: %v for %v data", ft, vt),
|
||||
}
|
||||
continue
|
||||
}
|
||||
f.Set(v)
|
||||
}
|
||||
if meta == nil {
|
||||
return err
|
||||
}
|
||||
for _, facet := range meta.Facets {
|
||||
i, ok := s.codec.facetByName[facet.Name]
|
||||
if !ok {
|
||||
// Note the error, but keep going.
|
||||
if err == nil {
|
||||
err = &ErrFacetMismatch{
|
||||
StructType: s.v.Type(),
|
||||
FacetName: facet.Name,
|
||||
Reason: "no matching field found",
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
f := s.v.Field(i)
|
||||
if !f.CanSet() {
|
||||
// Note the error, but keep going.
|
||||
if err == nil {
|
||||
err = &ErrFacetMismatch{
|
||||
StructType: s.v.Type(),
|
||||
FacetName: facet.Name,
|
||||
Reason: "unable to set unexported field of struct",
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
v := reflect.ValueOf(facet.Value)
|
||||
if ft, vt := f.Type(), v.Type(); ft != vt {
|
||||
if err == nil {
|
||||
err = &ErrFacetMismatch{
|
||||
StructType: s.v.Type(),
|
||||
FacetName: facet.Name,
|
||||
Reason: fmt.Sprintf("type mismatch: %v for %d data", ft, vt),
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
f.Set(v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s structFLS) Save() ([]Field, *DocumentMetadata, error) {
|
||||
fields := make([]Field, 0, len(s.codec.fieldByName))
|
||||
var facets []Facet
|
||||
for i, tag := range s.codec.byIndex {
|
||||
if tag.ignore {
|
||||
continue
|
||||
}
|
||||
f := s.v.Field(i)
|
||||
if !f.CanSet() {
|
||||
continue
|
||||
}
|
||||
if tag.facet {
|
||||
facets = append(facets, Facet{Name: tag.name, Value: f.Interface()})
|
||||
} else {
|
||||
fields = append(fields, Field{Name: tag.name, Value: f.Interface()})
|
||||
}
|
||||
}
|
||||
return fields, &DocumentMetadata{Facets: facets}, nil
|
||||
}
|
||||
|
||||
// newStructFLS returns a FieldLoadSaver for the struct pointer p.
|
||||
func newStructFLS(p interface{}) (FieldLoadSaver, error) {
|
||||
v := reflect.ValueOf(p)
|
||||
if v.Kind() != reflect.Ptr || v.IsNil() || v.Elem().Kind() != reflect.Struct {
|
||||
return nil, ErrInvalidDocumentType
|
||||
}
|
||||
codec, err := loadCodec(v.Elem().Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return structFLS{v.Elem(), codec}, nil
|
||||
}
|
||||
|
||||
func loadStructWithMeta(dst interface{}, f []Field, meta *DocumentMetadata) error {
|
||||
x, err := newStructFLS(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return x.Load(f, meta)
|
||||
}
|
||||
|
||||
func saveStructWithMeta(src interface{}) ([]Field, *DocumentMetadata, error) {
|
||||
x, err := newStructFLS(src)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return x.Save()
|
||||
}
|
||||
|
||||
// LoadStruct loads the fields from f to dst. dst must be a struct pointer.
|
||||
func LoadStruct(dst interface{}, f []Field) error {
|
||||
return loadStructWithMeta(dst, f, nil)
|
||||
}
|
||||
|
||||
// SaveStruct returns the fields from src as a slice of Field.
|
||||
// src must be a struct pointer.
|
||||
func SaveStruct(src interface{}) ([]Field, error) {
|
||||
f, _, err := saveStructWithMeta(src)
|
||||
return f, err
|
||||
}
|
213
vendor/google.golang.org/appengine/search/struct_test.go
generated
vendored
Normal file
213
vendor/google.golang.org/appengine/search/struct_test.go
generated
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright 2015 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadingStruct(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fields []Field
|
||||
meta *DocumentMetadata
|
||||
want interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "Basic struct",
|
||||
fields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
},
|
||||
want: &struct {
|
||||
Name string
|
||||
Legs float64
|
||||
}{"Gopher", 4},
|
||||
},
|
||||
{
|
||||
desc: "Struct with tags",
|
||||
fields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
{Name: "about", Value: "Likes slide rules."},
|
||||
},
|
||||
meta: &DocumentMetadata{Facets: []Facet{
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
{Name: "Fur", Value: Atom("furry")},
|
||||
}},
|
||||
want: &struct {
|
||||
Name string
|
||||
Info string `search:"about"`
|
||||
Legs float64 `search:",facet"`
|
||||
Fuzz Atom `search:"Fur,facet"`
|
||||
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
|
||||
},
|
||||
{
|
||||
desc: "Bad field from tag",
|
||||
want: &struct {
|
||||
AlphaBeta string `search:"αβ"`
|
||||
}{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Ignore missing field",
|
||||
fields: []Field{
|
||||
{Name: "Meaning", Value: float64(42)},
|
||||
},
|
||||
want: &struct{}{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Ignore unsettable field",
|
||||
fields: []Field{
|
||||
{Name: "meaning", Value: float64(42)},
|
||||
},
|
||||
want: &struct{ meaning float64 }{}, // field not populated.
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Error on missing facet",
|
||||
meta: &DocumentMetadata{Facets: []Facet{
|
||||
{Name: "Set", Value: Atom("yes")},
|
||||
{Name: "Missing", Value: Atom("no")},
|
||||
}},
|
||||
want: &struct {
|
||||
Set Atom `search:",facet"`
|
||||
}{Atom("yes")},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Error on unsettable facet",
|
||||
meta: &DocumentMetadata{Facets: []Facet{
|
||||
{Name: "Set", Value: Atom("yes")},
|
||||
{Name: "unset", Value: Atom("no")},
|
||||
}},
|
||||
want: &struct {
|
||||
Set Atom `search:",facet"`
|
||||
}{Atom("yes")},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Error setting ignored field",
|
||||
fields: []Field{
|
||||
{Name: "Set", Value: "yes"},
|
||||
{Name: "Ignored", Value: "no"},
|
||||
},
|
||||
want: &struct {
|
||||
Set string
|
||||
Ignored string `search:"-"`
|
||||
}{Set: "yes"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Error setting ignored facet",
|
||||
meta: &DocumentMetadata{Facets: []Facet{
|
||||
{Name: "Set", Value: Atom("yes")},
|
||||
{Name: "Ignored", Value: Atom("no")},
|
||||
}},
|
||||
want: &struct {
|
||||
Set Atom `search:",facet"`
|
||||
Ignored Atom `search:"-,facet"`
|
||||
}{Set: Atom("yes")},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
// Make a pointer to an empty version of what want points to.
|
||||
dst := reflect.New(reflect.TypeOf(tt.want).Elem()).Interface()
|
||||
err := loadStructWithMeta(dst, tt.fields, tt.meta)
|
||||
if err != nil != tt.wantErr {
|
||||
t.Errorf("%s: got err %v; want err %t", tt.desc, err, tt.wantErr)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(dst, tt.want) {
|
||||
t.Errorf("%s: doesn't match\ngot: %v\nwant: %v", tt.desc, dst, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavingStruct(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
doc interface{}
|
||||
wantFields []Field
|
||||
wantFacets []Facet
|
||||
}{
|
||||
{
|
||||
desc: "Basic struct",
|
||||
doc: &struct {
|
||||
Name string
|
||||
Legs float64
|
||||
}{"Gopher", 4},
|
||||
wantFields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Struct with tags",
|
||||
doc: &struct {
|
||||
Name string
|
||||
Info string `search:"about"`
|
||||
Legs float64 `search:",facet"`
|
||||
Fuzz Atom `search:"Fur,facet"`
|
||||
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
|
||||
wantFields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
{Name: "about", Value: "Likes slide rules."},
|
||||
},
|
||||
wantFacets: []Facet{
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
{Name: "Fur", Value: Atom("furry")},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ignore unexported struct fields",
|
||||
doc: &struct {
|
||||
Name string
|
||||
info string
|
||||
Legs float64 `search:",facet"`
|
||||
fuzz Atom `search:",facet"`
|
||||
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
|
||||
wantFields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
},
|
||||
wantFacets: []Facet{
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ignore fields marked -",
|
||||
doc: &struct {
|
||||
Name string
|
||||
Info string `search:"-"`
|
||||
Legs float64 `search:",facet"`
|
||||
Fuzz Atom `search:"-,facet"`
|
||||
}{"Gopher", "Likes slide rules.", 4, Atom("furry")},
|
||||
wantFields: []Field{
|
||||
{Name: "Name", Value: "Gopher"},
|
||||
},
|
||||
wantFacets: []Facet{
|
||||
{Name: "Legs", Value: float64(4)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
fields, meta, err := saveStructWithMeta(tt.doc)
|
||||
if err != nil {
|
||||
t.Errorf("%s: got err %v; want nil", tt.desc, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(fields, tt.wantFields) {
|
||||
t.Errorf("%s: fields don't match\ngot: %v\nwant: %v", tt.desc, fields, tt.wantFields)
|
||||
}
|
||||
if facets := meta.Facets; !reflect.DeepEqual(facets, tt.wantFacets) {
|
||||
t.Errorf("%s: facets don't match\ngot: %v\nwant: %v", tt.desc, facets, tt.wantFacets)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user