Skip to main content
Version: 0.11.0

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:

  • lucenia holds transport-level configuration (lucenia.Config).
  • luceniaapi exposes the typed API surface (luceniaapi.NewClient, request structs like IndicesCreateReq, 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
}
warning

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