Loading

esdsl - Elasticsearch DSL builders

The esdsl package provides fluent, type-safe builders for constructing Elasticsearch queries, aggregations, mappings, and sort options. It is designed to work alongside the typed API, giving you a concise, chainable syntax for building complex request bodies.

import "github.com/elastic/go-elasticsearch/v9/typedapi/esdsl"
		

The package is generated from the elasticsearch-specification, so every query type, aggregation, mapping property, and sort option available in Elasticsearch has a corresponding builder.

The typed API models requests as Go structs. This works well for simple operations, but deeply nested structures like bool queries with multiple clauses, aggregations with sub-aggregations, or complex mappings can become verbose:

// Typed API with structs - works, but verbose for complex queries
res, err := es.Search().
    Index("products").
    Request(&search.Request{
        Query: &types.Query{
            Bool: &types.BoolQuery{
                Must: []types.Query{
                    {Match: map[string]types.MatchQuery{
                        "title": {Query: "wireless"},
                    }},
                },
                Filter: []types.Query{
                    {Term: map[string]types.TermQuery{
                        "brand": {Value: "Samsung"},
                    }},
                },
            },
        },
    }).
    Do(context.Background())
		

The esdsl builders express the same intent in fewer lines, with each builder method guiding you through the available options:

// esdsl - same query, less nesting
res, err := es.Search().
    Index("products").
    Query(
        esdsl.NewBoolQuery().
            Must(esdsl.NewMatchQuery("title", "wireless")).
            Filter(esdsl.NewTermQuery("brand", esdsl.NewFieldValue().String("Samsung"))),
    ).
    Do(context.Background())
		

Every esdsl builder follows the same pattern:

  1. Create a builder with NewXxx().
  2. Configure it by chaining methods.
  3. Pass it directly to a typed API method.

Under the hood, each builder wraps the corresponding types.* struct and implements a *Variant interface (such as QueryVariant or AggregationsVariant). The typed API methods accept these interfaces, so builders plug in directly without any manual conversion.

Mermaid diagram

For example, the typed API Search endpoint accepts any QueryVariant:

func (r *Search) Query(query types.QueryVariant) *Search
		

And every esdsl query builder implements QueryVariant through its QueryCaster() method:

func (s *_matchQuery) QueryCaster() *types.Query
		

This means you never need to call the caster yourself - just pass the builder directly.

Build any Elasticsearch query type with NewXxxQuery() constructors:

// Simple match
esdsl.NewMatchQuery("title", "elasticsearch")

// Match all
esdsl.NewMatchAllQuery()

// Term (exact match)
esdsl.NewTermQuery("status", esdsl.NewFieldValue().String("published"))

// Range
esdsl.NewNumberRangeQuery("price").Gte(100).Lt(500)

// Bool (compound)
esdsl.NewBoolQuery().
    Must(
        esdsl.NewMatchQuery("title", "Go"),
        esdsl.NewNumberRangeQuery("year").Gte(2020),
    ).
    Filter(
        esdsl.NewTermQuery("lang", esdsl.NewFieldValue().String("go")),
    )
		

All query builders implement QueryVariant, so they work with es.Search().Query(...) and inside compound queries like BoolQuery.Must(...).

Build aggregations with NewXxxAggregation() constructors:

// Sum aggregation
esdsl.NewSumAggregation().Field("price")

// Terms aggregation
esdsl.NewTermsAggregation().Field("category.keyword").Size(10)

// Average aggregation
esdsl.NewAverageAggregation().Field("rating")
		

Aggregation builders implement AggregationsVariant, so they work with es.Search().AddAggregation(...):

res, err := es.Search().
    Index("products").
    Size(0).
    AddAggregation("avg_price", esdsl.NewAverageAggregation().Field("price")).
    AddAggregation("by_brand", esdsl.NewTermsAggregation().Field("brand.keyword").Size(5)).
    Do(context.Background())
		

Build index mappings with NewTypeMapping() and property builders:

mappings := esdsl.NewTypeMapping().
    AddProperty("title", esdsl.NewTextProperty()).
    AddProperty("price", esdsl.NewIntegerNumberProperty()).
    AddProperty("tags", esdsl.NewKeywordProperty()).
    AddProperty("embedding", esdsl.NewDenseVectorProperty().
        Dims(768).
        Index(true).
        Similarity(densevectorsimilarity.Cosine))

res, err := es.Indices.Create("my-index").
    Mappings(mappings).
    Do(context.Background())
		

Mapping builders implement TypeMappingVariant and PropertyVariant, so they work with Mappings(...) and AddProperty(...).

Build sort configurations with NewSortOptions() and NewFieldSort():

sort := esdsl.NewSortOptions().
    AddSortOption("_score", esdsl.NewFieldSort(sortorder.Desc)).
    AddSortOption("date", esdsl.NewFieldSort(sortorder.Asc))

res, err := es.Search().
    Index("articles").
    Query(esdsl.NewMatchQuery("title", "Go")).
    Sort(sort).
    Do(context.Background())
		
Scenario Recommended approach
Complex, nested queries (bool, nested, function_score) esdsl builders
Aggregations with sub-aggregations esdsl builders
Index mappings with many fields esdsl builders
Simple, flat requests (match_all, single term) Either - both are concise
Dynamic query construction at runtime esdsl builders (easier to compose)
Pre-built JSON templates Raw() method on the typed API

Both approaches produce identical JSON and share the same typed API transport. You can freely mix them in the same application.