In the example below, we create a client, an index with non-default settings, insert a document to the index, search for the document, delete the document and finally delete the index.
package main
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/opensearch-project/opensearch-go/v5"
"github.com/opensearch-project/opensearch-go/v5/errmask"
"github.com/opensearch-project/opensearch-go/v5/opensearchapi"
"github.com/opensearch-project/opensearch-go/v5/opensearchtransport"
"github.com/opensearch-project/opensearch-go/v5/opensearchutil"
)
const IndexName = "go-test-index1"
func main() {
if err := example(); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
func example() error {
// Initialize the client with SSL/TLS enabled.
router, err := opensearchtransport.NewDefaultRouter()
if err != nil {
return err
}
// Surface partial failures (bulk item errors, shard failures) as Go
// errors. See guides/usage-error_handling.md for the per-category bitmask.
errMask := errmask.Empty
discoverOnStart := true
client, err := opensearchapi.NewClient(
opensearchapi.Config{
Client: opensearch.Config{
InsecureSkipVerify: true, // For testing only. Use certificate for validation.
Addresses: []string{"https://localhost:9200"},
Username: "admin", // For testing only. Don't store credentials in code.
Password: "myStrongPassword123!",
// Optional: Enable node discovery
DiscoverNodesOnStart: &discoverOnStart,
DiscoverNodesInterval: 5 * time.Minute,
// Optional: Enable intelligent request routing
Router: router,
},
// Optional: Surface partial failures (bulk item errors, shard failures)
// as Go errors. See guides/usage-error_handling.md for details.
Errors: &errMask,
},
)
if err != nil {
return err
}
ctx := context.Background()
// Print OpenSearch version information on console.
infoResp, err := client.Info(ctx, nil)
if err != nil {
return err
}
fmt.Printf("Cluster INFO:\n Cluster Name: %s\n Cluster UUID: %s\n Version Number: %s\n", infoResp.ClusterName, infoResp.ClusterUUID, infoResp.Version.Number)
// Define index mapping.
// Note: these particular settings (eg, shards/replicas)
// will have no effect in AWS OpenSearch Serverless
mapping := strings.NewReader(`{
"settings": {
"index": {
"number_of_shards": 4
}
}
}`)
// Create an index with non-default settings.
createIndexResponse, err := client.Indices.Create(
ctx,
opensearchapi.IndicesCreateReq{
Index: IndexName,
BodyReader: mapping,
},
)
var opensearchError *opensearch.StructError
// Load err into opensearch.StructError to access the fields and tolerate if the index already exists
if err != nil {
if errors.As(err, &opensearchError) {
if opensearchError.Err.Type != "resource_already_exists_exception" {
return err
}
} else {
return err
}
}
fmt.Printf("Created Index: %s\n Shards Acknowledged: %t\n", createIndexResponse.Index, createIndexResponse.ShardsAcknowledged)
// When using a structure, the conversion process to io.Reader can be omitted using utility functions.
document := struct {
Title string `json:"title"`
Director string `json:"director"`
Year string `json:"year"`
}{
Title: "Moneyball",
Director: "Bennett Miller",
Year: "2011",
}
docId := "1"
insertResp, err := client.Doc.Index(
ctx,
opensearchapi.IndexReq{
Index: IndexName,
ID: docId,
Body: opensearchutil.NewJSONReader(&document),
Params: &opensearchapi.IndexParams{
Refresh: "true",
},
},
)
if err != nil {
return err
}
fmt.Printf("Created document in %s\n ID: %s\n", insertResp.Index, insertResp.ID)
// Search for the document.
content := strings.NewReader(`{
"size": 5,
"query": {
"multi_match": {
"query": "miller",
"fields": ["title^2", "director"]
}
}
}`)
searchResp, err := client.Search(
ctx,
&opensearchapi.SearchReq{
BodyReader: content,
},
)
if err != nil {
return err
}
fmt.Printf("Search hits: %v\n", searchResp.Hits.Total.Value)
if searchResp.Hits.Total.Value > 0 {
indices := make([]string, 0)
for _, hit := range searchResp.Hits.Hits {
add := true
for _, index := range indices {
if index == hit.Index {
add = false
}
}
if add {
indices = append(indices, hit.Index)
}
}
fmt.Printf("Search indices: %s\n", strings.Join(indices, ","))
}
// Delete the document.
deleteReq := opensearchapi.DeleteReq{
Index: IndexName,
ID: docId,
}
deleteResponse, err := client.Doc.Delete(ctx, deleteReq)
if err != nil {
return err
}
fmt.Printf("Deleted document: %t\n", deleteResponse.Result == "deleted")
// Delete previously created index.
deleteIndex := &opensearchapi.IndicesDeleteReq{Indices: []string{IndexName}}
deleteIndexResp, err := client.Indices.Delete(ctx, deleteIndex)
if err != nil {
return err
}
fmt.Printf("Deleted index: %t\n", deleteIndexResp.Acknowledged)
// Try to delete the index again which fails as it does not exist
_, err = client.Indices.Delete(ctx, deleteIndex)
// Load err into opensearch.StructError to access the fields and tolerate if the index is missing
if err != nil {
if errors.As(err, &opensearchError) {
if opensearchError.Err.Type != "index_not_found_exception" {
return err
}
} else {
return err
}
}
return nil
}Before starting, we strongly recommend reading the full AWS documentation regarding using IAM credentials to sign requests to OpenSearch APIs. See Identity and Access Management in Amazon OpenSearch Service.
Even if you configure a completely open resource-based access policy, all requests to the OpenSearch Service configuration API must be signed. If your policies specify IAM users or roles, requests to the OpenSearch APIs also must be signed using AWS Signature Version 4.
Import the request signer from signer/awsv2. It signs each request with AWS Signature Version 4 (SigV4) using AWS SDK for Go v2 and automatically discovers AWS credentials from the ~/.aws folder or environment variables.
To read more about SigV4 see Signature Version 4 signing process
The signer caches credentials so SigV4 signing does not call Credentials.Retrieve on every request, which matters most for STS-backed providers (assume-role, web identity, IRSA).
Use the AWS SDK v2 for Go to authenticate with Amazon OpenSearch service.
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/opensearch-project/opensearch-go/v5"
"github.com/opensearch-project/opensearch-go/v5/opensearchapi"
requestsigner "github.com/opensearch-project/opensearch-go/v5/signer/awsv2"
)
const endpoint = "" // e.g. https://opensearch-domain.region.com or Amazon OpenSearch Serverless endpoint
func main() {
if err := example(); err != nil {
fmt.Println(fmt.Sprintf("Error: %s", err))
os.Exit(1)
}
}
func example() error {
ctx := context.Background()
awsCfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("<AWS_REGION>"),
config.WithCredentialsProvider(
getCredentialProvider("<AWS_ACCESS_KEY>", "<AWS_SECRET_ACCESS_KEY>", "<AWS_SESSION_TOKEN>"),
),
)
if err != nil {
return err
}
// Create an AWS request Signer and load AWS configuration using default config folder or env vars.
signer, err := requestsigner.NewSignerWithService(awsCfg, "es") // Use "aoss" for Amazon OpenSearch Serverless
if err != nil {
return err
}
// Create an opensearch client and use the request-signer.
client, err := opensearchapi.NewClient(
opensearchapi.Config{
Client: opensearch.Config{
Addresses: []string{endpoint},
Signer: signer,
},
},
)
if err != nil {
return err
}
indexName := "go-test-index"
// Define index mapping.
mapping := strings.NewReader(`{
"settings": {
"index": {
"number_of_shards": 4
}
}
}`)
// Create an index with non-default settings.
createResp, err := client.Indices.Create(
ctx,
opensearchapi.IndicesCreateReq{
Index: indexName,
BodyReader: mapping,
},
)
if err != nil {
return err
}
fmt.Printf("created index: %s\n", createResp.Index)
delResp, err := client.Indices.Delete(ctx, &opensearchapi.IndicesDeleteReq{Indices: []string{indexName}})
if err != nil {
return err
}
fmt.Printf("deleted index: %#v\n", delResp.Acknowledged)
return nil
}
func getCredentialProvider(accessKey, secretAccessKey, token string) aws.CredentialsProviderFunc {
return func(ctx context.Context) (aws.Credentials, error) {
c := &aws.Credentials{
AccessKeyID: accessKey,
SecretAccessKey: secretAccessKey,
SessionToken: token,
}
return *c, nil
}
}For common TLS options, prefer the built-in config fields (InsecureSkipVerify, CACert) over constructing a custom http.Transport. These options clone the default transport internally, preserving connection pooling, HTTP/2, and timeout defaults.
If you need a custom transport (e.g. for mutual TLS or a corporate proxy), always clone http.DefaultTransport rather than constructing a bare &http.Transport{}. A bare transport defaults to MaxIdleConnsPerHost: 2, disables HTTP/2 multiplexing, and has no dialer timeouts -- causing excessive TLS handshakes and connection churn under concurrency.
// Good: clone DefaultTransport, then customize.
tp := http.DefaultTransport.(*http.Transport).Clone()
tp.TLSClientConfig.Certificates = []tls.Certificate{cert}
client, err := opensearch.NewClient(opensearch.Config{
Addresses: []string{"https://localhost:9200"},
Transport: tp,
})// Bad: bare transport loses all DefaultTransport defaults.
client, err := opensearch.NewClient(opensearch.Config{
Addresses: []string{"https://localhost:9200"},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
},
},
// MaxIdleConnsPerHost = 2, no HTTP/2, no dialer timeouts!
})Key settings to verify on any custom transport:
| Setting | DefaultTransport |
Bare &http.Transport{} |
|---|---|---|
MaxIdleConns |
100 | 0 (unlimited) |
MaxIdleConnsPerHost |
2 | 2 |
ForceAttemptHTTP2 |
true | false |
IdleConnTimeout |
90s | 0 (no timeout) |
TLSHandshakeTimeout |
10s | 0 (no timeout) |
DialContext timeouts |
30s | none |
Clone() copies all of these. If you must build from scratch (e.g. for a non-*http.Transport round tripper), set at minimum ForceAttemptHTTP2: true and MaxIdleConnsPerHost >= your expected concurrency.
The opensearchtransport.OperationClassifier maps HTTP method+path pairs to structured OperationID values. This enables transparent metrics, tracing, or access-control middleware at the http.RoundTripper layer without per-operation wrapper code.
import "github.com/opensearch-project/opensearch-go/v5/opensearchtransport"
// Build once, reuse across requests. Safe for concurrent use.
classifier := opensearchtransport.NewOperationClassifier()
// In an http.RoundTripper:
func (t *MetricsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
op := t.classifier.Classify(req.Method, req.URL.Path)
start := time.Now()
resp, err := t.next.RoundTrip(req)
duration := time.Since(start)
status := 0
if resp != nil {
status = resp.StatusCode
}
t.histogram.WithLabelValues(op.String(), strconv.Itoa(status)).Observe(
float64(duration.Milliseconds()),
)
return resp, err
}OperationID is a bit-packed int64 with masking helpers:
op := classifier.Classify("POST", "/my-index/_search")
op.String() // "search"
op.IsWrite() // false
op.IsRead() // true
op.Category() // CatSearchReturns OpOther for unrecognized patterns.
Set the OPENSEARCH_GO_DEBUG environment variable to enable debug logging for connection management, node discovery, and request routing. Debug output is written to stderr.
OPENSEARCH_GO_DEBUG=true go run myapp.goFor programmatic control, set EnableDebugLogger: true in the client configuration:
client, err := opensearchapi.NewClient(
opensearchapi.Config{
Client: opensearch.Config{
Addresses: []string{"http://localhost:9200"},
EnableDebugLogger: true,
},
},
)In tests, use the testutil.IsDebugEnabled(t) helper which also reads OPENSEARCH_GO_DEBUG:
OPENSEARCH_GO_DEBUG=true go test ./...Disable specific routing policies at startup via environment variables for debugging, A/B testing, or emergency overrides:
# Disable all connection scoring (fall back to plain role-based)
OPENSEARCH_GO_POLICY_ROUTER=false myapp
# Disable a specific policy instance by path
OPENSEARCH_GO_POLICY_ROLE=chain[0].mux[0].role[0]=false myapp
# Regex path matching
OPENSEARCH_GO_POLICY_ROLE=.*mux.*role.*=false myappSet OPENSEARCH_GO_DEBUG=true to see policy paths and override actions. See Request Routing for full documentation.
All OPENSEARCH_GO_* environment variables are evaluated once at client initialization and are immutable after. The canonical reference for every variable — accepted values, defaults, meanings, and the tokens accepted by OPENSEARCH_GO_ERROR_MASK — is guides/config-envvars.md. The sections below link to the relevant categories in that guide.
- Security - TLS, credentials, input validation, index patterns, error disclosure
- Index Lifecycle
- Document Lifecycle
- Search
- Bulk
- Advanced Index Actions
- Index Templates
- Data Streams
- Making Raw JSON REST Requests
- Request Routing
- Cluster Health Checking
- Node Discovery and Role Management
- Response Body Buffering
- Retry and Backoff
- Error Handling