Go client
The Lucenia Go client (lucenia-go) provides idiomatic Go bindings for the Lucenia REST API. The client offers typed request and response structs for every API, automatic node discovery, role-aware request routing, and pluggable transports for AWS Signature V4 signing.
Compatibility
The lucenia-go client requires Go 1.21 or later. The module version tracks the Lucenia server version: client v0.11.x is intended for use with Lucenia 0.11.x clusters.
Installation
If you are starting a new project, initialize a module first:
go mod init example.com/myapp
Then add the client:
go get github.com/lucenia/lucenia-go
Import the packages you need:
import (
"github.com/lucenia/lucenia-go"
"github.com/lucenia/lucenia-go/luceniaapi"
)
The two packages have distinct roles:
luceniaholds transport-level configuration (lucenia.Config).luceniaapiexposes the typed API surface (luceniaapi.NewClient, request structs likeIndicesCreateReq,IndexReq,SearchReq, etc.).
Creating a client
Connecting to a secured cluster
Lucenia clusters typically run with TLS and basic authentication. The snippet below creates a client suitable for local development against a cluster using self-signed certificates. Configure trusted certificates and disable InsecureSkipVerify in production.
package main
import (
"context"
"fmt"
"os"
"github.com/lucenia/lucenia-go"
"github.com/lucenia/lucenia-go/luceniaapi"
)
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
}
func run() error {
client, err := luceniaapi.NewClient(luceniaapi.Config{
Client: lucenia.Config{
Addresses: []string{"https://localhost:9200"},
Username: "admin",
Password: "MyStrongPassword123!",
InsecureSkipVerify: true, // For testing only. Use certificates in production.
},
})
if err != nil {
return err
}
ctx := context.Background()
info, err := client.Info(ctx, nil)
if err != nil {
return err
}
fmt.Printf("Connected to %s %s\n", info.Version.Distribution, info.Version.Number)
return nil
}
InsecureSkipVerify: true disables TLS verification and should only be used for local development. In production, supply a CACert (raw PEM bytes) on lucenia.Config so the client trusts your cluster's certificate authority.
To enable certificate verification with a CA bundle:
caCert, err := os.ReadFile("/path/to/root-ca.pem")
if err != nil {
return err
}
client, err := luceniaapi.NewClient(luceniaapi.Config{
Client: lucenia.Config{
Addresses: []string{"https://localhost:9200"},
Username: "admin",
Password: "MyStrongPassword123!",
CACert: caCert,
},
})
Connecting to an unsecured cluster
For development clusters without TLS or authentication:
client, err := luceniaapi.NewClient(luceniaapi.Config{
Client: lucenia.Config{
Addresses: []string{"http://localhost:9200"},
},
})
Node discovery and request routing
The client can discover all nodes in the cluster at startup and on a refresh interval, and route requests to nodes by role:
import "time"
client, err := luceniaapi.NewClient(luceniaapi.Config{
Client: lucenia.Config{
Addresses: []string{"https://localhost:9200"},
Username: "admin",
Password: "MyStrongPassword123!",
DiscoverNodesOnStart: true,
DiscoverNodesInterval: 5 * time.Minute,
},
})
The router automatically routes search requests to data nodes and bulk requests to ingest nodes.
Working with documents
Creating an index
Create an index with custom settings and mappings:
import "strings"
ctx := context.Background()
indexName := "my-index"
createResp, err := client.Indices.Create(
ctx,
luceniaapi.IndicesCreateReq{
Index: indexName,
Body: strings.NewReader(`{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 0
}
},
"mappings": {
"properties": {
"title": { "type": "text" },
"text": { "type": "text" },
"year": { "type": "integer" }
}
}
}`),
},
)
if err != nil {
return err
}
fmt.Printf("Index created: %t\n", createResp.Acknowledged)
Indexing a single document
indexResp, err := client.Index(
ctx,
luceniaapi.IndexReq{
Index: indexName,
DocumentID: "1",
Body: strings.NewReader(
`{"title": "Introduction to Lucenia", "text": "Lucenia is a high-performance search engine.", "year": 2025}`),
Params: luceniaapi.IndexParams{Refresh: "true"},
},
)
if err != nil {
return err
}
fmt.Printf("Indexed document 1: %s\n", indexResp.Result)
For typed documents, use luceniautil.NewJSONReader to convert a struct to an io.Reader:
import "github.com/lucenia/lucenia-go/luceniautil"
doc := struct {
Title string `json:"title"`
Text string `json:"text"`
Year int `json:"year"`
}{
Title: "Introduction to Lucenia",
Text: "Lucenia is a high-performance search engine.",
Year: 2025,
}
indexResp, err := client.Index(
ctx,
luceniaapi.IndexReq{
Index: indexName,
DocumentID: "1",
Body: luceniautil.NewJSONReader(&doc),
Params: luceniaapi.IndexParams{Refresh: "true"},
},
)
Bulk indexing
For higher throughput, send multiple operations in a single request. The bulk body uses newline-delimited JSON with an action line followed by a document line for each operation:
bulkBody := `{ "index": { "_index": "` + indexName + `", "_id": "2" } }
{ "title": "Getting Started", "text": "Use the lucenia-go client.", "year": 2025 }
{ "index": { "_index": "` + indexName + `", "_id": "3" } }
{ "title": "Search Features", "text": "Full-text search and more.", "year": 2025 }
{ "index": { "_index": "` + indexName + `", "_id": "4" } }
{ "title": "Bulk Operations", "text": "Index many docs at once.", "year": 2025 }
`
bulkResp, err := client.Bulk(ctx, luceniaapi.BulkReq{
Body: strings.NewReader(bulkBody),
Params: luceniaapi.BulkParams{Refresh: "wait_for"},
})
if err != nil {
return err
}
fmt.Printf("Bulk indexed %d documents (errors: %t)\n", len(bulkResp.Items), bulkResp.Errors)
Searching
Run a match_all query to return every document:
import "encoding/json"
searchResp, err := client.Search(
ctx,
&luceniaapi.SearchReq{
Indices: []string{indexName},
Body: strings.NewReader(`{ "query": { "match_all": {} } }`),
},
)
if err != nil {
return err
}
fmt.Printf("Found %d documents:\n", searchResp.Hits.Total.Value)
for _, hit := range searchResp.Hits.Hits {
var doc map[string]any
_ = json.Unmarshal(hit.Source, &doc)
fmt.Printf(" [%s] %s\n", hit.ID, doc["title"])
}
Run a match query against a specific field:
searchResp, err := client.Search(
ctx,
&luceniaapi.SearchReq{
Indices: []string{indexName},
Body: strings.NewReader(`{ "query": { "match": { "title": "Lucenia" } } }`),
},
)
The client also exposes search parameters via SearchParams, which is useful for URI search and pagination:
searchResp, err := client.Search(
ctx,
&luceniaapi.SearchReq{
Indices: []string{indexName},
Params: luceniaapi.SearchParams{
Query: `title: "Lucenia"`,
Size: luceniaapi.ToPointer(10),
From: luceniaapi.ToPointer(0),
Sort: []string{"year:desc"},
},
},
)
Deleting a document and the index
_, err = client.Document.Delete(ctx, luceniaapi.DocumentDeleteReq{
Index: indexName,
DocumentID: "1",
})
_, err = client.Indices.Delete(ctx, luceniaapi.IndicesDeleteReq{
Indices: []string{indexName},
})
Error handling
The client returns typed errors that you can inspect with errors.As to extract the Lucenia error type and ignore expected conditions like "index already exists":
import "errors"
_, err := client.Indices.Create(ctx, luceniaapi.IndicesCreateReq{Index: indexName})
var luceniaErr *lucenia.StructError
if err != nil && errors.As(err, &luceniaErr) {
if luceniaErr.Err.Type != "resource_already_exists_exception" {
return err
}
}
Debugging
Set the LUCENIA_GO_DEBUG environment variable to log connection management, node discovery, and request routing decisions to stderr:
LUCENIA_GO_DEBUG=true go run myapp.go
For programmatic control, set EnableDebugLogger: true on lucenia.Config.
Next steps
The Lucenia Go client supports a wider set of features beyond the basics covered here, including:
- Index lifecycle and document lifecycle helpers
- Search pagination, point-in-time, and scroll
- Bulk indexing
- Index templates and data streams
- Request routing and node discovery
- Retry and backoff policies
- Typed error handling