-
Notifications
You must be signed in to change notification settings - Fork 1.2k
QueryDSL
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 term query
termQuery := elastic.NewTermQuery("user", "olivere")
If you have a complex query, here are some tips for getting it to work in Elastic.
- Create and test your query with e.g. Sense and tweak it until it works fine for you.
- Construct the query from the inside out. Use e.g. builders to set up your queries via Go funcs (see below for an example).
- 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).
// 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.sort(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 = res.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")
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
}
// sort applies sorting to the service.
func (f *Finder) sort(service *elastic.SearchService) *elastic.SearchService {
if f.sort == "" {
// Sort by score by default
service = service.Sort("_score", false)
return service
}
// Sort by fields; prefix of "-" means: descending sort order.
for _, s := range strings.Split(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
}