Skip to content

Latest commit

 

History

History
231 lines (191 loc) · 6.75 KB

File metadata and controls

231 lines (191 loc) · 6.75 KB

Making Raw JSON REST Requests

Runnable example: _samples/usage-json.go

The OpenSearch client implements many high-level REST DSLs that invoke OpenSearch APIs. However you may find yourself in a situation that requires you to invoke an API that is not supported by the client. Use client.Perform to do so.

Setup

Let's create a client instance:

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/opensearch-project/opensearch-go/v5"
	"github.com/opensearch-project/opensearch-go/v5/opensearchapi"
)

func main() {
	if err := example(); err != nil {
		fmt.Println(fmt.Sprintf("Error: %s", err))
		os.Exit(1)
	}
}

func example() error {
	client, err := opensearchapi.NewDefaultClient()
	if err != nil {
		return err
	}

Using Execute for Typed Responses

When you need to call an API that opensearchapi doesn't cover -- plugin endpoints, newly released server APIs, or internal custom endpoints -- use opensearch.Execute[T]() to execute a request and automatically unmarshal the JSON response into a struct.

opensearch.Execute[T]() is generic: the type parameter enforces that the response destination is a pointer at compile time, so passing a non-pointer does not compile. Pass a nil *T (for example (*opensearch.NoBody)(nil)) to skip unmarshaling.

First, define a request type that satisfies opensearch.Request:

	// customReq builds an *http.Request from a path with a leading slash (e.g.
	// "/_plugins/my_plugin/status") to satisfy the opensearch.Request interface.
	// The transport prepends the base URL. method is an HTTP method
	// (e.g. http.MethodGet) forwarded by the caller.
	type customReq struct {
		path string
		body io.Reader
	}

	func (r customReq) GetRequest(method string) (*http.Request, error) {
		req, err := http.NewRequest(method, r.path, r.body)
		if err != nil {
			return nil, err
		}
		// opensearch.BuildRequest set this automatically for a non-nil body;
		// http.NewRequest does not, so set it here or OpenSearch may reject a
		// JSON body with 400/415.
		if r.body != nil {
			req.Header.Set("Content-Type", "application/json")
		}
		return req, nil
	}

Then use opensearch.Execute to call the endpoint with a typed response:

	type PluginStatusResp struct {
		Status  string `json:"status"`
		Version string `json:"version"`
	}

	ctx := context.Background()

	// opensearch.Execute[T] enforces *T at compile time.
	var pluginStatus PluginStatusResp
	req := customReq{path: "/_plugins/my_plugin/status"}
	resp, err := opensearch.Execute(ctx, client.Client, http.MethodGet, req, &pluginStatus)
	if err != nil {
		return err
	}
	fmt.Printf("plugin status: %s (v%s), http: %d\n", pluginStatus.Status, pluginStatus.Version, resp.StatusCode)

If you pass a non-pointer value to opensearch.Execute, the compiler rejects it:

	// Compile error: cannot use pluginStatus (variable of type PluginStatusResp)
	// as *PluginStatusResp value in argument to opensearch.Execute
	resp, err := opensearch.Execute(ctx, client.Client, http.MethodGet, req, pluginStatus)

The three levels of the client API, from lowest to highest:

Level Function Response handling When to use
Low client.Stream(req) Raw *http.Response; caller reads and closes body Streaming, incremental forwarding, full control needed
Mid opensearch.Execute(ctx, client, method, req, &resp) Automatic JSON unmarshal with compile-time pointer safety Plugin APIs, unsupported endpoints, custom Request types
High client.Search(ctx, req) / client.Indices.Create(ctx, req) etc. Fully typed request and response Standard OpenSearch APIs

GET

The following example returns the server version information via GET /.

	infoRequest, err := http.NewRequest("GET", "/", nil)
	if err != nil {
		return err
	}

	infoResponse, err := client.Request(infoRequest)
	if err != nil {
		return err
	}

	resBody, err := io.ReadAll(infoResponse.Body)
	if err != nil {
		return err
	}
	fmt.Printf("client info: %s\n", resBody)

PUT

The following example creates an index.

	var index_body = strings.NewReader(`{
    "settings": {
        "index": {
            "number_of_shards": 2,
            "number_of_replicas": 1
        }
    },
    "mappings": {
        "properties": {
            "title": {
                "type": "text"
            },
            "year": {
                "type": "integer"
            }
        }
    }
}`)

	createIndexRequest, err := http.NewRequest("PUT", "/movies", index_body)
	if err != nil {
		return err
	}
	createIndexRequest.Header["Content-Type"] = []string{"application/json"}
	createIndexResp, err := client.Request(createIndexRequest)
	if err != nil {
		return err
	}
	createIndexRespBody, err := io.ReadAll(createIndexResp.Body)
	if err != nil {
		return err
	}
	fmt.Println("create index: ", string(createIndexRespBody))

Note that the client will raise errors automatically. For example, if the index already exists, an error containing resource_already_exists_exception root cause will be thrown.

POST

The following example searches for a document.

	query := strings.NewReader(`{
    "size": 5,
    "query": {
        "multi_match": {
        "query": "miller",
        "fields": ["title^2", "director"]
        }
    }
}`)
	searchRequest, err := http.NewRequest("POST", "/movies/_search", query)
	if err != nil {
		return err
	}
	searchRequest.Header["Content-Type"] = []string{"application/json"}
	searchResp, err := client.Request(searchRequest)
	if err != nil {
		return err
	}
	searchRespBody, err := io.ReadAll(searchResp.Body)
	if err != nil {
		return err
	}
	fmt.Println("search: ", string(searchRespBody))

DELETE

The following example deletes an index.

	deleteIndexRequest, err := http.NewRequest("DELETE", "/movies", nil)
	if err != nil {
		return err
	}
	deleteIndexResp, err := client.Request(deleteIndexRequest)
	if err != nil {
		return err
	}
	deleteIndexRespBody, err := io.ReadAll(deleteIndexResp.Body)
	if err != nil {
		return err
	}
	fmt.Println("delete index: ", string(deleteIndexRespBody))
	return nil
}