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.
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
}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 |
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)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.
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))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
}