diff --git a/.circleci/config.yml b/.circleci/config.yml index edae4f3..e41af62 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,16 +3,16 @@ # Check https://circleci.com/docs/2.0/language-go/ for more details version: 2 jobs: - build: # test with redisearch:latest + build: # test with redisearch:edge docker: - image: circleci/golang:1.9 - - image: redislabs/redisearch:latest + - image: redislabs/redisearch:edge working_directory: /go/src/github.com/RediSearch/redisearch-go steps: - checkout - - run: go get -v -t -d ./... - - run: go test -v ./... -race -coverprofile=coverage.txt -covermode=atomic + - run: make get + - run: make coverage - run: bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} build_nightly: # test nightly with redisearch:edge @@ -23,8 +23,8 @@ jobs: working_directory: /go/src/github.com/RediSearch/redisearch-go steps: - checkout - - run: go get -v -t -d ./... - - run: go test -v ./... -race #no need for codecov on nightly + - run: make get + - run: make test workflows: version: 2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..186edbc --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# Go parameters +GOCMD=go +GOBUILD=$(GOCMD) build +GOINSTALL=$(GOCMD) install +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +GOMOD=$(GOCMD) mod + +.PHONY: all test coverage +all: test coverage + +get: + $(GOGET) -t -v ./... + +examples: get + $(GOBUILD) ./examples/quickstart/. + $(GOBUILD) ./examples/temporary/. + ./quickstart > /dev/null + +test: get examples + $(GOTEST) -race -covermode=atomic ./... + +coverage: get test + $(GOTEST) -race -coverprofile=coverage.txt -covermode=atomic ./redisearch + diff --git a/examples/quickstart/quickstart.go b/examples/quickstart/quickstart.go new file mode 100644 index 0000000..2569179 --- /dev/null +++ b/examples/quickstart/quickstart.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "github.com/RediSearch/redisearch-go/redisearch" + "log" + "time" +) + +/** + * This demo should be updated in RediSearch.io if changed + * Update at: https://github.com/RediSearch/RediSearch/blob/master/docs/go_client.md + */ +func main() { + // Create a client. By default a client is schemaless + // unless a schema is provided when creating the index + c := redisearch.NewClient("localhost:6379", "myIndex") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.IndexOptions(redisearch.DefaultIndexingOptions, doc); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 +} diff --git a/examples/temporary/temporary.go b/examples/temporary/temporary.go new file mode 100644 index 0000000..c979c6f --- /dev/null +++ b/examples/temporary/temporary.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "log" + "time" + + "github.com/RediSearch/redisearch-go/redisearch" +) + +/** + * This demo demonstrates the usage of CreateIndex(), to create a lightweight temporary index that will expire after the specified period of inactivity. + * The internal idle timer is reset whenever the index is searched or added to. + * Because such indexes are lightweight, you can create thousands of such indexes without negative performance implications. + */ +func main() { + // Create a client. By default a client is schemaless + // unless a schema is provided when creating the index + c := redisearch.NewClient("localhost:6379", "myTemporaryIndex") + + // Create a schema with a temporary index with temporary period of 10s + sc := redisearch.NewSchema(*redisearch.NewOptions().SetTemporaryPeriod(10)). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.IndexOptions(redisearch.DefaultIndexingOptions, doc); err != nil { + log.Fatal(err) + } + + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + // Verify that the we're able to search on the temporary created index + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 + + time.Sleep(15*time.Second) + // Searching with limit and sorting + _, err = c.Info() + fmt.Println(err) + // Output: Unknown Index name +} diff --git a/redisearch/client_test.go b/redisearch/client_test.go index cfdb2a3..5bd68c7 100644 --- a/redisearch/client_test.go +++ b/redisearch/client_test.go @@ -543,4 +543,33 @@ func TestClient_AddField(t *testing.T) { assert.Nil(t, err) err = c.Index(NewDocument("doc-n1",1.0).Set("age",15 )) assert.Nil(t, err) +} + +func TestClient_CreateIndex(t *testing.T) { + type fields struct { + pool ConnPool + name string + } + type args struct { + s *Schema + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &Client{ + pool: tt.fields.pool, + name: tt.fields.name, + } + if err := i.CreateIndex(tt.args.s); (err != nil) != tt.wantErr { + t.Errorf("CreateIndex() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } } \ No newline at end of file diff --git a/redisearch/example_schema_test.go b/redisearch/example_schema_test.go new file mode 100644 index 0000000..23d0d7a --- /dev/null +++ b/redisearch/example_schema_test.go @@ -0,0 +1,51 @@ +package redisearch_test + +import ( + "fmt" + "github.com/RediSearch/redisearch-go/redisearch" + "log" + "time" +) + +// exemplifies the CreateIndex function with a temporary index specification +func ExampleCreateIndex_temporary() { + // Create a client. By default a client is schemaless + // unless a schema is provided when creating the index + c := redisearch.NewClient("localhost:6379", "myTemporaryIndex") + + // Create a schema with a temporary period of 60seconds + sc := redisearch.NewSchema(*redisearch.NewOptions().SetTemporaryPeriod(10)). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.IndexOptions(redisearch.DefaultIndexingOptions, doc); err != nil { + log.Fatal(err) + } + + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + // Verify that the we're able to search on the temporary created index + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + + time.Sleep(15 * time.Second) + // Searching with limit and sorting + _, err = c.Info() + fmt.Println(err) + // Output: doc1 Hello world 1 + // Unknown Index name +} diff --git a/redisearch/schema.go b/redisearch/schema.go index 3fa6632..bc28055 100644 --- a/redisearch/schema.go +++ b/redisearch/schema.go @@ -35,6 +35,42 @@ type Options struct { // If the list is nil the default stop-words list is used. // See https://oss.redislabs.com/redisearch/Stopwords.html#default_stop-word_list Stopwords []string + + // If set to true, creates a lightweight temporary index which will expire after the specified period of inactivity. + // The internal idle timer is reset whenever the index is searched or added to. + // Because such indexes are lightweight, you can create thousands of such indexes without negative performance implications. + Temporary bool + TemporaryPeriod int +} + +func NewOptions() *Options { + var opts = DefaultOptions + return &opts +} + +// If set to true, creates a lightweight temporary index which will expire after the specified period of inactivity. +// The internal idle timer is reset whenever the index is searched or added to. +// To enable the temporary index creation, use SetTemporaryPeriod(). This method should be preferably used for disabling the flag +func (options *Options) SetTemporary(temporary bool) *Options { + options.Temporary = temporary + return options +} + +// If set to a positive integer, creates a lightweight temporary index which will expire after the specified period of inactivity (in seconds). +// The internal idle timer is reset whenever the index is searched or added to. +func (options *Options) SetTemporaryPeriod(period int) *Options { + options.TemporaryPeriod = period + options.Temporary = true + return options +} + +// Set the index with a custom stop-words list, to be ignored during indexing and search time +// This is an option that is applied and index level. +// If the list is nil the default stop-words list is used. +// See https://oss.redislabs.com/redisearch/Stopwords.html#default_stop-word_list +func (options *Options) SetStopWords(stopwords []string) *Options { + options.Stopwords = stopwords + return options } // DefaultOptions represents the default options @@ -44,6 +80,8 @@ var DefaultOptions = Options{ NoFrequencies: false, NoOffsetVectors: false, Stopwords: nil, + Temporary: false, + TemporaryPeriod: 0, } const ( @@ -204,15 +242,19 @@ func (m *Schema) AddField(f Field) *Schema { func SerializeSchema(s *Schema, args redis.Args) (argsOut redis.Args, err error) { argsOut = args + if s.Options.NoOffsetVectors { + argsOut = append(argsOut, "NOOFFSETS") + } + if s.Options.Temporary { + argsOut = append(argsOut, "TEMPORARY",s.Options.TemporaryPeriod) + } if s.Options.NoFieldFlags { argsOut = append(argsOut, "NOFIELDS") } if s.Options.NoFrequencies { argsOut = append(argsOut, "NOFREQS") } - if s.Options.NoOffsetVectors { - argsOut = append(argsOut, "NOOFFSETS") - } + if s.Options.Stopwords != nil { argsOut = argsOut.Add("STOPWORDS", len(s.Options.Stopwords)) if len(s.Options.Stopwords) > 0 { @@ -224,7 +266,7 @@ func SerializeSchema(s *Schema, args redis.Args) (argsOut redis.Args, err error) for _, f := range s.Fields { argsOut, err = serializeField(f, argsOut) if err != nil { - return nil,err + return nil, err } } return @@ -300,7 +342,7 @@ func serializeField(f Field, args redis.Args) (argsOut redis.Args, err error) { } } default: - err = fmt.Errorf("Unrecognized field type %v serialization", f.Type ) + err = fmt.Errorf("Unrecognized field type %v serialization", f.Type) return } return diff --git a/redisearch/schema_test.go b/redisearch/schema_test.go index 1f2d725..abf40eb 100644 --- a/redisearch/schema_test.go +++ b/redisearch/schema_test.go @@ -43,10 +43,14 @@ func TestSerializeSchema(t *testing.T) { want redis.Args wantErr bool }{ + {"default-args", args{NewSchema(DefaultOptions), redis.Args{}}, redis.Args{"SCHEMA"}, false}, + {"default-args-with-different-constructor", args{NewSchema(*NewOptions()), redis.Args{}}, redis.Args{"SCHEMA"}, false}, + {"temporary", args{NewSchema(*NewOptions().SetTemporaryPeriod(60)), redis.Args{}}, redis.Args{"TEMPORARY",60,"SCHEMA"}, false}, {"no-frequencies", args{NewSchema(Options{NoFrequencies: true}), redis.Args{}}, redis.Args{"NOFREQS", "SCHEMA"}, false}, {"no-fields", args{NewSchema(Options{NoFieldFlags: true}), redis.Args{}}, redis.Args{"NOFIELDS", "SCHEMA"}, false}, {"custom-stopwords", args{NewSchema(Options{Stopwords: []string{"custom"}}), redis.Args{}}, redis.Args{"STOPWORDS", 1, "custom", "SCHEMA"}, false}, + {"custom-stopwords-with-different-constructor", args{NewSchema(*NewOptions().SetStopWords([]string{"custom"})), redis.Args{}}, redis.Args{"STOPWORDS", 1, "custom", "SCHEMA"}, false}, {"no-offsets", args{NewSchema(Options{NoOffsetVectors: true}), redis.Args{}}, redis.Args{"NOOFFSETS", "SCHEMA"}, false}, {"default-and-numeric", args{NewSchema(DefaultOptions).AddField(NewNumericField("numeric-field")), redis.Args{}}, redis.Args{"SCHEMA", "numeric-field", "NUMERIC"}, false}, {"default-and-numeric-sortable", args{NewSchema(DefaultOptions).AddField(NewSortableNumericField("numeric-field")), redis.Args{}}, redis.Args{"SCHEMA", "numeric-field", "NUMERIC", "SORTABLE"}, false},