From 73b274a13b6ec3ed209b5567ecb89aca2897ce08 Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Thu, 27 Feb 2020 14:19:07 +0000 Subject: [PATCH] Add documentation, fix small bugs, improve CustomQuery (#6) This commit performs the following modifications: - The library is properly documented in Godoc format. - The CustomQuery function is made to be a bit more versatile by allowing it to also be used standalone (i.e. instead of passing a `CustomQuery` as a parameter to the `Query` function, they now have their own `Run` method). - Queries and aggregations can now also be executed using the `RunSearch` method. This method is the same as the `Run` method, except that instead of an `*elasticSearch.Client` value, it accepts an `esapi.Search` value. This is provided for consuming code that needs to implement mock clients of ElasticSearch (e.g. for test purposes). The ElasticSearch client does not provide an interface type describing its API, so its Search function (which is actually a field of a function type) can be used instead. - Bugfix: the CustomAgg function was unusable as it did not accept a name parameter and thus did not implement the Aggregation interface. - Bugfix: the enumeration types are rewritten according to Go standards, and the `RangeRelation` type's default value is now empty. - The golint and godox linters are added. --- .golangci.yml | 2 + aggregations.go | 47 ++++++- aggs_custom.go | 13 -- aggs_metric.go | 191 ++++++++++++++++++-------- custom.go | 63 +++++++++ aggs_custom_test.go => custom_test.go | 22 ++- es.go | 102 ++++++++++++++ queries.go | 35 +++++ queries_test.go | 2 +- query_boolean.go | 21 ++- query_boosting.go | 20 ++- query_constant_score.go | 13 +- query_custom.go | 13 -- query_custom_test.go | 23 ---- query_dis_max.go | 12 +- query_match.go | 106 ++++++++++---- query_match_all.go | 19 +-- query_match_test.go | 4 +- query_term_level.go | 169 ++++++++++++++++------- query_term_level_test.go | 4 +- 20 files changed, 657 insertions(+), 224 deletions(-) delete mode 100644 aggs_custom.go create mode 100644 custom.go rename aggs_custom_test.go => custom_test.go (64%) delete mode 100644 query_custom.go delete mode 100644 query_custom_test.go diff --git a/.golangci.yml b/.golangci.yml index ee46e7c..f07fa37 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,3 +27,5 @@ linters: - prealloc - unconvert - unparam + - golint + - godox diff --git a/aggregations.go b/aggregations.go index f1d7479..8cff870 100644 --- a/aggregations.go +++ b/aggregations.go @@ -8,15 +8,26 @@ import ( "github.com/elastic/go-elasticsearch/v7/esapi" ) +// AggregationRequest represents a complete request of type "aggregations" +// (a.k.a "aggs") to ElasticSearch's search API. It simply wraps a map of named +// aggregations, which are values of a type that implements the Mappable +// interface. +type AggregationRequest struct { + Aggs map[string]Mappable +} + +// Aggregation is an interface that each aggregation type must implement. It +// is simply an extension of the Mappable interface to include a Named function, +// which returns the name of the aggregation. type Aggregation interface { Mappable Name() string } -type AggregationRequest struct { - Aggs map[string]Mappable -} - +// Aggregate generates a search request of type "aggs", represented by a +// *AggregationRequest object. It receives a variadic amount of values that +// implement the Aggregation interface, whether provided internally by the +// library or custom aggregations provided by consuming code. func Aggregate(aggs ...Aggregation) *AggregationRequest { req := &AggregationRequest{ Aggs: make(map[string]Mappable), @@ -28,6 +39,8 @@ func Aggregate(aggs ...Aggregation) *AggregationRequest { return req } +// Map implements the Mappable interface. It converts the "aggs" request into a +// (potentially nested) map[string]interface{}. func (req *AggregationRequest) Map() map[string]interface{} { m := make(map[string]interface{}) @@ -40,10 +53,15 @@ func (req *AggregationRequest) Map() map[string]interface{} { } } +// MarshalJSON implements the json.Marshaler interface, it simply encodes the +// map representation of the request (provided by the Map method) as JSON. func (req *AggregationRequest) 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 *AggregationRequest) Run( api *elasticsearch.Client, o ...func(*esapi.SearchRequest), @@ -58,3 +76,24 @@ func (req *AggregationRequest) Run( return api.Search(opts...) } + +// 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 *AggregationRequest) 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...) +} diff --git a/aggs_custom.go b/aggs_custom.go deleted file mode 100644 index 6a03a1c..0000000 --- a/aggs_custom.go +++ /dev/null @@ -1,13 +0,0 @@ -package esquery - -type CustomAggregation struct { - m map[string]interface{} -} - -func CustomAgg(m map[string]interface{}) *CustomAggregation { - return &CustomAggregation{m} -} - -func (agg *CustomAggregation) Map() map[string]interface{} { - return agg.m -} diff --git a/aggs_metric.go b/aggs_metric.go index c120759..ab1262b 100644 --- a/aggs_metric.go +++ b/aggs_metric.go @@ -2,15 +2,21 @@ package esquery import "github.com/fatih/structs" +// BaseAgg contains several fields that are common for all aggregation types. type BaseAgg struct { name string apiName string *BaseAggParams `structs:",flatten"` } +// BaseAggParams contains fields that are common to most metric-aggregation +// types. type BaseAggParams struct { - Field string `structs:"field"` - Miss interface{} `structs:"missing,omitempty"` + // Field is the name of the field to aggregate on. + Field string `structs:"field"` + // Miss is a value to provide for documents that are missing a value for the + // field. + Miss interface{} `structs:"missing,omitempty"` } func newBaseAgg(apiName, name, field string) *BaseAgg { @@ -23,50 +29,60 @@ func newBaseAgg(apiName, name, field string) *BaseAgg { } } +// Name returns the name of the aggregation, allowing implementation of the +// Aggregation interface. func (agg *BaseAgg) Name() string { return agg.name } +// Map returns a map representation of the aggregation, implementing the +// Mappable interface. func (agg *BaseAgg) Map() map[string]interface{} { return map[string]interface{}{ agg.apiName: structs.Map(agg.BaseAggParams), } } -/******************************************************************************* - * Avg Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-avg-aggregation.html - ******************************************************************************/ - +// AvgAgg represents an aggregation of type "avg", as described in +// https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-avg-aggregation.html type AvgAgg struct { *BaseAgg `structs:",flatten"` } +// Avg creates an aggregation of type "avg", with the provided name and on the +// provided field. func Avg(name, field string) *AvgAgg { return &AvgAgg{ BaseAgg: newBaseAgg("avg", name, field), } } +// Missing sets the value to provide for documents missing a value for the +// selected field. func (agg *AvgAgg) Missing(val interface{}) *AvgAgg { agg.Miss = val return agg } -/******************************************************************************* - * Weighed Avg Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-weight-avg-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// WeightedAvgAgg represents an aggregation of type "weighted_avg", as described +// in https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-weight-avg-aggregation.html type WeightedAvgAgg struct { name string apiName string - Val *BaseAggParams `structs:"value"` - Weig *BaseAggParams `structs:"weight"` + + // Val is the value component of the aggregation + Val *BaseAggParams `structs:"value"` + + // Weig is the weight component of the aggregation + Weig *BaseAggParams `structs:"weight"` } +// WeightedAvg creates a new aggregation of type "weighted_agg" with the +// provided name. func WeightedAvg(name string) *WeightedAvgAgg { return &WeightedAvgAgg{ name: name, @@ -74,10 +90,13 @@ func WeightedAvg(name string) *WeightedAvgAgg { } } +// Name returns the name of the aggregation. func (agg *WeightedAvgAgg) Name() string { return agg.name } +// Value sets the value field and optionally a value to use when records are +// missing a value for the field. func (agg *WeightedAvgAgg) Value(field string, missing ...interface{}) *WeightedAvgAgg { agg.Val = new(BaseAggParams) agg.Val.Field = field @@ -87,6 +106,8 @@ func (agg *WeightedAvgAgg) Value(field string, missing ...interface{}) *Weighted return agg } +// Value sets the weight field and optionally a value to use when records are +// missing a value for the field. func (agg *WeightedAvgAgg) Weight(field string, missing ...interface{}) *WeightedAvgAgg { agg.Weig = new(BaseAggParams) agg.Weig.Field = field @@ -96,227 +117,279 @@ func (agg *WeightedAvgAgg) Weight(field string, missing ...interface{}) *Weighte return agg } +// Map returns a map representation of the aggregation, thus implementing the +// Mappable interface. func (agg *WeightedAvgAgg) Map() map[string]interface{} { return map[string]interface{}{ agg.apiName: structs.Map(agg), } } -/******************************************************************************* - * Cardinality Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-cardinality-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// CardinalityAgg represents an aggregation of type "cardinality", as described +// in https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-cardinality-aggregation.html type CardinalityAgg struct { - *BaseAgg `structs:",flatten"` + *BaseAgg `structs:",flatten"` + + // PrecisionThr is the precision threshold of the aggregation PrecisionThr uint16 `structs:"precision_threshold,omitempty"` } +// Cardinality creates a new aggregation of type "cardinality" with the provided +// name and on the provided field. func Cardinality(name, field string) *CardinalityAgg { return &CardinalityAgg{ BaseAgg: newBaseAgg("cardinality", name, field), } } +// Missing sets the value to provide for records that are missing a value for +// the field. func (agg *CardinalityAgg) Missing(val interface{}) *CardinalityAgg { agg.Miss = val return agg } +// PrecisionThreshold sets the precision threshold of the aggregation. func (agg *CardinalityAgg) PrecisionThreshold(val uint16) *CardinalityAgg { agg.PrecisionThr = val return agg } +// Map returns a map representation of the aggregation, thus implementing the +// Mappable interface func (agg *CardinalityAgg) Map() map[string]interface{} { return map[string]interface{}{ agg.apiName: structs.Map(agg), } } -/******************************************************************************* - * Max Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-max-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// MaxAgg represents an aggregation of type "max", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-max-aggregation.html type MaxAgg struct { *BaseAgg `structs:",flatten"` } +// Max creates a new aggregation of type "max", with the provided name and on +// the provided field. func Max(name, field string) *MaxAgg { return &MaxAgg{ BaseAgg: newBaseAgg("max", name, field), } } +// Missing sets the value to provide for records that are missing a value for +// the field. func (agg *MaxAgg) Missing(val interface{}) *MaxAgg { agg.Miss = val return agg } -/******************************************************************************* - * Min Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-min-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// MinAgg represents an aggregation of type "min", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-min-aggregation.html type MinAgg struct { *BaseAgg `structs:",flatten"` } +// Min creates a new aggregation of type "min", with the provided name and on +// the provided field. func Min(name, field string) *MinAgg { return &MinAgg{ BaseAgg: newBaseAgg("min", name, field), } } +// Missing sets the value to provide for records that are missing a value for +// the field. func (agg *MinAgg) Missing(val interface{}) *MinAgg { agg.Miss = val return agg } -/******************************************************************************* - * Sum Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-sum-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// SumAgg represents an aggregation of type "sum", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-sum-aggregation.html type SumAgg struct { *BaseAgg `structs:",flatten"` } +// Sum creates a new aggregation of type "sum", with the provided name and on +// the provided field. func Sum(name, field string) *SumAgg { return &SumAgg{ BaseAgg: newBaseAgg("sum", name, field), } } +// Missing sets the value to provide for records that are missing a value for +// the field. func (agg *SumAgg) Missing(val interface{}) *SumAgg { agg.Miss = val return agg } -/******************************************************************************* - * Value Count Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-valuecount-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// ValueCountAgg represents an aggregation of type "value_count", as described +// in https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-valuecount-aggregation.html type ValueCountAgg struct { *BaseAgg `structs:",flatten"` } +// ValueCount creates a new aggregation of type "value_count", with the provided +// name and on the provided field func ValueCount(name, field string) *ValueCountAgg { return &ValueCountAgg{ BaseAgg: newBaseAgg("value_count", name, field), } } -/******************************************************************************* - * Percentiles Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-percentile-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// PercentilesAgg represents an aggregation of type "percentiles", as described +// in https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-percentile-aggregation.html type PercentilesAgg struct { *BaseAgg `structs:",flatten"` - Prcnts []float32 `structs:"percents,omitempty"` - Key *bool `structs:"keyed,omitempty"` - TDigest struct { + + // Prcnts is the aggregation's percentages + Prcnts []float32 `structs:"percents,omitempty"` + + // Key denotes whether the aggregation is keyed or not + Key *bool `structs:"keyed,omitempty"` + + // TDigest includes options for the TDigest algorithm + TDigest struct { + // Compression is the compression level to use Compression uint16 `structs:"compression,omitempty"` } `structs:"tdigest,omitempty"` + + // HDR includes options for the HDR implementation HDR struct { + // NumHistogramDigits defines the resolution of values for the histogram + // in number of significant digits NumHistogramDigits uint8 `structs:"number_of_significant_value_digits,omitempty"` } `structs:"hdr,omitempty"` } +// Percentiles creates a new aggregation of type "percentiles" with the provided +// name and on the provided field. func Percentiles(name, field string) *PercentilesAgg { return &PercentilesAgg{ BaseAgg: newBaseAgg("percentiles", name, field), } } +// Percents sets the aggregation's percentages func (agg *PercentilesAgg) Percents(percents ...float32) *PercentilesAgg { agg.Prcnts = percents return agg } +// Missing sets the value to provide for records that are missing a value for +// the field. func (agg *PercentilesAgg) Missing(val interface{}) *PercentilesAgg { agg.Miss = val return agg } +// Keyed sets whether the aggregate is keyed or not. func (agg *PercentilesAgg) Keyed(b bool) *PercentilesAgg { agg.Key = &b return agg } +// Compression sets the compression level for the aggregation. func (agg *PercentilesAgg) Compression(val uint16) *PercentilesAgg { agg.TDigest.Compression = val return agg } +// NumHistogramDigits specifies the resolution of values for the histogram in +// number of significant digits. func (agg *PercentilesAgg) NumHistogramDigits(val uint8) *PercentilesAgg { agg.HDR.NumHistogramDigits = val return agg } +// Map returns a map representation of the aggregation, thus implementing the +// Mappable interface. func (agg *PercentilesAgg) Map() map[string]interface{} { return map[string]interface{}{ agg.apiName: structs.Map(agg), } } -/******************************************************************************* - * Stats Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-stats-aggregation.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// StatsAgg represents an aggregation of type "stats", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-stats-aggregation.html type StatsAgg struct { *BaseAgg `structs:",flatten"` } +// Stats creates a new "stats" aggregation with the provided name and on the +// provided field. func Stats(name, field string) *StatsAgg { return &StatsAgg{ BaseAgg: newBaseAgg("stats", name, field), } } +// Missing sets the value to provide for records missing a value for the field. func (agg *StatsAgg) Missing(val interface{}) *StatsAgg { agg.Miss = val return agg } -/******************************************************************************* - * String Stats Aggregation - * https://www.elastic.co/guide/en/elasticsearch/reference/ - * current/search-aggregations-metrics-string-stats-aggregation.html - ******************************************************************************/ +// ---------------------------------------------------------------------------// +// StringStatsAgg represents an aggregation of type "string_stats", as described +// in https://www.elastic.co/guide/en/elasticsearch/reference/ +// current/search-aggregations-metrics-string-stats-aggregation.html type StringStatsAgg struct { *BaseAgg `structs:",flatten"` + + // ShowDist indicates whether to ask ElasticSearch to return a probability + // distribution for all characters ShowDist *bool `structs:"show_distribution,omitempty"` } +// StringStats creates a new "string_stats" aggregation with the provided name +// and on the provided field. func StringStats(name, field string) *StringStatsAgg { return &StringStatsAgg{ BaseAgg: newBaseAgg("string_stats", name, field), } } +// Missing sets the value to provide for records missing a value for the field. func (agg *StringStatsAgg) Missing(val interface{}) *StringStatsAgg { agg.Miss = val return agg } +// ShowDistribution sets whether to show the probability distribution for all +// characters func (agg *StringStatsAgg) ShowDistribution(b bool) *StringStatsAgg { agg.ShowDist = &b return agg } +// Map returns a map representation of the aggregation, thus implementing the +// Mappable interface. func (agg *StringStatsAgg) Map() map[string]interface{} { return map[string]interface{}{ agg.apiName: structs.Map(agg), diff --git a/custom.go b/custom.go new file mode 100644 index 0000000..608bed4 --- /dev/null +++ b/custom.go @@ -0,0 +1,63 @@ +package esquery + +import ( + "github.com/elastic/go-elasticsearch/v7" + "github.com/elastic/go-elasticsearch/v7/esapi" +) + +// CustomQueryMap represents an arbitrary query map for custom queries. +type CustomQueryMap map[string]interface{} + +// CustomQuery generates a custom request of type "query" from an arbitrary map +// provided by the user. It is useful for issuing a search request with a syntax +// that is not yet supported by the library. CustomQuery values are versatile, +// they can either be used as parameters for the library's Query function, or +// standlone by invoking their Run method. +func CustomQuery(m map[string]interface{}) *CustomQueryMap { + q := CustomQueryMap(m) + return &q +} + +// Map returns the custom query as a map[string]interface{}, thus implementing +// the Mappable interface. +func (m *CustomQueryMap) Map() map[string]interface{} { + return map[string]interface{}(*m) +} + +// Run executes the custom query 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 (m *CustomQueryMap) Run( + api *elasticsearch.Client, + o ...func(*esapi.SearchRequest), +) (res *esapi.Response, err error) { + return Query(m).Run(api, o...) +} + +//----------------------------------------------------------------------------// + +// CustomAggMap represents an arbitrary aggregation map for custom aggregations. +type CustomAggMap struct { + name string + agg map[string]interface{} +} + +// CustomAgg generates a custom aggregation from an arbitrary map provided by +// the user. +func CustomAgg(name string, m map[string]interface{}) *CustomAggMap { + return &CustomAggMap{ + name: name, + agg: m, + } +} + +// Name returns the name of the aggregation +func (agg *CustomAggMap) Name() string { + return agg.name +} + +// Map returns a map representation of the custom aggregation, thus implementing +// the Mappable interface +func (agg *CustomAggMap) Map() map[string]interface{} { + return agg.agg +} diff --git a/aggs_custom_test.go b/custom_test.go similarity index 64% rename from aggs_custom_test.go rename to custom_test.go index a807f5f..f4d5e59 100644 --- a/aggs_custom_test.go +++ b/custom_test.go @@ -2,6 +2,26 @@ package esquery import "testing" +func TestCustomQuery(t *testing.T) { + m := map[string]interface{}{ + "geo_distance": map[string]interface{}{ + "distance": "200km", + "pin.location": map[string]interface{}{ + "lat": 40, + "lon": -70, + }, + }, + } + + runMapTests(t, []mapTest{ + { + "custom query", + CustomQuery(m), + m, + }, + }) +} + func TestCustomAgg(t *testing.T) { m := map[string]interface{}{ "genres": map[string]interface{}{ @@ -28,7 +48,7 @@ func TestCustomAgg(t *testing.T) { runMapTests(t, []mapTest{ { "custom aggregation", - CustomAgg(m), + CustomAgg("custom_agg", m), m, }, }) diff --git a/es.go b/es.go index 3c21c4b..ee269a6 100644 --- a/es.go +++ b/es.go @@ -1,5 +1,107 @@ +// Package esquery provides a non-obtrusive, idiomatic and easy-to-use query +// and aggregation builder for the official Go client +// (https://github.com/elastic/go-elasticsearch) for the ElasticSearch +// database (https://www.elastic.co/products/elasticsearch). +// +// esquery alleviates the need to use extremely nested maps +// (map[string]interface{}) and serializing queries to JSON manually. It also +// helps eliminating common mistakes such as misspelling query types, as +// everything is statically typed. +// +// Using `esquery` can make your code much easier to write, read and maintain, +// and significantly reduce the amount of code you write. +// +// +// +// Usage +// +// +// +// esquery provides a method chaining-style API for building and executing +// queries and aggregations. It does not wrap the official Go client nor does it +// require you to change your existing code in order to integrate the library. +// Queries can be directly built with `esquery`, and executed by passing an +// `*elasticsearch.Client` instance (with optional search parameters). Results +// are returned as-is from the official client (e.g. `*esapi.Response` objects). +// +// Getting started is extremely simple: +// +// package main +// +// import ( +// "context" +// "log" +// +// "github.com/aquasecurity/esquery" +// "github.com/elastic/go-elasticsearch/v7" +// ) +// +// func main() { +// // connect to an ElasticSearch instance +// es, err := elasticsearch.NewDefaultClient() +// if err != nil { +// log.Fatalf("Failed creating client: %s", err) +// } +// +// // run a boolean search query +// qRes, err := esquery.Query( +// esquery. +// Bool(). +// Must(esquery.Term("title", "Go and Stuff")). +// Filter(esquery.Term("tag", "tech")), +// ).Run( +// es, +// es.Search.WithContext(context.TODO()), +// es.Search.WithIndex("test"), +// ) +// if err != nil { +// log.Fatalf("Failed searching for stuff: %s", err) +// } +// +// defer qRes.Body.Close() +// +// // run an aggregation +// aRes, err := esquery.Aggregate( +// esquery.Avg("average_score", "score"), +// esquery.Max("max_score", "score"), +// ).Run( +// es, +// es.Search.WithContext(context.TODO()), +// es.Search.WithIndex("test"), +// ) +// if err != nil { +// log.Fatalf("Failed searching for stuff: %s", err) +// } +// +// defer aRes.Body.Close() +// +// // ... +// } +// +// +// +// Notes +// +// +// +//* esquery currently supports version 7 of the ElasticSearch Go client. +//* The library cannot currently generate "short queries". For example, +// whereas ElasticSearch can accept this: +// +// { "query": { "term": { "user": "Kimchy" } } } +// +// The library will always generate this: +// +// { "query": { "term": { "user": { "value": "Kimchy" } } } } +// +// This is also true for queries such as "bool", where fields like "must" can +// either receive one query object, or an array of query objects. `esquery` will +// generate an array even if there's only one query object. package esquery +// Mappable is the interface implemented by the various query and aggregation +// types provided by the package. It allows the library to easily transform the +// different queries to "generic" maps that can be easily encoded to JSON. type Mappable interface { Map() map[string]interface{} } diff --git a/queries.go b/queries.go index e4edfa7..1a1762a 100644 --- a/queries.go +++ b/queries.go @@ -8,24 +8,38 @@ import ( "github.com/elastic/go-elasticsearch/v7/esapi" ) +// QueryRequest represents a complete request of type "query" to ElasticSearch's +// search API. It simply wraps a value of a type that implements the Mappable +// interface. type QueryRequest struct { Query Mappable } +// Query generates a search request of type "query", represented by a +// *QueryRequest object. It receives any query type that implements the +// Mappable interface, whether provided internally by the library or custom +// types provided by consuming code. func Query(q Mappable) *QueryRequest { return &QueryRequest{q} } +// Map implements the Mappable interface. It converts the "query" request into a +// (potentially nested) map[string]interface{}. func (req *QueryRequest) Map() map[string]interface{} { return map[string]interface{}{ "query": req.Query.Map(), } } +// MarshalJSON implements the json.Marshaler interface, it simply encodes the +// map representation of the query (provided by the Map method) as JSON. func (req *QueryRequest) 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 *QueryRequest) Run( api *elasticsearch.Client, o ...func(*esapi.SearchRequest), @@ -40,3 +54,24 @@ func (req *QueryRequest) Run( return api.Search(opts...) } + +// 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 *QueryRequest) 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...) +} diff --git a/queries_test.go b/queries_test.go index 029f042..432e0ed 100644 --- a/queries_test.go +++ b/queries_test.go @@ -23,7 +23,7 @@ func TestQueryMaps(t *testing.T) { Range("date"). Gt("some time in the past"). Lte("now"). - Relation(CONTAINS). + Relation(RangeContains). TimeZone("Asia/Jerusalem"). Boost(2.3), diff --git a/query_boolean.go b/query_boolean.go index 3e93c28..18acb92 100644 --- a/query_boolean.go +++ b/query_boolean.go @@ -2,11 +2,8 @@ package esquery import "github.com/fatih/structs" -/******************************************************************************* - * Boolean Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html - ******************************************************************************/ - +// BoolQuery represents a compound query of type "bool", as described in +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html type BoolQuery struct { must []Mappable filter []Mappable @@ -16,40 +13,54 @@ type BoolQuery struct { boost float32 } +// Bool creates a new compound query of type "bool". func Bool() *BoolQuery { return &BoolQuery{} } +// Must adds one or more queries of type "must" to the bool query. Must can be +// called multiple times, queries will be appended to existing ones. func (q *BoolQuery) Must(must ...Mappable) *BoolQuery { q.must = append(q.must, must...) return q } +// Filter adds one or more queries of type "filter" to the bool query. Filter +// can be called multiple times, queries will be appended to existing ones. func (q *BoolQuery) Filter(filter ...Mappable) *BoolQuery { q.filter = append(q.filter, filter...) return q } +// Must adds one or more queries of type "must_not" to the bool query. MustNot +// can be called multiple times, queries will be appended to existing ones. func (q *BoolQuery) MustNot(mustnot ...Mappable) *BoolQuery { q.mustNot = append(q.mustNot, mustnot...) return q } +// Should adds one or more queries of type "should" to the bool query. Should can be +// called multiple times, queries will be appended to existing ones. func (q *BoolQuery) Should(should ...Mappable) *BoolQuery { q.should = append(q.should, should...) return q } +// MinimumShouldMatch sets the number or percentage of should clauses returned +// documents must match. func (q *BoolQuery) MinimumShouldMatch(val int16) *BoolQuery { q.minimumShouldMatch = val return q } +// Boost sets the boost value for the query. func (q *BoolQuery) Boost(val float32) *BoolQuery { q.boost = val return q } +// Map returns a map representation of the bool query, thus implementing +// the Mappable interface. func (q *BoolQuery) Map() map[string]interface{} { var data struct { Must []map[string]interface{} `structs:"must,omitempty"` diff --git a/query_boosting.go b/query_boosting.go index 9342164..24d2c9d 100644 --- a/query_boosting.go +++ b/query_boosting.go @@ -1,35 +1,41 @@ package esquery -/******************************************************************************* - * Boosting Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html - ******************************************************************************/ - +// BoostingQuery represents a compound query of type "boosting", as described in +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html type BoostingQuery struct { - Pos Mappable - Neg Mappable + // Pos is the positive part of the query. + Pos Mappable + // Neg is the negative part of the query. + Neg Mappable + // NegBoost is the negative boost value. NegBoost float32 } +// Boosting creates a new compound query of type "boosting". func Boosting() *BoostingQuery { return &BoostingQuery{} } +// Positive sets the positive part of the boosting query. func (q *BoostingQuery) Positive(p Mappable) *BoostingQuery { q.Pos = p return q } +// Negative sets the negative part of the boosting query. func (q *BoostingQuery) Negative(p Mappable) *BoostingQuery { q.Neg = p return q } +// NegativeBoost sets the negative boost value. func (q *BoostingQuery) NegativeBoost(b float32) *BoostingQuery { q.NegBoost = b return q } +// Map returns a map representation of the boosting query, thus implementing +// the Mappable interface. func (q *BoostingQuery) Map() map[string]interface{} { return map[string]interface{}{ "boosting": map[string]interface{}{ diff --git a/query_constant_score.go b/query_constant_score.go index cf65090..c93b74e 100644 --- a/query_constant_score.go +++ b/query_constant_score.go @@ -2,27 +2,30 @@ package esquery import "github.com/fatih/structs" -/******************************************************************************* - * Constant Score Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html - ******************************************************************************/ - +// ConstantScoreQuery represents a compound query of type "constant_score", as +// described in +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html type ConstantScoreQuery struct { filter Mappable boost float32 } +// ConstantScore creates a new query of type "contant_score" with the provided +// filter query. func ConstantScore(filter Mappable) *ConstantScoreQuery { return &ConstantScoreQuery{ filter: filter, } } +// Boost sets the boost value of the query. func (q *ConstantScoreQuery) Boost(b float32) *ConstantScoreQuery { q.boost = b return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *ConstantScoreQuery) Map() map[string]interface{} { return map[string]interface{}{ "constant_score": structs.Map(struct { diff --git a/query_custom.go b/query_custom.go deleted file mode 100644 index 8c69dae..0000000 --- a/query_custom.go +++ /dev/null @@ -1,13 +0,0 @@ -package esquery - -type CustomQry struct { - m map[string]interface{} -} - -func CustomQuery(m map[string]interface{}) *CustomQry { - return &CustomQry{m} -} - -func (q *CustomQry) Map() map[string]interface{} { - return q.m -} diff --git a/query_custom_test.go b/query_custom_test.go deleted file mode 100644 index 34ecfae..0000000 --- a/query_custom_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package esquery - -import "testing" - -func TestCustomQuery(t *testing.T) { - m := map[string]interface{}{ - "geo_distance": map[string]interface{}{ - "distance": "200km", - "pin.location": map[string]interface{}{ - "lat": 40, - "lon": -70, - }, - }, - } - - runMapTests(t, []mapTest{ - { - "custom query", - CustomQuery(m), - m, - }, - }) -} diff --git a/query_dis_max.go b/query_dis_max.go index 2889e3b..e0fda44 100644 --- a/query_dis_max.go +++ b/query_dis_max.go @@ -2,27 +2,29 @@ package esquery import "github.com/fatih/structs" -/******************************************************************************* - * Disjunction Max Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html - ******************************************************************************/ - +// DisMaxQuery represents a compound query of type "dis_max", as described in +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html type DisMaxQuery struct { queries []Mappable tieBreaker float32 } +// DisMax creates a new compound query of type "dis_max" with the provided +// queries. func DisMax(queries ...Mappable) *DisMaxQuery { return &DisMaxQuery{ queries: queries, } } +// TieBreaker sets the "tie_breaker" value for the query. func (q *DisMaxQuery) TieBreaker(b float32) *DisMaxQuery { q.tieBreaker = b return q } +// Map returns a map representation of the dis_max query, thus implementing +// the Mappable interface. func (q *DisMaxQuery) Map() map[string]interface{} { inner := make([]map[string]interface{}, len(q.queries)) for i, iq := range q.queries { diff --git a/query_match.go b/query_match.go index 0341746..f34779b 100644 --- a/query_match.go +++ b/query_match.go @@ -1,37 +1,45 @@ package esquery import ( - "bytes" - "io" - "github.com/fatih/structs" ) -/******************************************************************************* - * Match Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html - ******************************************************************************/ type matchType uint8 const ( + // TypeMatch denotes a query of type "match" TypeMatch matchType = iota + + // TypeMatchBool denotes a query of type "match_bool_prefix" TypeMatchBoolPrefix + + // TypeMatchPhrase denotes a query of type "match_phrase" TypeMatchPhrase + + // TypeMatchPhrasePrefix denotes a query of type "match_phrase_prefix" TypeMatchPhrasePrefix ) +// MatchQuery represents a query of type "match", "match_bool_prefix", +// "match_phrase" and "match_phrase_prefix". While all four share the same +// general structure, they don't necessarily support all the same options. The +// library does not attempt to verify provided options are supported. +// See the ElasticSearch documentation for more information: +// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html +// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html +// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html +// - https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html type MatchQuery struct { field string mType matchType params matchParams } -func (a *MatchQuery) Map() map[string]interface{} { +// Map returns a map representation of the query, thus implementing the +// Mappable interface. +func (q *MatchQuery) Map() map[string]interface{} { var mType string - switch a.mType { + switch q.mType { case TypeMatch: mType = "match" case TypeMatchBoolPrefix: @@ -44,7 +52,7 @@ func (a *MatchQuery) Map() map[string]interface{} { return map[string]interface{}{ mType: map[string]interface{}{ - a.field: structs.Map(a.params), + q.field: structs.Map(q.params), }, } } @@ -65,18 +73,30 @@ type matchParams struct { Slp uint16 `structs:"slop,omitempty"` // only relevant for match_phrase query } +// Match creates a new query of type "match" with the provided field name. +// A comparison value can optionally be provided to quickly create a simple +// query such as { "match": { "message": "this is a test" } } func Match(fieldName string, simpleQuery ...interface{}) *MatchQuery { return newMatch(TypeMatch, fieldName, simpleQuery...) } +// MatchBoolPrefix creates a new query of type "match_bool_prefix" with the +// provided field name. A comparison value can optionally be provided to quickly +// create a simple query such as { "match": { "message": "this is a test" } } func MatchBoolPrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery { return newMatch(TypeMatchBoolPrefix, fieldName, simpleQuery...) } +// MatchPhrase creates a new query of type "match_phrase" with the +// provided field name. A comparison value can optionally be provided to quickly +// create a simple query such as { "match": { "message": "this is a test" } } func MatchPhrase(fieldName string, simpleQuery ...interface{}) *MatchQuery { return newMatch(TypeMatchPhrase, fieldName, simpleQuery...) } +// MatchPhrasePrefix creates a new query of type "match_phrase_prefix" with the +// provided field name. A comparison value can optionally be provided to quickly +// create a simple query such as { "match": { "message": "this is a test" } } func MatchPhrasePrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery { return newMatch(TypeMatchPhrasePrefix, fieldName, simpleQuery...) } @@ -96,106 +116,136 @@ func newMatch(mType matchType, fieldName string, simpleQuery ...interface{}) *Ma } } +// Query sets the data to find in the query's field (it is the "query" component +// of the query). func (q *MatchQuery) Query(data interface{}) *MatchQuery { q.params.Qry = data return q } +// Analyzer sets the analyzer used to convert the text in the "query" value into +// tokens. func (q *MatchQuery) Analyzer(a string) *MatchQuery { q.params.Anl = a return q } +// AutoGenerateSynonymsPhraseQuery sets the "auto_generate_synonyms_phrase_query" +// boolean. func (q *MatchQuery) AutoGenerateSynonymsPhraseQuery(b bool) *MatchQuery { q.params.AutoGenerate = &b return q } +// Fuzziness set the maximum edit distance allowed for matching. func (q *MatchQuery) Fuzziness(f string) *MatchQuery { q.params.Fuzz = f return q } +// MaxExpansions sets the maximum number of terms to which the query will expand. func (q *MatchQuery) MaxExpansions(e uint16) *MatchQuery { q.params.MaxExp = e return q } +// PrefixLength sets the number of beginning characters left unchanged for fuzzy +// matching. func (q *MatchQuery) PrefixLength(l uint16) *MatchQuery { q.params.PrefLen = l return q } +// Transpositions sets whether edits for fuzzy matching include transpositions +// of two adjacent characters. func (q *MatchQuery) Transpositions(b bool) *MatchQuery { q.params.Trans = &b return q } +// FuzzyRewrite sets the method used to rewrite the query. func (q *MatchQuery) FuzzyRewrite(s string) *MatchQuery { q.params.FuzzyRw = s return q } +// Lenient sets whether format-based errors should be ignored. func (q *MatchQuery) Lenient(b bool) *MatchQuery { q.params.Lent = b return q } +// Operator sets the boolean logic used to interpret text in the query value. func (q *MatchQuery) Operator(op MatchOperator) *MatchQuery { q.params.Op = op return q } +// MinimumShouldMatch sets the minimum number of clauses that must match for a +// document to be returned. func (q *MatchQuery) MinimumShouldMatch(s string) *MatchQuery { q.params.MinMatch = s return q } +// Slop sets the maximum number of positions allowed between matching tokens. func (q *MatchQuery) Slop(n uint16) *MatchQuery { q.params.Slp = n return q } +// ZeroTermsQuery sets the "zero_terms_query" option to use. This indicates +// whether no documents are returned if the analyzer removes all tokens, such as +// when using a stop filter. func (q *MatchQuery) ZeroTermsQuery(s ZeroTerms) *MatchQuery { q.params.ZeroTerms = s return q } -func (q *MatchQuery) Reader() io.Reader { - var b bytes.Buffer - return &b -} - +// MatchOperator is an enumeration type representing supported values for a +// match query's "operator" parameter. type MatchOperator uint8 const ( - OR MatchOperator = iota - AND + // OperatorOr is the "or" operator + OperatorOr MatchOperator = iota + + // OperatorAnd is the "and" operator + OperatorAnd ) +// String returns a string representation of the match operator, as known to +// ElasticSearch. func (a MatchOperator) String() string { switch a { - case OR: - return "or" - case AND: - return "and" + case OperatorOr: + return "OR" + case OperatorAnd: + return "AND" default: return "" } } +// ZeroTerms is an enumeration type representing supported values for a match +// query's "zero_terms_query" parameter. type ZeroTerms uint8 const ( - None ZeroTerms = iota - All + // ZeroTermsNone is the "none" value + ZeroTermsNone ZeroTerms = iota + + // ZeroTermsAll is the "all" value + ZeroTermsAll ) +// String returns a string representation of the zero_terms_query parameter, as +// known to ElasticSearch. func (a ZeroTerms) String() string { switch a { - case None: + case ZeroTermsNone: return "none" - case All: + case ZeroTermsAll: return "all" default: return "" diff --git a/query_match_all.go b/query_match_all.go index 0d7f25a..e2c1161 100644 --- a/query_match_all.go +++ b/query_match_all.go @@ -2,11 +2,9 @@ package esquery import "github.com/fatih/structs" -/******************************************************************************* - * Match All Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html - ******************************************************************************/ - +// MatchAllQuery represents a query of type "match_all" or "match_none", as +// described in +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html type MatchAllQuery struct { all bool params matchAllParams @@ -16,9 +14,11 @@ type matchAllParams struct { Boost float32 `structs:"boost,omitempty"` } -func (a *MatchAllQuery) Map() map[string]interface{} { +// Map returns a map representation of the query, thus implementing the +// Mappable interface. +func (q *MatchAllQuery) Map() map[string]interface{} { var mType string - switch a.all { + switch q.all { case true: mType = "match_all" default: @@ -26,14 +26,16 @@ func (a *MatchAllQuery) Map() map[string]interface{} { } return map[string]interface{}{ - mType: structs.Map(a.params), + mType: structs.Map(q.params), } } +// MatchAll creates a new query of type "match_all". func MatchAll() *MatchAllQuery { return &MatchAllQuery{all: true} } +// Boost assigns a score boost for documents matching the query. func (q *MatchAllQuery) Boost(b float32) *MatchAllQuery { if q.all { q.params.Boost = b @@ -41,6 +43,7 @@ func (q *MatchAllQuery) Boost(b float32) *MatchAllQuery { return q } +// MatchNone creates a new query of type "match_none". func MatchNone() *MatchAllQuery { return &MatchAllQuery{all: false} } diff --git a/query_match_test.go b/query_match_test.go index 3a3d290..238c68d 100644 --- a/query_match_test.go +++ b/query_match_test.go @@ -19,14 +19,14 @@ func TestMatch(t *testing.T) { }, { "match with more params", - Match("issue_number").Query(16).Transpositions(false).MaxExpansions(32).Operator(AND), + Match("issue_number").Query(16).Transpositions(false).MaxExpansions(32).Operator(OperatorAnd), map[string]interface{}{ "match": map[string]interface{}{ "issue_number": map[string]interface{}{ "query": 16, "max_expansions": 32, "transpositions": false, - "operator": "and", + "operator": "AND", }, }, }, diff --git a/query_term_level.go b/query_term_level.go index 82e3adc..2604d3e 100644 --- a/query_term_level.go +++ b/query_term_level.go @@ -4,61 +4,70 @@ import ( "github.com/fatih/structs" ) -/******************************************************************************* - * Exists Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html - ******************************************************************************/ - +// ExistsQuery represents a query of type "exists", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html type ExistsQuery struct { + // Field is the name of the field to check for existence Field string `structs:"field"` } +// Exists creates a new query of type "exists" on the provided field. func Exists(field string) *ExistsQuery { return &ExistsQuery{field} } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *ExistsQuery) Map() map[string]interface{} { return map[string]interface{}{ "exists": structs.Map(q), } } -/******************************************************************************* - * IDs Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// IDsQuery represents a query of type "ids", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html type IDsQuery struct { + // IDs is the "ids" component of the query IDs struct { + // Values is the list of ID values Values []string `structs:"values"` } `structs:"ids"` } +// IDs creates a new query of type "ids" with the provided values. func IDs(vals ...string) *IDsQuery { q := &IDsQuery{} q.IDs.Values = vals return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *IDsQuery) Map() map[string]interface{} { return structs.Map(q) } -/******************************************************************************* - * Prefix Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// PrefixQuery represents query of type "prefix", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html type PrefixQuery struct { field string params prefixQueryParams } type prefixQueryParams struct { - Value string `structs:"value"` + // Value is the prefix value to look for + Value string `structs:"value"` + + // Rewrite is the method used to rewrite the query Rewrite string `structs:"rewrite,omitempty"` } +// Prefix creates a new query of type "prefix", on the provided field and using +// the provided prefix value. func Prefix(field, value string) *PrefixQuery { return &PrefixQuery{ field: field, @@ -66,11 +75,14 @@ func Prefix(field, value string) *PrefixQuery { } } +// Rewrite sets the rewrite method for the query func (q *PrefixQuery) Rewrite(s string) *PrefixQuery { q.params.Rewrite = s return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *PrefixQuery) Map() map[string]interface{} { return map[string]interface{}{ "prefix": map[string]interface{}{ @@ -79,11 +91,10 @@ func (q *PrefixQuery) Map() map[string]interface{} { } } -/******************************************************************************* - * Range Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// RangeQuery represents a query of type "range", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html type RangeQuery struct { field string params rangeQueryParams @@ -100,50 +111,63 @@ type rangeQueryParams struct { Boost float32 `structs:"boost,omitempty"` } +// Range creates a new query of type "range" on the provided field func Range(field string) *RangeQuery { return &RangeQuery{field: field} } +// Gt sets that the value of field must be greater than the provided value func (a *RangeQuery) Gt(val interface{}) *RangeQuery { a.params.Gt = val return a } +// Gt sets that the value of field must be greater than or equal to the provided +// value func (a *RangeQuery) Gte(val interface{}) *RangeQuery { a.params.Gte = val return a } +// Lt sets that the value of field must be lower than the provided value func (a *RangeQuery) Lt(val interface{}) *RangeQuery { a.params.Lt = val return a } +// Lte sets that the value of field must be lower than or equal to the provided +// value func (a *RangeQuery) Lte(val interface{}) *RangeQuery { a.params.Lte = val return a } +// Format sets the date format for date values func (a *RangeQuery) Format(f string) *RangeQuery { a.params.Format = f return a } +// Relation sets how the query matches values for range fields func (a *RangeQuery) Relation(r RangeRelation) *RangeQuery { a.params.Relation = r return a } +// TimeZone sets the time zone used for date values. func (a *RangeQuery) TimeZone(zone string) *RangeQuery { a.params.TimeZone = zone return a } +// Boost sets the boost value of the query. func (a *RangeQuery) Boost(b float32) *RangeQuery { a.params.Boost = b return a } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (a *RangeQuery) Map() map[string]interface{} { return map[string]interface{}{ "range": map[string]interface{}{ @@ -152,32 +176,41 @@ func (a *RangeQuery) Map() map[string]interface{} { } } +// RangeRelation is an enumeration type for a range query's "relation" field type RangeRelation uint8 const ( - INTERSECTS RangeRelation = iota - CONTAINS - WITHIN + _ RangeRelation = iota + + // RangeIntersects is the "INTERSECTS" relation + RangeIntersects + + // RangeContains is the "CONTAINS" relation + RangeContains + + // RangeWithin is the "WITHIN" relation + RangeWithin ) +// String returns a string representation of the RangeRelation value, as +// accepted by ElasticSearch func (a RangeRelation) String() string { switch a { - case INTERSECTS: + case RangeIntersects: return "INTERSECTS" - case CONTAINS: + case RangeContains: return "CONTAINS" - case WITHIN: + case RangeWithin: return "WITHIN" default: return "" } } -/******************************************************************************* - * Regexp Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// RegexpQuery represents a query of type "regexp", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html type RegexpQuery struct { field string wildcard bool @@ -191,6 +224,8 @@ type regexpQueryParams struct { Rewrite string `structs:"rewrite,omitempty"` } +// Regexp creates a new query of type "regexp" on the provided field and using +// the provided regular expression. func Regexp(field, value string) *RegexpQuery { return &RegexpQuery{ field: field, @@ -200,11 +235,13 @@ func Regexp(field, value string) *RegexpQuery { } } +// Value changes the regular expression value of the query. func (q *RegexpQuery) Value(v string) *RegexpQuery { q.params.Value = v return q } +// Flags sets the regular expression's optional flags. func (q *RegexpQuery) Flags(f string) *RegexpQuery { if !q.wildcard { q.params.Flags = f @@ -212,6 +249,8 @@ func (q *RegexpQuery) Flags(f string) *RegexpQuery { return q } +// MaxDeterminizedStates sets the maximum number of automaton states required +// for the query. func (q *RegexpQuery) MaxDeterminizedStates(m uint16) *RegexpQuery { if !q.wildcard { q.params.MaxDeterminizedStates = m @@ -219,11 +258,14 @@ func (q *RegexpQuery) MaxDeterminizedStates(m uint16) *RegexpQuery { return q } +// Rewrite sets the method used to rewrite the query. func (q *RegexpQuery) Rewrite(r string) *RegexpQuery { q.params.Rewrite = r return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *RegexpQuery) Map() map[string]interface{} { var qType string if q.wildcard { @@ -238,11 +280,13 @@ func (q *RegexpQuery) Map() map[string]interface{} { } } -/******************************************************************************* - * Wildcard Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// Wildcard creates a new query of type "wildcard" on the provided field and +// using the provided regular expression value. Internally, wildcard queries +// are simply specialized RegexpQuery values. +// Wildcard queries are described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html func Wildcard(field, value string) *RegexpQuery { return &RegexpQuery{ field: field, @@ -253,11 +297,10 @@ func Wildcard(field, value string) *RegexpQuery { } } -/******************************************************************************* - * Fuzzy Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// FuzzyQuery represents a query of type "fuzzy", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html type FuzzyQuery struct { field string params fuzzyQueryParams @@ -272,6 +315,8 @@ type fuzzyQueryParams struct { Rewrite string `structs:"rewrite,omitempty"` } +// Fuzzy creates a new query of type "fuzzy" on the provided field and using +// the provided value func Fuzzy(field, value string) *FuzzyQuery { return &FuzzyQuery{ field: field, @@ -281,36 +326,46 @@ func Fuzzy(field, value string) *FuzzyQuery { } } +// Value sets the value of the query. func (q *FuzzyQuery) Value(val string) *FuzzyQuery { q.params.Value = val return q } +// Fuzziness sets the maximum edit distance allowed for matching. func (q *FuzzyQuery) Fuzziness(fuzz string) *FuzzyQuery { q.params.Fuzziness = fuzz return q } +// MaxExpansions sets the maximum number of variations created. func (q *FuzzyQuery) MaxExpansions(m uint16) *FuzzyQuery { q.params.MaxExpansions = m return q } +// PrefixLength sets the number of beginning characters left unchanged when +// creating expansions func (q *FuzzyQuery) PrefixLength(l uint16) *FuzzyQuery { q.params.PrefixLength = l return q } +// Transpositions sets whether edits include transpositions of two adjacent +// characters. func (q *FuzzyQuery) Transpositions(b bool) *FuzzyQuery { q.params.Transpositions = &b return q } +// Rewrite sets the method used to rewrite the query. func (q *FuzzyQuery) Rewrite(s string) *FuzzyQuery { q.params.Rewrite = s return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *FuzzyQuery) Map() map[string]interface{} { return map[string]interface{}{ "fuzzy": map[string]interface{}{ @@ -319,11 +374,10 @@ func (q *FuzzyQuery) Map() map[string]interface{} { } } -/******************************************************************************* - * Term Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// TermQuery represents a query of type "term", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html type TermQuery struct { field string params termQueryParams @@ -334,6 +388,8 @@ type termQueryParams struct { Boost float32 `structs:"boost,omitempty"` } +// Term creates a new query of type "term" on the provided field and using the +// provide value func Term(field string, value interface{}) *TermQuery { return &TermQuery{ field: field, @@ -343,16 +399,20 @@ func Term(field string, value interface{}) *TermQuery { } } +// Value sets the term value for the query. func (q *TermQuery) Value(val interface{}) *TermQuery { q.params.Value = val return q } +// Boost sets the boost value of the query. func (q *TermQuery) Boost(b float32) *TermQuery { q.params.Boost = b return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q *TermQuery) Map() map[string]interface{} { return map[string]interface{}{ "term": map[string]interface{}{ @@ -361,17 +421,18 @@ func (q *TermQuery) Map() map[string]interface{} { } } -/******************************************************************************* - * Terms Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// TermsQuery represents a query of type "terms", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html type TermsQuery struct { field string values []interface{} boost float32 } +// Terms creates a new query of type "terms" on the provided field, and +// optionally with the provided term values. func Terms(field string, values ...interface{}) *TermsQuery { return &TermsQuery{ field: field, @@ -379,16 +440,20 @@ func Terms(field string, values ...interface{}) *TermsQuery { } } +// Values sets the term values for the query. func (q *TermsQuery) Values(values ...interface{}) *TermsQuery { q.values = values return q } +// Boost sets the boost value of the query. func (q *TermsQuery) Boost(b float32) *TermsQuery { q.boost = b return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q TermsQuery) Map() map[string]interface{} { innerMap := map[string]interface{}{q.field: q.values} if q.boost > 0 { @@ -398,11 +463,10 @@ func (q TermsQuery) Map() map[string]interface{} { return map[string]interface{}{"terms": innerMap} } -/******************************************************************************* - * Term Set Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html - ******************************************************************************/ +//----------------------------------------------------------------------------// +// TermsSetQuery represents a query of type "terms_set", as described in: +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html type TermsSetQuery struct { field string params termsSetQueryParams @@ -414,6 +478,8 @@ type termsSetQueryParams struct { MinimumShouldMatchScript string `structs:"minimum_should_match_script,omitempty"` } +// TermsSet creates a new query of type "terms_set" on the provided field and +// optionally using the provided terms. func TermsSet(field string, terms ...string) *TermsSetQuery { return &TermsSetQuery{ field: field, @@ -423,21 +489,28 @@ func TermsSet(field string, terms ...string) *TermsSetQuery { } } +// Terms sets the terms for the query. func (q *TermsSetQuery) Terms(terms ...string) *TermsSetQuery { q.params.Terms = terms return q } +// MinimumShouldMatchField sets the name of the field containing the number of +// matching terms required to return a document. func (q *TermsSetQuery) MinimumShouldMatchField(field string) *TermsSetQuery { q.params.MinimumShouldMatchField = field return q } +// MinimumShouldMatchScript sets the custom script containing the number of +// matching terms required to return a document. func (q *TermsSetQuery) MinimumShouldMatchScript(script string) *TermsSetQuery { q.params.MinimumShouldMatchScript = script return q } +// Map returns a map representation of the query, thus implementing the +// Mappable interface. func (q TermsSetQuery) Map() map[string]interface{} { return map[string]interface{}{ "terms_set": map[string]interface{}{ diff --git a/query_term_level_test.go b/query_term_level_test.go index 8878a68..eff83ad 100644 --- a/query_term_level_test.go +++ b/query_term_level_test.go @@ -62,13 +62,13 @@ func TestTermLevel(t *testing.T) { }, { "string range", - Range("timestamp").Gte("now-1d/d").Lt("now/d").Relation(CONTAINS), + Range("timestamp").Gte("now-1d/d").Lt("now/d").Relation(RangeIntersects), map[string]interface{}{ "range": map[string]interface{}{ "timestamp": map[string]interface{}{ "gte": "now-1d/d", "lt": "now/d", - "relation": "CONTAINS", + "relation": "INTERSECTS", }, }, },