github.com/weaviate/weaviate@v1.24.6/adapters/clients/remote_index.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package clients
    13  
    14  import (
    15  	"bytes"
    16  	"context"
    17  	"encoding/base64"
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"net/url"
    23  
    24  	"github.com/go-openapi/strfmt"
    25  	"github.com/pkg/errors"
    26  	"github.com/weaviate/weaviate/adapters/handlers/rest/clusterapi"
    27  	"github.com/weaviate/weaviate/entities/additional"
    28  	"github.com/weaviate/weaviate/entities/aggregation"
    29  	"github.com/weaviate/weaviate/entities/filters"
    30  	"github.com/weaviate/weaviate/entities/search"
    31  	"github.com/weaviate/weaviate/entities/searchparams"
    32  	"github.com/weaviate/weaviate/entities/storobj"
    33  	"github.com/weaviate/weaviate/usecases/objects"
    34  	"github.com/weaviate/weaviate/usecases/scaler"
    35  )
    36  
    37  type RemoteIndex struct {
    38  	retryClient
    39  }
    40  
    41  func NewRemoteIndex(httpClient *http.Client) *RemoteIndex {
    42  	return &RemoteIndex{retryClient: retryClient{
    43  		client:  httpClient,
    44  		retryer: newRetryer(),
    45  	}}
    46  }
    47  
    48  func (c *RemoteIndex) PutObject(ctx context.Context, hostName, indexName,
    49  	shardName string, obj *storobj.Object,
    50  ) error {
    51  	path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
    52  	method := http.MethodPost
    53  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
    54  
    55  	marshalled, err := clusterapi.IndicesPayloads.SingleObject.Marshal(obj)
    56  	if err != nil {
    57  		return errors.Wrap(err, "marshal payload")
    58  	}
    59  
    60  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
    61  		bytes.NewReader(marshalled))
    62  	if err != nil {
    63  		return errors.Wrap(err, "open http request")
    64  	}
    65  
    66  	clusterapi.IndicesPayloads.SingleObject.SetContentTypeHeaderReq(req)
    67  	res, err := c.client.Do(req)
    68  	if err != nil {
    69  		return errors.Wrap(err, "send http request")
    70  	}
    71  
    72  	defer res.Body.Close()
    73  	if res.StatusCode != http.StatusNoContent {
    74  		body, _ := io.ReadAll(res.Body)
    75  		return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
    76  			body)
    77  	}
    78  
    79  	return nil
    80  }
    81  
    82  func duplicateErr(in error, count int) []error {
    83  	out := make([]error, count)
    84  	for i := range out {
    85  		out[i] = in
    86  	}
    87  	return out
    88  }
    89  
    90  func (c *RemoteIndex) BatchPutObjects(ctx context.Context, hostName, indexName,
    91  	shardName string, objs []*storobj.Object, _ *additional.ReplicationProperties,
    92  ) []error {
    93  	path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
    94  	method := http.MethodPost
    95  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
    96  
    97  	marshalled, err := clusterapi.IndicesPayloads.ObjectList.Marshal(objs)
    98  	if err != nil {
    99  		return duplicateErr(errors.Wrap(err, "marshal payload"), len(objs))
   100  	}
   101  
   102  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
   103  		bytes.NewReader(marshalled))
   104  	if err != nil {
   105  		return duplicateErr(errors.Wrap(err, "open http request"), len(objs))
   106  	}
   107  
   108  	clusterapi.IndicesPayloads.ObjectList.SetContentTypeHeaderReq(req)
   109  
   110  	res, err := c.client.Do(req)
   111  	if err != nil {
   112  		return duplicateErr(errors.Wrap(err, "send http request"), len(objs))
   113  	}
   114  
   115  	defer res.Body.Close()
   116  	if res.StatusCode != http.StatusOK {
   117  		body, _ := io.ReadAll(res.Body)
   118  		return duplicateErr(errors.Errorf("unexpected status code %d (%s)",
   119  			res.StatusCode, body), len(objs))
   120  	}
   121  
   122  	if ct, ok := clusterapi.IndicesPayloads.ErrorList.
   123  		CheckContentTypeHeader(res); !ok {
   124  		return duplicateErr(errors.Errorf("unexpected content type: %s",
   125  			ct), len(objs))
   126  	}
   127  
   128  	resBytes, err := io.ReadAll(res.Body)
   129  	if err != nil {
   130  		return duplicateErr(errors.Wrap(err, "ready body"), len(objs))
   131  	}
   132  
   133  	return clusterapi.IndicesPayloads.ErrorList.Unmarshal(resBytes)
   134  }
   135  
   136  func (c *RemoteIndex) BatchAddReferences(ctx context.Context, hostName, indexName,
   137  	shardName string, refs objects.BatchReferences,
   138  ) []error {
   139  	path := fmt.Sprintf("/indices/%s/shards/%s/references", indexName, shardName)
   140  	method := http.MethodPost
   141  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   142  
   143  	marshalled, err := clusterapi.IndicesPayloads.ReferenceList.Marshal(refs)
   144  	if err != nil {
   145  		return duplicateErr(errors.Wrap(err, "marshal payload"), len(refs))
   146  	}
   147  
   148  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
   149  		bytes.NewReader(marshalled))
   150  	if err != nil {
   151  		return duplicateErr(errors.Wrap(err, "open http request"), len(refs))
   152  	}
   153  
   154  	clusterapi.IndicesPayloads.ReferenceList.SetContentTypeHeaderReq(req)
   155  
   156  	res, err := c.client.Do(req)
   157  	if err != nil {
   158  		return duplicateErr(errors.Wrap(err, "send http request"), len(refs))
   159  	}
   160  
   161  	defer res.Body.Close()
   162  	if res.StatusCode != http.StatusOK {
   163  		body, _ := io.ReadAll(res.Body)
   164  		return duplicateErr(errors.Errorf("unexpected status code %d (%s)",
   165  			res.StatusCode, body), len(refs))
   166  	}
   167  
   168  	if ct, ok := clusterapi.IndicesPayloads.ErrorList.
   169  		CheckContentTypeHeader(res); !ok {
   170  		return duplicateErr(errors.Errorf("unexpected content type: %s",
   171  			ct), len(refs))
   172  	}
   173  
   174  	resBytes, err := io.ReadAll(res.Body)
   175  	if err != nil {
   176  		return duplicateErr(errors.Wrap(err, "ready body"), len(refs))
   177  	}
   178  
   179  	return clusterapi.IndicesPayloads.ErrorList.Unmarshal(resBytes)
   180  }
   181  
   182  func (c *RemoteIndex) GetObject(ctx context.Context, hostName, indexName,
   183  	shardName string, id strfmt.UUID, selectProps search.SelectProperties,
   184  	additional additional.Properties,
   185  ) (*storobj.Object, error) {
   186  	selectPropsBytes, err := json.Marshal(selectProps)
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, "marshal selectProps props")
   189  	}
   190  
   191  	additionalBytes, err := json.Marshal(additional)
   192  	if err != nil {
   193  		return nil, errors.Wrap(err, "marshal additional props")
   194  	}
   195  
   196  	selectPropsEncoded := base64.StdEncoding.EncodeToString(selectPropsBytes)
   197  	additionalEncoded := base64.StdEncoding.EncodeToString(additionalBytes)
   198  
   199  	path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
   200  	method := http.MethodGet
   201  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   202  	q := url.Query()
   203  	q.Set("additional", additionalEncoded)
   204  	q.Set("selectProperties", selectPropsEncoded)
   205  	url.RawQuery = q.Encode()
   206  
   207  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   208  	if err != nil {
   209  		return nil, errors.Wrap(err, "open http request")
   210  	}
   211  
   212  	res, err := c.client.Do(req)
   213  	if err != nil {
   214  		return nil, errors.Wrap(err, "send http request")
   215  	}
   216  
   217  	defer res.Body.Close()
   218  	if res.StatusCode == http.StatusNotFound {
   219  		// this is a legitimate case - the requested ID doesn't exist, don't try
   220  		// to unmarshal anything
   221  		return nil, nil
   222  	}
   223  
   224  	if res.StatusCode != http.StatusOK {
   225  		body, _ := io.ReadAll(res.Body)
   226  		return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   227  			body)
   228  	}
   229  
   230  	ct, ok := clusterapi.IndicesPayloads.SingleObject.CheckContentTypeHeader(res)
   231  	if !ok {
   232  		return nil, errors.Errorf("unknown content type %s", ct)
   233  	}
   234  
   235  	objBytes, err := io.ReadAll(res.Body)
   236  	if err != nil {
   237  		return nil, errors.Wrap(err, "read body")
   238  	}
   239  
   240  	obj, err := clusterapi.IndicesPayloads.SingleObject.Unmarshal(objBytes)
   241  	if err != nil {
   242  		return nil, errors.Wrap(err, "unmarshal body")
   243  	}
   244  
   245  	return obj, nil
   246  }
   247  
   248  func (c *RemoteIndex) Exists(ctx context.Context, hostName, indexName,
   249  	shardName string, id strfmt.UUID,
   250  ) (bool, error) {
   251  	path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
   252  	method := http.MethodGet
   253  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   254  	q := url.Query()
   255  	q.Set("check_exists", "true")
   256  	url.RawQuery = q.Encode()
   257  
   258  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   259  	if err != nil {
   260  		return false, errors.Wrap(err, "open http request")
   261  	}
   262  
   263  	res, err := c.client.Do(req)
   264  	if err != nil {
   265  		return false, errors.Wrap(err, "send http request")
   266  	}
   267  
   268  	defer res.Body.Close()
   269  	if res.StatusCode == http.StatusNotFound {
   270  		// this is a legitimate case - the requested ID doesn't exist, don't try
   271  		// to unmarshal anything
   272  		return false, nil
   273  	}
   274  
   275  	if res.StatusCode != http.StatusNoContent {
   276  		body, _ := io.ReadAll(res.Body)
   277  		return false, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   278  			body)
   279  	}
   280  
   281  	return true, nil
   282  }
   283  
   284  func (c *RemoteIndex) DeleteObject(ctx context.Context, hostName, indexName,
   285  	shardName string, id strfmt.UUID,
   286  ) error {
   287  	path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
   288  	method := http.MethodDelete
   289  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   290  
   291  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   292  	if err != nil {
   293  		return errors.Wrap(err, "open http request")
   294  	}
   295  
   296  	res, err := c.client.Do(req)
   297  	if err != nil {
   298  		return errors.Wrap(err, "send http request")
   299  	}
   300  
   301  	defer res.Body.Close()
   302  	if res.StatusCode == http.StatusNotFound {
   303  		// this is a legitimate case - the requested ID doesn't exist, don't try
   304  		// to unmarshal anything, we can assume it was already deleted
   305  		return nil
   306  	}
   307  
   308  	if res.StatusCode != http.StatusNoContent {
   309  		body, _ := io.ReadAll(res.Body)
   310  		return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   311  			body)
   312  	}
   313  
   314  	return nil
   315  }
   316  
   317  func (c *RemoteIndex) MergeObject(ctx context.Context, hostName, indexName,
   318  	shardName string, mergeDoc objects.MergeDocument,
   319  ) error {
   320  	path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName,
   321  		mergeDoc.ID)
   322  	method := http.MethodPatch
   323  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   324  
   325  	marshalled, err := clusterapi.IndicesPayloads.MergeDoc.Marshal(mergeDoc)
   326  	if err != nil {
   327  		return errors.Wrap(err, "marshal payload")
   328  	}
   329  
   330  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
   331  		bytes.NewReader(marshalled))
   332  	if err != nil {
   333  		return errors.Wrap(err, "open http request")
   334  	}
   335  
   336  	clusterapi.IndicesPayloads.MergeDoc.SetContentTypeHeaderReq(req)
   337  	res, err := c.client.Do(req)
   338  	if err != nil {
   339  		return errors.Wrap(err, "send http request")
   340  	}
   341  
   342  	defer res.Body.Close()
   343  	if res.StatusCode != http.StatusNoContent {
   344  		body, _ := io.ReadAll(res.Body)
   345  		return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   346  			body)
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  func (c *RemoteIndex) MultiGetObjects(ctx context.Context, hostName, indexName,
   353  	shardName string, ids []strfmt.UUID,
   354  ) ([]*storobj.Object, error) {
   355  	idsBytes, err := json.Marshal(ids)
   356  	if err != nil {
   357  		return nil, errors.Wrap(err, "marshal selectProps props")
   358  	}
   359  
   360  	idsEncoded := base64.StdEncoding.EncodeToString(idsBytes)
   361  
   362  	path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
   363  	method := http.MethodGet
   364  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   365  	q := url.Query()
   366  	q.Set("ids", idsEncoded)
   367  	url.RawQuery = q.Encode()
   368  
   369  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   370  	if err != nil {
   371  		return nil, errors.Wrap(err, "open http request")
   372  	}
   373  
   374  	res, err := c.client.Do(req)
   375  	if err != nil {
   376  		return nil, errors.Wrap(err, "send http request")
   377  	}
   378  
   379  	defer res.Body.Close()
   380  	if res.StatusCode == http.StatusNotFound {
   381  		// this is a legitimate case - the requested ID doesn't exist, don't try
   382  		// to unmarshal anything
   383  		return nil, nil
   384  	}
   385  
   386  	if res.StatusCode != http.StatusOK {
   387  		body, _ := io.ReadAll(res.Body)
   388  		return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   389  			body)
   390  	}
   391  
   392  	ct, ok := clusterapi.IndicesPayloads.ObjectList.CheckContentTypeHeader(res)
   393  	if !ok {
   394  		return nil, errors.Errorf("unexpected content type: %s", ct)
   395  	}
   396  
   397  	bodyBytes, err := io.ReadAll(res.Body)
   398  	if err != nil {
   399  		return nil, errors.Wrap(err, "read response body")
   400  	}
   401  
   402  	objs, err := clusterapi.IndicesPayloads.ObjectList.Unmarshal(bodyBytes)
   403  	if err != nil {
   404  		return nil, errors.Wrap(err, "unmarshal objects")
   405  	}
   406  
   407  	return objs, nil
   408  }
   409  
   410  func (c *RemoteIndex) SearchShard(ctx context.Context, host, index, shard string,
   411  	vector []float32,
   412  	targetVector string,
   413  	limit int,
   414  	filters *filters.LocalFilter,
   415  	keywordRanking *searchparams.KeywordRanking,
   416  	sort []filters.Sort,
   417  	cursor *filters.Cursor,
   418  	groupBy *searchparams.GroupBy,
   419  	additional additional.Properties,
   420  ) ([]*storobj.Object, []float32, error) {
   421  	// new request
   422  	body, err := clusterapi.IndicesPayloads.SearchParams.
   423  		Marshal(vector, targetVector, limit, filters, keywordRanking, sort, cursor, groupBy, additional)
   424  	if err != nil {
   425  		return nil, nil, fmt.Errorf("marshal request payload: %w", err)
   426  	}
   427  	url := url.URL{
   428  		Scheme: "http",
   429  		Host:   host,
   430  		Path:   fmt.Sprintf("/indices/%s/shards/%s/objects/_search", index, shard),
   431  	}
   432  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(body))
   433  	if err != nil {
   434  		return nil, nil, fmt.Errorf("create http request: %w", err)
   435  	}
   436  	clusterapi.IndicesPayloads.SearchParams.SetContentTypeHeaderReq(req)
   437  
   438  	// send request
   439  	resp := &searchShardResp{}
   440  	err = c.doWithCustomMarshaller(c.timeoutUnit*20, req, body, resp.decode)
   441  	return resp.Objects, resp.Distributions, err
   442  }
   443  
   444  type searchShardResp struct {
   445  	Objects       []*storobj.Object
   446  	Distributions []float32
   447  }
   448  
   449  func (r *searchShardResp) decode(data []byte) (err error) {
   450  	r.Objects, r.Distributions, err = clusterapi.IndicesPayloads.SearchResults.Unmarshal(data)
   451  	return
   452  }
   453  
   454  type aggregateResp struct {
   455  	Result *aggregation.Result
   456  }
   457  
   458  func (r *aggregateResp) decode(data []byte) (err error) {
   459  	r.Result, err = clusterapi.IndicesPayloads.AggregationResult.Unmarshal(data)
   460  	return
   461  }
   462  
   463  func (c *RemoteIndex) Aggregate(ctx context.Context, hostName, index,
   464  	shard string, params aggregation.Params,
   465  ) (*aggregation.Result, error) {
   466  	// create new request
   467  	body, err := clusterapi.IndicesPayloads.AggregationParams.Marshal(params)
   468  	if err != nil {
   469  		return nil, fmt.Errorf("marshal request payload: %w", err)
   470  	}
   471  
   472  	url := &url.URL{
   473  		Scheme: "http",
   474  		Host:   hostName,
   475  		Path:   fmt.Sprintf("/indices/%s/shards/%s/objects/_aggregations", index, shard),
   476  	}
   477  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(body))
   478  	if err != nil {
   479  		return nil, fmt.Errorf("create http request: %w", err)
   480  	}
   481  	clusterapi.IndicesPayloads.AggregationParams.SetContentTypeHeaderReq(req)
   482  
   483  	// send request
   484  	resp := &aggregateResp{}
   485  	err = c.doWithCustomMarshaller(c.timeoutUnit*20, req, body, resp.decode)
   486  	return resp.Result, err
   487  }
   488  
   489  func (c *RemoteIndex) FindUUIDs(ctx context.Context, hostName, indexName,
   490  	shardName string, filters *filters.LocalFilter,
   491  ) ([]strfmt.UUID, error) {
   492  	paramsBytes, err := clusterapi.IndicesPayloads.FindUUIDsParams.Marshal(filters)
   493  	if err != nil {
   494  		return nil, errors.Wrap(err, "marshal request payload")
   495  	}
   496  
   497  	path := fmt.Sprintf("/indices/%s/shards/%s/objects/_find", indexName, shardName)
   498  	method := http.MethodPost
   499  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   500  
   501  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
   502  		bytes.NewReader(paramsBytes))
   503  	if err != nil {
   504  		return nil, errors.Wrap(err, "open http request")
   505  	}
   506  
   507  	clusterapi.IndicesPayloads.FindUUIDsParams.SetContentTypeHeaderReq(req)
   508  	res, err := c.client.Do(req)
   509  	if err != nil {
   510  		return nil, errors.Wrap(err, "send http request")
   511  	}
   512  
   513  	defer res.Body.Close()
   514  	if res.StatusCode != http.StatusOK {
   515  		body, _ := io.ReadAll(res.Body)
   516  		return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
   517  			body)
   518  	}
   519  
   520  	resBytes, err := io.ReadAll(res.Body)
   521  	if err != nil {
   522  		return nil, errors.Wrap(err, "read body")
   523  	}
   524  
   525  	ct, ok := clusterapi.IndicesPayloads.FindUUIDsResults.CheckContentTypeHeader(res)
   526  	if !ok {
   527  		return nil, errors.Errorf("unexpected content type: %s", ct)
   528  	}
   529  
   530  	uuids, err := clusterapi.IndicesPayloads.FindUUIDsResults.Unmarshal(resBytes)
   531  	if err != nil {
   532  		return nil, errors.Wrap(err, "unmarshal body")
   533  	}
   534  	return uuids, nil
   535  }
   536  
   537  func (c *RemoteIndex) DeleteObjectBatch(ctx context.Context, hostName, indexName, shardName string,
   538  	uuids []strfmt.UUID, dryRun bool,
   539  ) objects.BatchSimpleObjects {
   540  	path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
   541  	method := http.MethodDelete
   542  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   543  
   544  	marshalled, err := clusterapi.IndicesPayloads.BatchDeleteParams.Marshal(uuids, dryRun)
   545  	if err != nil {
   546  		err := errors.Wrap(err, "marshal payload")
   547  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   548  	}
   549  
   550  	req, err := http.NewRequestWithContext(ctx, method, url.String(),
   551  		bytes.NewReader(marshalled))
   552  	if err != nil {
   553  		err := errors.Wrap(err, "open http request")
   554  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   555  	}
   556  
   557  	clusterapi.IndicesPayloads.BatchDeleteParams.SetContentTypeHeaderReq(req)
   558  
   559  	res, err := c.client.Do(req)
   560  	if err != nil {
   561  		err := errors.Wrap(err, "send http request")
   562  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   563  	}
   564  
   565  	defer res.Body.Close()
   566  	if res.StatusCode != http.StatusOK {
   567  		body, _ := io.ReadAll(res.Body)
   568  		err := errors.Errorf("unexpected status code %d (%s)", res.StatusCode, body)
   569  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   570  	}
   571  
   572  	if ct, ok := clusterapi.IndicesPayloads.BatchDeleteResults.
   573  		CheckContentTypeHeader(res); !ok {
   574  		err := errors.Errorf("unexpected content type: %s", ct)
   575  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   576  	}
   577  
   578  	resBytes, err := io.ReadAll(res.Body)
   579  	if err != nil {
   580  		err := errors.Wrap(err, "ready body")
   581  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   582  	}
   583  
   584  	batchDeleteResults, err := clusterapi.IndicesPayloads.BatchDeleteResults.Unmarshal(resBytes)
   585  	if err != nil {
   586  		err := errors.Wrap(err, "unmarshal body")
   587  		return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
   588  	}
   589  
   590  	return batchDeleteResults
   591  }
   592  
   593  func (c *RemoteIndex) GetShardQueueSize(ctx context.Context,
   594  	hostName, indexName, shardName string,
   595  ) (int64, error) {
   596  	path := fmt.Sprintf("/indices/%s/shards/%s/queuesize", indexName, shardName)
   597  	method := http.MethodGet
   598  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   599  
   600  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   601  	if err != nil {
   602  		return 0, errors.Wrap(err, "open http request")
   603  	}
   604  	var size int64
   605  	clusterapi.IndicesPayloads.GetShardQueueSizeParams.SetContentTypeHeaderReq(req)
   606  	try := func(ctx context.Context) (bool, error) {
   607  		res, err := c.client.Do(req)
   608  		if err != nil {
   609  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   610  		}
   611  		defer res.Body.Close()
   612  
   613  		if code := res.StatusCode; code != http.StatusOK {
   614  			body, _ := io.ReadAll(res.Body)
   615  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   616  		}
   617  		resBytes, err := io.ReadAll(res.Body)
   618  		if err != nil {
   619  			return false, errors.Wrap(err, "read body")
   620  		}
   621  
   622  		ct, ok := clusterapi.IndicesPayloads.GetShardQueueSizeResults.CheckContentTypeHeader(res)
   623  		if !ok {
   624  			return false, errors.Errorf("unexpected content type: %s", ct)
   625  		}
   626  
   627  		size, err = clusterapi.IndicesPayloads.GetShardQueueSizeResults.Unmarshal(resBytes)
   628  		if err != nil {
   629  			return false, errors.Wrap(err, "unmarshal body")
   630  		}
   631  		return false, nil
   632  	}
   633  	return size, c.retry(ctx, 9, try)
   634  }
   635  
   636  func (c *RemoteIndex) GetShardStatus(ctx context.Context,
   637  	hostName, indexName, shardName string,
   638  ) (string, error) {
   639  	path := fmt.Sprintf("/indices/%s/shards/%s/status", indexName, shardName)
   640  	method := http.MethodGet
   641  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   642  
   643  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   644  	if err != nil {
   645  		return "", errors.Wrap(err, "open http request")
   646  	}
   647  	var status string
   648  	clusterapi.IndicesPayloads.GetShardStatusParams.SetContentTypeHeaderReq(req)
   649  	try := func(ctx context.Context) (bool, error) {
   650  		res, err := c.client.Do(req)
   651  		if err != nil {
   652  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   653  		}
   654  		defer res.Body.Close()
   655  
   656  		if code := res.StatusCode; code != http.StatusOK {
   657  			body, _ := io.ReadAll(res.Body)
   658  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   659  		}
   660  		resBytes, err := io.ReadAll(res.Body)
   661  		if err != nil {
   662  			return false, errors.Wrap(err, "read body")
   663  		}
   664  
   665  		ct, ok := clusterapi.IndicesPayloads.GetShardStatusResults.CheckContentTypeHeader(res)
   666  		if !ok {
   667  			return false, errors.Errorf("unexpected content type: %s", ct)
   668  		}
   669  
   670  		status, err = clusterapi.IndicesPayloads.GetShardStatusResults.Unmarshal(resBytes)
   671  		if err != nil {
   672  			return false, errors.Wrap(err, "unmarshal body")
   673  		}
   674  		return false, nil
   675  	}
   676  	return status, c.retry(ctx, 9, try)
   677  }
   678  
   679  func (c *RemoteIndex) UpdateShardStatus(ctx context.Context, hostName, indexName, shardName,
   680  	targetStatus string,
   681  ) error {
   682  	paramsBytes, err := clusterapi.IndicesPayloads.UpdateShardStatusParams.Marshal(targetStatus)
   683  	if err != nil {
   684  		return errors.Wrap(err, "marshal request payload")
   685  	}
   686  	path := fmt.Sprintf("/indices/%s/shards/%s/status", indexName, shardName)
   687  	method := http.MethodPost
   688  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   689  
   690  	try := func(ctx context.Context) (bool, error) {
   691  		req, err := http.NewRequestWithContext(ctx, method, url.String(),
   692  			bytes.NewReader(paramsBytes))
   693  		if err != nil {
   694  			return false, fmt.Errorf("create http request: %w", err)
   695  		}
   696  		clusterapi.IndicesPayloads.UpdateShardStatusParams.SetContentTypeHeaderReq(req)
   697  
   698  		res, err := c.client.Do(req)
   699  		if err != nil {
   700  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   701  		}
   702  		defer res.Body.Close()
   703  
   704  		if code := res.StatusCode; code != http.StatusOK {
   705  			body, _ := io.ReadAll(res.Body)
   706  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   707  		}
   708  
   709  		return false, nil
   710  	}
   711  
   712  	return c.retry(ctx, 9, try)
   713  }
   714  
   715  func (c *RemoteIndex) PutFile(ctx context.Context, hostName, indexName,
   716  	shardName, fileName string, payload io.ReadSeekCloser,
   717  ) error {
   718  	defer payload.Close()
   719  	path := fmt.Sprintf("/indices/%s/shards/%s/files/%s",
   720  		indexName, shardName, fileName)
   721  
   722  	method := http.MethodPost
   723  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   724  	try := func(ctx context.Context) (bool, error) {
   725  		req, err := http.NewRequestWithContext(ctx, method, url.String(), payload)
   726  		if err != nil {
   727  			return false, fmt.Errorf("create http request: %w", err)
   728  		}
   729  		clusterapi.IndicesPayloads.ShardFiles.SetContentTypeHeaderReq(req)
   730  		res, err := c.client.Do(req)
   731  		if err != nil {
   732  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   733  		}
   734  		defer res.Body.Close()
   735  
   736  		if code := res.StatusCode; code != http.StatusNoContent {
   737  			shouldRetry := shouldRetry(code)
   738  			if shouldRetry {
   739  				_, err := payload.Seek(0, 0)
   740  				shouldRetry = (err == nil)
   741  			}
   742  			body, _ := io.ReadAll(res.Body)
   743  			return shouldRetry, fmt.Errorf("status code: %v body: (%s)", code, body)
   744  		}
   745  		return false, nil
   746  	}
   747  
   748  	return c.retry(ctx, 12, try)
   749  }
   750  
   751  func (c *RemoteIndex) CreateShard(ctx context.Context,
   752  	hostName, indexName, shardName string,
   753  ) error {
   754  	path := fmt.Sprintf("/indices/%s/shards/%s", indexName, shardName)
   755  
   756  	method := http.MethodPost
   757  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   758  
   759  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   760  	if err != nil {
   761  		return fmt.Errorf("create http request: %w", err)
   762  	}
   763  	try := func(ctx context.Context) (bool, error) {
   764  		res, err := c.client.Do(req)
   765  		if err != nil {
   766  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   767  		}
   768  		defer res.Body.Close()
   769  
   770  		if code := res.StatusCode; code != http.StatusCreated {
   771  			body, _ := io.ReadAll(res.Body)
   772  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   773  		}
   774  		return false, nil
   775  	}
   776  
   777  	return c.retry(ctx, 9, try)
   778  }
   779  
   780  func (c *RemoteIndex) ReInitShard(ctx context.Context,
   781  	hostName, indexName, shardName string,
   782  ) error {
   783  	path := fmt.Sprintf("/indices/%s/shards/%s:reinit", indexName, shardName)
   784  
   785  	method := http.MethodPut
   786  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   787  
   788  	req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
   789  	if err != nil {
   790  		return fmt.Errorf("create http request: %w", err)
   791  	}
   792  	try := func(ctx context.Context) (bool, error) {
   793  		res, err := c.client.Do(req)
   794  		if err != nil {
   795  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   796  		}
   797  		defer res.Body.Close()
   798  
   799  		if code := res.StatusCode; code != http.StatusNoContent {
   800  			body, _ := io.ReadAll(res.Body)
   801  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   802  
   803  		}
   804  		return false, nil
   805  	}
   806  
   807  	return c.retry(ctx, 9, try)
   808  }
   809  
   810  func (c *RemoteIndex) IncreaseReplicationFactor(ctx context.Context,
   811  	hostName, indexName string, dist scaler.ShardDist,
   812  ) error {
   813  	path := fmt.Sprintf("/replicas/indices/%s/replication-factor:increase", indexName)
   814  
   815  	method := http.MethodPut
   816  	url := url.URL{Scheme: "http", Host: hostName, Path: path}
   817  
   818  	body, err := clusterapi.IndicesPayloads.IncreaseReplicationFactor.Marshall(dist)
   819  	if err != nil {
   820  		return err
   821  	}
   822  	try := func(ctx context.Context) (bool, error) {
   823  		req, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewReader(body))
   824  		if err != nil {
   825  			return false, fmt.Errorf("create http request: %w", err)
   826  		}
   827  
   828  		res, err := c.client.Do(req)
   829  		if err != nil {
   830  			return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
   831  		}
   832  		defer res.Body.Close()
   833  
   834  		if code := res.StatusCode; code != http.StatusNoContent {
   835  			body, _ := io.ReadAll(res.Body)
   836  			return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
   837  		}
   838  		return false, nil
   839  	}
   840  	return c.retry(ctx, 34, try)
   841  }