Low-level API
The low-level API provides a one-to-one mapping with the Elasticsearch REST API. Each endpoint accepts raw io.Reader request bodies and returns *esapi.Response objects whose Body is an io.ReadCloser you read and close yourself.
The rest of this documentation is built around the typed API and the esdsl builders, which together cover the vast majority of Elasticsearch endpoints with compile-time safety, automatic JSON encoding and decoding, and fluent query construction. We recommend the typed API for new code. If you are already using the low-level API, the migration guide shows how to adopt the typed API without rewriting your whole codebase.
Reach for the low-level API when:
- You need an endpoint that is not covered by the typed API.
- You want full control over request serialization, for example to plug in a custom JSON encoder or a streaming body.
- You are working with pre-baked JSON payloads and do not want to model them as Go structs.
If none of these apply, prefer the typed API.
Use elasticsearch.New with functional options:
package main
import (
"context"
"log"
"github.com/elastic/go-elasticsearch/v9"
)
func main() {
client, err := elasticsearch.New(
elasticsearch.WithAddresses("https://localhost:9200"),
elasticsearch.WithAPIKey("API_KEY"),
)
if err != nil {
log.Fatalf("creating client: %s", err)
}
defer client.Close(context.Background())
res, err := client.Info()
if err != nil {
log.Fatalf("info: %s", err)
}
defer res.Body.Close()
log.Println(res)
}
For full configuration options, see the Configuration reference.
NewClient(Config) and NewDefaultClient() still work but are deprecated. Use New with functional options instead.
Every low-level call returns an *esapi.Response whose body you must read and close. Failing to close the body prevents Go's HTTP client from reusing the underlying TCP connection.
res, err := client.Search(
client.Search.WithIndex("my-index"),
client.Search.WithBody(strings.NewReader(`{"query":{"match_all":{}}}`)),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.IsError() {
log.Fatalf("search failed: %s", res.String())
}
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
log.Fatal(err)
}
- Always close the response body, even on the success path.
IsError()returnstruefor HTTP status codes >= 400.- Decode the body yourself into whatever shape fits your application.
A condensed tour of the most common CRUD and search calls. For detailed tutorials, see the typed API equivalents; the low-level signatures below are stable and map directly to the corresponding REST endpoints.
mapping := `{"mappings":{"properties":{"price":{"type":"integer"}}}}`
res, err := client.Indices.Create(
"test-index",
client.Indices.Create.WithBody(strings.NewReader(mapping)),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
document := struct {
Name string `json:"name"`
Price int `json:"price"`
}{Name: "Foo", Price: 10}
data, _ := json.Marshal(document)
res, err := client.Index(
"test-index",
bytes.NewReader(data),
client.Index.WithDocumentID("1"),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
res, err := client.Get("test-index", "1")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
query := `{"query":{"match":{"name":{"query":"Foo"}}}}`
res, err := client.Search(
client.Search.WithIndex("test-index"),
client.Search.WithBody(strings.NewReader(query)),
)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
res, err := client.Delete("test-index", "1")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var buf bytes.Buffer
buf.WriteString(`{"index":{"_index":"test","_id":"1"}}` + "\n")
buf.WriteString(`{"title":"Test"}` + "\n")
res, err := client.Bulk(bytes.NewReader(buf.Bytes()))
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
For a higher-level alternative that handles batching, flushing, and concurrency, see esutil.BulkIndexer. The BulkIndexer works with both the low-level and typed clients.
The full set of low-level endpoints, parameters, and option functions is documented on pkg.go.dev:
You do not have to switch everything at once. An *elasticsearch.Client already satisfies the transport interface used by the typed API packages, so you can adopt typed endpoints one at a time while keeping the rest of your code untouched. See the migration guide for the details.