From 9ef149ec94a1df866bf3b9d881fb9cf6c527129a Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Tue, 18 Feb 2020 14:00:15 +0200 Subject: [PATCH 1/5] Initial commit This commit is the initial commit for a Go library providing an idiomatic, easy-to-use query builder for ElasticSearch. The library can build queries and execute them using the structures from the official Go SDK provided by the ES project (https://github.com/elastic/go-elasticsearch). The library currently provides the capabilities to create and execute simple ElasticSearch queries, specifically Match queries (match, match_bool_prefix, match_phrase and match_phrase_prefix), Match All queries (match_all, match_none), and all of the Term-level queries (e.g. range, regexp, etc.). Unit tests are included for each support query, and the code is linted using golangci-lint (see enabled linters in .golangci-lint). The unit tests currently only verify the builder creates valid JSON queries and does not attempt to actually run queries against a (mock) ES instance. --- .golangci.yml | 27 +++ es.go | 41 +++++ es_test.go | 27 +++ go.mod | 5 + go.sum | 2 + match.go | 261 ++++++++++++++++++++++++++ match_all.go | 48 +++++ match_all_test.go | 13 ++ match_test.go | 15 ++ term_level.go | 449 +++++++++++++++++++++++++++++++++++++++++++++ term_level_test.go | 33 ++++ 11 files changed, 921 insertions(+) create mode 100644 .golangci.yml create mode 100644 es.go create mode 100644 es_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 match.go create mode 100644 match_all.go create mode 100644 match_all_test.go create mode 100644 match_test.go create mode 100644 term_level.go create mode 100644 term_level_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..faf3f2f --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,27 @@ +linters: + disable-all: true + enable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck + - dupl + - funlen + - goconst + - gocyclo + - gocritic + - goimports + - goprintffuncname + - gosec + - lll + - misspell + - nakedret + - prealloc + - unconvert + - unparam diff --git a/es.go b/es.go new file mode 100644 index 0000000..8d6698c --- /dev/null +++ b/es.go @@ -0,0 +1,41 @@ +package esquery + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/elastic/go-elasticsearch/esapi" +) + +type ESQuery struct { + Query json.Marshaler `json:"query"` +} + +func encode(q json.Marshaler, b *bytes.Buffer) (err error) { + b.Reset() + err = json.NewEncoder(b).Encode(q) + if err != nil { + return fmt.Errorf("failed encoding query to JSON: %w", err) + } + + return nil +} + +func search(q json.Marshaler, api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { + var b bytes.Buffer + err = encode(ESQuery{q}, &b) + if err != nil { + return res, err + } + + opts := append([]func(*esapi.SearchRequest){api.Search.WithBody(&b)}, o...) + + return api.Search(opts...) +} + +func (q ESQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]json.Marshaler{ + "query": q.Query, + }) +} diff --git a/es_test.go b/es_test.go new file mode 100644 index 0000000..fad04b1 --- /dev/null +++ b/es_test.go @@ -0,0 +1,27 @@ +package esquery + +import ( + "bytes" + "encoding/json" + "testing" +) + +type queryTest struct { + name string + q json.Marshaler + expJSON string +} + +func runTests(t *testing.T, tests []queryTest) { + for _, test := range tests { + var b bytes.Buffer + t.Run(test.name, func(t *testing.T) { + err := encode(test.q, &b) + if err != nil { + t.Errorf("unexpectedly failed: %s", err) + } else if b.String() != test.expJSON { + t.Errorf("expected %q, got %q", test.expJSON, b.String()) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3b1d837 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module bitbucket.org/scalock/esquery + +go 1.13 + +require github.com/elastic/go-elasticsearch v0.0.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..06225ee --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA= +github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg= diff --git a/match.go b/match.go new file mode 100644 index 0000000..29e36e2 --- /dev/null +++ b/match.go @@ -0,0 +1,261 @@ +package esquery + +import ( + "bytes" + "encoding/json" + "errors" + "io" + + "github.com/elastic/go-elasticsearch/esapi" +) + +/******************************************************************************* + * 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 matchType = iota + TypeMatchBoolPrefix + TypeMatchPhrase + TypeMatchPhrasePrefix +) + +type MatchQuery struct { + field string + mType matchType + params matchParams +} + +func (a MatchQuery) MarshalJSON() ([]byte, error) { + var mType string + switch a.mType { + case TypeMatch: + mType = "match" + case TypeMatchBoolPrefix: + mType = "match_bool_prefix" + case TypeMatchPhrase: + mType = "match_phrase" + case TypeMatchPhrasePrefix: + mType = "match_phrase_prefix" + } + + return json.Marshal(map[string]interface{}{ + mType: map[string]interface{}{ + a.field: a.params, + }, + }) +} + +func (a *MatchQuery) Run(api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { + return search(*a, api, o...) +} + +type matchParams struct { + Qry interface{} `json:"query"` + Anl string `json:"analyzer,omitempty"` + AutoGenerate *bool `json:"auto_generate_synonyms_phrase_query,omitempty"` + Fuzz string `json:"fuzziness,omitempty"` + MaxExp uint16 `json:"max_expansions,omitempty"` + PrefLen uint16 `json:"prefix_length,omitempty"` + Trans *bool `json:"transpositions,omitempty"` + FuzzyRw string `json:"fuzzy_rewrite,omitempty"` + Lent bool `json:"lenient,omitempty"` + Op MatchOperator `json:"operator,omitempty"` + MinMatch string `json:"minimum_should_match,omitempty"` + ZeroTerms string `json:"zero_terms_query,omitempty"` + Slp uint16 `json:"slop,omitempty"` // only relevant for match_phrase query +} + +func Match(fieldName string, simpleQuery ...interface{}) *MatchQuery { + return newMatch(TypeMatch, fieldName, simpleQuery...) +} + +func MatchBoolPrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery { + return newMatch(TypeMatchBoolPrefix, fieldName, simpleQuery...) +} + +func MatchPhrase(fieldName string, simpleQuery ...interface{}) *MatchQuery { + return newMatch(TypeMatchPhrase, fieldName, simpleQuery...) +} + +func MatchPhrasePrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery { + return newMatch(TypeMatchPhrasePrefix, fieldName, simpleQuery...) +} + +func newMatch(mType matchType, fieldName string, simpleQuery ...interface{}) *MatchQuery { + var qry interface{} + if simpleQuery != nil && len(simpleQuery) > 0 { + qry = simpleQuery[len(simpleQuery)-1] + } + + return &MatchQuery{ + field: fieldName, + mType: mType, + params: matchParams{ + Qry: qry, + }, + } +} + +func (q *MatchQuery) Query(data interface{}) *MatchQuery { + q.params.Qry = data + return q +} + +func (q *MatchQuery) Analyzer(a string) *MatchQuery { + q.params.Anl = a + return q +} + +func (q *MatchQuery) AutoGenerateSynonymsPhraseQuery(b bool) *MatchQuery { + q.params.AutoGenerate = &b + return q +} + +func (q *MatchQuery) Fuzziness(f string) *MatchQuery { + q.params.Fuzz = f + return q +} + +func (q *MatchQuery) MaxExpansions(e uint16) *MatchQuery { + q.params.MaxExp = e + return q +} + +func (q *MatchQuery) PrefixLength(l uint16) *MatchQuery { + q.params.PrefLen = l + return q +} + +func (q *MatchQuery) Transpositions(b bool) *MatchQuery { + q.params.Trans = &b + return q +} + +func (q *MatchQuery) FuzzyRewrite(s string) *MatchQuery { + q.params.FuzzyRw = s + return q +} + +func (q *MatchQuery) Lenient(b bool) *MatchQuery { + q.params.Lent = b + return q +} + +func (q *MatchQuery) Operator(op MatchOperator) *MatchQuery { + q.params.Op = op + return q +} + +func (q *MatchQuery) MinimumShouldMatch(s string) *MatchQuery { + q.params.MinMatch = s + return q +} + +func (q *MatchQuery) Slop(n uint16) *MatchQuery { + q.params.Slp = n + return q +} + +func (q *MatchQuery) ZeroTermsQuery(s string) *MatchQuery { + q.params.ZeroTerms = s + return q +} + +func (q *MatchQuery) Reader() io.Reader { + var b bytes.Buffer + return &b +} + +type MatchOperator uint8 + +const ( + OR MatchOperator = iota + AND +) + +var ErrInvalidValue = errors.New("invalid constant value") + +func (a MatchOperator) MarshalJSON() ([]byte, error) { + var s string + switch a { + case OR: + s = "or" + case AND: + s = "and" + default: + return nil, ErrInvalidValue + } + + return json.Marshal(s) +} + +type ZeroTerms uint8 + +const ( + None ZeroTerms = iota + All +) + +func (a ZeroTerms) MarshalJSON() ([]byte, error) { + var s string + switch a { + case None: + s = "none" + case All: + s = "all" + default: + return nil, ErrInvalidValue + } + + return json.Marshal(s) +} + +/******************************************************************************* + * Multi-Match Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html + * NOTE: uncommented for now, article is too long + ******************************************************************************/ + +//type MultiMatchQuery struct { +//fields []string +//mType multiMatchType +//params multiMatchQueryParams +//} + +//type multiMatchType uint8 + +//const ( +//BestFields multiMatchType = iota +//MostFields +//CrossFields +//Phrase +//PhrasePrefix +//BoolPrefix +//) + +//func (a multiMatchType) MarshalJSON() ([]byte, error) { +//var s string +//switch a { +//case BestFields: +//s = "best_fields" +//case MostFields: +//s = "most_fields" +//case CrossFields: +//s = "cross_fields" +//case Phrase: +//s = "phrase" +//case PhrasePrefix: +//s = "phrase_prefix" +//case BoolPrefix: +//s = "bool_prefix" +//default: +//return nil, ErrInvalidValue +//} +//return json.Marshal(s) +//} diff --git a/match_all.go b/match_all.go new file mode 100644 index 0000000..5079115 --- /dev/null +++ b/match_all.go @@ -0,0 +1,48 @@ +package esquery + +import ( + "encoding/json" + + "github.com/elastic/go-elasticsearch/esapi" +) + +// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html +type MatchAllQuery struct { + all bool + params matchAllParams +} + +type matchAllParams struct { + Boost float32 `json:"boost,omitempty"` +} + +func (a MatchAllQuery) MarshalJSON() ([]byte, error) { + var mType string + switch a.all { + case true: + mType = "match_all" + default: + mType = "match_none" + } + + return json.Marshal(map[string]matchAllParams{mType: a.params}) +} + +func (a *MatchAllQuery) Run(api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { + return search(*a, api, o...) +} + +func MatchAll() *MatchAllQuery { + return &MatchAllQuery{all: true} +} + +func (q *MatchAllQuery) Boost(b float32) *MatchAllQuery { + if q.all { + q.params.Boost = b + } + return q +} + +func MatchNone() *MatchAllQuery { + return &MatchAllQuery{all: false} +} diff --git a/match_all_test.go b/match_all_test.go new file mode 100644 index 0000000..fd6f0ec --- /dev/null +++ b/match_all_test.go @@ -0,0 +1,13 @@ +package esquery + +import ( + "testing" +) + +func TestMatchAll(t *testing.T) { + runTests(t, []queryTest{ + {"match_all without a boost", MatchAll(), "{\"match_all\":{}}\n"}, + {"match_all with a boost", MatchAll().Boost(2.3), "{\"match_all\":{\"boost\":2.3}}\n"}, + {"match_none", MatchNone(), "{\"match_none\":{}}\n"}, + }) +} diff --git a/match_test.go b/match_test.go new file mode 100644 index 0000000..cfbbbbe --- /dev/null +++ b/match_test.go @@ -0,0 +1,15 @@ +package esquery + +import ( + "testing" +) + +func TestMatch(t *testing.T) { + runTests(t, []queryTest{ + {"simple match", Match("title", "sample text"), "{\"match\":{\"title\":{\"query\":\"sample text\"}}}\n"}, + {"match with more params", Match("issue_number").Query(16).Transpositions(false).MaxExpansions(32).Operator(AND), "{\"match\":{\"issue_number\":{\"query\":16,\"max_expansions\":32,\"transpositions\":false,\"operator\":\"and\"}}}\n"}, + {"match_bool_prefix", MatchBoolPrefix("title", "sample text"), "{\"match_bool_prefix\":{\"title\":{\"query\":\"sample text\"}}}\n"}, + {"match_phrase", MatchPhrase("title", "sample text"), "{\"match_phrase\":{\"title\":{\"query\":\"sample text\"}}}\n"}, + {"match_phrase_prefix", MatchPhrasePrefix("title", "sample text"), "{\"match_phrase_prefix\":{\"title\":{\"query\":\"sample text\"}}}\n"}, + }) +} diff --git a/term_level.go b/term_level.go new file mode 100644 index 0000000..e8f6b36 --- /dev/null +++ b/term_level.go @@ -0,0 +1,449 @@ +package esquery + +import ( + "encoding/json" +) + +/******************************************************************************* + * Exists Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html + ******************************************************************************/ + +type ExistsQuery string + +func Exists(field string) *ExistsQuery { + q := ExistsQuery(field) + return &q +} + +func (q ExistsQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "exists": map[string]string{ + "field": string(q), + }, + }) +} + +/******************************************************************************* + * IDs Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html + ******************************************************************************/ + +type IDsQuery []string + +func IDs(vals ...string) *IDsQuery { + q := IDsQuery(vals) + return &q +} + +func (q IDsQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "ids": map[string][]string{ + "values": []string(q), + }, + }) +} + +/******************************************************************************* + * Prefix Queries + * 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 `json:"value"` + Rewrite string `json:"rewrite,omitempty"` +} + +func Prefix(field, value string) *PrefixQuery { + return &PrefixQuery{ + field: field, + params: prefixQueryParams{Value: value}, + } +} + +func (q *PrefixQuery) Rewrite(s string) *PrefixQuery { + q.params.Rewrite = s + return q +} + +func (q PrefixQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "prefix": map[string]prefixQueryParams{ + q.field: q.params, + }, + }) +} + +/******************************************************************************* + * Range Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html + ******************************************************************************/ + +type RangeQuery struct { + field string + params rangeQueryParams +} + +type rangeQueryParams struct { + Gt interface{} `json:"gt,omitempty"` + Gte interface{} `json:"gte,omitempty"` + Lt interface{} `json:"lt,omitempty"` + Lte interface{} `json:"lte,omitempty"` + Format string `json:"format,omitempty"` + Relation RangeRelation `json:"relation,omitempty"` + TimeZone string `json:"time_zone,omitempty"` + Boost float32 `json:"boost,omitempty"` +} + +func Range(field string) *RangeQuery { + return &RangeQuery{field: field} +} + +func (a *RangeQuery) Gt(val interface{}) *RangeQuery { + a.params.Gt = val + return a +} + +func (a *RangeQuery) Gte(val interface{}) *RangeQuery { + a.params.Gte = val + return a +} + +func (a *RangeQuery) Lt(val interface{}) *RangeQuery { + a.params.Lt = val + return a +} + +func (a *RangeQuery) Lte(val interface{}) *RangeQuery { + a.params.Lte = val + return a +} + +func (a *RangeQuery) Format(f string) *RangeQuery { + a.params.Format = f + return a +} + +func (a *RangeQuery) Relation(r RangeRelation) *RangeQuery { + a.params.Relation = r + return a +} + +func (a *RangeQuery) TimeZone(zone string) *RangeQuery { + a.params.TimeZone = zone + return a +} + +func (a *RangeQuery) Boost(b float32) *RangeQuery { + a.params.Boost = b + return a +} + +func (a RangeQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "range": map[string]rangeQueryParams{ + a.field: a.params, + }, + }) +} + +type RangeRelation uint8 + +const ( + INTERSECTS RangeRelation = iota + CONTAINS + WITHIN +) + +func (a RangeRelation) MarshalJSON() ([]byte, error) { + var s string + switch a { + case INTERSECTS: + s = "INTERSECTS" + case CONTAINS: + s = "CONTAINS" + case WITHIN: + s = "WITHIN" + default: + return nil, ErrInvalidValue + } + + return json.Marshal(s) +} + +/******************************************************************************* + * Regexp Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html + ******************************************************************************/ + +type RegexpQuery struct { + field string + wildcard bool + params regexpQueryParams +} + +type regexpQueryParams struct { + Value string `json:"value"` + Flags string `json:"flags,omitempty"` + MaxDeterminizedStates uint16 `json:"max_determinized_states,omitempty"` + Rewrite string `json:"rewrite,omitempty"` +} + +func Regexp(field, value string) *RegexpQuery { + return &RegexpQuery{ + field: field, + params: regexpQueryParams{ + Value: value, + }, + } +} + +func (q *RegexpQuery) Value(v string) *RegexpQuery { + q.params.Value = v + return q +} + +func (q *RegexpQuery) Flags(f string) *RegexpQuery { + if !q.wildcard { + q.params.Flags = f + } + return q +} + +func (q *RegexpQuery) MaxDeterminizedStates(m uint16) *RegexpQuery { + if !q.wildcard { + q.params.MaxDeterminizedStates = m + } + return q +} + +func (q *RegexpQuery) Rewrite(r string) *RegexpQuery { + q.params.Rewrite = r + return q +} + +func (q RegexpQuery) MarshalJSON() ([]byte, error) { + var qType string + if q.wildcard { + qType = "wildcard" + } else { + qType = "regexp" + } + return json.Marshal(map[string]interface{}{ + qType: map[string]regexpQueryParams{ + q.field: q.params, + }, + }) +} + +/******************************************************************************* + * Wildcard Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html + ******************************************************************************/ + +func Wildcard(field, value string) *RegexpQuery { + return &RegexpQuery{ + field: field, + wildcard: true, + params: regexpQueryParams{ + Value: value, + }, + } +} + +/******************************************************************************* + * Fuzzy Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html + ******************************************************************************/ + +type FuzzyQuery struct { + field string + params fuzzyQueryParams +} + +type fuzzyQueryParams struct { + Value string `json:"value"` + Fuzziness string `json:"fuzziness,omitempty"` + MaxExpansions uint16 `json:"max_expansions,omitempty"` + PrefixLength uint16 `json:"prefix_length,omitempty"` + Transpositions *bool `json:"transpositions,omitempty"` + Rewrite string `json:"rewrite,omitempty"` +} + +func Fuzzy(field, value string) *FuzzyQuery { + return &FuzzyQuery{ + field: field, + params: fuzzyQueryParams{ + Value: value, + }, + } +} + +func (q *FuzzyQuery) Value(val string) *FuzzyQuery { + q.params.Value = val + return q +} + +func (q *FuzzyQuery) Fuzziness(fuzz string) *FuzzyQuery { + q.params.Fuzziness = fuzz + return q +} + +func (q *FuzzyQuery) MaxExpansions(m uint16) *FuzzyQuery { + q.params.MaxExpansions = m + return q +} + +func (q *FuzzyQuery) PrefixLength(l uint16) *FuzzyQuery { + q.params.PrefixLength = l + return q +} + +func (q *FuzzyQuery) Transpositions(b bool) *FuzzyQuery { + q.params.Transpositions = &b + return q +} + +func (q *FuzzyQuery) Rewrite(s string) *FuzzyQuery { + q.params.Rewrite = s + return q +} + +func (q FuzzyQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "fuzzy": map[string]fuzzyQueryParams{ + q.field: q.params, + }, + }) +} + +/******************************************************************************* + * Term Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html + ******************************************************************************/ + +type TermQuery struct { + field string + params termQueryParams +} + +type termQueryParams struct { + Value interface{} `json:"value"` + Boost float32 `json:"boost,omitempty"` +} + +func Term(field string, value interface{}) *TermQuery { + return &TermQuery{ + field: field, + params: termQueryParams{ + Value: value, + }, + } +} + +func (q *TermQuery) Value(val interface{}) *TermQuery { + q.params.Value = val + return q +} + +func (q *TermQuery) Boost(b float32) *TermQuery { + q.params.Boost = b + return q +} + +func (q TermQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "term": map[string]termQueryParams{ + q.field: q.params, + }, + }) +} + +/******************************************************************************* + * Terms Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html + ******************************************************************************/ + +type TermsQuery struct { + field string + values []interface{} + boost float32 +} + +func Terms(field string, values ...interface{}) *TermsQuery { + return &TermsQuery{ + field: field, + values: values, + } +} + +func (q *TermsQuery) Values(values ...interface{}) *TermsQuery { + q.values = values + return q +} + +func (q *TermsQuery) Boost(b float32) *TermsQuery { + q.boost = b + return q +} + +func (q TermsQuery) MarshalJSON() ([]byte, error) { + innerMap := map[string]interface{}{q.field: q.values} + if q.boost > 0 { + innerMap["boost"] = q.boost + } + return json.Marshal(map[string]interface{}{"terms": innerMap}) +} + +/******************************************************************************* + * Term Set Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html + ******************************************************************************/ + +type TermsSetQuery struct { + field string + params termsSetQueryParams +} + +type termsSetQueryParams struct { + Terms []string `json:"terms"` + MinimumShouldMatchField string `json:"minimum_should_match_field,omitempty"` + MinimumShouldMatchScript string `json:"minimum_should_match_script,omitempty"` +} + +func TermsSet(field string, terms ...string) *TermsSetQuery { + return &TermsSetQuery{ + field: field, + params: termsSetQueryParams{ + Terms: terms, + }, + } +} + +func (q *TermsSetQuery) Terms(terms ...string) *TermsSetQuery { + q.params.Terms = terms + return q +} + +func (q *TermsSetQuery) MinimumShouldMatchField(field string) *TermsSetQuery { + q.params.MinimumShouldMatchField = field + return q +} + +func (q *TermsSetQuery) MinimumShouldMatchScript(script string) *TermsSetQuery { + q.params.MinimumShouldMatchScript = script + return q +} + +func (q TermsSetQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "terms_set": map[string]termsSetQueryParams{ + q.field: q.params, + }, + }) +} diff --git a/term_level_test.go b/term_level_test.go new file mode 100644 index 0000000..02515f6 --- /dev/null +++ b/term_level_test.go @@ -0,0 +1,33 @@ +package esquery + +import ( + "testing" +) + +func TestTermLevel(t *testing.T) { + runTests(t, []queryTest{ + {"exists", Exists("title"), "{\"exists\":{\"field\":\"title\"}}\n"}, + + {"ids", IDs("1", "4", "100"), "{\"ids\":{\"values\":[\"1\",\"4\",\"100\"]}}\n"}, + + {"simple prefix", Prefix("user", "ki"), "{\"prefix\":{\"user\":{\"value\":\"ki\"}}}\n"}, + + {"complex prefix", Prefix("user", "ki").Rewrite("ji"), "{\"prefix\":{\"user\":{\"value\":\"ki\",\"rewrite\":\"ji\"}}}\n"}, + + {"int range", Range("age").Gte(10).Lte(20).Boost(2.0), "{\"range\":{\"age\":{\"gte\":10,\"lte\":20,\"boost\":2}}}\n"}, + + {"string range", Range("timestamp").Gte("now-1d/d").Lt("now/d").Relation(CONTAINS), "{\"range\":{\"timestamp\":{\"gte\":\"now-1d/d\",\"lt\":\"now/d\",\"relation\":\"CONTAINS\"}}}\n"}, + + {"regexp", Regexp("user", "k.*y").Flags("ALL").MaxDeterminizedStates(10000).Rewrite("constant_score"), "{\"regexp\":{\"user\":{\"value\":\"k.*y\",\"flags\":\"ALL\",\"max_determinized_states\":10000,\"rewrite\":\"constant_score\"}}}\n"}, + + {"wildcard", Wildcard("user", "ki*y").Rewrite("constant_score"), "{\"wildcard\":{\"user\":{\"value\":\"ki*y\",\"rewrite\":\"constant_score\"}}}\n"}, + + {"fuzzy", Fuzzy("user", "ki").Fuzziness("AUTO").MaxExpansions(50).Transpositions(true), "{\"fuzzy\":{\"user\":{\"value\":\"ki\",\"fuzziness\":\"AUTO\",\"max_expansions\":50,\"transpositions\":true}}}\n"}, + + {"term", Term("user", "Kimchy").Boost(1.3), "{\"term\":{\"user\":{\"value\":\"Kimchy\",\"boost\":1.3}}}\n"}, + + {"terms", Terms("user").Values("bla", "pl").Boost(1.3), "{\"terms\":{\"boost\":1.3,\"user\":[\"bla\",\"pl\"]}}\n"}, + + {"terms_set", TermsSet("programming_languages", "go", "rust", "COBOL").MinimumShouldMatchField("required_matches"), "{\"terms_set\":{\"programming_languages\":{\"terms\":[\"go\",\"rust\",\"COBOL\"],\"minimum_should_match_field\":\"required_matches\"}}}\n"}, + }) +} From 6c8e71c18874ea5720996bca6854c3d497768a37 Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Tue, 18 Feb 2020 17:40:57 +0200 Subject: [PATCH 2/5] Add support for compound queries This commit adds support for the compound queries "bool", "boosting", "constant_score" and "dis_max". The "function_score" query is not supported yet. Compound queries are simple. They act just like simple queries, except that they are recursive, wrapping other simple/compound queries. For example: esquery.Bool(). Must(Term("user", "kimchy"), Term("author", "kimchy")). Filter(Term("tag", "tech")) --- boolean.go | 61 ++++++++++++++++++++++++++++++++++++++++++ boolean_test.go | 31 +++++++++++++++++++++ boosting.go | 43 +++++++++++++++++++++++++++++ boosting_test.go | 18 +++++++++++++ constant_score.go | 29 ++++++++++++++++++++ constant_score_test.go | 20 ++++++++++++++ dis_max.go | 31 +++++++++++++++++++++ dis_max_test.go | 15 +++++++++++ 8 files changed, 248 insertions(+) create mode 100644 boolean.go create mode 100644 boolean_test.go create mode 100644 boosting.go create mode 100644 boosting_test.go create mode 100644 constant_score.go create mode 100644 constant_score_test.go create mode 100644 dis_max.go create mode 100644 dis_max_test.go diff --git a/boolean.go b/boolean.go new file mode 100644 index 0000000..251cb16 --- /dev/null +++ b/boolean.go @@ -0,0 +1,61 @@ +package esquery + +import "encoding/json" + +/******************************************************************************* + * Boolean Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html + ******************************************************************************/ + +type BoolQuery struct { + params boolQueryParams +} + +type boolQueryParams struct { + Must []json.Marshaler `json:"must,omitempty"` + Filter []json.Marshaler `json:"filter,omitempty"` + MustNot []json.Marshaler `json:"must_not,omitempty"` + Should []json.Marshaler `json:"should,omitempty"` + MinimumShouldMatch int16 `json:"minimum_should_match,omitempty"` + Boost float32 `json:"boost,omitempty"` +} + +func Bool() *BoolQuery { + return &BoolQuery{} +} + +func (q *BoolQuery) Must(must ...json.Marshaler) *BoolQuery { + q.params.Must = append(q.params.Must, must...) + return q +} + +func (q *BoolQuery) Filter(filter ...json.Marshaler) *BoolQuery { + q.params.Filter = append(q.params.Filter, filter...) + return q +} + +func (q *BoolQuery) MustNot(mustnot ...json.Marshaler) *BoolQuery { + q.params.MustNot = append(q.params.MustNot, mustnot...) + return q +} + +func (q *BoolQuery) Should(should ...json.Marshaler) *BoolQuery { + q.params.Should = append(q.params.Should, should...) + return q +} + +func (q *BoolQuery) MinimumShouldMatch(val int16) *BoolQuery { + q.params.MinimumShouldMatch = val + return q +} + +func (q *BoolQuery) Boost(val float32) *BoolQuery { + q.params.Boost = val + return q +} + +func (q BoolQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]boolQueryParams{ + "bool": q.params, + }) +} diff --git a/boolean_test.go b/boolean_test.go new file mode 100644 index 0000000..9fc5a9c --- /dev/null +++ b/boolean_test.go @@ -0,0 +1,31 @@ +package esquery + +import ( + "testing" +) + +func TestBool(t *testing.T) { + runTests(t, []queryTest{ + { + "bool with only a simple must", + Bool().Must(Term("tag", "tech")), + "{\"bool\":{\"must\":[{\"term\":{\"tag\":{\"value\":\"tech\"}}}]}}\n", + }, + { + "bool which must match_all and filter", + Bool().Must(MatchAll()).Filter(Term("status", "active")), + "{\"bool\":{\"must\":[{\"match_all\":{}}],\"filter\":[{\"term\":{\"status\":{\"value\":\"active\"}}}]}}\n", + }, + { + "bool with a lot of stuff", + Bool(). + Must(Term("user", "kimchy")). + Filter(Term("tag", "tech")). + MustNot(Range("age").Gte(10).Lte(20)). + Should(Term("tag", "wow"), Term("tag", "elasticsearch")). + MinimumShouldMatch(1). + Boost(1.1), + "{\"bool\":{\"must\":[{\"term\":{\"user\":{\"value\":\"kimchy\"}}}],\"filter\":[{\"term\":{\"tag\":{\"value\":\"tech\"}}}],\"must_not\":[{\"range\":{\"age\":{\"gte\":10,\"lte\":20}}}],\"should\":[{\"term\":{\"tag\":{\"value\":\"wow\"}}},{\"term\":{\"tag\":{\"value\":\"elasticsearch\"}}}],\"minimum_should_match\":1,\"boost\":1.1}}\n", + }, + }) +} diff --git a/boosting.go b/boosting.go new file mode 100644 index 0000000..245bf6a --- /dev/null +++ b/boosting.go @@ -0,0 +1,43 @@ +package esquery + +import "encoding/json" + +/******************************************************************************* + * Boosting Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html + ******************************************************************************/ + +type BoostingQuery struct { + params boostingQueryParams +} + +type boostingQueryParams struct { + Positive json.Marshaler `json:"positive"` + Negative json.Marshaler `json:"negative"` + NegativeBoost float32 `json:"negative_boost"` +} + +func Boosting() *BoostingQuery { + return &BoostingQuery{} +} + +func (q *BoostingQuery) Positive(p json.Marshaler) *BoostingQuery { + q.params.Positive = p + return q +} + +func (q *BoostingQuery) Negative(p json.Marshaler) *BoostingQuery { + q.params.Negative = p + return q +} + +func (q *BoostingQuery) NegativeBoost(b float32) *BoostingQuery { + q.params.NegativeBoost = b + return q +} + +func (q *BoostingQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]boostingQueryParams{ + "boosting": q.params, + }) +} diff --git a/boosting_test.go b/boosting_test.go new file mode 100644 index 0000000..1fb57a0 --- /dev/null +++ b/boosting_test.go @@ -0,0 +1,18 @@ +package esquery + +import ( + "testing" +) + +func TestBoost(t *testing.T) { + runTests(t, []queryTest{ + { + "boosting query", + Boosting(). + Positive(Term("text", "apple")). + Negative(Term("text", "pie tart")). + NegativeBoost(0.5), + "{\"boosting\":{\"positive\":{\"term\":{\"text\":{\"value\":\"apple\"}}},\"negative\":{\"term\":{\"text\":{\"value\":\"pie tart\"}}},\"negative_boost\":0.5}}\n", + }, + }) +} diff --git a/constant_score.go b/constant_score.go new file mode 100644 index 0000000..6398464 --- /dev/null +++ b/constant_score.go @@ -0,0 +1,29 @@ +package esquery + +import "encoding/json" + +type ConstantScoreQuery struct { + params constantScoreParams +} + +type constantScoreParams struct { + Filter json.Marshaler `json:"filter"` + Boost float32 `json:"boost,omitempty"` +} + +func ConstantScore(filter json.Marshaler) *ConstantScoreQuery { + return &ConstantScoreQuery{ + params: constantScoreParams{Filter: filter}, + } +} + +func (q *ConstantScoreQuery) Boost(b float32) *ConstantScoreQuery { + q.params.Boost = b + return q +} + +func (q ConstantScoreQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]constantScoreParams{ + "constant_score": q.params, + }) +} diff --git a/constant_score_test.go b/constant_score_test.go new file mode 100644 index 0000000..f3c23cc --- /dev/null +++ b/constant_score_test.go @@ -0,0 +1,20 @@ +package esquery + +import ( + "testing" +) + +func TestConstantScore(t *testing.T) { + runTests(t, []queryTest{ + { + "constant_score query without boost", + ConstantScore(Term("user", "kimchy")), + "{\"constant_score\":{\"filter\":{\"term\":{\"user\":{\"value\":\"kimchy\"}}}}}\n", + }, + { + "constant_score query with boost", + ConstantScore(Term("user", "kimchy")).Boost(2.2), + "{\"constant_score\":{\"filter\":{\"term\":{\"user\":{\"value\":\"kimchy\"}}},\"boost\":2.2}}\n", + }, + }) +} diff --git a/dis_max.go b/dis_max.go new file mode 100644 index 0000000..31fda6a --- /dev/null +++ b/dis_max.go @@ -0,0 +1,31 @@ +package esquery + +import "encoding/json" + +type DisMaxQuery struct { + params disMaxParams +} + +type disMaxParams struct { + Queries []json.Marshaler `json:"queries"` + TieBreaker float32 `json:"tie_breaker,omitempty"` +} + +func DisMax(queries ...json.Marshaler) *DisMaxQuery { + return &DisMaxQuery{ + params: disMaxParams{ + Queries: queries, + }, + } +} + +func (q *DisMaxQuery) TieBreaker(b float32) *DisMaxQuery { + q.params.TieBreaker = b + return q +} + +func (q DisMaxQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]disMaxParams{ + "dis_max": q.params, + }) +} diff --git a/dis_max_test.go b/dis_max_test.go new file mode 100644 index 0000000..8e2fc35 --- /dev/null +++ b/dis_max_test.go @@ -0,0 +1,15 @@ +package esquery + +import ( + "testing" +) + +func TestDisMax(t *testing.T) { + runTests(t, []queryTest{ + { + "dis_max", + DisMax(Term("title", "Quick pets"), Term("body", "Quick pets")).TieBreaker(0.7), + "{\"dis_max\":{\"queries\":[{\"term\":{\"title\":{\"value\":\"Quick pets\"}}},{\"term\":{\"body\":{\"value\":\"Quick pets\"}}}],\"tie_breaker\":0.7}}\n", + }, + }) +} From 55000abc77d2b4ceaece4c7e2b7b421281c3c01c Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Tue, 18 Feb 2020 18:43:19 +0200 Subject: [PATCH 3/5] Add Search() function, README and fix some lint errors This commit changes the internal `search()` function into an exposed `Search()` function that can be used to execute queries against an instance of an ElasticSearch client. The per-query-type methods of `Run()` are removed for now to prevent having to create them for every type. `Search()` is agnostic. A README.md file is added with some information, and a few lingering lint errors are fixed. --- .golangci.yml | 2 ++ README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++ es.go | 9 ++++-- go.mod | 6 +++- go.sum | 4 +++ match.go | 8 +---- match_all.go | 6 ---- 7 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 README.md diff --git a/.golangci.yml b/.golangci.yml index faf3f2f..ee46e7c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,5 @@ +run: + tests: false linters: disable-all: true enable: diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d9f611 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# esquery + +`esquery` is an idiomatic, easy-to-use query builder for the [official Go client](https://github.com/elastic/go-elasticsearch) for [ElasticSearch](https://www.elastic.co/products/elasticsearch). It alleviates the need to use extremely nested maps of empty interfaces and serializing queries to JSON manually. It also helps eliminating common mistakes such as misspelling query types, as everything is statically typed. + +## Usage + +`esquery` can be used directly to build queries, with no need for external dependencies. It can execute the queries against an existing instance of `*esapi.API`, but the queries can also be manually converted to JSON if necessary. + +```go +package main + +import ( + "context" + "log" + + "bitbucket.org/scalock/esquery" + "github.com/elastic/go-elasticsearch/v7" +) + +func main() { + es, err := elasticsearch.NewDefaultClient() + if err != nil { + log.Fatalf("Failed creating client: %s", err) + } + + res, err := esquery.Search( + es, + esquery. + Bool(). + Must(esquery.Term("title", "Go and Stuff")). + Filter(esquery.Term("tag", "tech")), + es.Search.WithContext(context.TODO()), + es.Search.WithIndex("test"), + ) + if err != nil { + log.Fatalf("Failed searching for stuff: %s", err) + } + + defer res.Body.Close() + + // ... +} +``` + +## Notes + +* Library currently supports v7 of the ElasticSearch Go client. +* The library cannot currently generate "short queries". For example, whereas + ElasticSearch can accept this: + +```json +{ "query": { "term": { "user": "Kimchy" } } } +``` + + The library will always generate this: + +```json +{ "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. + +## Supported queries + +The following queries are currently supported: + +| Query | `esquery` Function | +| ------------------------|---------------------- | +| `"match"` | `Match()` | +| `"match_bool_prefix"` | `MatchBoolPrefix()` | +| `"match_phrase"` | `MatchPhrase()` | +| `"match_phrase_prefix"` | `MatchPhrasePrefix()` | +| `"match_all"` | `MatchAll()` | +| `"match_none"` | `MatchNone()` | +| `"exists"` | `Exists()` | +| `"fuzzy"` | `Fuzzy()` | +| `"ids"` | `IDs()` | +| `"prefix"` | `Prefix()` | +| `"range"` | `Range()` | +| `"regexp"` | `Regexp()` | +| `"term"` | `Term()` | +| `"terms"` | `Terms()` | +| `"terms_set"` | `TermsSet()` | +| `"wildcard"` | `Wildcard()` | +| `"bool"` | `Bool()` | +| `"boosting"` | `Boosting()` | +| `"constant_score"` | `ConstantScore()` | +| `"dis_max"` | `DisMax()` | diff --git a/es.go b/es.go index 8d6698c..10e24eb 100644 --- a/es.go +++ b/es.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/elastic/go-elasticsearch/esapi" + "github.com/elastic/go-elasticsearch/v7" + "github.com/elastic/go-elasticsearch/v7/esapi" ) type ESQuery struct { @@ -22,7 +23,11 @@ func encode(q json.Marshaler, b *bytes.Buffer) (err error) { return nil } -func search(q json.Marshaler, api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { +func Search( + api *elasticsearch.Client, + q json.Marshaler, + o ...func(*esapi.SearchRequest), +) (res *esapi.Response, err error) { var b bytes.Buffer err = encode(ESQuery{q}, &b) if err != nil { diff --git a/go.mod b/go.mod index 3b1d837..223085a 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module bitbucket.org/scalock/esquery go 1.13 -require github.com/elastic/go-elasticsearch v0.0.0 +require ( + github.com/elastic/go-elasticsearch v0.0.0 + github.com/elastic/go-elasticsearch/v7 v7.6.0 + github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde +) diff --git a/go.sum b/go.sum index 06225ee..30afa0b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA= github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg= +github.com/elastic/go-elasticsearch/v7 v7.6.0 h1:sYpGLpEFHgLUKLsZUBfuaVI9QgHjS3JdH9fX4/z8QI8= +github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= +github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde h1:Y9SZx8RQqFycLxi5W5eFmxMqnmijULVc3LMjBTtZQdM= +github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde/go.mod h1:xe9a/L2aeOgFKKgrO3ibQTnMdpAeL0GC+5/HpGScSa4= diff --git a/match.go b/match.go index 29e36e2..896d42c 100644 --- a/match.go +++ b/match.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "io" - - "github.com/elastic/go-elasticsearch/esapi" ) /******************************************************************************* @@ -51,10 +49,6 @@ func (a MatchQuery) MarshalJSON() ([]byte, error) { }) } -func (a *MatchQuery) Run(api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { - return search(*a, api, o...) -} - type matchParams struct { Qry interface{} `json:"query"` Anl string `json:"analyzer,omitempty"` @@ -89,7 +83,7 @@ func MatchPhrasePrefix(fieldName string, simpleQuery ...interface{}) *MatchQuery func newMatch(mType matchType, fieldName string, simpleQuery ...interface{}) *MatchQuery { var qry interface{} - if simpleQuery != nil && len(simpleQuery) > 0 { + if len(simpleQuery) > 0 { qry = simpleQuery[len(simpleQuery)-1] } diff --git a/match_all.go b/match_all.go index 5079115..c26363c 100644 --- a/match_all.go +++ b/match_all.go @@ -2,8 +2,6 @@ package esquery import ( "encoding/json" - - "github.com/elastic/go-elasticsearch/esapi" ) // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html @@ -28,10 +26,6 @@ func (a MatchAllQuery) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]matchAllParams{mType: a.params}) } -func (a *MatchAllQuery) Run(api *esapi.API, o ...func(*esapi.SearchRequest)) (res *esapi.Response, err error) { - return search(*a, api, o...) -} - func MatchAll() *MatchAllQuery { return &MatchAllQuery{all: true} } From 1dd88421a204f2bb3f2d6d2774065c494cd17621 Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Wed, 19 Feb 2020 13:35:21 +0200 Subject: [PATCH 4/5] Refactor API, add aggregations and custom queries This commit introduces a refactor of the codebase and the API, to make it more user friendly. Queries can now directly be executed via the `Run()` method. Internally, the library no longer uses JSON generation as a major mechanism, instead all types need to implement a `Mappable` interface which simply turns each type in a `map[string]interface{}`, which is what the ElasticSearch client expects. This makes the code easier to write, and makes writing tests less error prone, as JSON need not be written directly. Support for metrics aggregations is also added. However, aggregations of type bucket, pipeline and matrix are not supported yet. To make the library more useful in its current state, support is added for running custom queries and aggregations, via the `CustomQuery()` and `CustomAgg()` functions, which both accepts an arbitrary `map[string]interface{}`. --- README.md | 65 +++++- aggregations.go | 56 +++++ aggregations_test.go | 62 +++++ aggs_custom.go | 13 ++ aggs_custom_test.go | 35 +++ aggs_metric.go | 324 +++++++++++++++++++++++++++ aggs_metric_test.go | 158 +++++++++++++ boolean.go | 61 ----- boolean_test.go | 31 --- boosting.go | 43 ---- boosting_test.go | 18 -- constant_score.go | 29 --- constant_score_test.go | 20 -- dis_max.go | 31 --- dis_max_test.go | 15 -- es.go | 45 +--- es_test.go | 37 ++- go.mod | 3 +- go.sum | 2 + match_all_test.go | 13 -- match_test.go | 15 -- queries.go | 38 ++++ queries_test.go | 68 ++++++ query_boolean.go | 97 ++++++++ query_boolean_test.go | 107 +++++++++ query_boosting.go | 41 ++++ query_boosting_test.go | 36 +++ query_constant_score.go | 33 +++ query_constant_score_test.go | 41 ++++ query_custom.go | 13 ++ query_custom_test.go | 23 ++ query_dis_max.go | 37 +++ query_dis_max_test.go | 35 +++ match.go => query_match.go | 108 +++------ match_all.go => query_match_all.go | 18 +- query_match_all_test.go | 33 +++ query_match_test.go | 68 ++++++ term_level.go => query_term_level.go | 174 +++++++------- query_term_level_test.go | 151 +++++++++++++ term_level_test.go | 33 --- 40 files changed, 1680 insertions(+), 550 deletions(-) create mode 100644 aggregations.go create mode 100644 aggregations_test.go create mode 100644 aggs_custom.go create mode 100644 aggs_custom_test.go create mode 100644 aggs_metric.go create mode 100644 aggs_metric_test.go delete mode 100644 boolean.go delete mode 100644 boolean_test.go delete mode 100644 boosting.go delete mode 100644 boosting_test.go delete mode 100644 constant_score.go delete mode 100644 constant_score_test.go delete mode 100644 dis_max.go delete mode 100644 dis_max_test.go delete mode 100644 match_all_test.go delete mode 100644 match_test.go create mode 100644 queries.go create mode 100644 queries_test.go create mode 100644 query_boolean.go create mode 100644 query_boolean_test.go create mode 100644 query_boosting.go create mode 100644 query_boosting_test.go create mode 100644 query_constant_score.go create mode 100644 query_constant_score_test.go create mode 100644 query_custom.go create mode 100644 query_custom_test.go create mode 100644 query_dis_max.go create mode 100644 query_dis_max_test.go rename match.go => query_match.go (59%) rename match_all.go => query_match_all.go (53%) create mode 100644 query_match_all_test.go create mode 100644 query_match_test.go rename term_level.go => query_term_level.go (71%) create mode 100644 query_term_level_test.go delete mode 100644 term_level_test.go diff --git a/README.md b/README.md index 4d9f611..78e7f3b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # esquery -`esquery` is an idiomatic, easy-to-use query builder for the [official Go client](https://github.com/elastic/go-elasticsearch) for [ElasticSearch](https://www.elastic.co/products/elasticsearch). It alleviates the need to use extremely nested maps of empty interfaces and serializing queries to JSON manually. It also helps eliminating common mistakes such as misspelling query types, as everything is statically typed. +**esquery** is a non-obtrusive, idiomatic and easy-to-use query and aggregation builder for the [official Go client](https://github.com/elastic/go-elasticsearch) for [ElasticSearch](https://www.elastic.co/products/elasticsearch). It 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. + +Save yourself some joint aches and many lines of code by switching for maps to `esquery`. Wanna know how much code you'll save? just read this project's test. ## Usage -`esquery` can be used directly to build queries, with no need for external dependencies. It can execute the queries against an existing instance of `*esapi.API`, but the queries can also be manually converted to JSON if necessary. +esquery provides a [method chaining](https://en.wikipedia.org/wiki/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: ```go package main @@ -18,17 +22,20 @@ import ( ) func main() { + // connect to an ElasticSearch instance es, err := elasticsearch.NewDefaultClient() if err != nil { log.Fatalf("Failed creating client: %s", err) } - res, err := esquery.Search( - es, + // 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"), ) @@ -36,15 +43,30 @@ func main() { log.Fatalf("Failed searching for stuff: %s", err) } - defer res.Body.Close() + 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 -* Library currently supports v7 of the ElasticSearch Go client. +* `esquery` currently supports version 7 of the ElasticSearch Go client. * The library cannot currently generate "short queries". For example, whereas ElasticSearch can accept this: @@ -62,11 +84,11 @@ func main() { either receive one query object, or an array of query objects. `esquery` will generate an array even if there's only one query object. -## Supported queries +## Supported Queries The following queries are currently supported: -| Query | `esquery` Function | +| ElasticSearch DSL | `esquery` Function | | ------------------------|---------------------- | | `"match"` | `Match()` | | `"match_bool_prefix"` | `MatchBoolPrefix()` | @@ -88,3 +110,28 @@ The following queries are currently supported: | `"boosting"` | `Boosting()` | | `"constant_score"` | `ConstantScore()` | | `"dis_max"` | `DisMax()` | + +### Custom Queries + +To execute an arbitrary query, or any query that is not natively supported by the library yet, use the `CustomQuery()` function, which accepts any `map[string]interface{}` value. + +## Supported Aggregations + +The following aggregations are currently supported: + +| ElasticSearch DSL | `esquery` Function | +| ------------------------|---------------------- | +| `"avg"` | `Avg()` | +| `"weighted_avg"` | `WeightedAvg()` | +| `"cardinality"` | `Cardinality()` | +| `"max"` | `Max()` | +| `"min"` | `Min()` | +| `"sum"` | `Sum()` | +| `"value_count"` | `ValueCount()` | +| `"percentiles"` | `Percentiles()` | +| `"stats"` | `Stats()` | +| `"string_stats"` | `StringStats()` | + +### Custom Aggregations + +To execute an arbitrary aggregation, or any aggregation that is not natively supported by the library yet, use the `CustomAgg()` function, which accepts any `map[string]interface{}` value. diff --git a/aggregations.go b/aggregations.go new file mode 100644 index 0000000..254f3fe --- /dev/null +++ b/aggregations.go @@ -0,0 +1,56 @@ +package esquery + +import ( + "bytes" + "encoding/json" + + "github.com/elastic/go-elasticsearch/v7" + "github.com/elastic/go-elasticsearch/v7/esapi" +) + +type Aggregation interface { + Mappable + Name() string +} + +type AggregationRequest struct { + Aggs map[string]Mappable +} + +func Aggregate(aggs ...Aggregation) *AggregationRequest { + req := &AggregationRequest{ + Aggs: make(map[string]Mappable), + } + for _, agg := range aggs { + req.Aggs[agg.Name()] = agg + } + + return req +} + +func (req *AggregationRequest) Map() map[string]interface{} { + m := make(map[string]interface{}) + + for name, agg := range req.Aggs { + m[name] = agg.Map() + } + + return map[string]interface{}{ + "aggs": m, + } +} + +func (req *AggregationRequest) Run( + api *elasticsearch.Client, + 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){api.Search.WithBody(&b)}, o...) + + return api.Search(opts...) +} diff --git a/aggregations_test.go b/aggregations_test.go new file mode 100644 index 0000000..d113f2d --- /dev/null +++ b/aggregations_test.go @@ -0,0 +1,62 @@ +package esquery + +import ( + "testing" +) + +func TestAggregations(t *testing.T) { + runMapTests(t, []mapTest{ + { + "a simple, single aggregation", + Aggregate( + Avg("average_score", "score"), + ), + map[string]interface{}{ + "aggs": map[string]interface{}{ + "average_score": map[string]interface{}{ + "avg": map[string]interface{}{ + "field": "score", + }, + }, + }, + }, + }, + { + "a complex, multi-aggregation", + Aggregate( + Sum("total_score", "score"), + WeightedAvg("weighted_score"). + Value("score", 50). + Weight("weight", 1), + StringStats("tag_stats", "tags").ShowDistribution(true), + ), + map[string]interface{}{ + "aggs": map[string]interface{}{ + "total_score": map[string]interface{}{ + "sum": map[string]interface{}{ + "field": "score", + }, + }, + "weighted_score": map[string]interface{}{ + "weighted_avg": map[string]interface{}{ + "value": map[string]interface{}{ + "field": "score", + "missing": 50, + }, + "weight": map[string]interface{}{ + "field": "weight", + "missing": 1, + }, + }, + }, + "tag_stats": map[string]interface{}{ + "string_stats": map[string]interface{}{ + "field": "tags", + "show_distribution": true, + }, + }, + }, + }, + }, + }) +} diff --git a/aggs_custom.go b/aggs_custom.go new file mode 100644 index 0000000..6a03a1c --- /dev/null +++ b/aggs_custom.go @@ -0,0 +1,13 @@ +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_custom_test.go b/aggs_custom_test.go new file mode 100644 index 0000000..a807f5f --- /dev/null +++ b/aggs_custom_test.go @@ -0,0 +1,35 @@ +package esquery + +import "testing" + +func TestCustomAgg(t *testing.T) { + m := map[string]interface{}{ + "genres": map[string]interface{}{ + "terms": map[string]interface{}{ + "field": "genre", + }, + "t_shirts": map[string]interface{}{ + "filter": map[string]interface{}{ + "term": map[string]interface{}{ + "type": "t-shirt", + }, + }, + "aggs": map[string]interface{}{ + "avg_price": map[string]interface{}{ + "avg": map[string]interface{}{ + "field": "price", + }, + }, + }, + }, + }, + } + + runMapTests(t, []mapTest{ + { + "custom aggregation", + CustomAgg(m), + m, + }, + }) +} diff --git a/aggs_metric.go b/aggs_metric.go new file mode 100644 index 0000000..c120759 --- /dev/null +++ b/aggs_metric.go @@ -0,0 +1,324 @@ +package esquery + +import "github.com/fatih/structs" + +type BaseAgg struct { + name string + apiName string + *BaseAggParams `structs:",flatten"` +} + +type BaseAggParams struct { + Field string `structs:"field"` + Miss interface{} `structs:"missing,omitempty"` +} + +func newBaseAgg(apiName, name, field string) *BaseAgg { + return &BaseAgg{ + name: name, + apiName: apiName, + BaseAggParams: &BaseAggParams{ + Field: field, + }, + } +} + +func (agg *BaseAgg) Name() string { + return agg.name +} + +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 + ******************************************************************************/ + +type AvgAgg struct { + *BaseAgg `structs:",flatten"` +} + +func Avg(name, field string) *AvgAgg { + return &AvgAgg{ + BaseAgg: newBaseAgg("avg", name, 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 + ******************************************************************************/ + +type WeightedAvgAgg struct { + name string + apiName string + Val *BaseAggParams `structs:"value"` + Weig *BaseAggParams `structs:"weight"` +} + +func WeightedAvg(name string) *WeightedAvgAgg { + return &WeightedAvgAgg{ + name: name, + apiName: "weighted_avg", + } +} + +func (agg *WeightedAvgAgg) Name() string { + return agg.name +} + +func (agg *WeightedAvgAgg) Value(field string, missing ...interface{}) *WeightedAvgAgg { + agg.Val = new(BaseAggParams) + agg.Val.Field = field + if len(missing) > 0 { + agg.Val.Miss = missing[len(missing)-1] + } + return agg +} + +func (agg *WeightedAvgAgg) Weight(field string, missing ...interface{}) *WeightedAvgAgg { + agg.Weig = new(BaseAggParams) + agg.Weig.Field = field + if len(missing) > 0 { + agg.Weig.Miss = missing[len(missing)-1] + } + return agg +} + +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 + ******************************************************************************/ + +type CardinalityAgg struct { + *BaseAgg `structs:",flatten"` + PrecisionThr uint16 `structs:"precision_threshold,omitempty"` +} + +func Cardinality(name, field string) *CardinalityAgg { + return &CardinalityAgg{ + BaseAgg: newBaseAgg("cardinality", name, field), + } +} + +func (agg *CardinalityAgg) Missing(val interface{}) *CardinalityAgg { + agg.Miss = val + return agg +} + +func (agg *CardinalityAgg) PrecisionThreshold(val uint16) *CardinalityAgg { + agg.PrecisionThr = val + return agg +} + +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 + ******************************************************************************/ + +type MaxAgg struct { + *BaseAgg `structs:",flatten"` +} + +func Max(name, field string) *MaxAgg { + return &MaxAgg{ + BaseAgg: newBaseAgg("max", name, 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 + ******************************************************************************/ + +type MinAgg struct { + *BaseAgg `structs:",flatten"` +} + +func Min(name, field string) *MinAgg { + return &MinAgg{ + BaseAgg: newBaseAgg("min", name, 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 + ******************************************************************************/ + +type SumAgg struct { + *BaseAgg `structs:",flatten"` +} + +func Sum(name, field string) *SumAgg { + return &SumAgg{ + BaseAgg: newBaseAgg("sum", name, 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 + ******************************************************************************/ + +type ValueCountAgg struct { + *BaseAgg `structs:",flatten"` +} + +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 + ******************************************************************************/ + +type PercentilesAgg struct { + *BaseAgg `structs:",flatten"` + Prcnts []float32 `structs:"percents,omitempty"` + Key *bool `structs:"keyed,omitempty"` + TDigest struct { + Compression uint16 `structs:"compression,omitempty"` + } `structs:"tdigest,omitempty"` + HDR struct { + NumHistogramDigits uint8 `structs:"number_of_significant_value_digits,omitempty"` + } `structs:"hdr,omitempty"` +} + +func Percentiles(name, field string) *PercentilesAgg { + return &PercentilesAgg{ + BaseAgg: newBaseAgg("percentiles", name, field), + } +} + +func (agg *PercentilesAgg) Percents(percents ...float32) *PercentilesAgg { + agg.Prcnts = percents + return agg +} + +func (agg *PercentilesAgg) Missing(val interface{}) *PercentilesAgg { + agg.Miss = val + return agg +} + +func (agg *PercentilesAgg) Keyed(b bool) *PercentilesAgg { + agg.Key = &b + return agg +} + +func (agg *PercentilesAgg) Compression(val uint16) *PercentilesAgg { + agg.TDigest.Compression = val + return agg +} + +func (agg *PercentilesAgg) NumHistogramDigits(val uint8) *PercentilesAgg { + agg.HDR.NumHistogramDigits = val + return agg +} + +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 + ******************************************************************************/ + +type StatsAgg struct { + *BaseAgg `structs:",flatten"` +} + +func Stats(name, field string) *StatsAgg { + return &StatsAgg{ + BaseAgg: newBaseAgg("stats", name, 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 + ******************************************************************************/ + +type StringStatsAgg struct { + *BaseAgg `structs:",flatten"` + ShowDist *bool `structs:"show_distribution,omitempty"` +} + +func StringStats(name, field string) *StringStatsAgg { + return &StringStatsAgg{ + BaseAgg: newBaseAgg("string_stats", name, field), + } +} + +func (agg *StringStatsAgg) Missing(val interface{}) *StringStatsAgg { + agg.Miss = val + return agg +} + +func (agg *StringStatsAgg) ShowDistribution(b bool) *StringStatsAgg { + agg.ShowDist = &b + return agg +} + +func (agg *StringStatsAgg) Map() map[string]interface{} { + return map[string]interface{}{ + agg.apiName: structs.Map(agg), + } +} diff --git a/aggs_metric_test.go b/aggs_metric_test.go new file mode 100644 index 0000000..54100e2 --- /dev/null +++ b/aggs_metric_test.go @@ -0,0 +1,158 @@ +package esquery + +import "testing" + +func TestMetricAggs(t *testing.T) { + runMapTests(t, []mapTest{ + { + "avg agg: simple", + Avg("average_score", "score"), + map[string]interface{}{ + "avg": map[string]interface{}{ + "field": "score", + }, + }, + }, + { + "avg agg: with missing", + Avg("average_score", "score").Missing(2), + map[string]interface{}{ + "avg": map[string]interface{}{ + "field": "score", + "missing": 2, + }, + }, + }, + { + "weighted avg", + WeightedAvg("weighted_grade").Value("grade", 2).Weight("weight"), + map[string]interface{}{ + "weighted_avg": map[string]interface{}{ + "value": map[string]interface{}{ + "field": "grade", + "missing": 2, + }, + "weight": map[string]interface{}{ + "field": "weight", + }, + }, + }, + }, + { + "cardinality: no precision threshold", + Cardinality("type_count", "type"), + map[string]interface{}{ + "cardinality": map[string]interface{}{ + "field": "type", + }, + }, + }, + { + "cardinality: with precision threshold", + Cardinality("type_count", "type").PrecisionThreshold(100), + map[string]interface{}{ + "cardinality": map[string]interface{}{ + "field": "type", + "precision_threshold": 100, + }, + }, + }, + { + "value_count agg: simple", + ValueCount("num_values", "score"), + map[string]interface{}{ + "value_count": map[string]interface{}{ + "field": "score", + }, + }, + }, + { + "sum agg: simple", + Sum("total_score", "score").Missing(1), + map[string]interface{}{ + "sum": map[string]interface{}{ + "field": "score", + "missing": 1, + }, + }, + }, + { + "max agg: simple", + Max("max_score", "score"), + map[string]interface{}{ + "max": map[string]interface{}{ + "field": "score", + }, + }, + }, + { + "min agg: simple", + Min("min_score", "score"), + map[string]interface{}{ + "min": map[string]interface{}{ + "field": "score", + }, + }, + }, + { + "percentiles: simple", + Percentiles("load_time_outlier", "load_time"), + map[string]interface{}{ + "percentiles": map[string]interface{}{ + "field": "load_time", + }, + }, + }, + { + "percentiles: complex", + Percentiles("load_time_outlier", "load_time"). + Keyed(true). + Percents(95, 99, 99.9). + Compression(200). + NumHistogramDigits(3). + Missing(20), + map[string]interface{}{ + "percentiles": map[string]interface{}{ + "field": "load_time", + "percents": []float32{95, 99, 99.9}, + "keyed": true, + "missing": 20, + "tdigest": map[string]interface{}{ + "compression": 200, + }, + "hdr": map[string]interface{}{ + "number_of_significant_value_digits": 3, + }, + }, + }, + }, + { + "stats agg", + Stats("grades_stats", "grade"), + map[string]interface{}{ + "stats": map[string]interface{}{ + "field": "grade", + }, + }, + }, + { + "string_stats agg: no show distribution", + StringStats("message_stats", "message.keyword"), + map[string]interface{}{ + "string_stats": map[string]interface{}{ + "field": "message.keyword", + }, + }, + }, + { + "string_stats agg: with show distribution", + StringStats("message_stats", "message.keyword").ShowDistribution(false), + map[string]interface{}{ + "string_stats": map[string]interface{}{ + "field": "message.keyword", + "show_distribution": false, + }, + }, + }, + }) +} diff --git a/boolean.go b/boolean.go deleted file mode 100644 index 251cb16..0000000 --- a/boolean.go +++ /dev/null @@ -1,61 +0,0 @@ -package esquery - -import "encoding/json" - -/******************************************************************************* - * Boolean Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html - ******************************************************************************/ - -type BoolQuery struct { - params boolQueryParams -} - -type boolQueryParams struct { - Must []json.Marshaler `json:"must,omitempty"` - Filter []json.Marshaler `json:"filter,omitempty"` - MustNot []json.Marshaler `json:"must_not,omitempty"` - Should []json.Marshaler `json:"should,omitempty"` - MinimumShouldMatch int16 `json:"minimum_should_match,omitempty"` - Boost float32 `json:"boost,omitempty"` -} - -func Bool() *BoolQuery { - return &BoolQuery{} -} - -func (q *BoolQuery) Must(must ...json.Marshaler) *BoolQuery { - q.params.Must = append(q.params.Must, must...) - return q -} - -func (q *BoolQuery) Filter(filter ...json.Marshaler) *BoolQuery { - q.params.Filter = append(q.params.Filter, filter...) - return q -} - -func (q *BoolQuery) MustNot(mustnot ...json.Marshaler) *BoolQuery { - q.params.MustNot = append(q.params.MustNot, mustnot...) - return q -} - -func (q *BoolQuery) Should(should ...json.Marshaler) *BoolQuery { - q.params.Should = append(q.params.Should, should...) - return q -} - -func (q *BoolQuery) MinimumShouldMatch(val int16) *BoolQuery { - q.params.MinimumShouldMatch = val - return q -} - -func (q *BoolQuery) Boost(val float32) *BoolQuery { - q.params.Boost = val - return q -} - -func (q BoolQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]boolQueryParams{ - "bool": q.params, - }) -} diff --git a/boolean_test.go b/boolean_test.go deleted file mode 100644 index 9fc5a9c..0000000 --- a/boolean_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestBool(t *testing.T) { - runTests(t, []queryTest{ - { - "bool with only a simple must", - Bool().Must(Term("tag", "tech")), - "{\"bool\":{\"must\":[{\"term\":{\"tag\":{\"value\":\"tech\"}}}]}}\n", - }, - { - "bool which must match_all and filter", - Bool().Must(MatchAll()).Filter(Term("status", "active")), - "{\"bool\":{\"must\":[{\"match_all\":{}}],\"filter\":[{\"term\":{\"status\":{\"value\":\"active\"}}}]}}\n", - }, - { - "bool with a lot of stuff", - Bool(). - Must(Term("user", "kimchy")). - Filter(Term("tag", "tech")). - MustNot(Range("age").Gte(10).Lte(20)). - Should(Term("tag", "wow"), Term("tag", "elasticsearch")). - MinimumShouldMatch(1). - Boost(1.1), - "{\"bool\":{\"must\":[{\"term\":{\"user\":{\"value\":\"kimchy\"}}}],\"filter\":[{\"term\":{\"tag\":{\"value\":\"tech\"}}}],\"must_not\":[{\"range\":{\"age\":{\"gte\":10,\"lte\":20}}}],\"should\":[{\"term\":{\"tag\":{\"value\":\"wow\"}}},{\"term\":{\"tag\":{\"value\":\"elasticsearch\"}}}],\"minimum_should_match\":1,\"boost\":1.1}}\n", - }, - }) -} diff --git a/boosting.go b/boosting.go deleted file mode 100644 index 245bf6a..0000000 --- a/boosting.go +++ /dev/null @@ -1,43 +0,0 @@ -package esquery - -import "encoding/json" - -/******************************************************************************* - * Boosting Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html - ******************************************************************************/ - -type BoostingQuery struct { - params boostingQueryParams -} - -type boostingQueryParams struct { - Positive json.Marshaler `json:"positive"` - Negative json.Marshaler `json:"negative"` - NegativeBoost float32 `json:"negative_boost"` -} - -func Boosting() *BoostingQuery { - return &BoostingQuery{} -} - -func (q *BoostingQuery) Positive(p json.Marshaler) *BoostingQuery { - q.params.Positive = p - return q -} - -func (q *BoostingQuery) Negative(p json.Marshaler) *BoostingQuery { - q.params.Negative = p - return q -} - -func (q *BoostingQuery) NegativeBoost(b float32) *BoostingQuery { - q.params.NegativeBoost = b - return q -} - -func (q *BoostingQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]boostingQueryParams{ - "boosting": q.params, - }) -} diff --git a/boosting_test.go b/boosting_test.go deleted file mode 100644 index 1fb57a0..0000000 --- a/boosting_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestBoost(t *testing.T) { - runTests(t, []queryTest{ - { - "boosting query", - Boosting(). - Positive(Term("text", "apple")). - Negative(Term("text", "pie tart")). - NegativeBoost(0.5), - "{\"boosting\":{\"positive\":{\"term\":{\"text\":{\"value\":\"apple\"}}},\"negative\":{\"term\":{\"text\":{\"value\":\"pie tart\"}}},\"negative_boost\":0.5}}\n", - }, - }) -} diff --git a/constant_score.go b/constant_score.go deleted file mode 100644 index 6398464..0000000 --- a/constant_score.go +++ /dev/null @@ -1,29 +0,0 @@ -package esquery - -import "encoding/json" - -type ConstantScoreQuery struct { - params constantScoreParams -} - -type constantScoreParams struct { - Filter json.Marshaler `json:"filter"` - Boost float32 `json:"boost,omitempty"` -} - -func ConstantScore(filter json.Marshaler) *ConstantScoreQuery { - return &ConstantScoreQuery{ - params: constantScoreParams{Filter: filter}, - } -} - -func (q *ConstantScoreQuery) Boost(b float32) *ConstantScoreQuery { - q.params.Boost = b - return q -} - -func (q ConstantScoreQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]constantScoreParams{ - "constant_score": q.params, - }) -} diff --git a/constant_score_test.go b/constant_score_test.go deleted file mode 100644 index f3c23cc..0000000 --- a/constant_score_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestConstantScore(t *testing.T) { - runTests(t, []queryTest{ - { - "constant_score query without boost", - ConstantScore(Term("user", "kimchy")), - "{\"constant_score\":{\"filter\":{\"term\":{\"user\":{\"value\":\"kimchy\"}}}}}\n", - }, - { - "constant_score query with boost", - ConstantScore(Term("user", "kimchy")).Boost(2.2), - "{\"constant_score\":{\"filter\":{\"term\":{\"user\":{\"value\":\"kimchy\"}}},\"boost\":2.2}}\n", - }, - }) -} diff --git a/dis_max.go b/dis_max.go deleted file mode 100644 index 31fda6a..0000000 --- a/dis_max.go +++ /dev/null @@ -1,31 +0,0 @@ -package esquery - -import "encoding/json" - -type DisMaxQuery struct { - params disMaxParams -} - -type disMaxParams struct { - Queries []json.Marshaler `json:"queries"` - TieBreaker float32 `json:"tie_breaker,omitempty"` -} - -func DisMax(queries ...json.Marshaler) *DisMaxQuery { - return &DisMaxQuery{ - params: disMaxParams{ - Queries: queries, - }, - } -} - -func (q *DisMaxQuery) TieBreaker(b float32) *DisMaxQuery { - q.params.TieBreaker = b - return q -} - -func (q DisMaxQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]disMaxParams{ - "dis_max": q.params, - }) -} diff --git a/dis_max_test.go b/dis_max_test.go deleted file mode 100644 index 8e2fc35..0000000 --- a/dis_max_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestDisMax(t *testing.T) { - runTests(t, []queryTest{ - { - "dis_max", - DisMax(Term("title", "Quick pets"), Term("body", "Quick pets")).TieBreaker(0.7), - "{\"dis_max\":{\"queries\":[{\"term\":{\"title\":{\"value\":\"Quick pets\"}}},{\"term\":{\"body\":{\"value\":\"Quick pets\"}}}],\"tie_breaker\":0.7}}\n", - }, - }) -} diff --git a/es.go b/es.go index 10e24eb..3c21c4b 100644 --- a/es.go +++ b/es.go @@ -1,46 +1,5 @@ package esquery -import ( - "bytes" - "encoding/json" - "fmt" - - "github.com/elastic/go-elasticsearch/v7" - "github.com/elastic/go-elasticsearch/v7/esapi" -) - -type ESQuery struct { - Query json.Marshaler `json:"query"` -} - -func encode(q json.Marshaler, b *bytes.Buffer) (err error) { - b.Reset() - err = json.NewEncoder(b).Encode(q) - if err != nil { - return fmt.Errorf("failed encoding query to JSON: %w", err) - } - - return nil -} - -func Search( - api *elasticsearch.Client, - q json.Marshaler, - o ...func(*esapi.SearchRequest), -) (res *esapi.Response, err error) { - var b bytes.Buffer - err = encode(ESQuery{q}, &b) - if err != nil { - return res, err - } - - opts := append([]func(*esapi.SearchRequest){api.Search.WithBody(&b)}, o...) - - return api.Search(opts...) -} - -func (q ESQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]json.Marshaler{ - "query": q.Query, - }) +type Mappable interface { + Map() map[string]interface{} } diff --git a/es_test.go b/es_test.go index fad04b1..346330a 100644 --- a/es_test.go +++ b/es_test.go @@ -1,27 +1,40 @@ package esquery import ( - "bytes" "encoding/json" + "reflect" "testing" ) -type queryTest struct { - name string - q json.Marshaler - expJSON string +type mapTest struct { + name string + q Mappable + exp map[string]interface{} } -func runTests(t *testing.T, tests []queryTest) { +func runMapTests(t *testing.T, tests []mapTest) { for _, test := range tests { - var b bytes.Buffer t.Run(test.name, func(t *testing.T) { - err := encode(test.q, &b) - if err != nil { - t.Errorf("unexpectedly failed: %s", err) - } else if b.String() != test.expJSON { - t.Errorf("expected %q, got %q", test.expJSON, b.String()) + m := test.q.Map() + + // convert both maps to JSON in order to compare them. we do not + // use reflect.DeepEqual on the maps as this doesn't always work + exp, got, ok := sameJSON(test.exp, m) + if !ok { + t.Errorf("expected %s, got %s", exp, got) } }) } } + +func sameJSON(a, b map[string]interface{}) (aJSON, bJSON []byte, ok bool) { + aJSON, aErr := json.Marshal(a) + bJSON, bErr := json.Marshal(b) + + if aErr != nil || bErr != nil { + return aJSON, bJSON, false + } + + ok = reflect.DeepEqual(aJSON, bJSON) + return aJSON, bJSON, ok +} diff --git a/go.mod b/go.mod index 223085a..591e5e5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module bitbucket.org/scalock/esquery go 1.13 require ( - github.com/elastic/go-elasticsearch v0.0.0 github.com/elastic/go-elasticsearch/v7 v7.6.0 - github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde + github.com/fatih/structs v1.1.0 ) diff --git a/go.sum b/go.sum index 30afa0b..680eeb3 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,5 @@ github.com/elastic/go-elasticsearch/v7 v7.6.0 h1:sYpGLpEFHgLUKLsZUBfuaVI9QgHjS3J github.com/elastic/go-elasticsearch/v7 v7.6.0/go.mod h1:OJ4wdbtDNk5g503kvlHLyErCgQwwzmDtaFC4XyOxXA4= github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde h1:Y9SZx8RQqFycLxi5W5eFmxMqnmijULVc3LMjBTtZQdM= github.com/elastic/go-elasticsearch/v8 v8.0.0-20200210103600-aff00e5adfde/go.mod h1:xe9a/L2aeOgFKKgrO3ibQTnMdpAeL0GC+5/HpGScSa4= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= diff --git a/match_all_test.go b/match_all_test.go deleted file mode 100644 index fd6f0ec..0000000 --- a/match_all_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestMatchAll(t *testing.T) { - runTests(t, []queryTest{ - {"match_all without a boost", MatchAll(), "{\"match_all\":{}}\n"}, - {"match_all with a boost", MatchAll().Boost(2.3), "{\"match_all\":{\"boost\":2.3}}\n"}, - {"match_none", MatchNone(), "{\"match_none\":{}}\n"}, - }) -} diff --git a/match_test.go b/match_test.go deleted file mode 100644 index cfbbbbe..0000000 --- a/match_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestMatch(t *testing.T) { - runTests(t, []queryTest{ - {"simple match", Match("title", "sample text"), "{\"match\":{\"title\":{\"query\":\"sample text\"}}}\n"}, - {"match with more params", Match("issue_number").Query(16).Transpositions(false).MaxExpansions(32).Operator(AND), "{\"match\":{\"issue_number\":{\"query\":16,\"max_expansions\":32,\"transpositions\":false,\"operator\":\"and\"}}}\n"}, - {"match_bool_prefix", MatchBoolPrefix("title", "sample text"), "{\"match_bool_prefix\":{\"title\":{\"query\":\"sample text\"}}}\n"}, - {"match_phrase", MatchPhrase("title", "sample text"), "{\"match_phrase\":{\"title\":{\"query\":\"sample text\"}}}\n"}, - {"match_phrase_prefix", MatchPhrasePrefix("title", "sample text"), "{\"match_phrase_prefix\":{\"title\":{\"query\":\"sample text\"}}}\n"}, - }) -} diff --git a/queries.go b/queries.go new file mode 100644 index 0000000..25a4fdd --- /dev/null +++ b/queries.go @@ -0,0 +1,38 @@ +package esquery + +import ( + "bytes" + "encoding/json" + + "github.com/elastic/go-elasticsearch/v7" + "github.com/elastic/go-elasticsearch/v7/esapi" +) + +type QueryRequest struct { + Query Mappable +} + +func Query(q Mappable) *QueryRequest { + return &QueryRequest{q} +} + +func (req *QueryRequest) Map() map[string]interface{} { + return map[string]interface{}{ + "query": req.Query.Map(), + } +} + +func (req *QueryRequest) Run( + api *elasticsearch.Client, + o ...func(*esapi.SearchRequest), +) (res *esapi.Response, err error) { + var b bytes.Buffer + err = json.NewEncoder(&b).Encode(req.Query.Map()) + if err != nil { + return nil, err + } + + opts := append([]func(*esapi.SearchRequest){api.Search.WithBody(&b)}, o...) + + return api.Search(opts...) +} diff --git a/queries_test.go b/queries_test.go new file mode 100644 index 0000000..64b4d60 --- /dev/null +++ b/queries_test.go @@ -0,0 +1,68 @@ +package esquery + +import ( + "testing" +) + +func TestQueries(t *testing.T) { + runMapTests(t, []mapTest{ + { + "a simple match_all query", + Query(MatchAll()), + map[string]interface{}{ + "query": map[string]interface{}{ + "match_all": map[string]interface{}{}, + }, + }, + }, + { + "a complex query", + Query( + Bool(). + Must( + Range("date"). + Gt("some time in the past"). + Lte("now"). + Relation(CONTAINS). + TimeZone("Asia/Jerusalem"). + Boost(2.3), + + Match("author"). + Query("some guy"). + Analyzer("analyzer?"). + Fuzziness("fuzz"), + ). + Boost(3.1), + ), + map[string]interface{}{ + "query": map[string]interface{}{ + "bool": map[string]interface{}{ + "must": []map[string]interface{}{ + { + "range": map[string]interface{}{ + "date": map[string]interface{}{ + "gt": "some time in the past", + "lte": "now", + "relation": "CONTAINS", + "time_zone": "Asia/Jerusalem", + "boost": 2.3, + }, + }, + }, + { + "match": map[string]interface{}{ + "author": map[string]interface{}{ + "query": "some guy", + "analyzer": "analyzer?", + "fuzziness": "fuzz", + }, + }, + }, + }, + "boost": 3.1, + }, + }, + }, + }, + }) +} diff --git a/query_boolean.go b/query_boolean.go new file mode 100644 index 0000000..3e93c28 --- /dev/null +++ b/query_boolean.go @@ -0,0 +1,97 @@ +package esquery + +import "github.com/fatih/structs" + +/******************************************************************************* + * Boolean Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html + ******************************************************************************/ + +type BoolQuery struct { + must []Mappable + filter []Mappable + mustNot []Mappable + should []Mappable + minimumShouldMatch int16 + boost float32 +} + +func Bool() *BoolQuery { + return &BoolQuery{} +} + +func (q *BoolQuery) Must(must ...Mappable) *BoolQuery { + q.must = append(q.must, must...) + return q +} + +func (q *BoolQuery) Filter(filter ...Mappable) *BoolQuery { + q.filter = append(q.filter, filter...) + return q +} + +func (q *BoolQuery) MustNot(mustnot ...Mappable) *BoolQuery { + q.mustNot = append(q.mustNot, mustnot...) + return q +} + +func (q *BoolQuery) Should(should ...Mappable) *BoolQuery { + q.should = append(q.should, should...) + return q +} + +func (q *BoolQuery) MinimumShouldMatch(val int16) *BoolQuery { + q.minimumShouldMatch = val + return q +} + +func (q *BoolQuery) Boost(val float32) *BoolQuery { + q.boost = val + return q +} + +func (q *BoolQuery) Map() map[string]interface{} { + var data struct { + Must []map[string]interface{} `structs:"must,omitempty"` + Filter []map[string]interface{} `structs:"filter,omitempty"` + MustNot []map[string]interface{} `structs:"must_not,omitempty"` + Should []map[string]interface{} `structs:"should,omitempty"` + MinimumShouldMatch int16 `structs:"minimum_should_match,omitempty"` + Boost float32 `structs:"boost,omitempty"` + } + + data.MinimumShouldMatch = q.minimumShouldMatch + data.Boost = q.boost + + if len(q.must) > 0 { + data.Must = make([]map[string]interface{}, len(q.must)) + for i, m := range q.must { + data.Must[i] = m.Map() + } + } + + if len(q.filter) > 0 { + data.Filter = make([]map[string]interface{}, len(q.filter)) + for i, m := range q.filter { + data.Filter[i] = m.Map() + } + } + + if len(q.mustNot) > 0 { + data.MustNot = make([]map[string]interface{}, len(q.mustNot)) + for i, m := range q.mustNot { + data.MustNot[i] = m.Map() + } + } + + if len(q.should) > 0 { + data.Should = make([]map[string]interface{}, len(q.should)) + for i, m := range q.should { + data.Should[i] = m.Map() + } + } + + return map[string]interface{}{ + "bool": structs.Map(data), + } +} diff --git a/query_boolean_test.go b/query_boolean_test.go new file mode 100644 index 0000000..7acbab6 --- /dev/null +++ b/query_boolean_test.go @@ -0,0 +1,107 @@ +package esquery + +import ( + "testing" +) + +func TestBool(t *testing.T) { + runMapTests(t, []mapTest{ + { + "bool with only a simple must", + Bool().Must(Term("tag", "tech")), + map[string]interface{}{ + "bool": map[string]interface{}{ + "must": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "tag": map[string]interface{}{ + "value": "tech", + }, + }, + }, + }, + }, + }, + }, + { + "bool which must match_all and filter", + Bool().Must(MatchAll()).Filter(Term("status", "active")), + map[string]interface{}{ + "bool": map[string]interface{}{ + "must": []map[string]interface{}{ + {"match_all": map[string]interface{}{}}, + }, + "filter": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "status": map[string]interface{}{ + "value": "active", + }, + }, + }, + }, + }, + }, + }, + { + "bool with a lot of stuff", + Bool(). + Must(Term("user", "kimchy")). + Filter(Term("tag", "tech")). + MustNot(Range("age").Gte(10).Lte(20)). + Should(Term("tag", "wow"), Term("tag", "elasticsearch")). + MinimumShouldMatch(1). + Boost(1.1), + map[string]interface{}{ + "bool": map[string]interface{}{ + "must": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "kimchy", + }, + }, + }, + }, + "filter": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "tag": map[string]interface{}{ + "value": "tech", + }, + }, + }, + }, + "must_not": []map[string]interface{}{ + { + "range": map[string]interface{}{ + "age": map[string]interface{}{ + "gte": 10, + "lte": 20, + }, + }, + }, + }, + "should": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "tag": map[string]interface{}{ + "value": "wow", + }, + }, + }, + { + "term": map[string]interface{}{ + "tag": map[string]interface{}{ + "value": "elasticsearch", + }, + }, + }, + }, + "minimum_should_match": 1, + "boost": 1.1, + }, + }, + }, + }) +} diff --git a/query_boosting.go b/query_boosting.go new file mode 100644 index 0000000..9342164 --- /dev/null +++ b/query_boosting.go @@ -0,0 +1,41 @@ +package esquery + +/******************************************************************************* + * Boosting Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html + ******************************************************************************/ + +type BoostingQuery struct { + Pos Mappable + Neg Mappable + NegBoost float32 +} + +func Boosting() *BoostingQuery { + return &BoostingQuery{} +} + +func (q *BoostingQuery) Positive(p Mappable) *BoostingQuery { + q.Pos = p + return q +} + +func (q *BoostingQuery) Negative(p Mappable) *BoostingQuery { + q.Neg = p + return q +} + +func (q *BoostingQuery) NegativeBoost(b float32) *BoostingQuery { + q.NegBoost = b + return q +} + +func (q *BoostingQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "boosting": map[string]interface{}{ + "positive": q.Pos.Map(), + "negative": q.Neg.Map(), + "negative_boost": q.NegBoost, + }, + } +} diff --git a/query_boosting_test.go b/query_boosting_test.go new file mode 100644 index 0000000..c88b35f --- /dev/null +++ b/query_boosting_test.go @@ -0,0 +1,36 @@ +package esquery + +import ( + "testing" +) + +func TestBoosting(t *testing.T) { + runMapTests(t, []mapTest{ + { + "boosting query", + Boosting(). + Positive(Term("text", "apple")). + Negative(Term("text", "pie tart")). + NegativeBoost(0.5), + map[string]interface{}{ + "boosting": map[string]interface{}{ + "positive": map[string]interface{}{ + "term": map[string]interface{}{ + "text": map[string]interface{}{ + "value": "apple", + }, + }, + }, + "negative": map[string]interface{}{ + "term": map[string]interface{}{ + "text": map[string]interface{}{ + "value": "pie tart", + }, + }, + }, + "negative_boost": 0.5, + }, + }, + }, + }) +} diff --git a/query_constant_score.go b/query_constant_score.go new file mode 100644 index 0000000..cf65090 --- /dev/null +++ b/query_constant_score.go @@ -0,0 +1,33 @@ +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 + ******************************************************************************/ + +type ConstantScoreQuery struct { + filter Mappable + boost float32 +} + +func ConstantScore(filter Mappable) *ConstantScoreQuery { + return &ConstantScoreQuery{ + filter: filter, + } +} + +func (q *ConstantScoreQuery) Boost(b float32) *ConstantScoreQuery { + q.boost = b + return q +} + +func (q *ConstantScoreQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "constant_score": structs.Map(struct { + Filter map[string]interface{} `structs:"filter"` + Boost float32 `structs:"boost,omitempty"` + }{q.filter.Map(), q.boost}), + } +} diff --git a/query_constant_score_test.go b/query_constant_score_test.go new file mode 100644 index 0000000..c62576a --- /dev/null +++ b/query_constant_score_test.go @@ -0,0 +1,41 @@ +package esquery + +import ( + "testing" +) + +func TestConstantScore(t *testing.T) { + runMapTests(t, []mapTest{ + { + "constant_score query without boost", + ConstantScore(Term("user", "kimchy")), + map[string]interface{}{ + "constant_score": map[string]interface{}{ + "filter": map[string]interface{}{ + "term": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "kimchy", + }, + }, + }, + }, + }, + }, + { + "constant_score query with boost", + ConstantScore(Term("user", "kimchy")).Boost(2.2), + map[string]interface{}{ + "constant_score": map[string]interface{}{ + "filter": map[string]interface{}{ + "term": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "kimchy", + }, + }, + }, + "boost": 2.2, + }, + }, + }, + }) +} diff --git a/query_custom.go b/query_custom.go new file mode 100644 index 0000000..8c69dae --- /dev/null +++ b/query_custom.go @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..34ecfae --- /dev/null +++ b/query_custom_test.go @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..2889e3b --- /dev/null +++ b/query_dis_max.go @@ -0,0 +1,37 @@ +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 + ******************************************************************************/ + +type DisMaxQuery struct { + queries []Mappable + tieBreaker float32 +} + +func DisMax(queries ...Mappable) *DisMaxQuery { + return &DisMaxQuery{ + queries: queries, + } +} + +func (q *DisMaxQuery) TieBreaker(b float32) *DisMaxQuery { + q.tieBreaker = b + return q +} + +func (q *DisMaxQuery) Map() map[string]interface{} { + inner := make([]map[string]interface{}, len(q.queries)) + for i, iq := range q.queries { + inner[i] = iq.Map() + } + return map[string]interface{}{ + "dis_max": structs.Map(struct { + Queries []map[string]interface{} `structs:"queries"` + TieBreaker float32 `structs:"tie_breaker,omitempty"` + }{inner, q.tieBreaker}), + } +} diff --git a/query_dis_max_test.go b/query_dis_max_test.go new file mode 100644 index 0000000..1152e14 --- /dev/null +++ b/query_dis_max_test.go @@ -0,0 +1,35 @@ +package esquery + +import ( + "testing" +) + +func TestDisMax(t *testing.T) { + runMapTests(t, []mapTest{ + { + "dis_max", + DisMax(Term("title", "Quick pets"), Term("body", "Quick pets")).TieBreaker(0.7), + map[string]interface{}{ + "dis_max": map[string]interface{}{ + "queries": []map[string]interface{}{ + { + "term": map[string]interface{}{ + "title": map[string]interface{}{ + "value": "Quick pets", + }, + }, + }, + { + "term": map[string]interface{}{ + "body": map[string]interface{}{ + "value": "Quick pets", + }, + }, + }, + }, + "tie_breaker": 0.7, + }, + }, + }, + }) +} diff --git a/match.go b/query_match.go similarity index 59% rename from match.go rename to query_match.go index 896d42c..0341746 100644 --- a/match.go +++ b/query_match.go @@ -2,9 +2,9 @@ package esquery import ( "bytes" - "encoding/json" - "errors" "io" + + "github.com/fatih/structs" ) /******************************************************************************* @@ -29,7 +29,7 @@ type MatchQuery struct { params matchParams } -func (a MatchQuery) MarshalJSON() ([]byte, error) { +func (a *MatchQuery) Map() map[string]interface{} { var mType string switch a.mType { case TypeMatch: @@ -42,27 +42,27 @@ func (a MatchQuery) MarshalJSON() ([]byte, error) { mType = "match_phrase_prefix" } - return json.Marshal(map[string]interface{}{ + return map[string]interface{}{ mType: map[string]interface{}{ - a.field: a.params, + a.field: structs.Map(a.params), }, - }) + } } type matchParams struct { - Qry interface{} `json:"query"` - Anl string `json:"analyzer,omitempty"` - AutoGenerate *bool `json:"auto_generate_synonyms_phrase_query,omitempty"` - Fuzz string `json:"fuzziness,omitempty"` - MaxExp uint16 `json:"max_expansions,omitempty"` - PrefLen uint16 `json:"prefix_length,omitempty"` - Trans *bool `json:"transpositions,omitempty"` - FuzzyRw string `json:"fuzzy_rewrite,omitempty"` - Lent bool `json:"lenient,omitempty"` - Op MatchOperator `json:"operator,omitempty"` - MinMatch string `json:"minimum_should_match,omitempty"` - ZeroTerms string `json:"zero_terms_query,omitempty"` - Slp uint16 `json:"slop,omitempty"` // only relevant for match_phrase query + Qry interface{} `structs:"query"` + Anl string `structs:"analyzer,omitempty"` + AutoGenerate *bool `structs:"auto_generate_synonyms_phrase_query,omitempty"` + Fuzz string `structs:"fuzziness,omitempty"` + MaxExp uint16 `structs:"max_expansions,omitempty"` + PrefLen uint16 `structs:"prefix_length,omitempty"` + Trans *bool `structs:"transpositions,omitempty"` + FuzzyRw string `structs:"fuzzy_rewrite,omitempty"` + Lent bool `structs:"lenient,omitempty"` + Op MatchOperator `structs:"operator,string,omitempty"` + MinMatch string `structs:"minimum_should_match,omitempty"` + ZeroTerms ZeroTerms `structs:"zero_terms_query,string,omitempty"` + Slp uint16 `structs:"slop,omitempty"` // only relevant for match_phrase query } func Match(fieldName string, simpleQuery ...interface{}) *MatchQuery { @@ -156,7 +156,7 @@ func (q *MatchQuery) Slop(n uint16) *MatchQuery { return q } -func (q *MatchQuery) ZeroTermsQuery(s string) *MatchQuery { +func (q *MatchQuery) ZeroTermsQuery(s ZeroTerms) *MatchQuery { q.params.ZeroTerms = s return q } @@ -173,20 +173,15 @@ const ( AND ) -var ErrInvalidValue = errors.New("invalid constant value") - -func (a MatchOperator) MarshalJSON() ([]byte, error) { - var s string +func (a MatchOperator) String() string { switch a { case OR: - s = "or" + return "or" case AND: - s = "and" + return "and" default: - return nil, ErrInvalidValue + return "" } - - return json.Marshal(s) } type ZeroTerms uint8 @@ -196,60 +191,13 @@ const ( All ) -func (a ZeroTerms) MarshalJSON() ([]byte, error) { - var s string +func (a ZeroTerms) String() string { switch a { case None: - s = "none" + return "none" case All: - s = "all" + return "all" default: - return nil, ErrInvalidValue + return "" } - - return json.Marshal(s) } - -/******************************************************************************* - * Multi-Match Queries - * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html - * NOTE: uncommented for now, article is too long - ******************************************************************************/ - -//type MultiMatchQuery struct { -//fields []string -//mType multiMatchType -//params multiMatchQueryParams -//} - -//type multiMatchType uint8 - -//const ( -//BestFields multiMatchType = iota -//MostFields -//CrossFields -//Phrase -//PhrasePrefix -//BoolPrefix -//) - -//func (a multiMatchType) MarshalJSON() ([]byte, error) { -//var s string -//switch a { -//case BestFields: -//s = "best_fields" -//case MostFields: -//s = "most_fields" -//case CrossFields: -//s = "cross_fields" -//case Phrase: -//s = "phrase" -//case PhrasePrefix: -//s = "phrase_prefix" -//case BoolPrefix: -//s = "bool_prefix" -//default: -//return nil, ErrInvalidValue -//} -//return json.Marshal(s) -//} diff --git a/match_all.go b/query_match_all.go similarity index 53% rename from match_all.go rename to query_match_all.go index c26363c..0d7f25a 100644 --- a/match_all.go +++ b/query_match_all.go @@ -1,20 +1,22 @@ package esquery -import ( - "encoding/json" -) +import "github.com/fatih/structs" + +/******************************************************************************* + * Match All Queries + * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html + ******************************************************************************/ -// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html type MatchAllQuery struct { all bool params matchAllParams } type matchAllParams struct { - Boost float32 `json:"boost,omitempty"` + Boost float32 `structs:"boost,omitempty"` } -func (a MatchAllQuery) MarshalJSON() ([]byte, error) { +func (a *MatchAllQuery) Map() map[string]interface{} { var mType string switch a.all { case true: @@ -23,7 +25,9 @@ func (a MatchAllQuery) MarshalJSON() ([]byte, error) { mType = "match_none" } - return json.Marshal(map[string]matchAllParams{mType: a.params}) + return map[string]interface{}{ + mType: structs.Map(a.params), + } } func MatchAll() *MatchAllQuery { diff --git a/query_match_all_test.go b/query_match_all_test.go new file mode 100644 index 0000000..dbc28b1 --- /dev/null +++ b/query_match_all_test.go @@ -0,0 +1,33 @@ +package esquery + +import ( + "testing" +) + +func TestMatchAll(t *testing.T) { + runMapTests(t, []mapTest{ + { + "match_all without a boost", + MatchAll(), + map[string]interface{}{ + "match_all": map[string]interface{}{}, + }, + }, + { + "match_all with a boost", + MatchAll().Boost(2.3), + map[string]interface{}{ + "match_all": map[string]interface{}{ + "boost": 2.3, + }, + }, + }, + { + "match_none", + MatchNone(), + map[string]interface{}{ + "match_none": map[string]interface{}{}, + }, + }, + }) +} diff --git a/query_match_test.go b/query_match_test.go new file mode 100644 index 0000000..3a3d290 --- /dev/null +++ b/query_match_test.go @@ -0,0 +1,68 @@ +package esquery + +import ( + "testing" +) + +func TestMatch(t *testing.T) { + runMapTests(t, []mapTest{ + { + "simple match", + Match("title", "sample text"), + map[string]interface{}{ + "match": map[string]interface{}{ + "title": map[string]interface{}{ + "query": "sample text", + }, + }, + }, + }, + { + "match with more params", + Match("issue_number").Query(16).Transpositions(false).MaxExpansions(32).Operator(AND), + map[string]interface{}{ + "match": map[string]interface{}{ + "issue_number": map[string]interface{}{ + "query": 16, + "max_expansions": 32, + "transpositions": false, + "operator": "and", + }, + }, + }, + }, + { + "match_bool_prefix", + MatchBoolPrefix("title", "sample text"), + map[string]interface{}{ + "match_bool_prefix": map[string]interface{}{ + "title": map[string]interface{}{ + "query": "sample text", + }, + }, + }, + }, + { + "match_phrase", + MatchPhrase("title", "sample text"), + map[string]interface{}{ + "match_phrase": map[string]interface{}{ + "title": map[string]interface{}{ + "query": "sample text", + }, + }, + }, + }, + { + "match_phrase_prefix", + MatchPhrasePrefix("title", "sample text"), + map[string]interface{}{ + "match_phrase_prefix": map[string]interface{}{ + "title": map[string]interface{}{ + "query": "sample text", + }, + }, + }, + }, + }) +} diff --git a/term_level.go b/query_term_level.go similarity index 71% rename from term_level.go rename to query_term_level.go index e8f6b36..82e3adc 100644 --- a/term_level.go +++ b/query_term_level.go @@ -1,7 +1,7 @@ package esquery import ( - "encoding/json" + "github.com/fatih/structs" ) /******************************************************************************* @@ -9,19 +9,18 @@ import ( * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html ******************************************************************************/ -type ExistsQuery string - -func Exists(field string) *ExistsQuery { - q := ExistsQuery(field) - return &q +type ExistsQuery struct { + Field string `structs:"field"` } -func (q ExistsQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "exists": map[string]string{ - "field": string(q), - }, - }) +func Exists(field string) *ExistsQuery { + return &ExistsQuery{field} +} + +func (q *ExistsQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "exists": structs.Map(q), + } } /******************************************************************************* @@ -29,19 +28,20 @@ func (q ExistsQuery) MarshalJSON() ([]byte, error) { * https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html ******************************************************************************/ -type IDsQuery []string - -func IDs(vals ...string) *IDsQuery { - q := IDsQuery(vals) - return &q +type IDsQuery struct { + IDs struct { + Values []string `structs:"values"` + } `structs:"ids"` } -func (q IDsQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "ids": map[string][]string{ - "values": []string(q), - }, - }) +func IDs(vals ...string) *IDsQuery { + q := &IDsQuery{} + q.IDs.Values = vals + return q +} + +func (q *IDsQuery) Map() map[string]interface{} { + return structs.Map(q) } /******************************************************************************* @@ -55,8 +55,8 @@ type PrefixQuery struct { } type prefixQueryParams struct { - Value string `json:"value"` - Rewrite string `json:"rewrite,omitempty"` + Value string `structs:"value"` + Rewrite string `structs:"rewrite,omitempty"` } func Prefix(field, value string) *PrefixQuery { @@ -71,12 +71,12 @@ func (q *PrefixQuery) Rewrite(s string) *PrefixQuery { return q } -func (q PrefixQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "prefix": map[string]prefixQueryParams{ - q.field: q.params, +func (q *PrefixQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "prefix": map[string]interface{}{ + q.field: structs.Map(q.params), }, - }) + } } /******************************************************************************* @@ -90,14 +90,14 @@ type RangeQuery struct { } type rangeQueryParams struct { - Gt interface{} `json:"gt,omitempty"` - Gte interface{} `json:"gte,omitempty"` - Lt interface{} `json:"lt,omitempty"` - Lte interface{} `json:"lte,omitempty"` - Format string `json:"format,omitempty"` - Relation RangeRelation `json:"relation,omitempty"` - TimeZone string `json:"time_zone,omitempty"` - Boost float32 `json:"boost,omitempty"` + Gt interface{} `structs:"gt,omitempty"` + Gte interface{} `structs:"gte,omitempty"` + Lt interface{} `structs:"lt,omitempty"` + Lte interface{} `structs:"lte,omitempty"` + Format string `structs:"format,omitempty"` + Relation RangeRelation `structs:"relation,string,omitempty"` + TimeZone string `structs:"time_zone,omitempty"` + Boost float32 `structs:"boost,omitempty"` } func Range(field string) *RangeQuery { @@ -144,12 +144,12 @@ func (a *RangeQuery) Boost(b float32) *RangeQuery { return a } -func (a RangeQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "range": map[string]rangeQueryParams{ - a.field: a.params, +func (a *RangeQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "range": map[string]interface{}{ + a.field: structs.Map(a.params), }, - }) + } } type RangeRelation uint8 @@ -160,20 +160,17 @@ const ( WITHIN ) -func (a RangeRelation) MarshalJSON() ([]byte, error) { - var s string +func (a RangeRelation) String() string { switch a { case INTERSECTS: - s = "INTERSECTS" + return "INTERSECTS" case CONTAINS: - s = "CONTAINS" + return "CONTAINS" case WITHIN: - s = "WITHIN" + return "WITHIN" default: - return nil, ErrInvalidValue + return "" } - - return json.Marshal(s) } /******************************************************************************* @@ -188,10 +185,10 @@ type RegexpQuery struct { } type regexpQueryParams struct { - Value string `json:"value"` - Flags string `json:"flags,omitempty"` - MaxDeterminizedStates uint16 `json:"max_determinized_states,omitempty"` - Rewrite string `json:"rewrite,omitempty"` + Value string `structs:"value"` + Flags string `structs:"flags,omitempty"` + MaxDeterminizedStates uint16 `structs:"max_determinized_states,omitempty"` + Rewrite string `structs:"rewrite,omitempty"` } func Regexp(field, value string) *RegexpQuery { @@ -227,18 +224,18 @@ func (q *RegexpQuery) Rewrite(r string) *RegexpQuery { return q } -func (q RegexpQuery) MarshalJSON() ([]byte, error) { +func (q *RegexpQuery) Map() map[string]interface{} { var qType string if q.wildcard { qType = "wildcard" } else { qType = "regexp" } - return json.Marshal(map[string]interface{}{ - qType: map[string]regexpQueryParams{ - q.field: q.params, + return map[string]interface{}{ + qType: map[string]interface{}{ + q.field: structs.Map(q.params), }, - }) + } } /******************************************************************************* @@ -267,12 +264,12 @@ type FuzzyQuery struct { } type fuzzyQueryParams struct { - Value string `json:"value"` - Fuzziness string `json:"fuzziness,omitempty"` - MaxExpansions uint16 `json:"max_expansions,omitempty"` - PrefixLength uint16 `json:"prefix_length,omitempty"` - Transpositions *bool `json:"transpositions,omitempty"` - Rewrite string `json:"rewrite,omitempty"` + Value string `structs:"value"` + Fuzziness string `structs:"fuzziness,omitempty"` + MaxExpansions uint16 `structs:"max_expansions,omitempty"` + PrefixLength uint16 `structs:"prefix_length,omitempty"` + Transpositions *bool `structs:"transpositions,omitempty"` + Rewrite string `structs:"rewrite,omitempty"` } func Fuzzy(field, value string) *FuzzyQuery { @@ -314,12 +311,12 @@ func (q *FuzzyQuery) Rewrite(s string) *FuzzyQuery { return q } -func (q FuzzyQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "fuzzy": map[string]fuzzyQueryParams{ - q.field: q.params, +func (q *FuzzyQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "fuzzy": map[string]interface{}{ + q.field: structs.Map(q.params), }, - }) + } } /******************************************************************************* @@ -333,8 +330,8 @@ type TermQuery struct { } type termQueryParams struct { - Value interface{} `json:"value"` - Boost float32 `json:"boost,omitempty"` + Value interface{} `structs:"value"` + Boost float32 `structs:"boost,omitempty"` } func Term(field string, value interface{}) *TermQuery { @@ -356,12 +353,12 @@ func (q *TermQuery) Boost(b float32) *TermQuery { return q } -func (q TermQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "term": map[string]termQueryParams{ - q.field: q.params, +func (q *TermQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "term": map[string]interface{}{ + q.field: structs.Map(q.params), }, - }) + } } /******************************************************************************* @@ -392,12 +389,13 @@ func (q *TermsQuery) Boost(b float32) *TermsQuery { return q } -func (q TermsQuery) MarshalJSON() ([]byte, error) { +func (q TermsQuery) Map() map[string]interface{} { innerMap := map[string]interface{}{q.field: q.values} if q.boost > 0 { innerMap["boost"] = q.boost } - return json.Marshal(map[string]interface{}{"terms": innerMap}) + + return map[string]interface{}{"terms": innerMap} } /******************************************************************************* @@ -411,9 +409,9 @@ type TermsSetQuery struct { } type termsSetQueryParams struct { - Terms []string `json:"terms"` - MinimumShouldMatchField string `json:"minimum_should_match_field,omitempty"` - MinimumShouldMatchScript string `json:"minimum_should_match_script,omitempty"` + Terms []string `structs:"terms"` + MinimumShouldMatchField string `structs:"minimum_should_match_field,omitempty"` + MinimumShouldMatchScript string `structs:"minimum_should_match_script,omitempty"` } func TermsSet(field string, terms ...string) *TermsSetQuery { @@ -440,10 +438,10 @@ func (q *TermsSetQuery) MinimumShouldMatchScript(script string) *TermsSetQuery { return q } -func (q TermsSetQuery) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "terms_set": map[string]termsSetQueryParams{ - q.field: q.params, +func (q TermsSetQuery) Map() map[string]interface{} { + return map[string]interface{}{ + "terms_set": map[string]interface{}{ + q.field: structs.Map(q.params), }, - }) + } } diff --git a/query_term_level_test.go b/query_term_level_test.go new file mode 100644 index 0000000..8878a68 --- /dev/null +++ b/query_term_level_test.go @@ -0,0 +1,151 @@ +package esquery + +import ( + "testing" +) + +func TestTermLevel(t *testing.T) { + runMapTests(t, []mapTest{ + { + "exists", + Exists("title"), + map[string]interface{}{ + "exists": map[string]interface{}{ + "field": "title", + }, + }, + }, + { + "ids", + IDs("1", "4", "100"), + map[string]interface{}{ + "ids": map[string]interface{}{ + "values": []string{"1", "4", "100"}, + }, + }, + }, + { + "simple prefix", + Prefix("user", "ki"), + map[string]interface{}{ + "prefix": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "ki", + }, + }, + }, + }, + { + "complex prefix", + Prefix("user", "ki").Rewrite("ji"), + map[string]interface{}{ + "prefix": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "ki", + "rewrite": "ji", + }, + }, + }, + }, + { + "int range", + Range("age").Gte(10).Lte(20).Boost(2.0), + map[string]interface{}{ + "range": map[string]interface{}{ + "age": map[string]interface{}{ + "gte": 10, + "lte": 20, + "boost": 2.0, + }, + }, + }, + }, + { + "string range", + Range("timestamp").Gte("now-1d/d").Lt("now/d").Relation(CONTAINS), + map[string]interface{}{ + "range": map[string]interface{}{ + "timestamp": map[string]interface{}{ + "gte": "now-1d/d", + "lt": "now/d", + "relation": "CONTAINS", + }, + }, + }, + }, + { + "regexp", + Regexp("user", "k.*y").Flags("ALL").MaxDeterminizedStates(10000).Rewrite("constant_score"), + map[string]interface{}{ + "regexp": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "k.*y", + "flags": "ALL", + "max_determinized_states": 10000, + "rewrite": "constant_score", + }, + }, + }, + }, + { + "wildcard", + Wildcard("user", "ki*y").Rewrite("constant_score"), + map[string]interface{}{ + "wildcard": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "ki*y", + "rewrite": "constant_score", + }, + }, + }, + }, + { + "fuzzy", + Fuzzy("user", "ki").Fuzziness("AUTO").MaxExpansions(50).Transpositions(true), + map[string]interface{}{ + "fuzzy": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "ki", + "fuzziness": "AUTO", + "max_expansions": 50, + "transpositions": true, + }, + }, + }, + }, + { + "term", + Term("user", "Kimchy").Boost(1.3), + map[string]interface{}{ + "term": map[string]interface{}{ + "user": map[string]interface{}{ + "value": "Kimchy", + "boost": 1.3, + }, + }, + }, + }, + { + "terms", + Terms("user").Values("bla", "pl").Boost(1.3), + map[string]interface{}{ + "terms": map[string]interface{}{ + "user": []string{"bla", "pl"}, + "boost": 1.3, + }, + }, + }, + { + "terms_set", + TermsSet("programming_languages", "go", "rust", "COBOL").MinimumShouldMatchField("required_matches"), + map[string]interface{}{ + "terms_set": map[string]interface{}{ + "programming_languages": map[string]interface{}{ + "terms": []string{"go", "rust", "COBOL"}, + "minimum_should_match_field": "required_matches", + }, + }, + }, + }, + }) +} diff --git a/term_level_test.go b/term_level_test.go deleted file mode 100644 index 02515f6..0000000 --- a/term_level_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package esquery - -import ( - "testing" -) - -func TestTermLevel(t *testing.T) { - runTests(t, []queryTest{ - {"exists", Exists("title"), "{\"exists\":{\"field\":\"title\"}}\n"}, - - {"ids", IDs("1", "4", "100"), "{\"ids\":{\"values\":[\"1\",\"4\",\"100\"]}}\n"}, - - {"simple prefix", Prefix("user", "ki"), "{\"prefix\":{\"user\":{\"value\":\"ki\"}}}\n"}, - - {"complex prefix", Prefix("user", "ki").Rewrite("ji"), "{\"prefix\":{\"user\":{\"value\":\"ki\",\"rewrite\":\"ji\"}}}\n"}, - - {"int range", Range("age").Gte(10).Lte(20).Boost(2.0), "{\"range\":{\"age\":{\"gte\":10,\"lte\":20,\"boost\":2}}}\n"}, - - {"string range", Range("timestamp").Gte("now-1d/d").Lt("now/d").Relation(CONTAINS), "{\"range\":{\"timestamp\":{\"gte\":\"now-1d/d\",\"lt\":\"now/d\",\"relation\":\"CONTAINS\"}}}\n"}, - - {"regexp", Regexp("user", "k.*y").Flags("ALL").MaxDeterminizedStates(10000).Rewrite("constant_score"), "{\"regexp\":{\"user\":{\"value\":\"k.*y\",\"flags\":\"ALL\",\"max_determinized_states\":10000,\"rewrite\":\"constant_score\"}}}\n"}, - - {"wildcard", Wildcard("user", "ki*y").Rewrite("constant_score"), "{\"wildcard\":{\"user\":{\"value\":\"ki*y\",\"rewrite\":\"constant_score\"}}}\n"}, - - {"fuzzy", Fuzzy("user", "ki").Fuzziness("AUTO").MaxExpansions(50).Transpositions(true), "{\"fuzzy\":{\"user\":{\"value\":\"ki\",\"fuzziness\":\"AUTO\",\"max_expansions\":50,\"transpositions\":true}}}\n"}, - - {"term", Term("user", "Kimchy").Boost(1.3), "{\"term\":{\"user\":{\"value\":\"Kimchy\",\"boost\":1.3}}}\n"}, - - {"terms", Terms("user").Values("bla", "pl").Boost(1.3), "{\"terms\":{\"boost\":1.3,\"user\":[\"bla\",\"pl\"]}}\n"}, - - {"terms_set", TermsSet("programming_languages", "go", "rust", "COBOL").MinimumShouldMatchField("required_matches"), "{\"terms_set\":{\"programming_languages\":{\"terms\":[\"go\",\"rust\",\"COBOL\"],\"minimum_should_match_field\":\"required_matches\"}}}\n"}, - }) -} From 8de6de1468a79a873761a895e823cc1837ffffd3 Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Thu, 20 Feb 2020 11:39:31 +0200 Subject: [PATCH 5/5] Improve README Add shields, TOC and more information to the README file. --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++----------- go.mod | 2 +- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 78e7f3b..91fbee2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,41 @@ # esquery -**esquery** is a non-obtrusive, idiomatic and easy-to-use query and aggregation builder for the [official Go client](https://github.com/elastic/go-elasticsearch) for [ElasticSearch](https://www.elastic.co/products/elasticsearch). It 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. +[![](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue&style=flat-square)](https://godoc.org/github.com/aquasecurity/esquery) [![](https://img.shields.io/github/license/aquasecurity/esquery?style=flat-square)](LICENSE) -Save yourself some joint aches and many lines of code by switching for maps to `esquery`. Wanna know how much code you'll save? just read this project's test. +**A non-obtrusive, idiomatic and easy-to-use query and aggregation builder for the [official Go client](https://github.com/elastic/go-elasticsearch) for [ElasticSearch](https://www.elastic.co/products/elasticsearch).** + +## Table of Contents + + + * [Description](#description) + * [Status](#status) + * [Installation](#installation) + * [Usage](#usage) + * [Notes](#notes) + * [Features](#features) + * [Supported Queries](#supported-queries) + * [Supported Aggregations](#supported-aggregations) + * [Custom Queries and Aggregations](#custom-queries-and-aggregations) + * [License](#license) + + +## Description + +`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. Wanna know how much code you'll save? just check this project's tests. + +## Status + +This is an early release, API may still change. + +## Installation + +`esquery` is a Go module. To install, simply run this in your project's root directory: + +```bash +go get github.com/aquasecurity/esquery +``` ## Usage @@ -17,7 +50,7 @@ import ( "context" "log" - "bitbucket.org/scalock/esquery" + "github.com/aquasecurity/esquery" "github.com/elastic/go-elasticsearch/v7" ) @@ -84,7 +117,9 @@ func main() { either receive one query object, or an array of query objects. `esquery` will generate an array even if there's only one query object. -## Supported Queries +## Features + +### Supported Queries The following queries are currently supported: @@ -111,11 +146,7 @@ The following queries are currently supported: | `"constant_score"` | `ConstantScore()` | | `"dis_max"` | `DisMax()` | -### Custom Queries - -To execute an arbitrary query, or any query that is not natively supported by the library yet, use the `CustomQuery()` function, which accepts any `map[string]interface{}` value. - -## Supported Aggregations +### Supported Aggregations The following aggregations are currently supported: @@ -132,6 +163,10 @@ The following aggregations are currently supported: | `"stats"` | `Stats()` | | `"string_stats"` | `StringStats()` | -### Custom Aggregations +#### Custom Queries and Aggregations -To execute an arbitrary aggregation, or any aggregation that is not natively supported by the library yet, use the `CustomAgg()` function, which accepts any `map[string]interface{}` value. +To execute an arbitrary query or aggregation (including those not yet supported by the library), use the `CustomQuery()` or `CustomAgg()` functions, respectively. Both accept any `map[string]interface{}` value. + +## License + +This library is distributed under the terms of the [Apache License 2.0](LICENSE). diff --git a/go.mod b/go.mod index 591e5e5..79ae130 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module bitbucket.org/scalock/esquery +module github.com/aquasecurity/esquery go 1.13