diff --git a/redisearch/filter.go b/redisearch/filter.go new file mode 100644 index 0000000..16e0caf --- /dev/null +++ b/redisearch/filter.go @@ -0,0 +1,33 @@ +package redisearch + +// Common filter +type Filter struct { + Field string + Options interface{} +} + +// Filter the results to a given radius from lon and lat. Radius is given as a number and units +type GeoFilterOptions struct { + Lon float64 + Lat float64 + Radius float64 + Unit Unit +} + +// Limit results to those having numeric values ranging between min and max. min and max follow ZRANGE syntax, and can be -inf, +inf +type NumericFilterOptions struct { + Min float64 + ExclusiveMin bool + Max float64 + ExclusiveMax bool +} + +// units of Radius +type Unit string + +const ( + KILOMETERS Unit = "km" + METERS Unit = "m" + FEET Unit = "ft" + MILES Unit = "mi" +) diff --git a/redisearch/query.go b/redisearch/query.go index 88c82ed..eed8f05 100644 --- a/redisearch/query.go +++ b/redisearch/query.go @@ -2,6 +2,7 @@ package redisearch import ( "github.com/gomodule/redigo/redis" + "math" ) // Flag is a type for query flags @@ -79,7 +80,7 @@ type Query struct { Flags Flag Slop int - Filters []Predicate + Filters []Filter InKeys []string ReturnFields []string Language string @@ -120,7 +121,7 @@ func (p Paging) serialize() redis.Args { func NewQuery(raw string) *Query { return &Query{ Raw: raw, - Filters: []Predicate{}, + Filters: []Filter{}, Paging: Paging{DefaultOffset, DefaultNum}, } } @@ -197,9 +198,49 @@ func (q Query) serialize() redis.Args { args = args.Add("SEPARATOR", q.SummarizeOpts.Separator) } } + + if q.Filters != nil { + for _, f := range q.Filters { + if f.Options != nil { + switch f.Options.(type) { + case NumericFilterOptions: + opts, _ := f.Options.(NumericFilterOptions) + args = append(args, "FILTER", f.Field) + args = appendNumArgs(opts.Min, opts.ExclusiveMin, args) + args = appendNumArgs(opts.Max, opts.ExclusiveMax, args) + case GeoFilterOptions: + opts, _ := f.Options.(GeoFilterOptions) + args = append(args, "GEOFILTER", f.Field, opts.Lon, opts.Lat, opts.Radius, opts.Unit) + } + } + } + } return args } +func appendNumArgs(num float64, exclude bool, args redis.Args) redis.Args { + if math.IsInf(num, 1) { + return append(args, "+inf") + } + if math.IsInf(num, -1) { + return append(args, "-inf") + } + + if exclude { + return append(args, "(", num) + } + return append(args, num) +} + +// AddFilter adds a filter to the query +func (q *Query) AddFilter(f Filter) *Query { + if q.Filters == nil { + q.Filters = []Filter{} + } + q.Filters = append(q.Filters, f) + return q +} + // // AddPredicate adds a predicate to the query's filters // func (q *Query) AddPredicate(p Predicate) *Query { // q.Predicates = append(q.Predicates, p) diff --git a/redisearch/redisearch_test.go b/redisearch/redisearch_test.go index c60612e..23bd406 100644 --- a/redisearch/redisearch_test.go +++ b/redisearch/redisearch_test.go @@ -25,7 +25,6 @@ func getTestConnectionDetails() (string, string) { return host, password } - func createClient(indexName string) *Client { host, password := getTestConnectionDetails() if password != "" { @@ -39,12 +38,11 @@ func createClient(indexName string) *Client { return err } return NewClientFromPool(pool, indexName) - }else{ + } else { return NewClient(host, indexName) } } - func createAutocompleter(dictName string) *Autocompleter { host, password := getTestConnectionDetails() if password != "" { @@ -398,9 +396,9 @@ func TestSpellCheck(t *testing.T) { } assert.Nil(t, c.Index(docs...)) - query := NewQuery("Anla Portuga" ) - opts := NewSpellCheckOptions(2 ) - sugs, total, err := c.SpellCheck(query,opts ) + query := NewQuery("Anla Portuga") + opts := NewSpellCheckOptions(2) + sugs, total, err := c.SpellCheck(query, opts) assert.Nil(t, err) assert.Equal(t, 2, len(sugs)) assert.Equal(t, 2, total) @@ -409,21 +407,68 @@ func TestSpellCheck(t *testing.T) { // 1) 1) "TERM" // 2) "an" // 3) (empty list or set) - queryEmpty := NewQuery("An" ) - sugs, total, err = c.SpellCheck(queryEmpty,opts ) + queryEmpty := NewQuery("An") + sugs, total, err = c.SpellCheck(queryEmpty, opts) assert.Nil(t, err) assert.Equal(t, 1, len(sugs)) assert.Equal(t, 0, total) // same query but now with a distance of 4 opts.SetDistance(4) - sugs, total, err = c.SpellCheck(queryEmpty,opts ) + sugs, total, err = c.SpellCheck(queryEmpty, opts) assert.Nil(t, err) assert.Equal(t, 1, len(sugs)) assert.Equal(t, 1, total) } +func TestFilter(t *testing.T) { + c := createClient("testFilter") + // Create a schema + sc := NewSchema(DefaultOptions). + AddField(NewTextField("body")). + AddField(NewTextFieldOptions("title", TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(NewNumericField("age")). + AddField(NewGeoFieldOptions("location", GeoFieldOptions{})) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + assert.Nil(t, c.CreateIndex(sc)) + + // Create a document with an id and given score + doc := NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("age", 18). + Set("location", "13.361389,38.115556") + + assert.Nil(t, c.IndexOptions(DefaultIndexingOptions, doc)) + // Searching with NumericFilter + docs, total, err := c.Search(NewQuery("hello world"). + AddFilter(Filter{Field: "age", Options: NumericFilterOptions{Min: 1, Max: 20}}). + SetSortBy("age", true). + SetReturnFields("body")) + assert.Nil(t, err) + assert.Equal(t, 1, total) + assert.Equal(t, "foo bar", docs[0].Properties["body"]) + + // Searching with GeoFilter + docs, total, err = c.Search(NewQuery("hello world"). + AddFilter(Filter{Field: "location", Options: GeoFilterOptions{Lon: 15, Lat: 37, Radius: 200, Unit: KILOMETERS}}). + SetSortBy("age", true). + SetReturnFields("age")) + assert.Nil(t, err) + assert.Equal(t, 1, total) + assert.Equal(t, "18", docs[0].Properties["age"]) + + docs, total, err = c.Search(NewQuery("hello world"). + AddFilter(Filter{Field: "location", Options: GeoFilterOptions{Lon: 10, Lat: 13, Radius: 1, Unit: KILOMETERS}}). + SetSortBy("age", true). + SetReturnFields("body")) + assert.Nil(t, err) + assert.Equal(t, 0, total) +} + func ExampleClient() { // Create a client. By default a client is schemaless @@ -434,7 +479,8 @@ func ExampleClient() { sc := NewSchema(DefaultOptions). AddField(NewTextField("body")). AddField(NewTextFieldOptions("title", TextFieldOptions{Weight: 5.0, Sortable: true})). - AddField(NewNumericField("date")) + AddField(NewNumericField("date")). + AddField(NewGeoFieldOptions("location", GeoFieldOptions{})) // Drop an existing index. If the index does not exist an error is returned c.Drop() @@ -448,7 +494,8 @@ func ExampleClient() { doc := NewDocument("doc1", 1.0) doc.Set("title", "Hello world"). Set("body", "foo bar"). - Set("date", time.Now().Unix()) + Set("date", time.Now().Unix()). + Set("location", "13.361389,38.115556") // Index the document. The API accepts multiple documents at a time if err := c.IndexOptions(DefaultIndexingOptions, doc); err != nil { @@ -463,5 +510,3 @@ func ExampleClient() { fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) // Output: doc1 Hello world 1 } - - diff --git a/redisearch/schema.go b/redisearch/schema.go index b08081c..b6c5373 100644 --- a/redisearch/schema.go +++ b/redisearch/schema.go @@ -91,6 +91,11 @@ type NumericFieldOptions struct { NoIndex bool } +// GeoFieldOptions Options for geo fields +type GeoFieldOptions struct { + NoIndex bool +} + // NewTextField creates a new text field with the given weight func NewTextField(name string) Field { return Field{ @@ -158,6 +163,22 @@ func NewSortableNumericField(name string) Field { return f } +// NewGeoField creates a new geo field with the given name +func NewGeoField(name string) Field { + return Field{ + Name: name, + Type: GeoField, + Options: nil, + } +} + +// NewGeoFieldOptions creates a new geo field with the given name and additional options +func NewGeoFieldOptions(name string, options GeoFieldOptions) Field { + f := NewGeoField(name) + f.Options = options + return f +} + // Schema represents an index schema Schema, or how the index would // treat documents sent to it. type Schema struct { @@ -168,7 +189,7 @@ type Schema struct { // NewSchema creates a new Schema object func NewSchema(opts Options) *Schema { return &Schema{ - Fields: []Field{}, + Fields: []Field{}, Options: opts, } } @@ -182,7 +203,6 @@ func (m *Schema) AddField(f Field) *Schema { return m } - func SerializeSchema(s *Schema, args redis.Args) (redis.Args, error) { if s.Options.NoFieldFlags { args = append(args, "NOFIELDS") @@ -262,10 +282,21 @@ func SerializeSchema(s *Schema, args redis.Args) (redis.Args, error) { args = append(args, "NOINDEX") } } + case GeoField: + args = append(args, f.Name, "GEO") + if f.Options != nil { + opts, ok := f.Options.(GeoFieldOptions) + if !ok { + return nil, errors.New("Invalid geo field options type") + } + if opts.NoIndex { + args = append(args, "NOINDEX") + } + } default: return nil, fmt.Errorf("Unsupported field type %v", f.Type) } } return args, nil -} \ No newline at end of file +}