diff --git a/Makefile b/Makefile index 96655c4..391afd6 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ SOLR ?="solr-go" .PHONY: unit-test unit-test: - go test -v -cover -race + go test -v -cover .PHONY: integration-test integration-test: - go test -tags integration -v -cover -race + go test -tags integration -v -cover .PHONY: start-solr start-solr: stop-solr diff --git a/README.md b/README.md index b6fda89..855080d 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,19 @@ A [Solr](https://lucene.apache.org/solr) client for [Go](https://golang.org/). +## Supported APIs + +- [Collections API](https://lucene.apache.org/solr/guide/8_8/collections-api.html) +- [Query API](https://lucene.apache.org/solr/guide/8_8/json-request-api.html) + - [Facet API](https://lucene.apache.org/solr/guide/8_8/json-facet-api.html) +- [Update API](https://lucene.apache.org/solr/guide/8_8/uploading-data-with-index-handlers.html#uploading-data-with-index-handlers) +- [Schema API](https://lucene.apache.org/solr/guide/8_8/schema-api.html) +- [Config API](https://lucene.apache.org/solr/guide/8_8/config-api.html) +- [Suggester API](https://lucene.apache.org/solr/guide/8_8/suggester.html) + ## Example -Please see [integration test](integration_test.go) for more examples. +See [integration test](integration_test.go) for more examples. ```go // Create a client @@ -59,7 +69,7 @@ queryResponse, err := client.Query(context.Background(), "techproducts", query) ## Contributing -All contributions are welcome! +Any contributions are welcome! ## License diff --git a/client.go b/client.go index 7aca662..e3742b8 100644 --- a/client.go +++ b/client.go @@ -21,7 +21,6 @@ type Client interface { Query(ctx context.Context, collection string, query *Query) (*QueryResponse, error) // Update can be used to add, update, or delete a document from the index. - // `body` is expected to contain the list of documents. // // Refer to https://lucene.apache.org/solr/guide/8_8/uploading-data-with-index-handlers.html Update(ctx context.Context, collection string, ct ContentType, body io.Reader) (*UpdateResponse, error) diff --git a/config.go b/config.go index 7c87117..6024846 100644 --- a/config.go +++ b/config.go @@ -28,11 +28,14 @@ func (ct ComponentType) String() string { // Component is a component type Component struct { - // Type is the component type - ct ComponentType - name string + // ct is the component type + ct ComponentType + // name is the component name + name string + // class is the component class class string - m M + // m is the component configurations + m M } // NewComponent returns a new Component @@ -68,6 +71,3 @@ func (c *Component) BuildComponent() M { return m } - -// UserProperty is a user property -type UserProperty struct{} diff --git a/config_test.go b/config_test.go index 9ff777d..81b4553 100644 --- a/config_test.go +++ b/config_test.go @@ -10,15 +10,13 @@ import ( func TestBuildComponent(t *testing.T) { got := solr.NewComponent(solr.SearchComponent). - Name("suggest"). - Class("solr.SearchComponent"). + Name("suggest").Class("solr.SearchComponent"). Config(solr.M{ "lookupImpl": "AnalyzingInfixLookupFactory", "dictionaryImpl": "DocumentDictionaryFactory", "field": "suggest", "suggestAnalyzerFieldType": "suggext_text", - }). - BuildComponent() + }).BuildComponent() expect := solr.M{ "name": "suggest", diff --git a/integration_test.go b/integration_test.go index 05b410d..31217ab 100644 --- a/integration_test.go +++ b/integration_test.go @@ -20,171 +20,161 @@ func TestJSONClient(t *testing.T) { client := solr.NewJSONClient(baseURL) ctx := context.Background() - t.Run("create collection", func(t *testing.T) { - collection := solr.NewCollectionParams().Name(collection). - NumShards(1).ReplicationFactor(1) - err := client.CreateCollection(ctx, collection) - require.NoError(t, err) - }) - - t.Run("initialize schema", func(t *testing.T) { - suggestText := solr.FieldType{ - Name: "suggest_text", - Class: "solr.TextField", - PositionIncrementGap: "100", - IndexAnalyzer: &solr.Analyzer{ - Tokenizer: &solr.Tokenizer{ - Class: "solr.WhitespaceTokenizerFactory", - }, - Filters: []solr.Filter{ - { - Class: "solr.LowerCaseFilterFactory", - }, - { - Class: "solr.ASCIIFoldingFilterFactory", - }, - { - Class: "solr.EdgeNGramFilterFactory", - MinGramSize: 1, - MaxGramSize: 20, - }, - }, + // Create a collection + err := client.CreateCollection(ctx, solr.NewCollectionParams(). + Name(collection).NumShards(1).ReplicationFactor(1)) + require.NoError(t, err, "creating a collection should not error") + + // Initialize schema + + // add new field type + suggestText := solr.FieldType{ + Name: "suggest_text", + Class: "solr.TextField", + PositionIncrementGap: "100", + IndexAnalyzer: &solr.Analyzer{ + Tokenizer: &solr.Tokenizer{ + Class: "solr.WhitespaceTokenizerFactory", }, - QueryAnalyzer: &solr.Analyzer{ - Tokenizer: &solr.Tokenizer{ - Class: "solr.WhitespaceTokenizerFactory", + Filters: []solr.Filter{ + { + Class: "solr.LowerCaseFilterFactory", }, - Filters: []solr.Filter{ - { - Class: "solr.LowerCaseFilterFactory", - }, - { - Class: "solr.ASCIIFoldingFilterFactory", - }, - { - Class: "solr.SynonymGraphFilterFactory", - Synonyms: "synonyms.txt", - }, + { + Class: "solr.ASCIIFoldingFilterFactory", + }, + { + Class: "solr.EdgeNGramFilterFactory", + MinGramSize: 1, + MaxGramSize: 20, }, }, - } - err := client.AddFieldTypes(ctx, collection, suggestText) - require.NoError(t, err) - - fields := []solr.Field{ - { - Name: "name", - Type: "text_general", - }, - { - Name: "suggest", - Type: "suggest_text", - }, - } - - err = client.AddFields(ctx, collection, fields...) - require.NoError(t, err) - - copyFields := []solr.CopyField{ - { - Source: "name", - Dest: "suggest", - }, - { - Source: "name", - Dest: "_text_", + }, + QueryAnalyzer: &solr.Analyzer{ + Tokenizer: &solr.Tokenizer{ + Class: "solr.WhitespaceTokenizerFactory", }, - } - - err = client.AddCopyFields(ctx, collection, copyFields...) - require.NoError(t, err) - }) - - t.Run("add suggester component", func(t *testing.T) { - suggestComponent := solr.NewComponent(solr.SearchComponent). - Name("suggest"). - Class("solr.SuggestComponent"). - Config(solr.M{ - "suggester": solr.M{ - "name": "default", - "lookupImpl": "AnalyzingInfixLookupFactory", - "dictionaryImpl": "DocumentDictionaryFactory", - "field": "suggest", - "suggestAnalyzerFieldType": "suggest_text", + Filters: []solr.Filter{ + { + Class: "solr.LowerCaseFilterFactory", }, - }) - - suggestHandler := solr.NewComponent(solr.RequestHandler). - Name("/suggest"). - Class("solr.SearchHandler"). - Config(solr.M{ - "startup": "lazy", - "defaults": solr.M{ - "suggest": true, - "suggest.count": 10, - "suggest.dictionary": "default", + { + Class: "solr.ASCIIFoldingFilterFactory", + }, + { + Class: "solr.SynonymGraphFilterFactory", + Synonyms: "synonyms.txt", }, - "components": []string{"suggest"}, - }) - - err := client.AddComponents(ctx, collection, suggestComponent, suggestHandler) - require.NoError(t, err) - }) - - t.Run("index data", func(t *testing.T) { - docs := []solr.M{ - { - "id": 1, - "name": "Solr", - }, - { - "id": 2, - "name": "Elastic", }, - { - "id": 3, - "name": "Blast", + }, + } + err = client.AddFieldTypes(ctx, collection, suggestText) + require.NoError(t, err, "adding field types should not error") + + // add fields + fields := []solr.Field{ + { + Name: "name", + Type: "text_general", + }, + { + Name: "suggest", + Type: "suggest_text", + }, + } + err = client.AddFields(ctx, collection, fields...) + require.NoError(t, err, "adding fields should not error") + + // add copy fields + copyFields := []solr.CopyField{ + { + Source: "name", + Dest: "suggest", + }, + { + Source: "name", + Dest: "_text_", + }, + } + err = client.AddCopyFields(ctx, collection, copyFields...) + require.NoError(t, err, "adding copy fields should not error") + + // Add suggester + suggestComponent := solr.NewComponent(solr.SearchComponent). + Name("suggest").Class("solr.SuggestComponent"). + Config(solr.M{ + "suggester": solr.M{ + "name": "default", + "lookupImpl": "AnalyzingInfixLookupFactory", + "dictionaryImpl": "DocumentDictionaryFactory", + "field": "suggest", + "suggestAnalyzerFieldType": "suggest_text", }, - { - "id": 4, - "name": "Bayard", + }) + + suggestHandler := solr.NewComponent(solr.RequestHandler). + Name("/suggest").Class("solr.SearchHandler"). + Config(solr.M{ + "startup": "lazy", + "defaults": solr.M{ + "suggest": true, + "suggest.count": 10, + "suggest.dictionary": "default", }, - } - buf := &bytes.Buffer{} - err := json.NewEncoder(buf).Encode(docs) - require.NoError(t, err) - - _, err = client.Update(ctx, collection, solr.JSON, buf) - require.NoError(t, err) - - err = client.Commit(ctx, collection) - require.NoError(t, err) - }) - - t.Run("query all", func(t *testing.T) { - queryParser := solr.NewStandardQueryParser().Query("*:*") - query := solr.NewQuery().QueryParser(queryParser) - queryResp, err := client.Query(ctx, collection, query) - require.NoError(t, err) - assert.NotNil(t, queryResp) - assert.Len(t, queryResp.Response.Documents, 4) - }) - - t.Run("query suggester", func(t *testing.T) { - queryStr := "solr" - suggestParams := solr.NewSuggesterParams("suggest"). - Build().Query(queryStr) - suggestResp, err := client.Suggest(ctx, collection, suggestParams) - require.NoError(t, err) - - suggest := *suggestResp.Suggest - termBody := suggest["default"][queryStr] - assert.Len(t, termBody.Suggestions, 1) - }) - - t.Run("delete the collection", func(t *testing.T) { - collection := solr.NewCollectionParams().Name(collection) - err := client.DeleteCollection(ctx, collection) - require.NoError(t, err) - }) + "components": []string{"suggest"}, + }) + + err = client.AddComponents(ctx, collection, suggestComponent, suggestHandler) + require.NoError(t, err, "adding suggester components should not error") + + // Index + docs := []solr.M{ + { + "id": 1, + "name": "Solr", + }, + { + "id": 2, + "name": "Elastic", + }, + { + "id": 3, + "name": "Blast", + }, + { + "id": 4, + "name": "Bayard", + }, + } + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(docs) + require.NoError(t, err, "encoding data should not error") + + _, err = client.Update(ctx, collection, solr.JSON, buf) + require.NoError(t, err, "indexing data should not eror") + + err = client.Commit(ctx, collection) + require.NoError(t, err, "commmit should not error") + + // Query + queryParser := solr.NewStandardQueryParser().Query("*:*") + query := solr.NewQuery().QueryParser(queryParser) + queryResp, err := client.Query(ctx, collection, query) + require.NoError(t, err, "query should not error") + require.NotNil(t, queryResp, "query response should not be nil") + assert.Len(t, queryResp.Response.Documents, 4, "query response is expected to have 4 documents") + + // Suggest + queryStr := "solr" + suggestParams := solr.NewSuggesterParams("suggest").Build().Query(queryStr) + suggestResp, err := client.Suggest(ctx, collection, suggestParams) + require.NoError(t, err, "suggest should not error") + + suggest := *suggestResp.Suggest + termBody := suggest["default"][queryStr] + assert.Len(t, termBody.Suggestions, 1, "expected to have one suggestion") + + // Delete the collection + err = client.DeleteCollection(ctx, solr.NewCollectionParams().Name(collection)) + require.NoError(t, err, "deleting collection should not error") } diff --git a/json_client.go b/json_client.go index e6ae1eb..9f7ca24 100644 --- a/json_client.go +++ b/json_client.go @@ -41,12 +41,7 @@ func (c *JSONClient) WithHTTPClient(httpClient *http.Client) *JSONClient { // Refer to https://lucene.apache.org/solr/guide/8_8/collection-management.html#create func (c *JSONClient) CreateCollection(ctx context.Context, params *CollectionParams) error { urlStr := fmt.Sprintf("%s/solr/admin/collections?action=CREATE&"+params.BuildParam(), c.baseURL) - theURL, err := url.Parse(urlStr) - if err != nil { - return errors.Wrap(err, "parse url") - } - - httpResp, err := c.sendRequest(ctx, http.MethodGet, theURL.String(), nil) + httpResp, err := c.sendRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return errors.Wrap(err, "send request") } @@ -69,12 +64,7 @@ func (c *JSONClient) CreateCollection(ctx context.Context, params *CollectionPar // Refer to https://lucene.apache.org/solr/guide/8_8/collection-management.html#delete func (c *JSONClient) DeleteCollection(ctx context.Context, params *CollectionParams) error { urlStr := fmt.Sprintf("%s/solr/admin/collections?action=DELETE&"+params.BuildParam(), c.baseURL) - theURL, err := url.Parse(urlStr) - if err != nil { - return errors.Wrap(err, "parse url") - } - - httpResp, err := c.sendRequest(ctx, http.MethodGet, theURL.String(), nil) + httpResp, err := c.sendRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return errors.Wrap(err, "send request") } @@ -96,19 +86,14 @@ func (c *JSONClient) DeleteCollection(ctx context.Context, params *CollectionPar // // Refer to https://lucene.apache.org/solr/guide/8_8/json-request-api.html func (c *JSONClient) Query(ctx context.Context, collection string, query *Query) (*QueryResponse, error) { - urlStr := fmt.Sprintf("%s/solr/%s/query", c.baseURL, collection) - theURL, err := url.Parse(urlStr) - if err != nil { - return nil, errors.Wrap(err, "parse url") - } - buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(query.BuildQuery()) + err := json.NewEncoder(buf).Encode(query.BuildQuery()) if err != nil { return nil, errors.Wrap(err, "encode query") } - httpResp, err := c.sendRequest(ctx, http.MethodPost, theURL.String(), buf) + urlStr := fmt.Sprintf("%s/solr/%s/query", c.baseURL, collection) + httpResp, err := c.sendRequest(ctx, http.MethodPost, urlStr, buf) if err != nil { return nil, errors.Wrap(err, "send request") } @@ -127,17 +112,11 @@ func (c *JSONClient) Query(ctx context.Context, collection string, query *Query) } // Update can be used to add, update, or delete a document from the index. -// `body` is expected to contain the list of documents. // // Refer to https://lucene.apache.org/solr/guide/8_8/uploading-data-with-index-handlers.html func (c *JSONClient) Update(ctx context.Context, collection string, ct ContentType, body io.Reader) (*UpdateResponse, error) { urlStr := fmt.Sprintf("%s/solr/%s/update", c.baseURL, collection) - theURL, err := url.Parse(urlStr) - if err != nil { - return nil, errors.Wrap(err, "parse url") - } - - httpResp, err := c.sendRequestWithContentType(ctx, http.MethodPost, theURL.String(), ct.String(), body) + httpResp, err := c.sendRequestWithContentType(ctx, http.MethodPost, urlStr, ct.String(), body) if err != nil { return nil, errors.Wrap(err, "send request") } @@ -157,17 +136,8 @@ func (c *JSONClient) Update(ctx context.Context, collection string, ct ContentTy // Commit commits the last update. func (c *JSONClient) Commit(ctx context.Context, collection string) error { - urlStr := fmt.Sprintf("%s/solr/%s/update", c.baseURL, collection) - theURL, err := url.Parse(urlStr) - if err != nil { - return errors.Wrap(err, "parse url") - } - - q := theURL.Query() - q.Add("commit", "true") - theURL.RawQuery = q.Encode() - - httpResp, err := c.sendRequest(ctx, http.MethodGet, theURL.String(), nil) + urlStr := fmt.Sprintf("%s/solr/%s/update?commit=true", c.baseURL, collection) + httpResp, err := c.sendRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return errors.Wrap(err, "send request") } @@ -267,23 +237,14 @@ func (c *JSONClient) modifySchema(ctx context.Context, collection, command strin return c.postJSON(ctx, urlStr, M{command: body}) } -func (c *JSONClient) postJSON( - ctx context.Context, - urlStr string, - reqBody interface{}, -) error { - theURL, err := url.Parse(urlStr) - if err != nil { - return errors.Wrap(err, "parse url") - } - +func (c *JSONClient) postJSON(ctx context.Context, urlStr string, reqBody interface{}) error { buf := &bytes.Buffer{} - err = json.NewEncoder(buf).Encode(reqBody) + err := json.NewEncoder(buf).Encode(reqBody) if err != nil { return errors.Wrap(err, "encode request body") } - httpResp, err := c.sendRequest(ctx, http.MethodPost, theURL.String(), buf) + httpResp, err := c.sendRequest(ctx, http.MethodPost, urlStr, buf) if err != nil { return errors.Wrap(err, "send request") } @@ -326,12 +287,6 @@ func (c *JSONClient) UnsetProperty(ctx context.Context, collection string, prope // // Refer to https://lucene.apache.org/solr/guide/8_8/config-api.html#commands-for-handlers-and-components func (c *JSONClient) AddComponents(ctx context.Context, collection string, components ...*Component) error { - urlStr := fmt.Sprintf("%s/solr/%s/config", c.baseURL, collection) - theURL, err := url.Parse(urlStr) - if err != nil { - return errors.Wrap(err, "parse url") - } - commands := []string{} for _, comp := range components { b, err := json.Marshal(comp.BuildComponent()) @@ -345,7 +300,8 @@ func (c *JSONClient) AddComponents(ctx context.Context, collection string, compo reqBody := "{" + strings.Join(commands, ",") + "}" - httpResp, err := c.sendRequest(ctx, http.MethodPost, theURL.String(), strings.NewReader(reqBody)) + urlStr := fmt.Sprintf("%s/solr/%s/config", c.baseURL, collection) + httpResp, err := c.sendRequest(ctx, http.MethodPost, urlStr, strings.NewReader(reqBody)) if err != nil { return errors.Wrap(err, "send request") } @@ -382,14 +338,8 @@ func (c *JSONClient) DeleteComponents(ctx context.Context, collection string, co // // Refer to https://lucene.apache.org/solr/guide/8_8/suggester.html#get-suggestions-with-weights func (c *JSONClient) Suggest(ctx context.Context, collection string, params *SuggestParams) (*SuggestResponse, error) { - urlStr := fmt.Sprintf("%s/solr/%s/%s", c.baseURL, collection, params.endpoint) - theURL, err := url.Parse(urlStr) - if err != nil { - return nil, errors.Wrap(err, "parse url") - } - theURL.RawQuery = params.BuildParams() - - httpResp, err := c.sendRequest(ctx, http.MethodGet, theURL.String(), nil) + urlStr := fmt.Sprintf("%s/solr/%s/%s?%s", c.baseURL, collection, params.endpoint, params.BuildParams()) + httpResp, err := c.sendRequest(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, errors.Wrap(err, "send request") } @@ -412,8 +362,13 @@ func (c *JSONClient) sendRequest(ctx context.Context, httpMethod, urlStr string, } func (c *JSONClient) sendRequestWithContentType(ctx context.Context, httpMethod, urlStr, contentType string, body io.Reader) (*http.Response, error) { + theURL, err := url.Parse(urlStr) + if err != nil { + return nil, errors.Wrap(err, "parse url") + } + httpReq, err := http.NewRequestWithContext(ctx, - httpMethod, urlStr, body) + httpMethod, theURL.String(), body) if err != nil { return nil, errors.Wrap(err, "new http request") }