Merge pull request #1 from ido50/master

Merge initial version of the project
This commit is contained in:
Oran Moshai 2020-02-20 12:01:36 +02:00 committed by GitHub
commit abbec7e12d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2427 additions and 0 deletions

29
.golangci.yml Normal file
View File

@ -0,0 +1,29 @@
run:
tests: false
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

172
README.md Normal file
View File

@ -0,0 +1,172 @@
# esquery
[![](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)
**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
<!--ts-->
* [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)
<!--te-->
## 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
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
import (
"context"
"log"
"github.com/aquasecurity/esquery"
"github.com/elastic/go-elasticsearch/v7"
)
func main() {
// connect to an ElasticSearch instance
es, err := elasticsearch.NewDefaultClient()
if err != nil {
log.Fatalf("Failed creating client: %s", err)
}
// run a boolean search query
qRes, err := esquery.Query(
esquery.
Bool().
Must(esquery.Term("title", "Go and Stuff")).
Filter(esquery.Term("tag", "tech")),
).Run(
es,
es.Search.WithContext(context.TODO()),
es.Search.WithIndex("test"),
)
if err != nil {
log.Fatalf("Failed searching for stuff: %s", err)
}
defer qRes.Body.Close()
// run an aggregation
aRes, err := esquery.Aggregate(
esquery.Avg("average_score", "score"),
esquery.Max("max_score", "score"),
).Run(
es,
es.Search.WithContext(context.TODO()),
es.Search.WithIndex("test"),
)
if err != nil {
log.Fatalf("Failed searching for stuff: %s", err)
}
defer aRes.Body.Close()
// ...
}
```
## Notes
* `esquery` currently supports version 7 of the ElasticSearch Go client.
* The library cannot currently generate "short queries". For example, whereas
ElasticSearch can accept this:
```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.
## Features
### Supported Queries
The following queries are currently supported:
| ElasticSearch DSL | `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()` |
### 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 Queries and Aggregations
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).

56
aggregations.go Normal file
View File

@ -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...)
}

62
aggregations_test.go Normal file
View File

@ -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,
},
},
},
},
},
})
}

13
aggs_custom.go Normal file
View File

@ -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
}

35
aggs_custom_test.go Normal file
View File

@ -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,
},
})
}

324
aggs_metric.go Normal file
View File

@ -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),
}
}

158
aggs_metric_test.go Normal file
View File

@ -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,
},
},
},
})
}

5
es.go Normal file
View File

@ -0,0 +1,5 @@
package esquery
type Mappable interface {
Map() map[string]interface{}
}

40
es_test.go Normal file
View File

@ -0,0 +1,40 @@
package esquery
import (
"encoding/json"
"reflect"
"testing"
)
type mapTest struct {
name string
q Mappable
exp map[string]interface{}
}
func runMapTests(t *testing.T, tests []mapTest) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
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
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/aquasecurity/esquery
go 1.13
require (
github.com/elastic/go-elasticsearch/v7 v7.6.0
github.com/fatih/structs v1.1.0
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
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=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=

38
queries.go Normal file
View File

@ -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...)
}

68
queries_test.go Normal file
View File

@ -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,
},
},
},
},
})
}

97
query_boolean.go Normal file
View File

@ -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),
}
}

107
query_boolean_test.go Normal file
View File

@ -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,
},
},
},
})
}

41
query_boosting.go Normal file
View File

@ -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,
},
}
}

36
query_boosting_test.go Normal file
View File

@ -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,
},
},
},
})
}

33
query_constant_score.go Normal file
View File

@ -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}),
}
}

View File

@ -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,
},
},
},
})
}

13
query_custom.go Normal file
View File

@ -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
}

23
query_custom_test.go Normal file
View File

@ -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,
},
})
}

37
query_dis_max.go Normal file
View File

@ -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}),
}
}

35
query_dis_max_test.go Normal file
View File

@ -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,
},
},
},
})
}

203
query_match.go Normal file
View File

@ -0,0 +1,203 @@
package esquery
import (
"bytes"
"io"
"github.com/fatih/structs"
)
/*******************************************************************************
* Match Queries
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html
******************************************************************************/
type matchType uint8
const (
TypeMatch matchType = iota
TypeMatchBoolPrefix
TypeMatchPhrase
TypeMatchPhrasePrefix
)
type MatchQuery struct {
field string
mType matchType
params matchParams
}
func (a *MatchQuery) Map() map[string]interface{} {
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 map[string]interface{}{
mType: map[string]interface{}{
a.field: structs.Map(a.params),
},
}
}
type matchParams struct {
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 {
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 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 ZeroTerms) *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
)
func (a MatchOperator) String() string {
switch a {
case OR:
return "or"
case AND:
return "and"
default:
return ""
}
}
type ZeroTerms uint8
const (
None ZeroTerms = iota
All
)
func (a ZeroTerms) String() string {
switch a {
case None:
return "none"
case All:
return "all"
default:
return ""
}
}

46
query_match_all.go Normal file
View File

@ -0,0 +1,46 @@
package esquery
import "github.com/fatih/structs"
/*******************************************************************************
* Match All Queries
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html
******************************************************************************/
type MatchAllQuery struct {
all bool
params matchAllParams
}
type matchAllParams struct {
Boost float32 `structs:"boost,omitempty"`
}
func (a *MatchAllQuery) Map() map[string]interface{} {
var mType string
switch a.all {
case true:
mType = "match_all"
default:
mType = "match_none"
}
return map[string]interface{}{
mType: structs.Map(a.params),
}
}
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}
}

33
query_match_all_test.go Normal file
View File

@ -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{}{},
},
},
})
}

68
query_match_test.go Normal file
View File

@ -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",
},
},
},
},
})
}

447
query_term_level.go Normal file
View File

@ -0,0 +1,447 @@
package esquery
import (
"github.com/fatih/structs"
)
/*******************************************************************************
* Exists Queries
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html
******************************************************************************/
type ExistsQuery struct {
Field string `structs:"field"`
}
func Exists(field string) *ExistsQuery {
return &ExistsQuery{field}
}
func (q *ExistsQuery) Map() map[string]interface{} {
return map[string]interface{}{
"exists": structs.Map(q),
}
}
/*******************************************************************************
* IDs Queries
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html
******************************************************************************/
type IDsQuery struct {
IDs struct {
Values []string `structs:"values"`
} `structs:"ids"`
}
func IDs(vals ...string) *IDsQuery {
q := &IDsQuery{}
q.IDs.Values = vals
return q
}
func (q *IDsQuery) Map() map[string]interface{} {
return structs.Map(q)
}
/*******************************************************************************
* Prefix Queries
* https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html
******************************************************************************/
type PrefixQuery struct {
field string
params prefixQueryParams
}
type prefixQueryParams struct {
Value string `structs:"value"`
Rewrite string `structs:"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) Map() map[string]interface{} {
return map[string]interface{}{
"prefix": map[string]interface{}{
q.field: structs.Map(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{} `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 {
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) Map() map[string]interface{} {
return map[string]interface{}{
"range": map[string]interface{}{
a.field: structs.Map(a.params),
},
}
}
type RangeRelation uint8
const (
INTERSECTS RangeRelation = iota
CONTAINS
WITHIN
)
func (a RangeRelation) String() string {
switch a {
case INTERSECTS:
return "INTERSECTS"
case CONTAINS:
return "CONTAINS"
case WITHIN:
return "WITHIN"
default:
return ""
}
}
/*******************************************************************************
* 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 `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 {
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) Map() map[string]interface{} {
var qType string
if q.wildcard {
qType = "wildcard"
} else {
qType = "regexp"
}
return map[string]interface{}{
qType: map[string]interface{}{
q.field: structs.Map(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 `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 {
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) Map() map[string]interface{} {
return map[string]interface{}{
"fuzzy": map[string]interface{}{
q.field: structs.Map(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{} `structs:"value"`
Boost float32 `structs:"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) Map() map[string]interface{} {
return map[string]interface{}{
"term": map[string]interface{}{
q.field: structs.Map(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) Map() map[string]interface{} {
innerMap := map[string]interface{}{q.field: q.values}
if q.boost > 0 {
innerMap["boost"] = q.boost
}
return 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 `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 {
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) Map() map[string]interface{} {
return map[string]interface{}{
"terms_set": map[string]interface{}{
q.field: structs.Map(q.params),
},
}
}

151
query_term_level_test.go Normal file
View File

@ -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",
},
},
},
},
})
}