package gocb import ( "encoding/json" "fmt" "io/ioutil" "strings" "time" "github.com/pkg/errors" ) type jsonSearchIndexResp struct { Status string `json:"status"` IndexDef *jsonSearchIndex `json:"indexDef"` } type jsonSearchIndexDefs struct { IndexDefs map[string]jsonSearchIndex `json:"indexDefs"` ImplVersion string `json:"implVersion"` } type jsonSearchIndexesResp struct { Status string `json:"status"` IndexDefs jsonSearchIndexDefs `json:"indexDefs"` } type jsonSearchIndex struct { UUID string `json:"uuid"` Name string `json:"name"` SourceName string `json:"sourceName"` Type string `json:"type"` Params map[string]interface{} `json:"params"` SourceUUID string `json:"sourceUUID"` SourceParams map[string]interface{} `json:"sourceParams"` SourceType string `json:"sourceType"` PlanParams map[string]interface{} `json:"planParams"` } // SearchIndex is used to define a search index. type SearchIndex struct { // UUID is required for updates. It provides a means of ensuring consistency, the UUID must match the UUID value // for the index on the server. UUID string // Name represents the name of this index. Name string // SourceName is the name of the source of the data for the index e.g. bucket name. SourceName string // Type is the type of index, e.g. fulltext-index or fulltext-alias. Type string // IndexParams are index properties such as store type and mappings. Params map[string]interface{} // SourceUUID is the UUID of the data source, this can be used to more tightly tie the index to a source. SourceUUID string // SourceParams are extra parameters to be defined. These are usually things like advanced connection and tuning // parameters. SourceParams map[string]interface{} // SourceType is the type of the data source, e.g. couchbase or nil depending on the Type field. SourceType string // PlanParams are plan properties such as number of replicas and number of partitions. PlanParams map[string]interface{} } func (si *SearchIndex) fromData(data jsonSearchIndex) error { si.UUID = data.UUID si.Name = data.Name si.SourceName = data.SourceName si.Type = data.Type si.Params = data.Params si.SourceUUID = data.SourceUUID si.SourceParams = data.SourceParams si.SourceType = data.SourceType si.PlanParams = data.PlanParams return nil } func (si *SearchIndex) toData() (jsonSearchIndex, error) { var data jsonSearchIndex data.UUID = si.UUID data.Name = si.Name data.SourceName = si.SourceName data.Type = si.Type data.Params = si.Params data.SourceUUID = si.SourceUUID data.SourceParams = si.SourceParams data.SourceType = si.SourceType data.PlanParams = si.PlanParams return data, nil } // SearchIndexManager provides methods for performing Couchbase search index management. type SearchIndexManager struct { mgmtProvider mgmtProvider tracer requestTracer } func (sm *SearchIndexManager) tryParseErrorMessage(req *mgmtRequest, resp *mgmtResponse) error { b, err := ioutil.ReadAll(resp.Body) if err != nil { logDebugf("Failed to read search index response body: %s", err) return nil } var bodyErr error if strings.Contains(strings.ToLower(string(b)), "index not found") { bodyErr = ErrIndexNotFound } else if strings.Contains(strings.ToLower(string(b)), "index with the same name already exists") { bodyErr = ErrIndexExists } else { bodyErr = errors.New(string(b)) } return makeGenericMgmtError(bodyErr, req, resp) } func (sm *SearchIndexManager) doMgmtRequest(req mgmtRequest) (*mgmtResponse, error) { resp, err := sm.mgmtProvider.executeMgmtRequest(req) if err != nil { return nil, err } return resp, nil } // GetAllSearchIndexOptions is the set of options available to the search indexes GetAllIndexes operation. type GetAllSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // GetAllIndexes retrieves all of the search indexes for the cluster. func (sm *SearchIndexManager) GetAllIndexes(opts *GetAllSearchIndexOptions) ([]SearchIndex, error) { if opts == nil { opts = &GetAllSearchIndexOptions{} } span := sm.tracer.StartSpan("GetAllIndexes", nil). SetTag("couchbase.service", "search") defer span.Finish() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: "/api/index", IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to get index", &req, resp) } var indexesResp jsonSearchIndexesResp jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&indexesResp) if err != nil { return nil, err } indexDefs := indexesResp.IndexDefs.IndexDefs var indexes []SearchIndex for _, indexData := range indexDefs { var index SearchIndex err := index.fromData(indexData) if err != nil { return nil, err } indexes = append(indexes, index) } return indexes, nil } // GetSearchIndexOptions is the set of options available to the search indexes GetIndex operation. type GetSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // GetIndex retrieves a specific search index by name. func (sm *SearchIndexManager) GetIndex(indexName string, opts *GetSearchIndexOptions) (*SearchIndex, error) { if opts == nil { opts = &GetSearchIndexOptions{} } span := sm.tracer.StartSpan("GetIndex", nil). SetTag("couchbase.service", "search") defer span.Finish() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: fmt.Sprintf("/api/index/%s", indexName), IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to get index", &req, resp) } var indexResp jsonSearchIndexResp jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&indexResp) if err != nil { return nil, err } var indexDef SearchIndex err = indexDef.fromData(*indexResp.IndexDef) if err != nil { return nil, err } return &indexDef, nil } // UpsertSearchIndexOptions is the set of options available to the search index manager UpsertIndex operation. type UpsertSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // UpsertIndex creates or updates a search index. func (sm *SearchIndexManager) UpsertIndex(indexDefinition SearchIndex, opts *UpsertSearchIndexOptions) error { if opts == nil { opts = &UpsertSearchIndexOptions{} } if indexDefinition.Name == "" { return invalidArgumentsError{"index name cannot be empty"} } if indexDefinition.Type == "" { return invalidArgumentsError{"index type cannot be empty"} } span := sm.tracer.StartSpan("UpsertIndex", nil). SetTag("couchbase.service", "search") defer span.Finish() indexData, err := indexDefinition.toData() if err != nil { return err } b, err := json.Marshal(indexData) if err != nil { return err } req := mgmtRequest{ Service: ServiceTypeSearch, Method: "PUT", Path: fmt.Sprintf("/api/index/%s", indexDefinition.Name), Headers: map[string]string{ "cache-control": "no-cache", }, Body: b, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return idxErr } return makeMgmtBadStatusError("failed to create index", &req, resp) } return nil } // DropSearchIndexOptions is the set of options available to the search index DropIndex operation. type DropSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // DropIndex removes the search index with the specific name. func (sm *SearchIndexManager) DropIndex(indexName string, opts *DropSearchIndexOptions) error { if opts == nil { opts = &DropSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("DropIndex", nil). SetTag("couchbase.service", "search") defer span.Finish() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "DELETE", Path: fmt.Sprintf("/api/index/%s", indexName), RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { return makeMgmtBadStatusError("failed to drop the index", &req, resp) } return nil } // AnalyzeDocumentOptions is the set of options available to the search index AnalyzeDocument operation. type AnalyzeDocumentOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // AnalyzeDocument returns how a doc is analyzed against a specific index. func (sm *SearchIndexManager) AnalyzeDocument(indexName string, doc interface{}, opts *AnalyzeDocumentOptions) ([]interface{}, error) { if opts == nil { opts = &AnalyzeDocumentOptions{} } if indexName == "" { return nil, invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("AnalyzeDocument", nil). SetTag("couchbase.service", "search") defer span.Finish() b, err := json.Marshal(doc) if err != nil { return nil, err } req := mgmtRequest{ Service: ServiceTypeSearch, Method: "POST", Path: fmt.Sprintf("/api/index/%s/analyzeDoc", indexName), Body: b, IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return nil, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return nil, idxErr } return nil, makeMgmtBadStatusError("failed to analyze document", &req, resp) } var analysis struct { Status string `json:"status"` Analyzed []interface{} `json:"analyzed"` } jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&analysis) if err != nil { return nil, err } return analysis.Analyzed, nil } // GetIndexedDocumentsCountOptions is the set of options available to the search index GetIndexedDocumentsCount operation. type GetIndexedDocumentsCountOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // GetIndexedDocumentsCount retrieves the document count for a search index. func (sm *SearchIndexManager) GetIndexedDocumentsCount(indexName string, opts *GetIndexedDocumentsCountOptions) (uint64, error) { if opts == nil { opts = &GetIndexedDocumentsCountOptions{} } if indexName == "" { return 0, invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("GetIndexedDocumentsCount", nil). SetTag("couchbase.service", "search") defer span.Finish() req := mgmtRequest{ Service: ServiceTypeSearch, Method: "GET", Path: fmt.Sprintf("/api/index/%s/count", indexName), IsIdempotent: true, RetryStrategy: opts.RetryStrategy, Timeout: opts.Timeout, parentSpan: span.Context(), } resp, err := sm.doMgmtRequest(req) if err != nil { return 0, err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return 0, idxErr } return 0, makeMgmtBadStatusError("failed to get the indexed documents count", &req, resp) } var count struct { Count uint64 `json:"count"` } jsonDec := json.NewDecoder(resp.Body) err = jsonDec.Decode(&count) if err != nil { return 0, err } return count.Count, nil } func (sm *SearchIndexManager) performControlRequest( tracectx requestSpanContext, method, uri string, timeout time.Duration, retryStrategy RetryStrategy, ) error { req := mgmtRequest{ Service: ServiceTypeSearch, Method: method, Path: uri, IsIdempotent: true, Timeout: timeout, RetryStrategy: retryStrategy, parentSpan: tracectx, } resp, err := sm.doMgmtRequest(req) if err != nil { return err } defer ensureBodyClosed(resp.Body) if resp.StatusCode != 200 { idxErr := sm.tryParseErrorMessage(&req, resp) if idxErr != nil { return idxErr } return makeMgmtBadStatusError("failed to perform the control request", &req, resp) } return nil } // PauseIngestSearchIndexOptions is the set of options available to the search index PauseIngest operation. type PauseIngestSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // PauseIngest pauses updates and maintenance for an index. func (sm *SearchIndexManager) PauseIngest(indexName string, opts *PauseIngestSearchIndexOptions) error { if opts == nil { opts = &PauseIngestSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("PauseIngest", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/ingestControl/pause", indexName), opts.Timeout, opts.RetryStrategy) } // ResumeIngestSearchIndexOptions is the set of options available to the search index ResumeIngest operation. type ResumeIngestSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // ResumeIngest resumes updates and maintenance for an index. func (sm *SearchIndexManager) ResumeIngest(indexName string, opts *ResumeIngestSearchIndexOptions) error { if opts == nil { opts = &ResumeIngestSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("ResumeIngest", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/ingestControl/resume", indexName), opts.Timeout, opts.RetryStrategy) } // AllowQueryingSearchIndexOptions is the set of options available to the search index AllowQuerying operation. type AllowQueryingSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // AllowQuerying allows querying against an index. func (sm *SearchIndexManager) AllowQuerying(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("AllowQuerying", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/queryControl/allow", indexName), opts.Timeout, opts.RetryStrategy) } // DisallowQueryingSearchIndexOptions is the set of options available to the search index DisallowQuerying operation. type DisallowQueryingSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // DisallowQuerying disallows querying against an index. func (sm *SearchIndexManager) DisallowQuerying(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("DisallowQuerying", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/queryControl/disallow", indexName), opts.Timeout, opts.RetryStrategy) } // FreezePlanSearchIndexOptions is the set of options available to the search index FreezePlan operation. type FreezePlanSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // FreezePlan freezes the assignment of index partitions to nodes. func (sm *SearchIndexManager) FreezePlan(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("FreezePlan", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/planFreezeControl/freeze", indexName), opts.Timeout, opts.RetryStrategy) } // UnfreezePlanSearchIndexOptions is the set of options available to the search index UnfreezePlan operation. type UnfreezePlanSearchIndexOptions struct { Timeout time.Duration RetryStrategy RetryStrategy } // UnfreezePlan unfreezes the assignment of index partitions to nodes. func (sm *SearchIndexManager) UnfreezePlan(indexName string, opts *AllowQueryingSearchIndexOptions) error { if opts == nil { opts = &AllowQueryingSearchIndexOptions{} } if indexName == "" { return invalidArgumentsError{"indexName cannot be empty"} } span := sm.tracer.StartSpan("UnfreezePlan", nil). SetTag("couchbase.service", "search") defer span.Finish() return sm.performControlRequest( span.Context(), "POST", fmt.Sprintf("/api/index/%s/planFreezeControl/unfreeze", indexName), opts.Timeout, opts.RetryStrategy) }