From ee8471702887186531ff8e2217fcd01798f2c3c4 Mon Sep 17 00:00:00 2001 From: Hayden Hung Hoang <94696449+phiHero@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:02:03 +0700 Subject: [PATCH] Add support for stopwords, metrics & stats (#179) * feat: retrieve & upsert stopwords * feat: retrieve & delete a stopword set * test-integration: retrieve & upsert stopwords sets * test-integration: retrieve & delete a stopword set * test-integration: single collection & multi search with stopwords * fix: multi-search integration test * feat: retrieve API stats * fix: lint * feat: retrieve cluster metrics * fix: bump to v2 after git rebase * update README: stopwords, metrics & stats --- README.md | 40 +++++ typesense/api/client_gen.go | 208 +++++++++++++++++++++++++- typesense/api/generator/generator.yml | 91 ++++++++++- typesense/api/generator/openapi.yml | 85 ++++++++++- typesense/api/pointer/pointer.go | 8 + typesense/api/pointer/pointer_test.go | 7 + typesense/api/types_gen.go | 24 ++- typesense/client.go | 16 ++ typesense/metrics.go | 24 +++ typesense/metrics_test.go | 57 +++++++ typesense/mocks/mock_client.go | 80 ++++++++++ typesense/stats.go | 26 ++++ typesense/stats_test.go | 56 +++++++ typesense/stopword.go | 43 ++++++ typesense/stopword_test.go | 75 ++++++++++ typesense/stopwords.go | 38 +++++ typesense/stopwords_test.go | 92 ++++++++++++ typesense/test/metrics_test.go | 17 +++ typesense/test/multi_search_test.go | 83 +++++++++- typesense/test/search_test.go | 52 +++++++ typesense/test/stats_test.go | 16 ++ typesense/test/stopword_test.go | 65 ++++++++ typesense/test/stopwords_test.go | 74 +++++++++ 23 files changed, 1262 insertions(+), 15 deletions(-) create mode 100644 typesense/metrics.go create mode 100644 typesense/metrics_test.go create mode 100644 typesense/stats.go create mode 100644 typesense/stats_test.go create mode 100644 typesense/stopword.go create mode 100644 typesense/stopword_test.go create mode 100644 typesense/stopwords.go create mode 100644 typesense/stopwords_test.go create mode 100644 typesense/test/metrics_test.go create mode 100644 typesense/test/stats_test.go create mode 100644 typesense/test/stopword_test.go create mode 100644 typesense/test/stopwords_test.go diff --git a/README.md b/README.md index c663c15..44a4942 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,34 @@ client.Collection("products").Synonyms().Retrieve(context.Background()) client.Collection("products").Synonym("coat-synonyms").Delete(context.Background()) ``` +### Create or update a stopwords set + +```go + stopwords := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + client.Stopwords().Upsert(context.Background(), "stopword_set1", stopwords) +``` + +### Retrieve a stopwords set + +```go +client.Stopword("stopword_set1").Retrieve(context.Background()) +``` + +### List all stopwords sets + +```go +client.Stopwords().Retrieve(context.Background()) +``` + +### Delete a stopwords set + +```go +client.Stopword("stopword_set1").Delete(context.Background()) +``` + ### Create or update a preset ```go @@ -445,6 +473,18 @@ client.Operations().Snapshot(context.Background(), "/tmp/typesense-data-snapshot client.Operations().Vote(context.Background()) ``` +### Cluster Metrics + +```go +client.Metrics().Retrieve(context.Background()) +``` + +### API Stats + +```go +client.Stats().Retrieve(context.Background()) +``` + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/typesense/typesense-go. diff --git a/typesense/api/client_gen.go b/typesense/api/client_gen.go index 01b49f4..04bc669 100644 --- a/typesense/api/client_gen.go +++ b/typesense/api/client_gen.go @@ -222,6 +222,9 @@ type ClientInterface interface { // GetKey request GetKey(ctx context.Context, keyId int64, reqEditors ...RequestEditorFn) (*http.Response, error) + // RetrieveMetrics request + RetrieveMetrics(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // MultiSearchWithBody request with any body MultiSearchWithBody(ctx context.Context, params *MultiSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -247,6 +250,9 @@ type ClientInterface interface { UpsertPreset(ctx context.Context, presetId string, body UpsertPresetJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // RetrieveAPIStats request + RetrieveAPIStats(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // RetrieveStopwordsSets request RetrieveStopwordsSets(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -838,6 +844,18 @@ func (c *Client) GetKey(ctx context.Context, keyId int64, reqEditors ...RequestE return c.Client.Do(req) } +func (c *Client) RetrieveMetrics(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRetrieveMetricsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) MultiSearchWithBody(ctx context.Context, params *MultiSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewMultiSearchRequestWithBody(c.Server, params, contentType, body) if err != nil { @@ -946,6 +964,18 @@ func (c *Client) UpsertPreset(ctx context.Context, presetId string, body UpsertP return c.Client.Do(req) } +func (c *Client) RetrieveAPIStats(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRetrieveAPIStatsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) RetrieveStopwordsSets(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewRetrieveStopwordsSetsRequest(c.Server) if err != nil { @@ -3501,6 +3531,33 @@ func NewGetKeyRequest(server string, keyId int64) (*http.Request, error) { return req, nil } +// NewRetrieveMetricsRequest generates requests for RetrieveMetrics +func NewRetrieveMetricsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/metrics.json") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewMultiSearchRequest calls the generic MultiSearch builder with application/json body func NewMultiSearchRequest(server string, params *MultiSearchParams, body MultiSearchJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4641,6 +4698,33 @@ func NewUpsertPresetRequestWithBody(server string, presetId string, contentType return req, nil } +// NewRetrieveAPIStatsRequest generates requests for RetrieveAPIStats +func NewRetrieveAPIStatsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/stats.json") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewRetrieveStopwordsSetsRequest generates requests for RetrieveStopwordsSets func NewRetrieveStopwordsSetsRequest(server string) (*http.Request, error) { var err error @@ -4959,6 +5043,9 @@ type ClientWithResponsesInterface interface { // GetKeyWithResponse request GetKeyWithResponse(ctx context.Context, keyId int64, reqEditors ...RequestEditorFn) (*GetKeyResponse, error) + // RetrieveMetricsWithResponse request + RetrieveMetricsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveMetricsResponse, error) + // MultiSearchWithBodyWithResponse request with any body MultiSearchWithBodyWithResponse(ctx context.Context, params *MultiSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MultiSearchResponse, error) @@ -4984,6 +5071,9 @@ type ClientWithResponsesInterface interface { UpsertPresetWithResponse(ctx context.Context, presetId string, body UpsertPresetJSONRequestBody, reqEditors ...RequestEditorFn) (*UpsertPresetResponse, error) + // RetrieveAPIStatsWithResponse request + RetrieveAPIStatsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveAPIStatsResponse, error) + // RetrieveStopwordsSetsWithResponse request RetrieveStopwordsSetsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveStopwordsSetsResponse, error) @@ -5855,6 +5945,28 @@ func (r GetKeyResponse) StatusCode() int { return 0 } +type RetrieveMetricsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *map[string]interface{} +} + +// Status returns HTTPResponse.Status +func (r RetrieveMetricsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RetrieveMetricsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type MultiSearchResponse struct { Body []byte HTTPResponse *http.Response @@ -6013,10 +6125,32 @@ func (r UpsertPresetResponse) StatusCode() int { return 0 } +type RetrieveAPIStatsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *APIStatsResponse +} + +// Status returns HTTPResponse.Status +func (r RetrieveAPIStatsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RetrieveAPIStatsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type RetrieveStopwordsSetsResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *StopwordsSetRetrieveSchema + JSON200 *StopwordsSetsRetrieveAllSchema } // Status returns HTTPResponse.Status @@ -6527,6 +6661,15 @@ func (c *ClientWithResponses) GetKeyWithResponse(ctx context.Context, keyId int6 return ParseGetKeyResponse(rsp) } +// RetrieveMetricsWithResponse request returning *RetrieveMetricsResponse +func (c *ClientWithResponses) RetrieveMetricsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveMetricsResponse, error) { + rsp, err := c.RetrieveMetrics(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseRetrieveMetricsResponse(rsp) +} + // MultiSearchWithBodyWithResponse request with arbitrary body returning *MultiSearchResponse func (c *ClientWithResponses) MultiSearchWithBodyWithResponse(ctx context.Context, params *MultiSearchParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MultiSearchResponse, error) { rsp, err := c.MultiSearchWithBody(ctx, params, contentType, body, reqEditors...) @@ -6606,6 +6749,15 @@ func (c *ClientWithResponses) UpsertPresetWithResponse(ctx context.Context, pres return ParseUpsertPresetResponse(rsp) } +// RetrieveAPIStatsWithResponse request returning *RetrieveAPIStatsResponse +func (c *ClientWithResponses) RetrieveAPIStatsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveAPIStatsResponse, error) { + rsp, err := c.RetrieveAPIStats(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseRetrieveAPIStatsResponse(rsp) +} + // RetrieveStopwordsSetsWithResponse request returning *RetrieveStopwordsSetsResponse func (c *ClientWithResponses) RetrieveStopwordsSetsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*RetrieveStopwordsSetsResponse, error) { rsp, err := c.RetrieveStopwordsSets(ctx, reqEditors...) @@ -7864,6 +8016,32 @@ func ParseGetKeyResponse(rsp *http.Response) (*GetKeyResponse, error) { return response, nil } +// ParseRetrieveMetricsResponse parses an HTTP response from a RetrieveMetricsWithResponse call +func ParseRetrieveMetricsResponse(rsp *http.Response) (*RetrieveMetricsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RetrieveMetricsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest map[string]interface{} + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseMultiSearchResponse parses an HTTP response from a MultiSearchWithResponse call func ParseMultiSearchResponse(rsp *http.Response) (*MultiSearchResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -8074,6 +8252,32 @@ func ParseUpsertPresetResponse(rsp *http.Response) (*UpsertPresetResponse, error return response, nil } +// ParseRetrieveAPIStatsResponse parses an HTTP response from a RetrieveAPIStatsWithResponse call +func ParseRetrieveAPIStatsResponse(rsp *http.Response) (*RetrieveAPIStatsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &RetrieveAPIStatsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest APIStatsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseRetrieveStopwordsSetsResponse parses an HTTP response from a RetrieveStopwordsSetsWithResponse call func ParseRetrieveStopwordsSetsResponse(rsp *http.Response) (*RetrieveStopwordsSetsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -8089,7 +8293,7 @@ func ParseRetrieveStopwordsSetsResponse(rsp *http.Response) (*RetrieveStopwordsS switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest StopwordsSetRetrieveSchema + var dest StopwordsSetsRetrieveAllSchema if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } diff --git a/typesense/api/generator/generator.yml b/typesense/api/generator/generator.yml index bf1751d..8b38c18 100644 --- a/typesense/api/generator/generator.yml +++ b/typesense/api/generator/generator.yml @@ -1,5 +1,47 @@ components: schemas: + APIStatsResponse: + properties: + delete_latency_ms: + format: double + type: number + delete_requests_per_second: + format: double + type: number + import_latency_ms: + format: double + type: number + import_requests_per_second: + format: double + type: number + latency_ms: + type: object + x-go-type: map[string]float64 + overloaded_requests_per_second: + format: double + type: number + pending_write_batches: + format: double + type: number + requests_per_second: + type: object + x-go-type: map[string]float64 + search_latency_ms: + format: double + type: number + search_requests_per_second: + format: double + type: number + total_requests_per_second: + format: double + type: number + write_latency_ms: + format: double + type: number + write_requests_per_second: + format: double + type: number + type: object AnalyticsRuleParameters: properties: destination: @@ -1083,12 +1125,10 @@ components: type: object StopwordsSetRetrieveSchema: example: | - {"stopwords": [{"id": "countries", "stopwords": ["Germany", "France", "Italy"], "locale": "en"}]} + {"stopwords": {"id": "countries", "stopwords": ["Germany", "France", "Italy"], "locale": "en"}} properties: stopwords: - items: - $ref: '#/components/schemas/StopwordsSetSchema' - type: array + $ref: '#/components/schemas/StopwordsSetSchema' required: - stopwords type: object @@ -1121,6 +1161,17 @@ components: required: - stopwords type: object + StopwordsSetsRetrieveAllSchema: + example: | + {"stopwords": [{"id": "countries", "stopwords": ["Germany", "France", "Italy"], "locale": "en"}]} + properties: + stopwords: + items: + $ref: '#/components/schemas/StopwordsSetSchema' + type: array + required: + - stopwords + type: object SuccessStatus: properties: success: @@ -2504,6 +2555,20 @@ paths: summary: Retrieve (metadata about) a key tags: - keys + /metrics.json: + get: + description: Retrieve the metrics. + operationId: retrieveMetrics + responses: + 200: + content: + application/json: + schema: + type: object + description: Metrics fetched. + summary: Get current RAM, CPU, Disk & Network usage metrics. + tags: + - operations /multi_search: post: description: This is especially useful to avoid round-trip network latencies incurred otherwise if each of these requests are sent in separate HTTP requests. You can also use this feature to do a federated search across multiple collections in a single HTTP request. @@ -2888,6 +2953,20 @@ paths: summary: Upserts a preset. tags: - presets + /stats.json: + get: + description: Retrieve the stats about API endpoints. + operationId: retrieveAPIStats + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/APIStatsResponse' + description: Stats fetched. + summary: Get stats about API endpoints. + tags: + - operations /stopwords: get: description: Retrieve the details of all stopwords sets @@ -2897,7 +2976,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/StopwordsSetRetrieveSchema' + $ref: '#/components/schemas/StopwordsSetsRetrieveAllSchema' description: Stopwords sets fetched. summary: Retrieves all stopwords sets. tags: @@ -3031,7 +3110,7 @@ tags: - description: Manage Typesense cluster externalDocs: description: Find out more - url: https://typesense.org/docs/0.23.0/api/#cluster-operations + url: https://typesense.org/docs/26.0/api/cluster-operations.html name: operations - description: Manage stopwords sets externalDocs: diff --git a/typesense/api/generator/openapi.yml b/typesense/api/generator/openapi.yml index 16f56c6..249a696 100644 --- a/typesense/api/generator/openapi.yml +++ b/typesense/api/generator/openapi.yml @@ -40,7 +40,7 @@ tags: description: Manage Typesense cluster externalDocs: description: Find out more - url: https://typesense.org/docs/0.23.0/api/#cluster-operations + url: https://typesense.org/docs/26.0/api/cluster-operations.html - name: stopwords description: Manage stopwords sets externalDocs: @@ -1336,6 +1336,36 @@ paths: application/json: schema: $ref: "#/components/schemas/ApiResponse" + /metrics.json: + get: + tags: + - operations + summary: Get current RAM, CPU, Disk & Network usage metrics. + description: + Retrieve the metrics. + operationId: retrieveMetrics + responses: + 200: + description: Metrics fetched. + content: + application/json: + schema: + type: object + /stats.json: + get: + tags: + - operations + summary: Get stats about API endpoints. + description: + Retrieve the stats about API endpoints. + operationId: retrieveAPIStats + responses: + 200: + description: Stats fetched. + content: + application/json: + schema: + $ref: "#/components/schemas/APIStatsResponse" /stopwords: get: tags: @@ -1350,7 +1380,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/StopwordsSetRetrieveSchema" + $ref: "#/components/schemas/StopwordsSetsRetrieveAllSchema" /stopwords/{setId}: put: tags: @@ -2875,6 +2905,48 @@ components: type: array items: $ref: "#/components/schemas/AnalyticsRuleSchema" + APIStatsResponse: + type: object + properties: + delete_latency_ms: + type: number + format: double + delete_requests_per_second: + type: number + format: double + import_latency_ms: + type: number + format: double + import_requests_per_second: + type: number + format: double + latency_ms: + type: object + x-go-type: "map[string]float64" + overloaded_requests_per_second: + type: number + format: double + pending_write_batches: + type: number + format: double + requests_per_second: + type: object + x-go-type: "map[string]float64" + search_latency_ms: + type: number + format: double + search_requests_per_second: + type: number + format: double + total_requests_per_second: + type: number + format: double + write_latency_ms: + type: number + format: double + write_requests_per_second: + type: number + format: double StopwordsSetUpsertSchema: type: object properties: @@ -2905,6 +2977,15 @@ components: example: | {"id": "countries", "stopwords": ["Germany", "France", "Italy"], "locale": "en"} StopwordsSetRetrieveSchema: + type: object + properties: + stopwords: + $ref: "#/components/schemas/StopwordsSetSchema" + required: + - stopwords + example: | + {"stopwords": {"id": "countries", "stopwords": ["Germany", "France", "Italy"], "locale": "en"}} + StopwordsSetsRetrieveAllSchema: type: object properties: stopwords: diff --git a/typesense/api/pointer/pointer.go b/typesense/api/pointer/pointer.go index e78a698..b53887c 100644 --- a/typesense/api/pointer/pointer.go +++ b/typesense/api/pointer/pointer.go @@ -10,6 +10,14 @@ func False() *bool { return &res } +func Float32(v float32) *float32 { + return &v +} + +func Float64(v float64) *float64 { + return &v +} + func Int(v int) *int { return &v } diff --git a/typesense/api/pointer/pointer_test.go b/typesense/api/pointer/pointer_test.go index 9a619a2..c17a4e3 100644 --- a/typesense/api/pointer/pointer_test.go +++ b/typesense/api/pointer/pointer_test.go @@ -16,6 +16,13 @@ func TestPointerValues(t *testing.T) { assert.NotNil(t, String("abc")) assert.Equal(t, "abc", *String("abc")) + var expectedFloat32 float32 = 9.5 + assert.NotNil(t, Float32(9.5)) + assert.Equal(t, expectedFloat32, *Float32(9.5)) + + assert.NotNil(t, Float64(9.5)) + assert.Equal(t, 9.5, *Float64(9.5)) + v := struct{ field string }{field: "abc"} assert.NotNil(t, Interface(v)) assert.Equal(t, v, *Interface(v)) diff --git a/typesense/api/types_gen.go b/typesense/api/types_gen.go index b72d95a..309d760 100644 --- a/typesense/api/types_gen.go +++ b/typesense/api/types_gen.go @@ -32,6 +32,23 @@ const ( Reject ImportDocumentsParamsDirtyValues = "reject" ) +// APIStatsResponse defines model for APIStatsResponse. +type APIStatsResponse struct { + DeleteLatencyMs *float64 `json:"delete_latency_ms,omitempty"` + DeleteRequestsPerSecond *float64 `json:"delete_requests_per_second,omitempty"` + ImportLatencyMs *float64 `json:"import_latency_ms,omitempty"` + ImportRequestsPerSecond *float64 `json:"import_requests_per_second,omitempty"` + LatencyMs *map[string]float64 `json:"latency_ms,omitempty"` + OverloadedRequestsPerSecond *float64 `json:"overloaded_requests_per_second,omitempty"` + PendingWriteBatches *float64 `json:"pending_write_batches,omitempty"` + RequestsPerSecond *map[string]float64 `json:"requests_per_second,omitempty"` + SearchLatencyMs *float64 `json:"search_latency_ms,omitempty"` + SearchRequestsPerSecond *float64 `json:"search_requests_per_second,omitempty"` + TotalRequestsPerSecond *float64 `json:"total_requests_per_second,omitempty"` + WriteLatencyMs *float64 `json:"write_latency_ms,omitempty"` + WriteRequestsPerSecond *float64 `json:"write_requests_per_second,omitempty"` +} + // AnalyticsRuleParameters defines model for AnalyticsRuleParameters. type AnalyticsRuleParameters struct { Destination struct { @@ -920,7 +937,7 @@ type SearchSynonymsResponse struct { // StopwordsSetRetrieveSchema defines model for StopwordsSetRetrieveSchema. type StopwordsSetRetrieveSchema struct { - Stopwords []StopwordsSetSchema `json:"stopwords"` + Stopwords StopwordsSetSchema `json:"stopwords"` } // StopwordsSetSchema defines model for StopwordsSetSchema. @@ -936,6 +953,11 @@ type StopwordsSetUpsertSchema struct { Stopwords []string `json:"stopwords"` } +// StopwordsSetsRetrieveAllSchema defines model for StopwordsSetsRetrieveAllSchema. +type StopwordsSetsRetrieveAllSchema struct { + Stopwords []StopwordsSetSchema `json:"stopwords"` +} + // SuccessStatus defines model for SuccessStatus. type SuccessStatus struct { Success bool `json:"success"` diff --git a/typesense/client.go b/typesense/client.go index 3f7199f..fe714c1 100644 --- a/typesense/client.go +++ b/typesense/client.go @@ -64,6 +64,22 @@ func (c *Client) Preset(presetName string) PresetInterface { return &preset{apiClient: c.apiClient, presetName: presetName} } +func (c *Client) Stopwords() StopwordsInterface { + return &stopwords{apiClient: c.apiClient} +} + +func (c *Client) Stopword(stopwordsSetId string) StopwordInterface { + return &stopword{apiClient: c.apiClient, stopwordsSetId: stopwordsSetId} +} + +func (c *Client) Stats() StatsInterface { + return &stats{apiClient: c.apiClient} +} + +func (c *Client) Metrics() MetricsInterface { + return &metrics{apiClient: c.apiClient} +} + type HTTPError struct { Status int Body []byte diff --git a/typesense/metrics.go b/typesense/metrics.go new file mode 100644 index 0000000..08e5ec7 --- /dev/null +++ b/typesense/metrics.go @@ -0,0 +1,24 @@ +package typesense + +import ( + "context" +) + +type MetricsInterface interface { + Retrieve(ctx context.Context) (map[string]interface{}, error) +} + +type metrics struct { + apiClient APIClientInterface +} + +func (m *metrics) Retrieve(ctx context.Context) (map[string]interface{}, error) { + response, err := m.apiClient.RetrieveMetricsWithResponse(ctx) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return *response.JSON200, nil +} diff --git a/typesense/metrics_test.go b/typesense/metrics_test.go new file mode 100644 index 0000000..9814d1e --- /dev/null +++ b/typesense/metrics_test.go @@ -0,0 +1,57 @@ +package typesense + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMetricsRetrieve(t *testing.T) { + expectedData := map[string]interface{}{ + "system_cpu1_active_percentage": "0.00", + "system_cpu2_active_percentage": "0.00", + "system_cpu3_active_percentage": "0.00", + "system_cpu4_active_percentage": "0.00", + "system_cpu_active_percentage": "0.00", + "system_disk_total_bytes": "1043447808", + "system_disk_used_bytes": "561152", + "system_memory_total_bytes": "2086899712", + "system_memory_used_bytes": "1004507136", + "system_memory_total_swap_bytes": "1004507136", + "system_memory_used_swap_bytes": "0.00", + "system_network_received_bytes": "1466", + "system_network_sent_bytes": "182", + "typesense_memory_active_bytes": "29630464", + "typesense_memory_allocated_bytes": "27886840", + "typesense_memory_fragmentation_ratio": "0.06", + "typesense_memory_mapped_bytes": "69701632", + "typesense_memory_metadata_bytes": "4588768", + "typesense_memory_resident_bytes": "29630464", + "typesense_memory_retained_bytes": "25718784", + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/metrics.json", http.MethodGet) + data := jsonEncode(t, expectedData) + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Metrics().Retrieve(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestMetricsRetrieveOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/metrics.json", http.MethodGet) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Metrics().Retrieve(context.Background()) + assert.ErrorContains(t, err, "status: 409") +} diff --git a/typesense/mocks/mock_client.go b/typesense/mocks/mock_client.go index b0a403c..5d66b65 100644 --- a/typesense/mocks/mock_client.go +++ b/typesense/mocks/mock_client.go @@ -1442,6 +1442,46 @@ func (mr *MockAPIClientInterfaceMockRecorder) MultiSearchWithResponse(ctx, param return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MultiSearchWithResponse", reflect.TypeOf((*MockAPIClientInterface)(nil).MultiSearchWithResponse), varargs...) } +// RetrieveAPIStats mocks base method. +func (m *MockAPIClientInterface) RetrieveAPIStats(ctx context.Context, reqEditors ...api.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RetrieveAPIStats", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetrieveAPIStats indicates an expected call of RetrieveAPIStats. +func (mr *MockAPIClientInterfaceMockRecorder) RetrieveAPIStats(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveAPIStats", reflect.TypeOf((*MockAPIClientInterface)(nil).RetrieveAPIStats), varargs...) +} + +// RetrieveAPIStatsWithResponse mocks base method. +func (m *MockAPIClientInterface) RetrieveAPIStatsWithResponse(ctx context.Context, reqEditors ...api.RequestEditorFn) (*api.RetrieveAPIStatsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RetrieveAPIStatsWithResponse", varargs...) + ret0, _ := ret[0].(*api.RetrieveAPIStatsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetrieveAPIStatsWithResponse indicates an expected call of RetrieveAPIStatsWithResponse. +func (mr *MockAPIClientInterfaceMockRecorder) RetrieveAPIStatsWithResponse(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveAPIStatsWithResponse", reflect.TypeOf((*MockAPIClientInterface)(nil).RetrieveAPIStatsWithResponse), varargs...) +} + // RetrieveAllPresets mocks base method. func (m *MockAPIClientInterface) RetrieveAllPresets(ctx context.Context, reqEditors ...api.RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() @@ -1562,6 +1602,46 @@ func (mr *MockAPIClientInterfaceMockRecorder) RetrieveAnalyticsRulesWithResponse return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveAnalyticsRulesWithResponse", reflect.TypeOf((*MockAPIClientInterface)(nil).RetrieveAnalyticsRulesWithResponse), varargs...) } +// RetrieveMetrics mocks base method. +func (m *MockAPIClientInterface) RetrieveMetrics(ctx context.Context, reqEditors ...api.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RetrieveMetrics", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetrieveMetrics indicates an expected call of RetrieveMetrics. +func (mr *MockAPIClientInterfaceMockRecorder) RetrieveMetrics(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveMetrics", reflect.TypeOf((*MockAPIClientInterface)(nil).RetrieveMetrics), varargs...) +} + +// RetrieveMetricsWithResponse mocks base method. +func (m *MockAPIClientInterface) RetrieveMetricsWithResponse(ctx context.Context, reqEditors ...api.RequestEditorFn) (*api.RetrieveMetricsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RetrieveMetricsWithResponse", varargs...) + ret0, _ := ret[0].(*api.RetrieveMetricsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RetrieveMetricsWithResponse indicates an expected call of RetrieveMetricsWithResponse. +func (mr *MockAPIClientInterfaceMockRecorder) RetrieveMetricsWithResponse(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetrieveMetricsWithResponse", reflect.TypeOf((*MockAPIClientInterface)(nil).RetrieveMetricsWithResponse), varargs...) +} + // RetrievePreset mocks base method. func (m *MockAPIClientInterface) RetrievePreset(ctx context.Context, presetId string, reqEditors ...api.RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() diff --git a/typesense/stats.go b/typesense/stats.go new file mode 100644 index 0000000..6a75412 --- /dev/null +++ b/typesense/stats.go @@ -0,0 +1,26 @@ +package typesense + +import ( + "context" + + "github.com/typesense/typesense-go/v2/typesense/api" +) + +type StatsInterface interface { + Retrieve(ctx context.Context) (*api.APIStatsResponse, error) +} + +type stats struct { + apiClient APIClientInterface +} + +func (s *stats) Retrieve(ctx context.Context) (*api.APIStatsResponse, error) { + response, err := s.apiClient.RetrieveAPIStatsWithResponse(ctx) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return response.JSON200, nil +} diff --git a/typesense/stats_test.go b/typesense/stats_test.go new file mode 100644 index 0000000..2bf2409 --- /dev/null +++ b/typesense/stats_test.go @@ -0,0 +1,56 @@ +package typesense + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/typesense/typesense-go/v2/typesense/api" + "github.com/typesense/typesense-go/v2/typesense/api/pointer" +) + +func TestStatsRetrieve(t *testing.T) { + expectedData := &api.APIStatsResponse{ + DeleteLatencyMs: pointer.Float64(10.5), + DeleteRequestsPerSecond: pointer.Float64(5.0), + ImportLatencyMs: pointer.Float64(3.7142857142857144), + ImportRequestsPerSecond: pointer.Float64(9.5), + LatencyMs: &map[string]float64{ + "GET /stats.json": *pointer.Float64(32.5), + }, + OverloadedRequestsPerSecond: pointer.Float64(9.5), + PendingWriteBatches: pointer.Float64(9.5), + RequestsPerSecond: &map[string]float64{ + "GET /stats.json": *pointer.Float64(0.1111111111111111), + }, + SearchLatencyMs: pointer.Float64(9.5), + SearchRequestsPerSecond: pointer.Float64(9.5), + TotalRequestsPerSecond: pointer.Float64(3.3), + WriteLatencyMs: pointer.Float64(9.5), + WriteRequestsPerSecond: pointer.Float64(9.5), + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stats.json", http.MethodGet) + data := jsonEncode(t, expectedData) + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Stats().Retrieve(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestStatsRetrieveOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stats.json", http.MethodGet) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Stats().Retrieve(context.Background()) + assert.ErrorContains(t, err, "status: 409") +} diff --git a/typesense/stopword.go b/typesense/stopword.go new file mode 100644 index 0000000..b4e5a04 --- /dev/null +++ b/typesense/stopword.go @@ -0,0 +1,43 @@ +package typesense + +import ( + "context" + + "github.com/typesense/typesense-go/v2/typesense/api" +) + +type StopwordInterface interface { + Retrieve(ctx context.Context) (*api.StopwordsSetSchema, error) + Delete(ctx context.Context) (*struct { + Id string "json:\"id\"" + }, error) +} + +type stopword struct { + apiClient APIClientInterface + stopwordsSetId string +} + +func (s *stopword) Retrieve(ctx context.Context) (*api.StopwordsSetSchema, error) { + response, err := s.apiClient.RetrieveStopwordsSetWithResponse(ctx, s.stopwordsSetId) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return &response.JSON200.Stopwords, nil +} + +func (s *stopword) Delete(ctx context.Context) (*struct { + Id string "json:\"id\"" +}, error) { + response, err := s.apiClient.DeleteStopwordsSetWithResponse(ctx, s.stopwordsSetId) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return response.JSON200, nil +} diff --git a/typesense/stopword_test.go b/typesense/stopword_test.go new file mode 100644 index 0000000..73305f7 --- /dev/null +++ b/typesense/stopword_test.go @@ -0,0 +1,75 @@ +package typesense + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/typesense/typesense-go/v2/typesense/api" + "github.com/typesense/typesense-go/v2/typesense/api/pointer" +) + +func TestStopwordRetrieve(t *testing.T) { + expectedData := &api.StopwordsSetSchema{ + Id: "stopwords_set1", + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodGet) + data := jsonEncode(t, map[string]any{ + "stopwords": expectedData, + }) + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Stopword(expectedData.Id).Retrieve(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestStopwordRetrieveOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodGet) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Stopword("stopwords_set1").Retrieve(context.Background()) + assert.ErrorContains(t, err, "status: 409") +} + +func TestStopwordDelete(t *testing.T) { + expectedData := &struct { + Id string "json:\"id\"" + }{ + Id: "stopwords_set1", + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodDelete) + data := jsonEncode(t, expectedData) + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Stopword(expectedData.Id).Delete(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestStopwordDeleteOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodDelete) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Stopword("stopwords_set1").Delete(context.Background()) + assert.ErrorContains(t, err, "status: 409") +} diff --git a/typesense/stopwords.go b/typesense/stopwords.go new file mode 100644 index 0000000..a687002 --- /dev/null +++ b/typesense/stopwords.go @@ -0,0 +1,38 @@ +package typesense + +import ( + "context" + + "github.com/typesense/typesense-go/v2/typesense/api" +) + +type StopwordsInterface interface { + Retrieve(ctx context.Context) ([]api.StopwordsSetSchema, error) + Upsert(ctx context.Context, stopwordsSetId string, stopwordsSetUpsertSchema *api.StopwordsSetUpsertSchema) (*api.StopwordsSetSchema, error) +} + +type stopwords struct { + apiClient APIClientInterface +} + +func (s *stopwords) Retrieve(ctx context.Context) ([]api.StopwordsSetSchema, error) { + response, err := s.apiClient.RetrieveStopwordsSetsWithResponse(ctx) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return response.JSON200.Stopwords, nil +} + +func (s *stopwords) Upsert(ctx context.Context, stopwordsSetId string, stopwordsSetUpsertSchema *api.StopwordsSetUpsertSchema) (*api.StopwordsSetSchema, error) { + response, err := s.apiClient.UpsertStopwordsSetWithResponse(ctx, stopwordsSetId, *stopwordsSetUpsertSchema) + if err != nil { + return nil, err + } + if response.JSON200 == nil { + return nil, &HTTPError{Status: response.StatusCode(), Body: response.Body} + } + return response.JSON200, nil +} diff --git a/typesense/stopwords_test.go b/typesense/stopwords_test.go new file mode 100644 index 0000000..a835e6e --- /dev/null +++ b/typesense/stopwords_test.go @@ -0,0 +1,92 @@ +package typesense + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/typesense/typesense-go/v2/typesense/api" + "github.com/typesense/typesense-go/v2/typesense/api/pointer" +) + +func TestStopwordsRetrieve(t *testing.T) { + expectedData := []api.StopwordsSetSchema{ + { + Id: "stopwords_set1", + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + }, + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords", http.MethodGet) + data := jsonEncode(t, map[string][]api.StopwordsSetSchema{ + "stopwords": expectedData, + }) + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Stopwords().Retrieve(context.Background()) + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestStopwordsRetrieveOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords", http.MethodGet) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Stopwords().Retrieve(context.Background()) + assert.ErrorContains(t, err, "status: 409") +} + +func TestStopwordsUpsert(t *testing.T) { + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + expectedData := &api.StopwordsSetSchema{ + Id: "stopwords_set1", + Locale: upsertData.Locale, + Stopwords: upsertData.Stopwords, + } + + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodPut) + + var reqBody api.StopwordsSetUpsertSchema + err := json.NewDecoder(r.Body).Decode(&reqBody) + + assert.NoError(t, err) + assert.Equal(t, *upsertData, reqBody) + + data := jsonEncode(t, expectedData) + + w.Header().Set("Content-Type", "application/json") + w.Write(data) + }) + defer server.Close() + + res, err := client.Stopwords().Upsert(context.Background(), "stopwords_set1", upsertData) + + assert.NoError(t, err) + assert.Equal(t, expectedData, res) +} + +func TestStopwordsUpsertOnHttpStatusErrorCodeReturnsError(t *testing.T) { + server, client := newTestServerAndClient(func(w http.ResponseWriter, r *http.Request) { + validateRequestMetadata(t, r, "/stopwords/stopwords_set1", http.MethodPut) + w.WriteHeader(http.StatusConflict) + }) + defer server.Close() + + _, err := client.Stopwords().Upsert(context.Background(), "stopwords_set1", &api.StopwordsSetUpsertSchema{}) + assert.ErrorContains(t, err, "status: 409") +} diff --git a/typesense/test/metrics_test.go b/typesense/test/metrics_test.go new file mode 100644 index 0000000..5c72f5b --- /dev/null +++ b/typesense/test/metrics_test.go @@ -0,0 +1,17 @@ +//go:build integration +// +build integration + +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMetricsRetrieve(t *testing.T) { + result, err := typesenseClient.Metrics().Retrieve(context.Background()) + require.NoError(t, err) + require.NotNil(t, result["system_memory_total_bytes"]) +} diff --git a/typesense/test/multi_search_test.go b/typesense/test/multi_search_test.go index 2e70c1b..bfda00f 100644 --- a/typesense/test/multi_search_test.go +++ b/typesense/test/multi_search_test.go @@ -27,23 +27,24 @@ func TestMultiSearch(t *testing.T) { _, err := typesenseClient.Collection(collectionName1).Documents().Import(context.Background(), documents, params) require.NoError(t, err) - _, err = typesenseClient.Collection(collectionName1).Documents().Import(context.Background(), documents, params) + _, err = typesenseClient.Collection(collectionName2).Documents().Import(context.Background(), documents, params) require.NoError(t, err) searchParams := &api.MultiSearchParams{ FilterBy: pointer.String("num_employees:>100"), - Q: pointer.String("Company"), QueryBy: pointer.String("company_name"), } searches := api.MultiSearchSearchesParameter{ Searches: []api.MultiSearchCollectionParameters{ { + Q: pointer.String("Company"), Collection: collectionName1, FilterBy: pointer.String("num_employees:>100"), SortBy: pointer.String("num_employees:desc"), }, { + Q: pointer.String("Company"), Collection: collectionName1, FilterBy: pointer.String("num_employees:>1000"), }, @@ -70,6 +71,7 @@ func TestMultiSearch(t *testing.T) { require.Equal(t, 3, len(result.Results)) // Check first result + require.Equal(t, len(expectedDocs1), len(*result.Results[0].Hits), "Number of docs in first result did not equal") for i, doc := range *result.Results[0].Hits { require.Equal(t, *doc.Document, expectedDocs1[i]) } @@ -78,6 +80,7 @@ func TestMultiSearch(t *testing.T) { require.Equal(t, 0, len(*result.Results[1].Hits)) // Check third result + require.Equal(t, len(expectedDocs2), len(*result.Results[2].Hits), "Number of docs in third result did not equal") for i, doc := range *result.Results[2].Hits { require.Equal(t, *doc.Document, expectedDocs2[i]) } @@ -208,17 +211,19 @@ func TestMultiSearchWithPreset(t *testing.T) { _, err := typesenseClient.Collection(collectionName1).Documents().Import(context.Background(), documents, params) require.NoError(t, err) - _, err = typesenseClient.Collection(collectionName1).Documents().Import(context.Background(), documents, params) + _, err = typesenseClient.Collection(collectionName2).Documents().Import(context.Background(), documents, params) require.NoError(t, err) searches := api.MultiSearchSearchesParameter{ Searches: []api.MultiSearchCollectionParameters{ { + Q: pointer.String("Company"), Collection: collectionName1, FilterBy: pointer.String("num_employees:>100"), SortBy: pointer.String("num_employees:desc"), }, { + Q: pointer.String("Company"), Collection: collectionName1, FilterBy: pointer.String("num_employees:>1000"), }, @@ -239,7 +244,6 @@ func TestMultiSearchWithPreset(t *testing.T) { searchParams := &api.MultiSearchParams{ FilterBy: pointer.String("num_employees:>100"), - Q: pointer.String("Company"), QueryBy: pointer.String("company_name"), Preset: &presetName, } @@ -259,6 +263,7 @@ func TestMultiSearchWithPreset(t *testing.T) { require.Equal(t, 3, len(result.Results)) // Check first result + require.Equal(t, len(expectedDocs1), len(*result.Results[0].Hits), "Number of docs in first result did not equal") for i, doc := range *result.Results[0].Hits { require.Equal(t, *doc.Document, expectedDocs1[i]) } @@ -267,7 +272,77 @@ func TestMultiSearchWithPreset(t *testing.T) { require.Equal(t, 0, len(*result.Results[1].Hits)) // Check third result + require.Equal(t, len(expectedDocs2), len(*result.Results[2].Hits), "Number of docs in third result did not equal") for i, doc := range *result.Results[2].Hits { require.Equal(t, *doc.Document, expectedDocs2[i]) } } + +func TestMultiSearchWithStopwords(t *testing.T) { + collectionName1 := createNewCollection(t, "companies") + collectionName2 := createNewCollection(t, "companies") + documents := []interface{}{ + newDocument("123", withCompanyName("Company 1"), withNumEmployees(50)), + newDocument("125", withCompanyName("Company 2"), withNumEmployees(150)), + newDocument("127", withCompanyName("Company Stark Industries 3"), withNumEmployees(1000)), + newDocument("129", withCompanyName("Stark Industries 4"), withNumEmployees(1500)), + } + + params := &api.ImportDocumentsParams{Action: pointer.String("create")} + _, err := typesenseClient.Collection(collectionName1).Documents().Import(context.Background(), documents, params) + require.NoError(t, err) + + _, err = typesenseClient.Collection(collectionName2).Documents().Import(context.Background(), documents, params) + require.NoError(t, err) + + stopwordsSetID := newUUIDName("stopwordsSet-test") + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Stark Industries"}, + } + + _, err = typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetID, upsertData) + require.NoError(t, err) + + searchParams := &api.MultiSearchParams{ + QueryBy: pointer.String("company_name"), + Stopwords: pointer.String(stopwordsSetID), + } + + searches := api.MultiSearchSearchesParameter{ + Searches: []api.MultiSearchCollectionParameters{ + { + Q: pointer.String("Company Stark"), + Collection: collectionName1, + SortBy: pointer.String("num_employees:desc"), + }, + { + Q: pointer.String("Stark"), + Collection: collectionName2, + }, + }, + } + + expectedDocs1 := []map[string]interface{}{ + newDocumentResponse("127", withResponseCompanyName("Company Stark Industries 3"), + withResponseNumEmployees(1000)), + newDocumentResponse("125", withResponseCompanyName("Company 2"), + withResponseNumEmployees(150)), + newDocumentResponse("123", withResponseCompanyName("Company 1"), + withResponseNumEmployees(50)), + } + + result, err := typesenseClient.MultiSearch.Perform(context.Background(), searchParams, searches) + require.NoError(t, err) + + require.Equal(t, 2, len(result.Results)) + + // Check first result + require.Equal(t, len(expectedDocs1), len(*result.Results[0].Hits), "Number of docs in first result did not equal") + for i, doc := range *result.Results[0].Hits { + require.Equal(t, *doc.Document, expectedDocs1[i]) + } + + // Check second result + require.Equal(t, 0, len(*result.Results[1].Hits), "Number of docs in second result did not equal") +} diff --git a/typesense/test/search_test.go b/typesense/test/search_test.go index e8945af..4578105 100644 --- a/typesense/test/search_test.go +++ b/typesense/test/search_test.go @@ -204,3 +204,55 @@ func TestCollectionSearchWithPreset(t *testing.T) { require.Equal(t, expectedDocs, docs) } + +func TestCollectionSearchWithStopwords(t *testing.T) { + collectionName := createNewCollection(t, "companies") + documents := []interface{}{ + newDocument("123", withCompanyName("Company 1"), withNumEmployees(50)), + newDocument("125", withCompanyName("Company 2"), withNumEmployees(150)), + newDocument("127", withCompanyName("Company Stark Industries 3"), withNumEmployees(1000)), + newDocument("129", withCompanyName("Stark Industries 4"), withNumEmployees(2000)), + } + + params := &api.ImportDocumentsParams{Action: pointer.String("create")} + _, err := typesenseClient.Collection(collectionName).Documents().Import(context.Background(), documents, params) + require.NoError(t, err) + + stopwordsSetID := newUUIDName("stopwordsSet-test") + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Stark Industries"}, + } + + _, err = typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetID, upsertData) + require.NoError(t, err) + + searchParams := &api.SearchCollectionParams{ + Q: pointer.String("Company Stark"), + QueryBy: pointer.String("company_name"), + SortBy: pointer.String("num_employees:desc"), + Stopwords: pointer.String(stopwordsSetID), + } + + expectedDocs := []map[string]interface{}{ + newDocumentResponse("127", withResponseCompanyName("Company Stark Industries 3"), + withResponseNumEmployees(1000)), + newDocumentResponse("125", withResponseCompanyName("Company 2"), + withResponseNumEmployees(150)), + newDocumentResponse("123", withResponseCompanyName("Company 1"), + withResponseNumEmployees(50)), + } + + result, err := typesenseClient.Collection(collectionName).Documents().Search(context.Background(), searchParams) + + require.NoError(t, err) + require.Equal(t, 3, *result.Found, "found documents number is invalid") + require.Equal(t, 3, len(*result.Hits), "number of hits is invalid") + + docs := make([]map[string]interface{}, len(*result.Hits)) + for i, hit := range *result.Hits { + docs[i] = *hit.Document + } + + require.Equal(t, expectedDocs, docs) +} diff --git a/typesense/test/stats_test.go b/typesense/test/stats_test.go new file mode 100644 index 0000000..61cc03a --- /dev/null +++ b/typesense/test/stats_test.go @@ -0,0 +1,16 @@ +//go:build integration +// +build integration + +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStatsRetrieve(t *testing.T) { + _, err := typesenseClient.Stats().Retrieve(context.Background()) + require.NoError(t, err) +} diff --git a/typesense/test/stopword_test.go b/typesense/test/stopword_test.go new file mode 100644 index 0000000..b3cc382 --- /dev/null +++ b/typesense/test/stopword_test.go @@ -0,0 +1,65 @@ +//go:build integration +// +build integration + +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/typesense/typesense-go/v2/typesense/api" + "github.com/typesense/typesense-go/v2/typesense/api/pointer" +) + +func TestStopwordRetrieve(t *testing.T) { + stopwordsSetID := newUUIDName("stopwordsSet-test") + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + expectedData := &api.StopwordsSetSchema{ + Id: stopwordsSetID, + Locale: upsertData.Locale, + Stopwords: upsertData.Stopwords, + } + + result, err := typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetID, upsertData) + + require.NoError(t, err) + require.Equal(t, expectedData, result) + + result, err = typesenseClient.Stopword(stopwordsSetID).Retrieve(context.Background()) + + expectedData.Stopwords = []string{"states", "united", "france", "germany", "italy"} + + require.NoError(t, err) + require.Equal(t, expectedData, result) +} + +func TestStopwordDelete(t *testing.T) { + stopwordsSetID := newUUIDName("stopwordsSet-test") + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + expectedData := &api.StopwordsSetSchema{ + Id: stopwordsSetID, + Locale: upsertData.Locale, + Stopwords: upsertData.Stopwords, + } + + result, err := typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetID, upsertData) + require.NoError(t, err) + require.Equal(t, expectedData, result) + + result2, err := typesenseClient.Stopword(stopwordsSetID).Delete(context.Background()) + + require.NoError(t, err) + require.Equal(t, stopwordsSetID, result2.Id) + + _, err = typesenseClient.Stopword(stopwordsSetID).Retrieve(context.Background()) + require.Error(t, err) +} diff --git a/typesense/test/stopwords_test.go b/typesense/test/stopwords_test.go new file mode 100644 index 0000000..adfec1e --- /dev/null +++ b/typesense/test/stopwords_test.go @@ -0,0 +1,74 @@ +//go:build integration +// +build integration + +package test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typesense/typesense-go/v2/typesense/api" + "github.com/typesense/typesense-go/v2/typesense/api/pointer" +) + +func TestStopwordsUpsert(t *testing.T) { + stopwordsSetID := newUUIDName("stopwordsSet-test") + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + expectedData := &api.StopwordsSetSchema{ + Id: stopwordsSetID, + Locale: upsertData.Locale, + Stopwords: upsertData.Stopwords, + } + + result, err := typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetID, upsertData) + + require.NoError(t, err) + require.Equal(t, expectedData, result) +} + +func TestStopwordsRetrieve(t *testing.T) { + total := 3 + stopwordsSetIDs := make([]string, total) + for i := 0; i < total; i++ { + stopwordsSetIDs[i] = newUUIDName("stopwordsSet-test") + } + + upsertData := &api.StopwordsSetUpsertSchema{ + Locale: pointer.String("en"), + Stopwords: []string{"Germany", "France", "Italy", "United States"}, + } + + expectedResult := map[string]api.StopwordsSetSchema{} + for i := 0; i < total; i++ { + expectedResult[stopwordsSetIDs[i]] = api.StopwordsSetSchema{ + Id: stopwordsSetIDs[i], + Locale: upsertData.Locale, + Stopwords: []string{"states", "united", "france", "germany", "italy"}, + } + } + + for i := 0; i < total; i++ { + _, err := typesenseClient.Stopwords().Upsert(context.Background(), stopwordsSetIDs[i], upsertData) + require.NoError(t, err) + } + + result, err := typesenseClient.Stopwords().Retrieve(context.Background()) + + require.NoError(t, err) + require.True(t, len(result) >= total, "number of stopwordsSets is invalid") + + resultMap := map[string]api.StopwordsSetSchema{} + for _, stopwordsSet := range result { + resultMap[stopwordsSet.Id] = stopwordsSet + } + + for k, v := range expectedResult { + assert.Equal(t, v, resultMap[k]) + } +}