Skip to content
Oliver Eilhard edited this page Jan 9, 2016 · 5 revisions

The Query DSL in Elasticsearch is a complex beast. While you can simply build the query yourself and use .BodyString(...) to make Elastic to execute it, Elastic has support for programmatically setting up your queries in Go.

Here's a simple example of setting up a BoolQuery with Elastic (v3).

// Search with a bool query
query := elastic.NewBoolQuery()
query = query.Must(elastic.NewTermQuery("user", "olivere"))
query = query.Filter(elastic.NewTermQuery("account", 1))
src, err := query.Source()
if err != nil {
  panic(err)
}
data, err := json.Marshal(src)
if err != nil {
  panic(err)
}
s := string(data)
fmt.Println(s)
// Output:
// {"bool":{"filter":{"term":{"account":1}},"must":{"term":{"user":"olivere"}}}}

If you have a complex query, here are some tips for getting it to work in Elastic.

  1. Create and test your query with e.g. Sense and tweak it until it works fine for you.
  2. Construct the query from the inside out. Use e.g. builders to set up your queries via Go funcs (see below for an example).
  3. If you are unsure how to construct a specific query or aggregation in Elastic, look up the tests. They serve not only for tests but also for documentation purposes.

Here's a simple finder for e.g. films that resembles the builder pattern used throughout the Elastic API (as well as the Elasticsearch Java API).

package main

import (
	"encoding/json"
	"fmt"
	"strings"

	"gopkg.in/olivere/elastic.v3"
)

func main() {
	client, err := elastic.NewClient()
	if err != nil {
		panic(err)
	}

	// Create and execute finder
	f := NewFinder()
	f = f.Year(2014)
	res, err := f.Find(client)
	if err != nil {
		panic(err)
	}

	// Output results
	fmt.Printf("%#v", res)
}

type Film struct {
	Title    string `json:"title"`
	Genre    string `json:"genre"`
	Year     string `json:"year"`
	Director string `json:"director"`
}

// Finder specifies a finder for films.
type Finder struct {
	genre      string
	year       int
	from, size int
	sort       []string
}

// FinderResponse is the outcome of calling Finder.Find.
type FinderResponse struct {
	Total  int64
	Films  []*Film
	Genres map[string]int64
}

// NewFinder creates a new finder for films.
// Use the funcs to set up filters and search properties,
// then call Find to execute.
func NewFinder() *Finder {
	return &Finder{}
}

// Genre filters the results by the given genre.
func (f *Finder) Genre(genre string) *Finder {
	f.genre = genre
	return f
}

// Year filters the results by the specified year.
func (f *Finder) Year(year int) *Finder {
	f.year = year
	return f
}

// From specifies the start index for pagination.
func (f *Finder) From(from int) *Finder {
	f.from = from
	return f
}

// Size specifies the number of items to return in pagination.
func (f *Finder) Size(size int) *Finder {
	f.size = size
	return f
}

// Sort specifies one or more sort orders.
// Use a dash (-) to make the sort order descending.
// Example: "name" or "-year".
func (f *Finder) Sort(sort ...string) *Finder {
	if f.sort == nil {
		f.sort = make([]string, 0)
	}
	f.sort = append(f.sort, sort...)
	return f
}

// Find executes the search and returns a response.
func (f *Finder) Find(client *elastic.Client) (FinderResponse, error) {
	var resp FinderResponse

	// Create service and use query, aggregations, sort, filter, pagination funcs
	search := client.Search().Index("hollywood").Type("film")
	search = f.query(search)
	search = f.aggs(search)
	search = f.sorting(search)
	search = f.paginate(search)

	// TODO Add other properties here, e.g. timeouts, explain or pretty printing

	// Execute query
	sr, err := search.Do()
	if err != nil {
		return resp, err
	}

	// Decode response
	films, err := f.decodeFilms(sr)
	if err != nil {
		return resp, err
	}
	resp.Films = films
	resp.Total = sr.Hits.TotalHits

	// Deserialize aggregations
	if agg, found := sr.Aggregations.Terms("genres"); found {
		resp.Genres = make(map[string]int64)
		for _, bucket := range agg.Buckets {
			resp.Genres[bucket.Key.(string)] = bucket.DocCount
		}
	}

	return resp, nil
}

// query sets up the query in the search service.
func (f *Finder) query(service *elastic.SearchService) *elastic.SearchService {
	if f.genre == "" && f.year == 0 {
		service = service.Query(elastic.NewMatchAllQuery())
		return service
	}

	q := elastic.NewBoolQuery()
	if f.genre != "" {
		q = q.Must(elastic.NewTermQuery("genre", f.genre))
	}
	if f.year > 0 {
		q = q.Must(elastic.NewTermQuery("year", f.year))
	}

	// TODO Add other queries and filters here, maybe differentiating between AND/OR etc.

	service = service.Query(q)
	return service
}

// aggs sets up the aggregations in the service.
func (f *Finder) aggs(service *elastic.SearchService) *elastic.SearchService {
	// Terms aggregation by genre
	agg := elastic.NewTermsAggregation().Field("genre")
	service = service.Aggregation("genres", agg)

	return service
}

// paginate sets up pagination in the service.
func (f *Finder) paginate(service *elastic.SearchService) *elastic.SearchService {
	if f.from > 0 {
		service = service.From(f.from)
	}
	if f.size > 0 {
		service = service.Size(f.size)
	}
	return service
}

// sorting applies sorting to the service.
func (f *Finder) sorting(service *elastic.SearchService) *elastic.SearchService {
	if len(f.sort) == 0 {
		// Sort by score by default
		service = service.Sort("_score", false)
		return service
	}

	// Sort by fields; prefix of "-" means: descending sort order.
	for _, s := range f.sort {
		s = strings.TrimSpace(s)

		var field string
		var asc bool

		if strings.HasPrefix(s, "-") {
			field = s[1:]
			asc = false
		} else {
			field = s
			asc = true
		}

		// Maybe check for permitted fields to sort

		service = service.Sort(field, asc)
	}
	return service
}

// decodeFilms takes a search result and deserializes the films.
func (f *Finder) decodeFilms(res *elastic.SearchResult) ([]*Film, error) {
	if res == nil || res.TotalHits() == 0 {
		return nil, nil
	}

	var films []*Film
	for _, hit := range res.Hits.Hits {
		film := new(Film)
		if err := json.Unmarshal(*hit.Source, &film); err != nil {
			return nil, err
		}
		// TODO Add Score here, e.g.:
		// film.Score = *hit.Score
		films = append(films, film)
	}
	return films, nil
}
Clone this wiki locally