From 9ef149ec94a1df866bf3b9d881fb9cf6c527129a Mon Sep 17 00:00:00 2001 From: Ido Perlmuter Date: Tue, 18 Feb 2020 14:00:15 +0200 Subject: [PATCH] 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"}, + }) +}