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 term query
termQuery := elastic.NewTermQuery("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).

// 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
}
Clone this wiki locally