esquery/search.go
Ido Perlmuter 8e5dea816b
Add Search() function, two new aggregations (#7)
This commit implements a Search() function, which allow for running
search requests with both a query and aggregations. This function is
meant to more accurately implement the structure of search requests
accepted by ElasticSearch's Search API.

The Query() and Aggregate() functions are still included by the library,
but now simply call Search() internally, making them simple shortcuts.

Two new aggregations are also added: "terms" and "top_hits". These are
implemented a bit differently than previously implemented ones. The
structs and methods for ElasticSearch queries and aggregations will
eventually be auto-generated from a specification file, and will look
more like the new implementations of these new aggregations.
2020-04-06 12:03:42 +03:00

148 lines
4.2 KiB
Go

package esquery
import (
"bytes"
"encoding/json"
"fmt"
"time"
"github.com/elastic/go-elasticsearch/v7"
"github.com/elastic/go-elasticsearch/v7/esapi"
)
// SearchRequest represents a request to ElasticSearch's Search API, described
// in https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html.
// Not all features of the search API are currently supported, but a request can
// currently include a query, aggregations, and more.
type SearchRequest struct {
query Mappable
aggs []Aggregation
from *uint64
size *uint64
explain *bool
timeout *time.Duration
}
// Search creates a new SearchRequest object, to be filled via method chaining.
func Search() *SearchRequest {
return &SearchRequest{}
}
// Query sets a query for the request.
func (req *SearchRequest) Query(q Mappable) *SearchRequest {
req.query = q
return req
}
// Aggs sets one or more aggregations for the request.
func (req *SearchRequest) Aggs(aggs ...Aggregation) *SearchRequest {
req.aggs = aggs
return req
}
// From sets a document offset to start from.
func (req *SearchRequest) From(offset uint64) *SearchRequest {
req.from = &offset
return req
}
// Size sets the number of hits to return. The default - according to the ES
// documentation - is 10.
func (req *SearchRequest) Size(size uint64) *SearchRequest {
req.size = &size
return req
}
// Explain sets whether the ElasticSearch API should return an explanation for
// how each hit's score was calculated.
func (req *SearchRequest) Explain(b bool) *SearchRequest {
req.explain = &b
return req
}
// Timeout sets a timeout for the request.
func (req *SearchRequest) Timeout(dur time.Duration) *SearchRequest {
req.timeout = &dur
return req
}
// Map implements the Mappable interface. It converts the request to into a
// nested map[string]interface{}, as expected by the go-elasticsearch library.
func (req *SearchRequest) Map() map[string]interface{} {
m := make(map[string]interface{})
if req.query != nil {
m["query"] = req.query.Map()
}
if len(req.aggs) > 0 {
aggs := make(map[string]interface{})
for _, agg := range req.aggs {
aggs[agg.Name()] = agg.Map()
}
m["aggs"] = aggs
}
if req.size != nil {
m["size"] = *req.size
}
if req.from != nil {
m["from"] = *req.from
}
if req.explain != nil {
m["explain"] = *req.explain
}
if req.timeout != nil {
m["timeout"] = fmt.Sprintf("%.0fs", req.timeout.Seconds())
}
return m
}
// MarshalJSON implements the json.Marshaler interface. It returns a JSON
// representation of the map generated by the SearchRequest's Map method.
func (req *SearchRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(req.Map())
}
// Run executes the request using the provided ElasticSearch client. Zero or
// more search options can be provided as well. It returns the standard Response
// type of the official Go client.
func (req *SearchRequest) Run(
api *elasticsearch.Client,
o ...func(*esapi.SearchRequest),
) (res *esapi.Response, err error) {
return req.RunSearch(api.Search, o...)
}
// RunSearch is the same as the Run method, except that it accepts a value of
// type esapi.Search (usually this is the Search field of an elasticsearch.Client
// object). Since the ElasticSearch client does not provide an interface type
// for its API (which would allow implementation of mock clients), this provides
// a workaround. The Search function in the ES client is actually a field of a
// function type.
func (req *SearchRequest) RunSearch(
search esapi.Search,
o ...func(*esapi.SearchRequest),
) (res *esapi.Response, err error) {
var b bytes.Buffer
err = json.NewEncoder(&b).Encode(req.Map())
if err != nil {
return nil, err
}
opts := append([]func(*esapi.SearchRequest){search.WithBody(&b)}, o...)
return search(opts...)
}
// Query is a shortcut for creating a SearchRequest with only a query. It is
// mostly included to maintain the API provided by esquery in early releases.
func Query(q Mappable) *SearchRequest {
return Search().Query(q)
}
// Aggregate is a shortcut for creating a SearchRequest with aggregations. It is
// mostly included to maintain the API provided by esquery in early releases.
func Aggregate(aggs ...Aggregation) *SearchRequest {
return Search().Aggs(aggs...)
}