KevinStephenson commited on
Commit
b110593
·
1 Parent(s): 9f9f982

Adding in weaviate code

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .codeclimate.yml +11 -0
  2. .dockerignore +39 -0
  3. .github/CODEOWNERS +3 -0
  4. .github/ISSUE_TEMPLATE/config.yml +9 -0
  5. .github/ISSUE_TEMPLATE/create_issue.yml +59 -0
  6. .github/ISSUE_TEMPLATE/feature_request.yml +29 -0
  7. .github/PULL_REQUEST_TEMPLATE.md +9 -0
  8. .github/dependabot.yml +7 -0
  9. .github/stale.yml +33 -0
  10. .github/workflows/branch.yaml +33 -0
  11. .github/workflows/linter.yml +29 -0
  12. .github/workflows/pull_requests.yaml +225 -0
  13. .github/workflows/release.yaml +32 -0
  14. .gitignore +109 -0
  15. .golangci.yml +62 -0
  16. .goreleaser.yaml +24 -0
  17. .pre-commit-config.yaml +13 -0
  18. .protolint.yaml +4 -0
  19. CITATION.cff +23 -0
  20. CODE_OF_CONDUCT.md +77 -0
  21. CONTRIBUTING.md +43 -0
  22. DockerfileHld +49 -0
  23. LICENSE +27 -0
  24. adapters/clients/client.go +96 -0
  25. adapters/clients/cluster_backups.go +178 -0
  26. adapters/clients/cluster_classifications.go +127 -0
  27. adapters/clients/cluster_node.go +66 -0
  28. adapters/clients/cluster_schema.go +164 -0
  29. adapters/clients/cluster_schema_test.go +441 -0
  30. adapters/clients/remote_index.go +839 -0
  31. adapters/clients/remote_index_test.go +312 -0
  32. adapters/clients/replication.go +311 -0
  33. adapters/clients/replication_test.go +592 -0
  34. adapters/handlers/graphql/common/fetch/filter.go +146 -0
  35. adapters/handlers/graphql/common/json_number.go +60 -0
  36. adapters/handlers/graphql/common/json_number_test.go +79 -0
  37. adapters/handlers/graphql/descriptions/aggregate.go +83 -0
  38. adapters/handlers/graphql/descriptions/explore.go +30 -0
  39. adapters/handlers/graphql/descriptions/fetch.go +55 -0
  40. adapters/handlers/graphql/descriptions/filters.go +154 -0
  41. adapters/handlers/graphql/descriptions/get.go +50 -0
  42. adapters/handlers/graphql/descriptions/getMeta.go +88 -0
  43. adapters/handlers/graphql/descriptions/introspect.go +30 -0
  44. adapters/handlers/graphql/descriptions/merge.go +31 -0
  45. adapters/handlers/graphql/descriptions/rootQuery.go +28 -0
  46. adapters/handlers/graphql/graphiql/graphiql.go +233 -0
  47. adapters/handlers/graphql/local/aggregate/aggregate.go +281 -0
  48. adapters/handlers/graphql/local/aggregate/explore_argument.go +25 -0
  49. adapters/handlers/graphql/local/aggregate/helpers_for_test.go +56 -0
  50. adapters/handlers/graphql/local/aggregate/hybrid_search.go +90 -0
.codeclimate.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "2"
2
+ exclude_pattern:
3
+ - client/
4
+ - cmd/weaviate-server/main.go
5
+
6
+ # exclude all but the configure_weaviate.go file in restapi/,
7
+ # given that the other files are automatically generated.
8
+ - restapi/
9
+ - !restapi/configure_weaviate*
10
+
11
+ - test/
.dockerignore ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ Dockerfile.*
3
+ docker-compose.yml
4
+ data*
5
+ logs/
6
+ snapshots/
7
+ snapshots-fs/
8
+ snapshots-node2/
9
+ snapshots-s3/
10
+ snapshots-gcs/
11
+ backups/
12
+ backups-fs/
13
+ backups-node2/
14
+ backups-s3/
15
+ backups-gcs/
16
+ test/
17
+ tools/
18
+ **/*.fvecs # sift data
19
+ **/*.png # profiling
20
+ **/*.out
21
+ **/*.csv
22
+ **/*.md
23
+ **/*_test.go
24
+ **/.DS_Store
25
+ **/testdata/
26
+ coverage-integration.txt
27
+ coverage-unit.txt
28
+
29
+ # wikipedia dataset when downloaded according to docs
30
+ weaviate-wikipedia-*
31
+ # the local path when extracting the wiki dataset
32
+ var/weaviate/
33
+
34
+ # IDE files
35
+ .idea
36
+ .vscode/
37
+
38
+ # ignore archives, they are most likely part of some debugging and never part of actual code
39
+ *.tar.gz
.github/CODEOWNERS ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Ci related folders
2
+ /.github/ @weaviate/core
3
+ /ci/ @weaviate/core
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Need Help? Go to the community forum
4
+ url: https://forum.weaviate.io
5
+ about: "Community-powered knowledge repository: search, ask questions, give feedback"
6
+
7
+ - name: Want to chat? Go to the community slack
8
+ url: https://weaviate.io/slack
9
+ about: "Chat with the community about issues, questions, networking and feedback"
.github/ISSUE_TEMPLATE/create_issue.yml ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Found a bug?
2
+ description: Report your bug here.
3
+ labels: ["bug", "triage"]
4
+ body:
5
+
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ #### Before you get started
10
+ * Check to make sure someone hasn't already opened a similar [issue](https://github.com/weaviate/weaviate-io/issues).
11
+ * Check this example of a [good bug report](https://github.com/weaviate/weaviate/issues/3762).
12
+ * Read the [Contributor Guide](https://weaviate.io/developers/contributor-guide) and [Code of Conduct](https://weaviate.io/service/code-of-conduct).
13
+
14
+ - type: textarea
15
+ id: how_to_reproduce
16
+ attributes:
17
+ label: How to reproduce this bug?
18
+ description: Specify the steps here in order to reproduce this bug.
19
+ validations:
20
+ required: true
21
+
22
+ - type: textarea
23
+ id: expected_behavior
24
+ attributes:
25
+ label: What is the expected behavior?
26
+ validations:
27
+ required: true
28
+
29
+ - type: textarea
30
+ id: actual_behavior
31
+ attributes:
32
+ label: What is the actual behavior?
33
+ validations:
34
+ required: true
35
+
36
+ - type: textarea
37
+ id: suporting_information
38
+ attributes:
39
+ label: Supporting information
40
+ description: Please, paste any logs, context information (client version? environment variables?) or other details in here.
41
+ validations:
42
+ required: false
43
+
44
+ - type: input
45
+ id: server_version
46
+ attributes:
47
+ label: Server Version
48
+ description: What Weaviate Version are you running?
49
+ validations:
50
+ required: true
51
+
52
+ - type: checkboxes
53
+ id: terms
54
+ attributes:
55
+ label: Code of Conduct
56
+ description: This project has a Code of Conduct. All participants are expected to understand and follow the CoC.
57
+ options:
58
+ - label: I have read and agree to the Weaviate's [Contributor Guide](https://weaviate.io/developers/contributor-guide) and [Code of Conduct](https://weaviate.io/service/code-of-conduct)
59
+ required: true
.github/ISSUE_TEMPLATE/feature_request.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Missing a Feature?
2
+ description: Suggest it here.
3
+ labels: ["feature request", "triage"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ #### Before you get started
9
+ * Is this feature already in our [roadmap](https://weaviate.io/developers/weaviate/roadmap)?
10
+ * Check to make sure someone hasn't already [requested this feature](https://github.com/weaviate/weaviate/issues?q=is:issue+label:"feature+request"). if that's the case, give it a thumbs up 👍
11
+ * Read the [Contributor Guide](https://weaviate.io/developers/contributor-guide) and [Code of Conduct](https://weaviate.io/service/code-of-conduct).
12
+
13
+ - type: textarea
14
+ id: feature_request_body
15
+ attributes:
16
+ label: Describe your feature request
17
+ description: |
18
+ - Describe here your requested feature. Provide all necessary context, use cases and the reasoning on why this feature is a good one.
19
+ validations:
20
+ required: true
21
+
22
+ - type: checkboxes
23
+ id: terms
24
+ attributes:
25
+ label: Code of Conduct
26
+ description: This project has a Code of Conduct that all participants are expected to understand and follow.
27
+ options:
28
+ - label: I have read and agree to the Weaviate's [Contributor Guide](https://weaviate.io/developers/contributor-guide) and [Code of Conduct](https://weaviate.io/service/code-of-conduct)
29
+ required: true
.github/PULL_REQUEST_TEMPLATE.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ### What's being changed:
2
+
3
+
4
+ ### Review checklist
5
+
6
+ - [ ] Documentation has been updated, if necessary. Link to changed documentation:
7
+ - [ ] Chaos pipeline run or not necessary. Link to pipeline:
8
+ - [ ] All new code is covered by tests where it is reasonable.
9
+ - [ ] Performance tests have been run or not necessary.
.github/dependabot.yml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ # Check for updates to GitHub Actions every week
7
+ interval: "weekly"
.github/stale.yml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Number of days of inactivity before an issue becomes stale
2
+ daysUntilStale: 60
3
+ # Number of days of inactivity before a stale issue is closed
4
+ daysUntilClose: 7
5
+ # Issues with these labels will never be considered stale
6
+ exemptLabels:
7
+ - pinned
8
+ - security
9
+ - bug
10
+ - backlog
11
+ - needs-investigation
12
+ # Label to use when marking an issue as stale
13
+ staleLabel: autoclosed
14
+ # Comment to post when marking an issue as stale. Set to `false` to disable
15
+ markComment: >
16
+ Thank you for your contribution to Weaviate. This issue has not received any
17
+ activity in a while and has therefore been marked as stale. Stale issues will
18
+ eventually be autoclosed. This does not mean that we are ruling out to work
19
+ on this issue, but it most likely has not been prioritized high enough in the
20
+ last months.
21
+
22
+ If you believe that this issue should remain open, please leave a short reply.
23
+ This lets us know that the issue is not abandoned and acts as a reminder for our
24
+ team to consider prioritizing this again.
25
+
26
+ Please also consider if you can make a contribution to help with the solution
27
+ of this issue. If you are willing to contribute, but don't know where to start,
28
+ please leave a quick message and we'll try to help you.
29
+
30
+ Thank you,
31
+ The Weaviate Team
32
+ # Comment to post when closing a stale issue. Set to `false` to disable
33
+ closeComment: false
.github/workflows/branch.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**' # run on ever branch except master (is covered by PR)
7
+ - '!master'
8
+ jobs:
9
+ Unit-Tests:
10
+ name: unit-tests
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Go
15
+ uses: actions/setup-go@v5
16
+ with:
17
+ go-version: '1.21'
18
+ cache: true
19
+ - name: Unit test
20
+ run: ./test/run.sh --unit-only
21
+ Integration-Tests:
22
+ name: integration-tests
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - name: Set up Go
27
+ uses: actions/setup-go@v5
28
+ with:
29
+ go-version: '1.21'
30
+ cache: true
31
+ - name: Integration test
32
+ run: ./test/run.sh --integration-only
33
+
.github/workflows/linter.yml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: golangci-lint
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ tags:
7
+ - '**'
8
+ pull_request:
9
+ jobs:
10
+ golangci:
11
+ name: golangci
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/setup-go@v5
15
+ with:
16
+ go-version: '1.21'
17
+ - uses: actions/checkout@v4
18
+ - name: golangci-lint
19
+ uses: golangci/golangci-lint-action@v3
20
+ with:
21
+ # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
22
+ version: v1.54
23
+ args: --timeout=5m
24
+ protolint:
25
+ name: protolint
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: yoheimuta/action-protolint@v1
.github/workflows/pull_requests.yaml ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ tags:
8
+ - '**'
9
+ pull_request:
10
+
11
+ jobs:
12
+ Run-Swagger:
13
+ name: run-swagger
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Set up Go
18
+ uses: actions/setup-go@v5
19
+ with:
20
+ go-version: '1.21'
21
+ cache: true
22
+ - name: Run Swagger
23
+ run: ./tools/gen-code-from-swagger.sh
24
+ - name: Error on change
25
+ run: |
26
+ # check if anything is different
27
+ CHANGED=$(git status -s | wc -l)
28
+ if [ "$CHANGED" -gt 0 ]; then
29
+ echo "Please run ./tools/gen-code-from-swagger.sh script and commit changes:"
30
+ git status -s
31
+ exit 1
32
+ else
33
+ exit 0
34
+ fi
35
+ Vulnerability-Scanning:
36
+ name: vulnerability-scanning
37
+ runs-on: ubuntu-latest
38
+ if: ${{ !github.event.pull_request.head.repo.fork }} # no PRs from fork
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - name: Scan for Vulnerabilities in Code
42
+ uses: Templum/[email protected]
43
+ with:
44
+ go-version: '1.21'
45
+ package: ./...
46
+ fail-on-vuln: true
47
+ Unit-Tests:
48
+ name: unit-tests
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - name: Set up Go
53
+ uses: actions/setup-go@v5
54
+ with:
55
+ go-version: '1.21'
56
+ cache: true
57
+ - name: Unit test
58
+ run: ./test/run.sh --unit-only
59
+ - name: Archive code coverage results
60
+ uses: actions/upload-artifact@v4
61
+ with:
62
+ name: coverage-report-unit
63
+ path: coverage-unit.txt
64
+ Integration-Tests:
65
+ name: integration-tests
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - name: Set up Go
70
+ uses: actions/setup-go@v5
71
+ with:
72
+ go-version: '1.21'
73
+ cache: true
74
+ - name: Integration test
75
+ run: ./test/run.sh --integration-only
76
+ - name: Archive code coverage results
77
+ uses: actions/upload-artifact@v4
78
+ with:
79
+ name: coverage-report-integration
80
+ path: coverage-integration.txt
81
+ Modules-Acceptance-Tests:
82
+ name: modules-acceptance-tests
83
+ runs-on: ubuntu-latest-4-cores
84
+ strategy:
85
+ fail-fast: false
86
+ matrix:
87
+ test: ["--modules-backup-only", "--modules-except-backup"]
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+ - name: Set up Go
91
+ uses: actions/setup-go@v5
92
+ with:
93
+ go-version: '1.21'
94
+ cache: true
95
+ - name: Acceptance tests (modules)
96
+ run: ./test/run.sh ${{ matrix.test }}
97
+ Acceptance-Tests:
98
+ name: acceptance-tests
99
+ runs-on: ubuntu-latest
100
+ steps:
101
+ - uses: actions/checkout@v4
102
+ - name: Set up Go
103
+ uses: actions/setup-go@v5
104
+ with:
105
+ go-version: '1.21'
106
+ cache: true
107
+ - name: Acceptance tests
108
+ env:
109
+ WCS_DUMMY_CI_PW: ${{ secrets.WCS_DUMMY_CI_PW }}
110
+ WCS_DUMMY_CI_PW_2: ${{ secrets.WCS_DUMMY_CI_PW_2 }}
111
+ run: ./test/run.sh --acceptance-only
112
+ Codecov:
113
+ needs: [Unit-Tests, Integration-Tests]
114
+ name: codecov
115
+ runs-on: ubuntu-latest
116
+ if: ${{ (github.ref_type == 'branch') && (github.ref_name != 'master') }}
117
+ steps:
118
+ - uses: actions/checkout@v4
119
+ - name: Download coverage artifacts integration
120
+ uses: actions/download-artifact@v4
121
+ with:
122
+ name: coverage-report-unit
123
+ - name: Download coverage unit
124
+ uses: actions/download-artifact@v4
125
+ with:
126
+ name: coverage-report-integration
127
+ - name: Codecov
128
+ uses: codecov/codecov-action@v3
129
+ with:
130
+ fail_ci_if_error: false
131
+ files: ./coverage-integration.txt, ./coverage-unit.txt
132
+ verbose: true
133
+ Compile-and-upload-binaries:
134
+ name: compile-and-upload-binaries
135
+ runs-on: ubuntu-latest-4-cores
136
+ steps:
137
+ - uses: actions/checkout@v4
138
+ - name: Set up Go
139
+ uses: actions/setup-go@v5
140
+ with:
141
+ go-version: '1.21'
142
+ cache: true
143
+ - name: goreleaser
144
+ run: |
145
+ echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list
146
+ sudo apt update
147
+ sudo apt install goreleaser
148
+ GIT_HASH=$(git rev-parse --short HEAD) goreleaser build --clean --snapshot
149
+ - name: Upload macos
150
+ uses: actions/upload-artifact@v4
151
+ with:
152
+ name: binaries-macos-unsigned
153
+ path: dist/weaviate_darwin_all
154
+ - name: Upload windows
155
+ uses: actions/upload-artifact@v4
156
+ with:
157
+ name: binaries-windows-amd64
158
+ path: dist/weaviate_windows_amd64_v1
159
+ - name: Upload windows
160
+ uses: actions/upload-artifact@v4
161
+ with:
162
+ name: binaries-windows-arm64
163
+ path: dist/weaviate_windows_arm64
164
+ - name: Upload linux amd64
165
+ uses: actions/upload-artifact@v4
166
+ with:
167
+ name: binaries-linux-amd64
168
+ path: dist/weaviate_linux_amd64_v1
169
+ - name: Upload linux arm64
170
+ uses: actions/upload-artifact@v4
171
+ with:
172
+ name: binaries-linux-arm64
173
+ path: dist/weaviate_linux_arm64
174
+
175
+
176
+ Acceptance-Tests-windows:
177
+ name: acceptance-tests-windows
178
+ needs: Compile-and-upload-binaries
179
+ runs-on: windows-latest
180
+ env:
181
+ AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true
182
+ PERSISTENCE_DATA_PATH: /tmp
183
+ QUERY_DEFAULTS_LIMIT: 20
184
+ steps:
185
+ - uses: actions/checkout@v4
186
+ - name: Download binaries
187
+ uses: actions/download-artifact@v4
188
+ with:
189
+ name: binaries-windows-amd64
190
+ - name: Set up Go
191
+ uses: actions/setup-go@v5
192
+ with:
193
+ go-version: '1.21'
194
+ cache: true
195
+ - name: start weaviate
196
+ shell: bash
197
+ # Weaviate is started without a Vectorizer as running text2vec-contextionary on GH actions is difficult:
198
+ # - docker on GHA only supports windows container - which we currently are not build
199
+ # - building those containers without a windows machine is difficult to figure out
200
+ run: ./weaviate.exe --scheme http --port 8080 &
201
+ - name: run acceptance tests
202
+ shell: bash
203
+ run: go test -count 1 -race test/acceptance/actions/*.go # tests that don't need a Vectorizer
204
+
205
+ Push-Docker:
206
+ needs: [Acceptance-Tests, Modules-Acceptance-Tests, Unit-Tests, Integration-Tests, Vulnerability-Scanning, Run-Swagger]
207
+ name: push-docker
208
+ runs-on: ubuntu-latest-8-cores
209
+ if: ${{ !github.event.pull_request.head.repo.fork }} # no PRs from fork
210
+ steps:
211
+ - uses: actions/checkout@v4
212
+ - name: Login to Docker Hub
213
+ uses: docker/login-action@v3
214
+ with:
215
+ username: ${{secrets.DOCKER_USERNAME}}
216
+ password: ${{secrets.DOCKER_PASSWORD}}
217
+ - name: Push container
218
+ id: push-container
219
+ run: ./ci/push_docker.sh
220
+ env:
221
+ PR_TITLE: "${{ github.event.pull_request.title }}"
222
+ - name: Generate Report
223
+ env:
224
+ PREVIEW_TAG: "${{ steps.push-container.outputs.PREVIEW_TAG }}"
225
+ run: ./ci/generate_docker_report.sh
.github/workflows/release.yaml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Generate release assets
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ env:
8
+ CGO_ENABLED: 0
9
+
10
+ permissions:
11
+ contents: write
12
+
13
+ jobs:
14
+ releases-matrix:
15
+ name: Release precompiled binaries
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ matrix:
19
+ goos: [linux]
20
+ goarch: [amd64, arm64]
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: wangyoucao577/[email protected]
24
+ with:
25
+ github_token: ${{ secrets.GITHUB_TOKEN }}
26
+ goos: ${{ matrix.goos }}
27
+ goarch: ${{ matrix.goarch }}
28
+ goversion: "1.21"
29
+ project_path: "./cmd/weaviate-server"
30
+ extra_files: LICENSE README.md
31
+ ldflags: -w -extldflags "-static" -X github.com/weaviate/weaviate/usecases/config.GitHash='"$GITHASH"'
32
+ sha256sum: true
.gitignore ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ignore release dirs
2
+ nightly/*
3
+ dist/*
4
+ dist
5
+ nightly
6
+
7
+ # ignore confectionary files
8
+ contextionary/contextionary.vocab
9
+ contextionary/contextionary.idx
10
+ contextionary/contextionary.knn
11
+ contextionary/stopwords.json
12
+ wget-log*
13
+
14
+ # ignore update files
15
+ semver
16
+ cc-test-reporter
17
+
18
+ # Ignore OLD files
19
+ *.OLD
20
+ *.OLD.go
21
+
22
+ # Do not include dist tmp
23
+ dist_tmp
24
+
25
+ # No weaviate bins
26
+ ./weaviate
27
+
28
+ # Logs
29
+ logs
30
+ *.log
31
+ npm-debug.log*
32
+
33
+ # Runtime data
34
+ pids
35
+ *.pid
36
+ *.seed
37
+
38
+ # Contents of temp-directory
39
+ temp
40
+ .temp
41
+
42
+ # Directory for instrumented libs generated by jscoverage/JSCover
43
+ lib-cov
44
+
45
+ # Coverage directory used by tools like istanbul
46
+ coverage
47
+
48
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
49
+ .grunt
50
+
51
+ # node-waf configuration
52
+ .lock-wscript
53
+
54
+ # Compiled binary addons (http://nodejs.org/api/addons.html)
55
+ build/Release
56
+
57
+ # Dependency directory
58
+ node_modules
59
+
60
+ # Optional npm cache directory
61
+ .npm
62
+
63
+ # Optional REPL history
64
+ .node_repl_history
65
+
66
+ # MAC stuff
67
+ .DS_Store
68
+ .!
69
+
70
+ # IDE files
71
+ .idea/
72
+ .vscode/
73
+
74
+ # Local files
75
+ _local
76
+
77
+ # Remove weave files
78
+ weave-current.json
79
+ weave-current-discovery.json
80
+
81
+ # local nodes folders
82
+ graphqlapi/prototype/node_modules/
83
+
84
+ /data*/
85
+ /snapshots*/
86
+ /backups*/
87
+ docker-compose/runtime-stable/data
88
+ docker-compose/runtime-unstable/data
89
+ c.out
90
+ /cmd/weaviate-server/build_artifacts/
91
+
92
+ esbackups/
93
+ **/testdata/
94
+
95
+ # coverage files
96
+ coverage*.txt
97
+
98
+ # go mod
99
+ vendor/
100
+
101
+ test/benchmark/benchmark_results.json
102
+ # benchmark results files copied from a remote run
103
+ benchmark_results_*.json
104
+
105
+ # local credentials
106
+ creds.json
107
+
108
+ # VSCode
109
+ cmd/weaviate-server/__debug_bin*
.golangci.yml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ linters:
2
+ disable:
3
+ - errcheck
4
+ enable:
5
+ - misspell
6
+ - bodyclose
7
+ - gofumpt
8
+ - exhaustive
9
+ - govet
10
+ - unused
11
+ - nolintlint
12
+ linters-settings:
13
+ exhaustive:
14
+ # Presence of "default" case in switch statements satisfies exhaustiveness,
15
+ # even if all enum members are not listed.
16
+ default-signifies-exhaustive: true
17
+ issues:
18
+ exclude-rules:
19
+ # Exclude some staticcheck messages
20
+ - linters:
21
+ - staticcheck
22
+ text: "S1034"
23
+ - linters:
24
+ - staticcheck
25
+ text: "SA1029:"
26
+ - linters:
27
+ - staticcheck
28
+ text: "SA1015:"
29
+ - linters:
30
+ - staticcheck
31
+ text: "SA5011"
32
+ - linters:
33
+ - govet
34
+ text: "composites"
35
+ - linters:
36
+ - staticcheck
37
+ path: adapters/handlers/grpc/v1/filters.go
38
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
39
+ - linters:
40
+ - staticcheck
41
+ path: adapters/handlers/grpc/v1/prepare_reply.go
42
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
43
+ - linters:
44
+ - staticcheck
45
+ path: adapters/handlers/grpc/v1/parse_search_request.go
46
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
47
+ - linters:
48
+ - staticcheck
49
+ path: adapters/handlers/grpc/v1/batch_parse_request.go
50
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
51
+ - linters:
52
+ - staticcheck
53
+ path: adapters/handlers/grpc/v1/service.go
54
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
55
+ - linters:
56
+ - staticcheck
57
+ path: test/acceptance/grpc/grpc_test_deprecated.go
58
+ text: 'SA1019' # TODO: remove this once deprecated gRPC fields are removed from API
59
+ run:
60
+ build-tags:
61
+ - integrationTest
62
+ - integrationTestSlow
.goreleaser.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ before:
2
+ hooks:
3
+ # You may remove this if you don't use go modules.
4
+ - go mod tidy
5
+ builds:
6
+ - env:
7
+ - CGO_ENABLED=0
8
+ goos:
9
+ - linux
10
+ - darwin
11
+ - windows
12
+ main: ./cmd/weaviate-server
13
+ goarch:
14
+ - amd64
15
+ - arm64
16
+ ldflags:
17
+ - -w
18
+ - -extldflags "-static"
19
+ - -X github.com/weaviate/weaviate/usecases/config.GitHash={{ .Env.GIT_HASH }}
20
+
21
+ # create a "fat" binary for MacOS
22
+ universal_binaries:
23
+ - replace: true
24
+
.pre-commit-config.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: gofumpt
5
+ name: gofumpt
6
+ entry: gofumpt -w
7
+ language: system
8
+ types: [ go ]
9
+ - id: swagger
10
+ name: swagger
11
+ entry: ./tools/gen-code-from-swagger.sh
12
+ language: system
13
+ types: [ go ]
.protolint.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ lint:
2
+ rules_option:
3
+ max_line_length:
4
+ max_chars: 120
CITATION.cff ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: "Weaviate"
4
+ url: "https://github.com/weaviate/weaviate"
5
+ authors:
6
+ - family-names: "Dilocker"
7
+ given-names: "Etienne"
8
+ - family-names: "van Luijt"
9
+ given-names: "Bob"
10
+ orcid: "https://orcid.org/0000-0002-8239-7421"
11
+ - family-names: "Voorbach"
12
+ given-names: "Byron"
13
+ - family-names: "Hasan"
14
+ given-names: "Mohd Shukri"
15
+ - family-names: "Rodriguez"
16
+ given-names: "Abdel"
17
+ - family-names: "Kulawiak"
18
+ given-names: "Dirk Alexander"
19
+ orcid: "https://orcid.org/0000-0002-9848-3818"
20
+ - family-names: "Antas"
21
+ given-names: "Marcin"
22
+ - family-names: "Duckworth"
23
+ given-names: "Parker"
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to make participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all project spaces, and it also applies when
49
+ an individual is representing the project or its community in public spaces.
50
+ Examples of representing a project or community include using an official
51
+ project e-mail address, posting via an official social media account, or acting
52
+ as an appointed representative at an online or offline event. Representation of
53
+ a project may be further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at [email protected]. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
77
+
CONTRIBUTING.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Thanks for looking into contributing to Weaviate!
2
+ We really appreciate that you are willing to spend some time and effort to make Weaviate better for everyone!
3
+
4
+ We have a detailed [contributor guide](https://weaviate.io/developers/contributor-guide/current/) as a part of our documentation. If you are new, we recommend reading through the [getting started guide for contributors](https://weaviate.io/developers/contributor-guide/current/getting-started/index.html) after reading this overview.
5
+
6
+ ## Finding a good issue to get started
7
+ We use the `good-first-issue` labels on issues that we think are great to get started. Typically, they are issues that are isolated to a specific area of Weaviate, e.g. only the API layer or only a specific package.
8
+
9
+ ## What do you need to contribute
10
+ To contribute to Weaviate Core you should have at least basic Golang skills. It is also tremendously helpful if you have successfully used Weaviate before, so that you have a rough understanding of the effects of changes etc. Any PR must include tests. We understand that not everyone is very familiar with testing strategies, and how we have setup our tests. This [guide on testing](https://weaviate.io/developers/contributor-guide/current/weaviate-core/tests.html) should be a great start. If you have completed a change, but are struggling with the test, you can also open a Draft PR and ask someone from our team for help.
11
+
12
+ ## If in doubt, ask.
13
+ The Weaviate team consists of some of the nicest people on this planet. If something is unclear or you'd like a second opinion, please don't hesitate to ask. We are glad that you want to help us, so naturally we will also do our best to help you on this journey
14
+
15
+ ## Development Environment
16
+ *Please note that the Weaviate team uses Linux and Mac (darwin/arm64) machines exclusively. Development on Windows may lead to unexpected issues.*
17
+
18
+ Here are some guides to get started in no time:
19
+ * [Development Setup](https://weaviate.io/developers/contributor-guide/current/weaviate-core/setup.html)
20
+ * [Code Structure](https://weaviate.io/developers/contributor-guide/current/weaviate-core/structure.html)
21
+ * [CI/CD](https://weaviate.io/developers/contributor-guide/current/weaviate-core/cicd.html)
22
+ * [Testing Philosophy](https://weaviate.io/developers/contributor-guide/current/weaviate-core/tests.html)
23
+
24
+ ## Tagging your commit
25
+ Please tag your commit(s) with the appropriate GH issue that your change refers to, e.g. `gh-9001 reduce memory allocations of ACME widget`. Please also include something in your PR description to indicate which issue it will close, e.g. `fixes #9001` or `closes #9001`.
26
+
27
+ ## Pull Request
28
+ If you open an external pull request our CI pipeline will get started. This external run will not have access to secrets. This prevents people from submitting a malicious PR to steal secrets. As a result the CI run will be slightly different from an internal one. For example, it will not automatically push a Docker image. If your PR is merged, a container with your changes will be built from the trunk.
29
+
30
+ ## Agreements
31
+
32
+ ### Code of Conduct
33
+ Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
34
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)
35
+
36
+ ### Contributor License Agreement
37
+
38
+ Contributions to Weaviate must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of Weaviate. Go to [this page](https://www.semi.technology/playbooks/misc/contributor-license-agreement.html) to read the current agreement.
39
+
40
+ The process works as follows:
41
+
42
+ - You contribute by opening a [pull request](#pull-request).
43
+ - If you have not contributed before, our bot will ask to agree with the CLA
DockerfileHld ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for development purposes.
2
+ # Read docs/development.md for more information
3
+ # vi: ft=dockerfile
4
+
5
+ ###############################################################################
6
+ # Base build image
7
+ FROM golang:1.21-alpine AS build_base
8
+ RUN apk add bash ca-certificates git gcc g++ libc-dev
9
+ WORKDIR /go/src/github.com/weaviate/weaviate
10
+ ENV GO111MODULE=on
11
+ # Populate the module cache based on the go.{mod,sum} files.
12
+ COPY go.mod .
13
+ COPY go.sum .
14
+ RUN go mod download
15
+
16
+ ###############################################################################
17
+ # This image builds the weaviate server
18
+ FROM build_base AS server_builder
19
+ ARG TARGETARCH
20
+ ARG GITHASH="unknown"
21
+ ARG EXTRA_BUILD_ARGS=""
22
+ COPY . .
23
+ RUN GOOS=linux GOARCH=$TARGETARCH go build $EXTRA_BUILD_ARGS \
24
+ -ldflags '-w -extldflags "-static" -X github.com/weaviate/weaviate/usecases/config.GitHash='"$GITHASH"'' \
25
+ -o /weaviate-server ./cmd/weaviate-server
26
+
27
+ ###############################################################################
28
+ # This creates an image that can be used to fake an api for telemetry acceptance test purposes
29
+ FROM build_base AS telemetry_mock_api
30
+ COPY . .
31
+ ENTRYPOINT ["./tools/dev/telemetry_mock_api.sh"]
32
+
33
+ ###############################################################################
34
+ # This image gets grpc health check probe
35
+ FROM build_base AS grpc_health_probe_builder
36
+ ARG TARGETARCH
37
+ RUN GRPC_HEALTH_PROBE_VERSION=v0.4.22 && \
38
+ wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-${TARGETARCH} && \
39
+ chmod +x /bin/grpc_health_probe
40
+
41
+ ###############################################################################
42
+ # Weaviate (no differentiation between dev/test/prod - 12 factor!)
43
+ FROM alpine AS weaviate
44
+ ENTRYPOINT ["/bin/weaviate"]
45
+ COPY --from=grpc_health_probe_builder /bin/grpc_health_probe /bin/
46
+ COPY --from=server_builder /weaviate-server /bin/weaviate
47
+ RUN apk add --no-cache --upgrade ca-certificates openssl
48
+ RUN mkdir ./modules
49
+ CMD [ "--host", "0.0.0.0", "--port", "8080", "--scheme", "http"]
LICENSE ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2020-2024, Weaviate B.V.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
adapters/clients/client.go ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "fmt"
18
+ "io"
19
+ "net/http"
20
+ "time"
21
+ )
22
+
23
+ type retryClient struct {
24
+ client *http.Client
25
+ *retryer
26
+ }
27
+
28
+ func (c *retryClient) doWithCustomMarshaller(timeout time.Duration,
29
+ req *http.Request, body []byte, decode func([]byte) error,
30
+ ) (err error) {
31
+ ctx, cancel := context.WithTimeout(req.Context(), timeout)
32
+ defer cancel()
33
+ try := func(ctx context.Context) (bool, error) {
34
+ if body != nil {
35
+ req.Body = io.NopCloser(bytes.NewReader(body))
36
+ }
37
+ res, err := c.client.Do(req)
38
+ if err != nil {
39
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
40
+ }
41
+
42
+ respBody, err := io.ReadAll(res.Body)
43
+ if err != nil {
44
+ return shouldRetry(res.StatusCode), fmt.Errorf("read response: %w", err)
45
+ }
46
+ defer res.Body.Close()
47
+
48
+ if code := res.StatusCode; code != http.StatusOK {
49
+ return shouldRetry(code), fmt.Errorf("status code: %v, error: %s", code, respBody)
50
+ }
51
+
52
+ if err := decode(respBody); err != nil {
53
+ return false, fmt.Errorf("unmarshal response: %w", err)
54
+ }
55
+
56
+ return false, nil
57
+ }
58
+ return c.retry(ctx, 9, try)
59
+ }
60
+
61
+ type retryer struct {
62
+ minBackOff time.Duration
63
+ maxBackOff time.Duration
64
+ timeoutUnit time.Duration
65
+ }
66
+
67
+ func newRetryer() *retryer {
68
+ return &retryer{
69
+ minBackOff: time.Millisecond * 250,
70
+ maxBackOff: time.Second * 30,
71
+ timeoutUnit: time.Second, // used by unit tests
72
+ }
73
+ }
74
+
75
+ func (r *retryer) retry(ctx context.Context, n int, work func(context.Context) (bool, error)) error {
76
+ delay := r.minBackOff
77
+ for {
78
+ keepTrying, err := work(ctx)
79
+ if !keepTrying || n < 1 || err == nil {
80
+ return err
81
+ }
82
+
83
+ n--
84
+ if delay = backOff(delay); delay > r.maxBackOff {
85
+ delay = r.maxBackOff
86
+ }
87
+ timer := time.NewTimer(delay)
88
+ select {
89
+ case <-ctx.Done():
90
+ timer.Stop()
91
+ return fmt.Errorf("%v: %w", err, ctx.Err())
92
+ case <-timer.C:
93
+ }
94
+ timer.Stop()
95
+ }
96
+ }
adapters/clients/cluster_backups.go ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "encoding/json"
18
+ "fmt"
19
+ "io"
20
+ "net/http"
21
+ "net/url"
22
+
23
+ "github.com/weaviate/weaviate/usecases/backup"
24
+ )
25
+
26
+ const (
27
+ pathCanCommit = "/backups/can-commit"
28
+ pathCommit = "/backups/commit"
29
+ pathStatus = "/backups/status"
30
+ pathAbort = "/backups/abort"
31
+ )
32
+
33
+ type ClusterBackups struct {
34
+ client *http.Client
35
+ }
36
+
37
+ func NewClusterBackups(client *http.Client) *ClusterBackups {
38
+ return &ClusterBackups{client: client}
39
+ }
40
+
41
+ func (c *ClusterBackups) CanCommit(ctx context.Context,
42
+ host string, req *backup.Request,
43
+ ) (*backup.CanCommitResponse, error) {
44
+ url := url.URL{Scheme: "http", Host: host, Path: pathCanCommit}
45
+
46
+ b, err := json.Marshal(req)
47
+ if err != nil {
48
+ return nil, fmt.Errorf("marshal can-commit request: %w", err)
49
+ }
50
+
51
+ httpReq, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
52
+ if err != nil {
53
+ return nil, fmt.Errorf("new can-commit request: %w", err)
54
+ }
55
+
56
+ respBody, statusCode, err := c.do(httpReq)
57
+ if err != nil {
58
+ return nil, fmt.Errorf("can-commit request: %w", err)
59
+ }
60
+
61
+ if statusCode != http.StatusOK {
62
+ return nil, fmt.Errorf("unexpected status code %d (%s)",
63
+ statusCode, respBody)
64
+ }
65
+
66
+ var resp backup.CanCommitResponse
67
+ err = json.Unmarshal(respBody, &resp)
68
+ if err != nil {
69
+ return nil, fmt.Errorf("unmarshal can-commit response: %w", err)
70
+ }
71
+
72
+ return &resp, nil
73
+ }
74
+
75
+ func (c *ClusterBackups) Commit(ctx context.Context,
76
+ host string, req *backup.StatusRequest,
77
+ ) error {
78
+ url := url.URL{Scheme: "http", Host: host, Path: pathCommit}
79
+
80
+ b, err := json.Marshal(req)
81
+ if err != nil {
82
+ return fmt.Errorf("marshal commit request: %w", err)
83
+ }
84
+
85
+ httpReq, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
86
+ if err != nil {
87
+ return fmt.Errorf("new commit request: %w", err)
88
+ }
89
+
90
+ respBody, statusCode, err := c.do(httpReq)
91
+ if err != nil {
92
+ return fmt.Errorf("commit request: %w", err)
93
+ }
94
+
95
+ if statusCode != http.StatusCreated {
96
+ return fmt.Errorf("unexpected status code %d (%s)",
97
+ statusCode, respBody)
98
+ }
99
+
100
+ return nil
101
+ }
102
+
103
+ func (c *ClusterBackups) Status(ctx context.Context,
104
+ host string, req *backup.StatusRequest,
105
+ ) (*backup.StatusResponse, error) {
106
+ url := url.URL{Scheme: "http", Host: host, Path: pathStatus}
107
+
108
+ b, err := json.Marshal(req)
109
+ if err != nil {
110
+ return nil, fmt.Errorf("marshal status request: %w", err)
111
+ }
112
+
113
+ httpReq, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
114
+ if err != nil {
115
+ return nil, fmt.Errorf("new status request: %w", err)
116
+ }
117
+
118
+ respBody, statusCode, err := c.do(httpReq)
119
+ if err != nil {
120
+ return nil, fmt.Errorf("status request: %w", err)
121
+ }
122
+
123
+ if statusCode != http.StatusOK {
124
+ return nil, fmt.Errorf("unexpected status code %d (%s)",
125
+ statusCode, respBody)
126
+ }
127
+
128
+ var resp backup.StatusResponse
129
+ err = json.Unmarshal(respBody, &resp)
130
+ if err != nil {
131
+ return nil, fmt.Errorf("unmarshal status response: %w", err)
132
+ }
133
+
134
+ return &resp, nil
135
+ }
136
+
137
+ func (c *ClusterBackups) Abort(_ context.Context,
138
+ host string, req *backup.AbortRequest,
139
+ ) error {
140
+ url := url.URL{Scheme: "http", Host: host, Path: pathAbort}
141
+
142
+ b, err := json.Marshal(req)
143
+ if err != nil {
144
+ return fmt.Errorf("marshal abort request: %w", err)
145
+ }
146
+
147
+ httpReq, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(b))
148
+ if err != nil {
149
+ return fmt.Errorf("new abort request: %w", err)
150
+ }
151
+
152
+ respBody, statusCode, err := c.do(httpReq)
153
+ if err != nil {
154
+ return fmt.Errorf("abort request: %w", err)
155
+ }
156
+
157
+ if statusCode != http.StatusNoContent {
158
+ return fmt.Errorf("unexpected status code %d (%s)",
159
+ statusCode, respBody)
160
+ }
161
+
162
+ return nil
163
+ }
164
+
165
+ func (c *ClusterBackups) do(req *http.Request) (body []byte, statusCode int, err error) {
166
+ httpResp, err := c.client.Do(req)
167
+ if err != nil {
168
+ return nil, 0, fmt.Errorf("make request: %w", err)
169
+ }
170
+
171
+ body, err = io.ReadAll(httpResp.Body)
172
+ if err != nil {
173
+ return nil, httpResp.StatusCode, fmt.Errorf("read response: %w", err)
174
+ }
175
+ defer httpResp.Body.Close()
176
+
177
+ return body, httpResp.StatusCode, nil
178
+ }
adapters/clients/cluster_classifications.go ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "encoding/json"
18
+ "io"
19
+ "net/http"
20
+ "net/url"
21
+
22
+ "github.com/pkg/errors"
23
+ "github.com/weaviate/weaviate/usecases/cluster"
24
+ )
25
+
26
+ type ClusterClassifications struct {
27
+ client *http.Client
28
+ }
29
+
30
+ func NewClusterClassifications(httpClient *http.Client) *ClusterClassifications {
31
+ return &ClusterClassifications{client: httpClient}
32
+ }
33
+
34
+ func (c *ClusterClassifications) OpenTransaction(ctx context.Context, host string,
35
+ tx *cluster.Transaction,
36
+ ) error {
37
+ path := "/classifications/transactions/"
38
+ method := http.MethodPost
39
+ url := url.URL{Scheme: "http", Host: host, Path: path}
40
+
41
+ pl := txPayload{
42
+ Type: tx.Type,
43
+ ID: tx.ID,
44
+ Payload: tx.Payload,
45
+ }
46
+
47
+ jsonBytes, err := json.Marshal(pl)
48
+ if err != nil {
49
+ return errors.Wrap(err, "marshal transaction payload")
50
+ }
51
+
52
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
53
+ bytes.NewReader(jsonBytes))
54
+ if err != nil {
55
+ return errors.Wrap(err, "open http request")
56
+ }
57
+
58
+ req.Header.Set("content-type", "application/json")
59
+
60
+ res, err := c.client.Do(req)
61
+ if err != nil {
62
+ return errors.Wrap(err, "send http request")
63
+ }
64
+
65
+ defer res.Body.Close()
66
+ if res.StatusCode != http.StatusCreated {
67
+ if res.StatusCode == http.StatusConflict {
68
+ return cluster.ErrConcurrentTransaction
69
+ }
70
+
71
+ body, _ := io.ReadAll(res.Body)
72
+ return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
73
+ body)
74
+ }
75
+
76
+ return nil
77
+ }
78
+
79
+ func (c *ClusterClassifications) AbortTransaction(ctx context.Context, host string,
80
+ tx *cluster.Transaction,
81
+ ) error {
82
+ path := "/classifications/transactions/" + tx.ID
83
+ method := http.MethodDelete
84
+ url := url.URL{Scheme: "http", Host: host, Path: path}
85
+
86
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
87
+ if err != nil {
88
+ return errors.Wrap(err, "open http request")
89
+ }
90
+
91
+ res, err := c.client.Do(req)
92
+ if err != nil {
93
+ return errors.Wrap(err, "send http request")
94
+ }
95
+
96
+ defer res.Body.Close()
97
+ if res.StatusCode != http.StatusNoContent {
98
+ return errors.Errorf("unexpected status code %d", res.StatusCode)
99
+ }
100
+
101
+ return nil
102
+ }
103
+
104
+ func (c *ClusterClassifications) CommitTransaction(ctx context.Context, host string,
105
+ tx *cluster.Transaction,
106
+ ) error {
107
+ path := "/classifications/transactions/" + tx.ID + "/commit"
108
+ method := http.MethodPut
109
+ url := url.URL{Scheme: "http", Host: host, Path: path}
110
+
111
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
112
+ if err != nil {
113
+ return errors.Wrap(err, "open http request")
114
+ }
115
+
116
+ res, err := c.client.Do(req)
117
+ if err != nil {
118
+ return errors.Wrap(err, "send http request")
119
+ }
120
+
121
+ defer res.Body.Close()
122
+ if res.StatusCode != http.StatusNoContent {
123
+ return errors.Errorf("unexpected status code %d", res.StatusCode)
124
+ }
125
+
126
+ return nil
127
+ }
adapters/clients/cluster_node.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "context"
16
+ "encoding/json"
17
+ "io"
18
+ "net/http"
19
+ "net/url"
20
+ "path"
21
+
22
+ enterrors "github.com/weaviate/weaviate/entities/errors"
23
+ "github.com/weaviate/weaviate/entities/models"
24
+ )
25
+
26
+ type RemoteNode struct {
27
+ client *http.Client
28
+ }
29
+
30
+ func NewRemoteNode(httpClient *http.Client) *RemoteNode {
31
+ return &RemoteNode{client: httpClient}
32
+ }
33
+
34
+ func (c *RemoteNode) GetNodeStatus(ctx context.Context, hostName, className, output string) (*models.NodeStatus, error) {
35
+ p := "/nodes/status"
36
+ if className != "" {
37
+ p = path.Join(p, className)
38
+ }
39
+ method := http.MethodGet
40
+ params := url.Values{"output": []string{output}}
41
+ url := url.URL{Scheme: "http", Host: hostName, Path: p, RawQuery: params.Encode()}
42
+
43
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
44
+ if err != nil {
45
+ return nil, enterrors.NewErrOpenHttpRequest(err)
46
+ }
47
+
48
+ res, err := c.client.Do(req)
49
+ if err != nil {
50
+ return nil, enterrors.NewErrSendHttpRequest(err)
51
+ }
52
+
53
+ defer res.Body.Close()
54
+ body, _ := io.ReadAll(res.Body)
55
+ if res.StatusCode != http.StatusOK {
56
+ return nil, enterrors.NewErrUnexpectedStatusCode(res.StatusCode, body)
57
+ }
58
+
59
+ var nodeStatus models.NodeStatus
60
+ err = json.Unmarshal(body, &nodeStatus)
61
+ if err != nil {
62
+ return nil, enterrors.NewErrUnmarshalBody(err)
63
+ }
64
+
65
+ return &nodeStatus, nil
66
+ }
adapters/clients/cluster_schema.go ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "encoding/json"
18
+ "fmt"
19
+ "io"
20
+ "net/http"
21
+ "net/url"
22
+
23
+ "github.com/weaviate/weaviate/usecases/cluster"
24
+ )
25
+
26
+ type ClusterSchema struct {
27
+ client *http.Client
28
+ }
29
+
30
+ func NewClusterSchema(httpClient *http.Client) *ClusterSchema {
31
+ return &ClusterSchema{client: httpClient}
32
+ }
33
+
34
+ func (c *ClusterSchema) OpenTransaction(ctx context.Context, host string,
35
+ tx *cluster.Transaction,
36
+ ) error {
37
+ path := "/schema/transactions/"
38
+ method := http.MethodPost
39
+ url := url.URL{Scheme: "http", Host: host, Path: path}
40
+
41
+ pl := txPayload{
42
+ Type: tx.Type,
43
+ ID: tx.ID,
44
+ Payload: tx.Payload,
45
+ DeadlineMilli: tx.Deadline.UnixMilli(),
46
+ }
47
+
48
+ jsonBytes, err := json.Marshal(pl)
49
+ if err != nil {
50
+ return fmt.Errorf("marshal transaction payload: %w", err)
51
+ }
52
+
53
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
54
+ bytes.NewReader(jsonBytes))
55
+ if err != nil {
56
+ return fmt.Errorf("open http request: %w", err)
57
+ }
58
+
59
+ req.Header.Set("content-type", "application/json")
60
+
61
+ res, err := c.client.Do(req)
62
+ if err != nil {
63
+ return fmt.Errorf("send http request: %w", err)
64
+ }
65
+
66
+ defer res.Body.Close()
67
+ body, _ := io.ReadAll(res.Body)
68
+ if res.StatusCode != http.StatusCreated {
69
+ if res.StatusCode == http.StatusConflict {
70
+ return cluster.ErrConcurrentTransaction
71
+ }
72
+
73
+ return fmt.Errorf("unexpected status code %d (%s)", res.StatusCode,
74
+ body)
75
+ }
76
+
77
+ // optional for backward-compatibility before v1.17 where only
78
+ // write-transactions where supported. They had no return value other than
79
+ // the status code. With the introduction of read-transactions it is now
80
+ // possible to return the requested value
81
+ if len(body) == 0 {
82
+ return nil
83
+ }
84
+
85
+ var txRes txResponsePayload
86
+ err = json.Unmarshal(body, &txRes)
87
+ if err != nil {
88
+ return fmt.Errorf("unexpected error unmarshalling tx response: %w", err)
89
+ }
90
+
91
+ if tx.ID != txRes.ID {
92
+ return fmt.Errorf("unexpected mismatch between outgoing and incoming tx ids:"+
93
+ "%s vs %s", tx.ID, txRes.ID)
94
+ }
95
+
96
+ tx.Payload = txRes.Payload
97
+
98
+ return nil
99
+ }
100
+
101
+ func (c *ClusterSchema) AbortTransaction(ctx context.Context, host string,
102
+ tx *cluster.Transaction,
103
+ ) error {
104
+ path := "/schema/transactions/" + tx.ID
105
+ method := http.MethodDelete
106
+ url := url.URL{Scheme: "http", Host: host, Path: path}
107
+
108
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
109
+ if err != nil {
110
+ return fmt.Errorf("open http request: %w", err)
111
+ }
112
+
113
+ res, err := c.client.Do(req)
114
+ if err != nil {
115
+ return fmt.Errorf("send http request: %w", err)
116
+ }
117
+
118
+ defer res.Body.Close()
119
+ if res.StatusCode != http.StatusNoContent {
120
+ errBody, _ := io.ReadAll(res.Body)
121
+ return fmt.Errorf("unexpected status code %d: %s", res.StatusCode, errBody)
122
+ }
123
+
124
+ return nil
125
+ }
126
+
127
+ func (c *ClusterSchema) CommitTransaction(ctx context.Context, host string,
128
+ tx *cluster.Transaction,
129
+ ) error {
130
+ path := "/schema/transactions/" + tx.ID + "/commit"
131
+ method := http.MethodPut
132
+ url := url.URL{Scheme: "http", Host: host, Path: path}
133
+
134
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
135
+ if err != nil {
136
+ return fmt.Errorf("open http request: %w", err)
137
+ }
138
+
139
+ res, err := c.client.Do(req)
140
+ if err != nil {
141
+ return fmt.Errorf("send http request: %w", err)
142
+ }
143
+
144
+ defer res.Body.Close()
145
+ if res.StatusCode != http.StatusNoContent {
146
+ errBody, _ := io.ReadAll(res.Body)
147
+ return fmt.Errorf("unexpected status code %d: %s", res.StatusCode, errBody)
148
+ }
149
+
150
+ return nil
151
+ }
152
+
153
+ type txPayload struct {
154
+ Type cluster.TransactionType `json:"type"`
155
+ ID string `json:"id"`
156
+ Payload interface{} `json:"payload"`
157
+ DeadlineMilli int64 `json:"deadlineMilli"`
158
+ }
159
+
160
+ type txResponsePayload struct {
161
+ Type cluster.TransactionType `json:"type"`
162
+ ID string `json:"id"`
163
+ Payload json.RawMessage `json:"payload"`
164
+ }
adapters/clients/cluster_schema_test.go ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "context"
16
+ "encoding/json"
17
+ "io"
18
+ "net/http"
19
+ "net/http/httptest"
20
+ "net/url"
21
+ "testing"
22
+ "time"
23
+
24
+ "github.com/stretchr/testify/assert"
25
+ "github.com/stretchr/testify/require"
26
+ "github.com/weaviate/weaviate/usecases/cluster"
27
+ )
28
+
29
+ func TestOpenTransactionNoReturnPayload(t *testing.T) {
30
+ // The No-Return-Payload is the situation that existed prior to v1.17 where
31
+ // the only option for transactions was to broadcast updates. By keeping this
32
+ // test around, we can make sure that we are not breaking backward
33
+ // compatibility.
34
+
35
+ handler := func(w http.ResponseWriter, r *http.Request) {
36
+ defer r.Body.Close()
37
+ body, err := io.ReadAll(r.Body)
38
+ require.Nil(t, err)
39
+
40
+ var pl txPayload
41
+ err = json.Unmarshal(body, &pl)
42
+ require.Nil(t, err)
43
+
44
+ assert.Equal(t, "mamma-mia-paylodia-belissima", pl.Payload.(string))
45
+ w.WriteHeader(http.StatusCreated)
46
+ }
47
+ server := httptest.NewServer(http.HandlerFunc(handler))
48
+ defer server.Close()
49
+
50
+ u, _ := url.Parse(server.URL)
51
+
52
+ c := NewClusterSchema(&http.Client{})
53
+
54
+ tx := &cluster.Transaction{
55
+ ID: "12345",
56
+ Type: "the best",
57
+ Payload: "mamma-mia-paylodia-belissima",
58
+ }
59
+
60
+ err := c.OpenTransaction(context.Background(), u.Host, tx)
61
+ assert.Nil(t, err)
62
+ }
63
+
64
+ func TestOpenTransactionWithReturnPayload(t *testing.T) {
65
+ // Newly added as part of v1.17 where read-transactions were introduced which
66
+ // are used to sync up cluster schema state when nodes join
67
+ handler := func(w http.ResponseWriter, r *http.Request) {
68
+ resTx := txPayload{
69
+ Type: "my-tx",
70
+ ID: "987",
71
+ Payload: "gracie-mille-per-payload",
72
+ }
73
+ resBody, err := json.Marshal(resTx)
74
+ require.Nil(t, err)
75
+
76
+ w.WriteHeader(http.StatusCreated)
77
+ w.Write(resBody)
78
+ }
79
+ server := httptest.NewServer(http.HandlerFunc(handler))
80
+ defer server.Close()
81
+
82
+ u, _ := url.Parse(server.URL)
83
+
84
+ c := NewClusterSchema(&http.Client{})
85
+
86
+ txIn := &cluster.Transaction{
87
+ ID: "987",
88
+ Type: "my-tx",
89
+ }
90
+
91
+ err := c.OpenTransaction(context.Background(), u.Host, txIn)
92
+ assert.Nil(t, err)
93
+
94
+ expectedTxOut := &cluster.Transaction{
95
+ ID: "987",
96
+ Type: "my-tx",
97
+ Payload: json.RawMessage("\"gracie-mille-per-payload\""),
98
+ }
99
+
100
+ assert.Equal(t, expectedTxOut, txIn)
101
+ }
102
+
103
+ func TestOpenTransactionWithTTL(t *testing.T) {
104
+ deadline, err := time.Parse(time.RFC3339Nano, "2040-01-02T15:04:05.00Z")
105
+ require.Nil(t, err)
106
+
107
+ handler := func(w http.ResponseWriter, r *http.Request) {
108
+ defer r.Body.Close()
109
+ body, err := io.ReadAll(r.Body)
110
+ require.Nil(t, err)
111
+
112
+ var pl txPayload
113
+ err = json.Unmarshal(body, &pl)
114
+ require.Nil(t, err)
115
+
116
+ parsedDL := time.UnixMilli(pl.DeadlineMilli)
117
+ assert.Equal(t, deadline.UnixNano(), parsedDL.UnixNano())
118
+ w.WriteHeader(http.StatusCreated)
119
+ }
120
+ server := httptest.NewServer(http.HandlerFunc(handler))
121
+ defer server.Close()
122
+
123
+ u, _ := url.Parse(server.URL)
124
+
125
+ c := NewClusterSchema(&http.Client{})
126
+
127
+ tx := &cluster.Transaction{
128
+ ID: "12345",
129
+ Type: "the best",
130
+ Payload: "mamma-mia-paylodia-belissima",
131
+ Deadline: deadline,
132
+ }
133
+
134
+ err = c.OpenTransaction(context.Background(), u.Host, tx)
135
+ assert.Nil(t, err)
136
+ }
137
+
138
+ func TestOpenTransactionUnhappyPaths(t *testing.T) {
139
+ type test struct {
140
+ name string
141
+ handler http.HandlerFunc
142
+ expectedErr error
143
+ expectedErrContains string
144
+ ctx context.Context
145
+ shutdownPrematurely bool
146
+ }
147
+
148
+ expiredCtx, cancel := context.WithCancel(context.Background())
149
+ cancel()
150
+
151
+ tests := []test{
152
+ {
153
+ name: "concurrent transaction",
154
+ handler: func(w http.ResponseWriter, r *http.Request) {
155
+ defer r.Body.Close()
156
+ w.WriteHeader(http.StatusConflict)
157
+ },
158
+ expectedErr: cluster.ErrConcurrentTransaction,
159
+ },
160
+ {
161
+ name: "arbitrary 500",
162
+ handler: func(w http.ResponseWriter, r *http.Request) {
163
+ defer r.Body.Close()
164
+ w.WriteHeader(http.StatusInternalServerError)
165
+ w.Write([]byte("nope!"))
166
+ },
167
+ expectedErrContains: "nope!",
168
+ },
169
+ {
170
+ name: "invalid json",
171
+ handler: func(w http.ResponseWriter, r *http.Request) {
172
+ defer r.Body.Close()
173
+ w.WriteHeader(http.StatusCreated)
174
+ w.Write([]byte("<<<!@#*)!@#****@!''"))
175
+ },
176
+ expectedErrContains: "error unmarshalling",
177
+ },
178
+ {
179
+ name: "expired ctx",
180
+ ctx: expiredCtx,
181
+ handler: func(w http.ResponseWriter, r *http.Request) {
182
+ },
183
+ expectedErrContains: "context",
184
+ },
185
+ {
186
+ name: "remote server shut down",
187
+ shutdownPrematurely: true,
188
+ handler: func(w http.ResponseWriter, r *http.Request) {
189
+ },
190
+ expectedErrContains: "refused",
191
+ },
192
+ {
193
+ name: "tx id mismatch",
194
+ handler: func(w http.ResponseWriter, r *http.Request) {
195
+ resTx := txPayload{
196
+ Type: "wrong-tx-id",
197
+ ID: "987",
198
+ Payload: "gracie-mille-per-payload",
199
+ }
200
+ resBody, err := json.Marshal(resTx)
201
+ require.Nil(t, err)
202
+
203
+ w.WriteHeader(http.StatusCreated)
204
+ w.Write(resBody)
205
+ },
206
+ expectedErrContains: "mismatch between outgoing and incoming tx ids",
207
+ },
208
+ }
209
+
210
+ for _, test := range tests {
211
+ t.Run(test.name, func(t *testing.T) {
212
+ server := httptest.NewServer(http.HandlerFunc(test.handler))
213
+ if test.shutdownPrematurely {
214
+ server.Close()
215
+ } else {
216
+ defer server.Close()
217
+ }
218
+
219
+ u, _ := url.Parse(server.URL)
220
+
221
+ c := NewClusterSchema(&http.Client{})
222
+
223
+ tx := &cluster.Transaction{
224
+ ID: "12345",
225
+ Type: "the best",
226
+ Payload: "mamma-mia-paylodia-belissima",
227
+ }
228
+
229
+ if test.ctx == nil {
230
+ test.ctx = context.Background()
231
+ }
232
+
233
+ err := c.OpenTransaction(test.ctx, u.Host, tx)
234
+ assert.NotNil(t, err)
235
+
236
+ if test.expectedErr != nil {
237
+ assert.Equal(t, test.expectedErr, err)
238
+ }
239
+
240
+ if test.expectedErrContains != "" {
241
+ assert.Contains(t, err.Error(), test.expectedErrContains)
242
+ }
243
+ })
244
+ }
245
+ }
246
+
247
+ func TestAbortTransaction(t *testing.T) {
248
+ handler := func(w http.ResponseWriter, r *http.Request) {
249
+ defer r.Body.Close()
250
+ w.WriteHeader(http.StatusNoContent)
251
+ }
252
+ server := httptest.NewServer(http.HandlerFunc(handler))
253
+ defer server.Close()
254
+
255
+ u, _ := url.Parse(server.URL)
256
+
257
+ c := NewClusterSchema(&http.Client{})
258
+
259
+ tx := &cluster.Transaction{
260
+ ID: "am-i-going-to-be-cancelled",
261
+ Type: "the worst",
262
+ Payload: "",
263
+ }
264
+
265
+ err := c.AbortTransaction(context.Background(), u.Host, tx)
266
+ assert.Nil(t, err)
267
+ }
268
+
269
+ func TestAbortTransactionUnhappyPaths(t *testing.T) {
270
+ type test struct {
271
+ name string
272
+ handler http.HandlerFunc
273
+ expectedErr error
274
+ expectedErrContains string
275
+ ctx context.Context
276
+ shutdownPrematurely bool
277
+ }
278
+
279
+ expiredCtx, cancel := context.WithCancel(context.Background())
280
+ cancel()
281
+
282
+ tests := []test{
283
+ {
284
+ name: "arbitrary 500",
285
+ handler: func(w http.ResponseWriter, r *http.Request) {
286
+ defer r.Body.Close()
287
+ w.WriteHeader(http.StatusInternalServerError)
288
+ w.Write([]byte("nope!"))
289
+ },
290
+ expectedErrContains: "nope!",
291
+ },
292
+ {
293
+ name: "expired ctx",
294
+ ctx: expiredCtx,
295
+ handler: func(w http.ResponseWriter, r *http.Request) {
296
+ },
297
+ expectedErrContains: "context",
298
+ },
299
+ {
300
+ name: "remote server shut down",
301
+ shutdownPrematurely: true,
302
+ handler: func(w http.ResponseWriter, r *http.Request) {
303
+ },
304
+ expectedErrContains: "refused",
305
+ },
306
+ }
307
+
308
+ for _, test := range tests {
309
+ t.Run(test.name, func(t *testing.T) {
310
+ server := httptest.NewServer(http.HandlerFunc(test.handler))
311
+ if test.shutdownPrematurely {
312
+ server.Close()
313
+ } else {
314
+ defer server.Close()
315
+ }
316
+
317
+ u, _ := url.Parse(server.URL)
318
+
319
+ c := NewClusterSchema(&http.Client{})
320
+
321
+ tx := &cluster.Transaction{
322
+ ID: "12345",
323
+ Type: "the best",
324
+ Payload: "mamma-mia-paylodia-belissima",
325
+ }
326
+
327
+ if test.ctx == nil {
328
+ test.ctx = context.Background()
329
+ }
330
+
331
+ err := c.AbortTransaction(test.ctx, u.Host, tx)
332
+ assert.NotNil(t, err)
333
+
334
+ if test.expectedErr != nil {
335
+ assert.Equal(t, test.expectedErr, err)
336
+ }
337
+
338
+ if test.expectedErrContains != "" {
339
+ assert.Contains(t, err.Error(), test.expectedErrContains)
340
+ }
341
+ })
342
+ }
343
+ }
344
+
345
+ func TestCommitTransaction(t *testing.T) {
346
+ handler := func(w http.ResponseWriter, r *http.Request) {
347
+ defer r.Body.Close()
348
+ w.WriteHeader(http.StatusNoContent)
349
+ }
350
+ server := httptest.NewServer(http.HandlerFunc(handler))
351
+ defer server.Close()
352
+
353
+ u, _ := url.Parse(server.URL)
354
+
355
+ c := NewClusterSchema(&http.Client{})
356
+
357
+ tx := &cluster.Transaction{
358
+ ID: "am-i-going-to-be-cancelled",
359
+ Type: "the worst",
360
+ Payload: "",
361
+ }
362
+
363
+ err := c.CommitTransaction(context.Background(), u.Host, tx)
364
+ assert.Nil(t, err)
365
+ }
366
+
367
+ func TestCommitTransactionUnhappyPaths(t *testing.T) {
368
+ type test struct {
369
+ name string
370
+ handler http.HandlerFunc
371
+ expectedErr error
372
+ expectedErrContains string
373
+ ctx context.Context
374
+ shutdownPrematurely bool
375
+ }
376
+
377
+ expiredCtx, cancel := context.WithCancel(context.Background())
378
+ cancel()
379
+
380
+ tests := []test{
381
+ {
382
+ name: "arbitrary 500",
383
+ handler: func(w http.ResponseWriter, r *http.Request) {
384
+ defer r.Body.Close()
385
+ w.WriteHeader(http.StatusInternalServerError)
386
+ w.Write([]byte("nope!"))
387
+ },
388
+ expectedErrContains: "nope!",
389
+ },
390
+ {
391
+ name: "expired ctx",
392
+ ctx: expiredCtx,
393
+ handler: func(w http.ResponseWriter, r *http.Request) {
394
+ },
395
+ expectedErrContains: "context",
396
+ },
397
+ {
398
+ name: "remote server shut down",
399
+ shutdownPrematurely: true,
400
+ handler: func(w http.ResponseWriter, r *http.Request) {
401
+ },
402
+ expectedErrContains: "refused",
403
+ },
404
+ }
405
+
406
+ for _, test := range tests {
407
+ t.Run(test.name, func(t *testing.T) {
408
+ server := httptest.NewServer(http.HandlerFunc(test.handler))
409
+ if test.shutdownPrematurely {
410
+ server.Close()
411
+ } else {
412
+ defer server.Close()
413
+ }
414
+
415
+ u, _ := url.Parse(server.URL)
416
+
417
+ c := NewClusterSchema(&http.Client{})
418
+
419
+ tx := &cluster.Transaction{
420
+ ID: "12345",
421
+ Type: "the best",
422
+ Payload: "mamma-mia-paylodia-belissima",
423
+ }
424
+
425
+ if test.ctx == nil {
426
+ test.ctx = context.Background()
427
+ }
428
+
429
+ err := c.CommitTransaction(test.ctx, u.Host, tx)
430
+ assert.NotNil(t, err)
431
+
432
+ if test.expectedErr != nil {
433
+ assert.Equal(t, test.expectedErr, err)
434
+ }
435
+
436
+ if test.expectedErrContains != "" {
437
+ assert.Contains(t, err.Error(), test.expectedErrContains)
438
+ }
439
+ })
440
+ }
441
+ }
adapters/clients/remote_index.go ADDED
@@ -0,0 +1,839 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "encoding/base64"
18
+ "encoding/json"
19
+ "fmt"
20
+ "io"
21
+ "net/http"
22
+ "net/url"
23
+
24
+ "github.com/go-openapi/strfmt"
25
+ "github.com/pkg/errors"
26
+ "github.com/weaviate/weaviate/adapters/handlers/rest/clusterapi"
27
+ "github.com/weaviate/weaviate/entities/additional"
28
+ "github.com/weaviate/weaviate/entities/aggregation"
29
+ "github.com/weaviate/weaviate/entities/filters"
30
+ "github.com/weaviate/weaviate/entities/search"
31
+ "github.com/weaviate/weaviate/entities/searchparams"
32
+ "github.com/weaviate/weaviate/entities/storobj"
33
+ "github.com/weaviate/weaviate/usecases/objects"
34
+ "github.com/weaviate/weaviate/usecases/scaler"
35
+ )
36
+
37
+ type RemoteIndex struct {
38
+ retryClient
39
+ }
40
+
41
+ func NewRemoteIndex(httpClient *http.Client) *RemoteIndex {
42
+ return &RemoteIndex{retryClient: retryClient{
43
+ client: httpClient,
44
+ retryer: newRetryer(),
45
+ }}
46
+ }
47
+
48
+ func (c *RemoteIndex) PutObject(ctx context.Context, hostName, indexName,
49
+ shardName string, obj *storobj.Object,
50
+ ) error {
51
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
52
+ method := http.MethodPost
53
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
54
+
55
+ marshalled, err := clusterapi.IndicesPayloads.SingleObject.Marshal(obj)
56
+ if err != nil {
57
+ return errors.Wrap(err, "marshal payload")
58
+ }
59
+
60
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
61
+ bytes.NewReader(marshalled))
62
+ if err != nil {
63
+ return errors.Wrap(err, "open http request")
64
+ }
65
+
66
+ clusterapi.IndicesPayloads.SingleObject.SetContentTypeHeaderReq(req)
67
+ res, err := c.client.Do(req)
68
+ if err != nil {
69
+ return errors.Wrap(err, "send http request")
70
+ }
71
+
72
+ defer res.Body.Close()
73
+ if res.StatusCode != http.StatusNoContent {
74
+ body, _ := io.ReadAll(res.Body)
75
+ return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
76
+ body)
77
+ }
78
+
79
+ return nil
80
+ }
81
+
82
+ func duplicateErr(in error, count int) []error {
83
+ out := make([]error, count)
84
+ for i := range out {
85
+ out[i] = in
86
+ }
87
+ return out
88
+ }
89
+
90
+ func (c *RemoteIndex) BatchPutObjects(ctx context.Context, hostName, indexName,
91
+ shardName string, objs []*storobj.Object, _ *additional.ReplicationProperties,
92
+ ) []error {
93
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
94
+ method := http.MethodPost
95
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
96
+
97
+ marshalled, err := clusterapi.IndicesPayloads.ObjectList.Marshal(objs)
98
+ if err != nil {
99
+ return duplicateErr(errors.Wrap(err, "marshal payload"), len(objs))
100
+ }
101
+
102
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
103
+ bytes.NewReader(marshalled))
104
+ if err != nil {
105
+ return duplicateErr(errors.Wrap(err, "open http request"), len(objs))
106
+ }
107
+
108
+ clusterapi.IndicesPayloads.ObjectList.SetContentTypeHeaderReq(req)
109
+
110
+ res, err := c.client.Do(req)
111
+ if err != nil {
112
+ return duplicateErr(errors.Wrap(err, "send http request"), len(objs))
113
+ }
114
+
115
+ defer res.Body.Close()
116
+ if res.StatusCode != http.StatusOK {
117
+ body, _ := io.ReadAll(res.Body)
118
+ return duplicateErr(errors.Errorf("unexpected status code %d (%s)",
119
+ res.StatusCode, body), len(objs))
120
+ }
121
+
122
+ if ct, ok := clusterapi.IndicesPayloads.ErrorList.
123
+ CheckContentTypeHeader(res); !ok {
124
+ return duplicateErr(errors.Errorf("unexpected content type: %s",
125
+ ct), len(objs))
126
+ }
127
+
128
+ resBytes, err := io.ReadAll(res.Body)
129
+ if err != nil {
130
+ return duplicateErr(errors.Wrap(err, "ready body"), len(objs))
131
+ }
132
+
133
+ return clusterapi.IndicesPayloads.ErrorList.Unmarshal(resBytes)
134
+ }
135
+
136
+ func (c *RemoteIndex) BatchAddReferences(ctx context.Context, hostName, indexName,
137
+ shardName string, refs objects.BatchReferences,
138
+ ) []error {
139
+ path := fmt.Sprintf("/indices/%s/shards/%s/references", indexName, shardName)
140
+ method := http.MethodPost
141
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
142
+
143
+ marshalled, err := clusterapi.IndicesPayloads.ReferenceList.Marshal(refs)
144
+ if err != nil {
145
+ return duplicateErr(errors.Wrap(err, "marshal payload"), len(refs))
146
+ }
147
+
148
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
149
+ bytes.NewReader(marshalled))
150
+ if err != nil {
151
+ return duplicateErr(errors.Wrap(err, "open http request"), len(refs))
152
+ }
153
+
154
+ clusterapi.IndicesPayloads.ReferenceList.SetContentTypeHeaderReq(req)
155
+
156
+ res, err := c.client.Do(req)
157
+ if err != nil {
158
+ return duplicateErr(errors.Wrap(err, "send http request"), len(refs))
159
+ }
160
+
161
+ defer res.Body.Close()
162
+ if res.StatusCode != http.StatusOK {
163
+ body, _ := io.ReadAll(res.Body)
164
+ return duplicateErr(errors.Errorf("unexpected status code %d (%s)",
165
+ res.StatusCode, body), len(refs))
166
+ }
167
+
168
+ if ct, ok := clusterapi.IndicesPayloads.ErrorList.
169
+ CheckContentTypeHeader(res); !ok {
170
+ return duplicateErr(errors.Errorf("unexpected content type: %s",
171
+ ct), len(refs))
172
+ }
173
+
174
+ resBytes, err := io.ReadAll(res.Body)
175
+ if err != nil {
176
+ return duplicateErr(errors.Wrap(err, "ready body"), len(refs))
177
+ }
178
+
179
+ return clusterapi.IndicesPayloads.ErrorList.Unmarshal(resBytes)
180
+ }
181
+
182
+ func (c *RemoteIndex) GetObject(ctx context.Context, hostName, indexName,
183
+ shardName string, id strfmt.UUID, selectProps search.SelectProperties,
184
+ additional additional.Properties,
185
+ ) (*storobj.Object, error) {
186
+ selectPropsBytes, err := json.Marshal(selectProps)
187
+ if err != nil {
188
+ return nil, errors.Wrap(err, "marshal selectProps props")
189
+ }
190
+
191
+ additionalBytes, err := json.Marshal(additional)
192
+ if err != nil {
193
+ return nil, errors.Wrap(err, "marshal additional props")
194
+ }
195
+
196
+ selectPropsEncoded := base64.StdEncoding.EncodeToString(selectPropsBytes)
197
+ additionalEncoded := base64.StdEncoding.EncodeToString(additionalBytes)
198
+
199
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
200
+ method := http.MethodGet
201
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
202
+ q := url.Query()
203
+ q.Set("additional", additionalEncoded)
204
+ q.Set("selectProperties", selectPropsEncoded)
205
+ url.RawQuery = q.Encode()
206
+
207
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
208
+ if err != nil {
209
+ return nil, errors.Wrap(err, "open http request")
210
+ }
211
+
212
+ res, err := c.client.Do(req)
213
+ if err != nil {
214
+ return nil, errors.Wrap(err, "send http request")
215
+ }
216
+
217
+ defer res.Body.Close()
218
+ if res.StatusCode == http.StatusNotFound {
219
+ // this is a legitimate case - the requested ID doesn't exist, don't try
220
+ // to unmarshal anything
221
+ return nil, nil
222
+ }
223
+
224
+ if res.StatusCode != http.StatusOK {
225
+ body, _ := io.ReadAll(res.Body)
226
+ return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
227
+ body)
228
+ }
229
+
230
+ ct, ok := clusterapi.IndicesPayloads.SingleObject.CheckContentTypeHeader(res)
231
+ if !ok {
232
+ return nil, errors.Errorf("unknown content type %s", ct)
233
+ }
234
+
235
+ objBytes, err := io.ReadAll(res.Body)
236
+ if err != nil {
237
+ return nil, errors.Wrap(err, "read body")
238
+ }
239
+
240
+ obj, err := clusterapi.IndicesPayloads.SingleObject.Unmarshal(objBytes)
241
+ if err != nil {
242
+ return nil, errors.Wrap(err, "unmarshal body")
243
+ }
244
+
245
+ return obj, nil
246
+ }
247
+
248
+ func (c *RemoteIndex) Exists(ctx context.Context, hostName, indexName,
249
+ shardName string, id strfmt.UUID,
250
+ ) (bool, error) {
251
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
252
+ method := http.MethodGet
253
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
254
+ q := url.Query()
255
+ q.Set("check_exists", "true")
256
+ url.RawQuery = q.Encode()
257
+
258
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
259
+ if err != nil {
260
+ return false, errors.Wrap(err, "open http request")
261
+ }
262
+
263
+ res, err := c.client.Do(req)
264
+ if err != nil {
265
+ return false, errors.Wrap(err, "send http request")
266
+ }
267
+
268
+ defer res.Body.Close()
269
+ if res.StatusCode == http.StatusNotFound {
270
+ // this is a legitimate case - the requested ID doesn't exist, don't try
271
+ // to unmarshal anything
272
+ return false, nil
273
+ }
274
+
275
+ if res.StatusCode != http.StatusNoContent {
276
+ body, _ := io.ReadAll(res.Body)
277
+ return false, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
278
+ body)
279
+ }
280
+
281
+ return true, nil
282
+ }
283
+
284
+ func (c *RemoteIndex) DeleteObject(ctx context.Context, hostName, indexName,
285
+ shardName string, id strfmt.UUID,
286
+ ) error {
287
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName, id)
288
+ method := http.MethodDelete
289
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
290
+
291
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
292
+ if err != nil {
293
+ return errors.Wrap(err, "open http request")
294
+ }
295
+
296
+ res, err := c.client.Do(req)
297
+ if err != nil {
298
+ return errors.Wrap(err, "send http request")
299
+ }
300
+
301
+ defer res.Body.Close()
302
+ if res.StatusCode == http.StatusNotFound {
303
+ // this is a legitimate case - the requested ID doesn't exist, don't try
304
+ // to unmarshal anything, we can assume it was already deleted
305
+ return nil
306
+ }
307
+
308
+ if res.StatusCode != http.StatusNoContent {
309
+ body, _ := io.ReadAll(res.Body)
310
+ return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
311
+ body)
312
+ }
313
+
314
+ return nil
315
+ }
316
+
317
+ func (c *RemoteIndex) MergeObject(ctx context.Context, hostName, indexName,
318
+ shardName string, mergeDoc objects.MergeDocument,
319
+ ) error {
320
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects/%s", indexName, shardName,
321
+ mergeDoc.ID)
322
+ method := http.MethodPatch
323
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
324
+
325
+ marshalled, err := clusterapi.IndicesPayloads.MergeDoc.Marshal(mergeDoc)
326
+ if err != nil {
327
+ return errors.Wrap(err, "marshal payload")
328
+ }
329
+
330
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
331
+ bytes.NewReader(marshalled))
332
+ if err != nil {
333
+ return errors.Wrap(err, "open http request")
334
+ }
335
+
336
+ clusterapi.IndicesPayloads.MergeDoc.SetContentTypeHeaderReq(req)
337
+ res, err := c.client.Do(req)
338
+ if err != nil {
339
+ return errors.Wrap(err, "send http request")
340
+ }
341
+
342
+ defer res.Body.Close()
343
+ if res.StatusCode != http.StatusNoContent {
344
+ body, _ := io.ReadAll(res.Body)
345
+ return errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
346
+ body)
347
+ }
348
+
349
+ return nil
350
+ }
351
+
352
+ func (c *RemoteIndex) MultiGetObjects(ctx context.Context, hostName, indexName,
353
+ shardName string, ids []strfmt.UUID,
354
+ ) ([]*storobj.Object, error) {
355
+ idsBytes, err := json.Marshal(ids)
356
+ if err != nil {
357
+ return nil, errors.Wrap(err, "marshal selectProps props")
358
+ }
359
+
360
+ idsEncoded := base64.StdEncoding.EncodeToString(idsBytes)
361
+
362
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
363
+ method := http.MethodGet
364
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
365
+ q := url.Query()
366
+ q.Set("ids", idsEncoded)
367
+ url.RawQuery = q.Encode()
368
+
369
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
370
+ if err != nil {
371
+ return nil, errors.Wrap(err, "open http request")
372
+ }
373
+
374
+ res, err := c.client.Do(req)
375
+ if err != nil {
376
+ return nil, errors.Wrap(err, "send http request")
377
+ }
378
+
379
+ defer res.Body.Close()
380
+ if res.StatusCode == http.StatusNotFound {
381
+ // this is a legitimate case - the requested ID doesn't exist, don't try
382
+ // to unmarshal anything
383
+ return nil, nil
384
+ }
385
+
386
+ if res.StatusCode != http.StatusOK {
387
+ body, _ := io.ReadAll(res.Body)
388
+ return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
389
+ body)
390
+ }
391
+
392
+ ct, ok := clusterapi.IndicesPayloads.ObjectList.CheckContentTypeHeader(res)
393
+ if !ok {
394
+ return nil, errors.Errorf("unexpected content type: %s", ct)
395
+ }
396
+
397
+ bodyBytes, err := io.ReadAll(res.Body)
398
+ if err != nil {
399
+ return nil, errors.Wrap(err, "read response body")
400
+ }
401
+
402
+ objs, err := clusterapi.IndicesPayloads.ObjectList.Unmarshal(bodyBytes)
403
+ if err != nil {
404
+ return nil, errors.Wrap(err, "unmarshal objects")
405
+ }
406
+
407
+ return objs, nil
408
+ }
409
+
410
+ func (c *RemoteIndex) SearchShard(ctx context.Context, host, index, shard string,
411
+ vector []float32, limit int,
412
+ filters *filters.LocalFilter,
413
+ keywordRanking *searchparams.KeywordRanking,
414
+ sort []filters.Sort,
415
+ cursor *filters.Cursor,
416
+ groupBy *searchparams.GroupBy,
417
+ additional additional.Properties,
418
+ ) ([]*storobj.Object, []float32, error) {
419
+ // new request
420
+ body, err := clusterapi.IndicesPayloads.SearchParams.
421
+ Marshal(vector, limit, filters, keywordRanking, sort, cursor, groupBy, additional)
422
+ if err != nil {
423
+ return nil, nil, fmt.Errorf("marshal request payload: %w", err)
424
+ }
425
+ url := url.URL{
426
+ Scheme: "http",
427
+ Host: host,
428
+ Path: fmt.Sprintf("/indices/%s/shards/%s/objects/_search", index, shard),
429
+ }
430
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(body))
431
+ if err != nil {
432
+ return nil, nil, fmt.Errorf("create http request: %w", err)
433
+ }
434
+ clusterapi.IndicesPayloads.SearchParams.SetContentTypeHeaderReq(req)
435
+
436
+ // send request
437
+ resp := &searchShardResp{}
438
+ err = c.doWithCustomMarshaller(c.timeoutUnit*20, req, body, resp.decode)
439
+ return resp.Objects, resp.Distributions, err
440
+ }
441
+
442
+ type searchShardResp struct {
443
+ Objects []*storobj.Object
444
+ Distributions []float32
445
+ }
446
+
447
+ func (r *searchShardResp) decode(data []byte) (err error) {
448
+ r.Objects, r.Distributions, err = clusterapi.IndicesPayloads.SearchResults.Unmarshal(data)
449
+ return
450
+ }
451
+
452
+ type aggregateResp struct {
453
+ Result *aggregation.Result
454
+ }
455
+
456
+ func (r *aggregateResp) decode(data []byte) (err error) {
457
+ r.Result, err = clusterapi.IndicesPayloads.AggregationResult.Unmarshal(data)
458
+ return
459
+ }
460
+
461
+ func (c *RemoteIndex) Aggregate(ctx context.Context, hostName, index,
462
+ shard string, params aggregation.Params,
463
+ ) (*aggregation.Result, error) {
464
+ // create new request
465
+ body, err := clusterapi.IndicesPayloads.AggregationParams.Marshal(params)
466
+ if err != nil {
467
+ return nil, fmt.Errorf("marshal request payload: %w", err)
468
+ }
469
+
470
+ url := &url.URL{
471
+ Scheme: "http",
472
+ Host: hostName,
473
+ Path: fmt.Sprintf("/indices/%s/shards/%s/objects/_aggregations", index, shard),
474
+ }
475
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(body))
476
+ if err != nil {
477
+ return nil, fmt.Errorf("create http request: %w", err)
478
+ }
479
+ clusterapi.IndicesPayloads.AggregationParams.SetContentTypeHeaderReq(req)
480
+
481
+ // send request
482
+ resp := &aggregateResp{}
483
+ err = c.doWithCustomMarshaller(c.timeoutUnit*20, req, body, resp.decode)
484
+ return resp.Result, err
485
+ }
486
+
487
+ func (c *RemoteIndex) FindUUIDs(ctx context.Context, hostName, indexName,
488
+ shardName string, filters *filters.LocalFilter,
489
+ ) ([]strfmt.UUID, error) {
490
+ paramsBytes, err := clusterapi.IndicesPayloads.FindUUIDsParams.Marshal(filters)
491
+ if err != nil {
492
+ return nil, errors.Wrap(err, "marshal request payload")
493
+ }
494
+
495
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects/_find", indexName, shardName)
496
+ method := http.MethodPost
497
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
498
+
499
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
500
+ bytes.NewReader(paramsBytes))
501
+ if err != nil {
502
+ return nil, errors.Wrap(err, "open http request")
503
+ }
504
+
505
+ clusterapi.IndicesPayloads.FindUUIDsParams.SetContentTypeHeaderReq(req)
506
+ res, err := c.client.Do(req)
507
+ if err != nil {
508
+ return nil, errors.Wrap(err, "send http request")
509
+ }
510
+
511
+ defer res.Body.Close()
512
+ if res.StatusCode != http.StatusOK {
513
+ body, _ := io.ReadAll(res.Body)
514
+ return nil, errors.Errorf("unexpected status code %d (%s)", res.StatusCode,
515
+ body)
516
+ }
517
+
518
+ resBytes, err := io.ReadAll(res.Body)
519
+ if err != nil {
520
+ return nil, errors.Wrap(err, "read body")
521
+ }
522
+
523
+ ct, ok := clusterapi.IndicesPayloads.FindUUIDsResults.CheckContentTypeHeader(res)
524
+ if !ok {
525
+ return nil, errors.Errorf("unexpected content type: %s", ct)
526
+ }
527
+
528
+ uuids, err := clusterapi.IndicesPayloads.FindUUIDsResults.Unmarshal(resBytes)
529
+ if err != nil {
530
+ return nil, errors.Wrap(err, "unmarshal body")
531
+ }
532
+ return uuids, nil
533
+ }
534
+
535
+ func (c *RemoteIndex) DeleteObjectBatch(ctx context.Context, hostName, indexName, shardName string,
536
+ uuids []strfmt.UUID, dryRun bool,
537
+ ) objects.BatchSimpleObjects {
538
+ path := fmt.Sprintf("/indices/%s/shards/%s/objects", indexName, shardName)
539
+ method := http.MethodDelete
540
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
541
+
542
+ marshalled, err := clusterapi.IndicesPayloads.BatchDeleteParams.Marshal(uuids, dryRun)
543
+ if err != nil {
544
+ err := errors.Wrap(err, "marshal payload")
545
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
546
+ }
547
+
548
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
549
+ bytes.NewReader(marshalled))
550
+ if err != nil {
551
+ err := errors.Wrap(err, "open http request")
552
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
553
+ }
554
+
555
+ clusterapi.IndicesPayloads.BatchDeleteParams.SetContentTypeHeaderReq(req)
556
+
557
+ res, err := c.client.Do(req)
558
+ if err != nil {
559
+ err := errors.Wrap(err, "send http request")
560
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
561
+ }
562
+
563
+ defer res.Body.Close()
564
+ if res.StatusCode != http.StatusOK {
565
+ body, _ := io.ReadAll(res.Body)
566
+ err := errors.Errorf("unexpected status code %d (%s)", res.StatusCode, body)
567
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
568
+ }
569
+
570
+ if ct, ok := clusterapi.IndicesPayloads.BatchDeleteResults.
571
+ CheckContentTypeHeader(res); !ok {
572
+ err := errors.Errorf("unexpected content type: %s", ct)
573
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
574
+ }
575
+
576
+ resBytes, err := io.ReadAll(res.Body)
577
+ if err != nil {
578
+ err := errors.Wrap(err, "ready body")
579
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
580
+ }
581
+
582
+ batchDeleteResults, err := clusterapi.IndicesPayloads.BatchDeleteResults.Unmarshal(resBytes)
583
+ if err != nil {
584
+ err := errors.Wrap(err, "unmarshal body")
585
+ return objects.BatchSimpleObjects{objects.BatchSimpleObject{Err: err}}
586
+ }
587
+
588
+ return batchDeleteResults
589
+ }
590
+
591
+ func (c *RemoteIndex) GetShardQueueSize(ctx context.Context,
592
+ hostName, indexName, shardName string,
593
+ ) (int64, error) {
594
+ path := fmt.Sprintf("/indices/%s/shards/%s/queuesize", indexName, shardName)
595
+ method := http.MethodGet
596
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
597
+
598
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
599
+ if err != nil {
600
+ return 0, errors.Wrap(err, "open http request")
601
+ }
602
+ var size int64
603
+ clusterapi.IndicesPayloads.GetShardQueueSizeParams.SetContentTypeHeaderReq(req)
604
+ try := func(ctx context.Context) (bool, error) {
605
+ res, err := c.client.Do(req)
606
+ if err != nil {
607
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
608
+ }
609
+ defer res.Body.Close()
610
+
611
+ if code := res.StatusCode; code != http.StatusOK {
612
+ body, _ := io.ReadAll(res.Body)
613
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
614
+ }
615
+ resBytes, err := io.ReadAll(res.Body)
616
+ if err != nil {
617
+ return false, errors.Wrap(err, "read body")
618
+ }
619
+
620
+ ct, ok := clusterapi.IndicesPayloads.GetShardQueueSizeResults.CheckContentTypeHeader(res)
621
+ if !ok {
622
+ return false, errors.Errorf("unexpected content type: %s", ct)
623
+ }
624
+
625
+ size, err = clusterapi.IndicesPayloads.GetShardQueueSizeResults.Unmarshal(resBytes)
626
+ if err != nil {
627
+ return false, errors.Wrap(err, "unmarshal body")
628
+ }
629
+ return false, nil
630
+ }
631
+ return size, c.retry(ctx, 9, try)
632
+ }
633
+
634
+ func (c *RemoteIndex) GetShardStatus(ctx context.Context,
635
+ hostName, indexName, shardName string,
636
+ ) (string, error) {
637
+ path := fmt.Sprintf("/indices/%s/shards/%s/status", indexName, shardName)
638
+ method := http.MethodGet
639
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
640
+
641
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
642
+ if err != nil {
643
+ return "", errors.Wrap(err, "open http request")
644
+ }
645
+ var status string
646
+ clusterapi.IndicesPayloads.GetShardStatusParams.SetContentTypeHeaderReq(req)
647
+ try := func(ctx context.Context) (bool, error) {
648
+ res, err := c.client.Do(req)
649
+ if err != nil {
650
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
651
+ }
652
+ defer res.Body.Close()
653
+
654
+ if code := res.StatusCode; code != http.StatusOK {
655
+ body, _ := io.ReadAll(res.Body)
656
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
657
+ }
658
+ resBytes, err := io.ReadAll(res.Body)
659
+ if err != nil {
660
+ return false, errors.Wrap(err, "read body")
661
+ }
662
+
663
+ ct, ok := clusterapi.IndicesPayloads.GetShardStatusResults.CheckContentTypeHeader(res)
664
+ if !ok {
665
+ return false, errors.Errorf("unexpected content type: %s", ct)
666
+ }
667
+
668
+ status, err = clusterapi.IndicesPayloads.GetShardStatusResults.Unmarshal(resBytes)
669
+ if err != nil {
670
+ return false, errors.Wrap(err, "unmarshal body")
671
+ }
672
+ return false, nil
673
+ }
674
+ return status, c.retry(ctx, 9, try)
675
+ }
676
+
677
+ func (c *RemoteIndex) UpdateShardStatus(ctx context.Context, hostName, indexName, shardName,
678
+ targetStatus string,
679
+ ) error {
680
+ paramsBytes, err := clusterapi.IndicesPayloads.UpdateShardStatusParams.Marshal(targetStatus)
681
+ if err != nil {
682
+ return errors.Wrap(err, "marshal request payload")
683
+ }
684
+ path := fmt.Sprintf("/indices/%s/shards/%s/status", indexName, shardName)
685
+ method := http.MethodPost
686
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
687
+
688
+ try := func(ctx context.Context) (bool, error) {
689
+ req, err := http.NewRequestWithContext(ctx, method, url.String(),
690
+ bytes.NewReader(paramsBytes))
691
+ if err != nil {
692
+ return false, fmt.Errorf("create http request: %w", err)
693
+ }
694
+ clusterapi.IndicesPayloads.UpdateShardStatusParams.SetContentTypeHeaderReq(req)
695
+
696
+ res, err := c.client.Do(req)
697
+ if err != nil {
698
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
699
+ }
700
+ defer res.Body.Close()
701
+
702
+ if code := res.StatusCode; code != http.StatusOK {
703
+ body, _ := io.ReadAll(res.Body)
704
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
705
+ }
706
+
707
+ return false, nil
708
+ }
709
+
710
+ return c.retry(ctx, 9, try)
711
+ }
712
+
713
+ func (c *RemoteIndex) PutFile(ctx context.Context, hostName, indexName,
714
+ shardName, fileName string, payload io.ReadSeekCloser,
715
+ ) error {
716
+ defer payload.Close()
717
+ path := fmt.Sprintf("/indices/%s/shards/%s/files/%s",
718
+ indexName, shardName, fileName)
719
+
720
+ method := http.MethodPost
721
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
722
+ try := func(ctx context.Context) (bool, error) {
723
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), payload)
724
+ if err != nil {
725
+ return false, fmt.Errorf("create http request: %w", err)
726
+ }
727
+ clusterapi.IndicesPayloads.ShardFiles.SetContentTypeHeaderReq(req)
728
+ res, err := c.client.Do(req)
729
+ if err != nil {
730
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
731
+ }
732
+ defer res.Body.Close()
733
+
734
+ if code := res.StatusCode; code != http.StatusNoContent {
735
+ shouldRetry := shouldRetry(code)
736
+ if shouldRetry {
737
+ _, err := payload.Seek(0, 0)
738
+ shouldRetry = (err == nil)
739
+ }
740
+ body, _ := io.ReadAll(res.Body)
741
+ return shouldRetry, fmt.Errorf("status code: %v body: (%s)", code, body)
742
+ }
743
+ return false, nil
744
+ }
745
+
746
+ return c.retry(ctx, 12, try)
747
+ }
748
+
749
+ func (c *RemoteIndex) CreateShard(ctx context.Context,
750
+ hostName, indexName, shardName string,
751
+ ) error {
752
+ path := fmt.Sprintf("/indices/%s/shards/%s", indexName, shardName)
753
+
754
+ method := http.MethodPost
755
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
756
+
757
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
758
+ if err != nil {
759
+ return fmt.Errorf("create http request: %w", err)
760
+ }
761
+ try := func(ctx context.Context) (bool, error) {
762
+ res, err := c.client.Do(req)
763
+ if err != nil {
764
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
765
+ }
766
+ defer res.Body.Close()
767
+
768
+ if code := res.StatusCode; code != http.StatusCreated {
769
+ body, _ := io.ReadAll(res.Body)
770
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
771
+ }
772
+ return false, nil
773
+ }
774
+
775
+ return c.retry(ctx, 9, try)
776
+ }
777
+
778
+ func (c *RemoteIndex) ReInitShard(ctx context.Context,
779
+ hostName, indexName, shardName string,
780
+ ) error {
781
+ path := fmt.Sprintf("/indices/%s/shards/%s:reinit", indexName, shardName)
782
+
783
+ method := http.MethodPut
784
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
785
+
786
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), nil)
787
+ if err != nil {
788
+ return fmt.Errorf("create http request: %w", err)
789
+ }
790
+ try := func(ctx context.Context) (bool, error) {
791
+ res, err := c.client.Do(req)
792
+ if err != nil {
793
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
794
+ }
795
+ defer res.Body.Close()
796
+
797
+ if code := res.StatusCode; code != http.StatusNoContent {
798
+ body, _ := io.ReadAll(res.Body)
799
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
800
+
801
+ }
802
+ return false, nil
803
+ }
804
+
805
+ return c.retry(ctx, 9, try)
806
+ }
807
+
808
+ func (c *RemoteIndex) IncreaseReplicationFactor(ctx context.Context,
809
+ hostName, indexName string, dist scaler.ShardDist,
810
+ ) error {
811
+ path := fmt.Sprintf("/replicas/indices/%s/replication-factor:increase", indexName)
812
+
813
+ method := http.MethodPut
814
+ url := url.URL{Scheme: "http", Host: hostName, Path: path}
815
+
816
+ body, err := clusterapi.IndicesPayloads.IncreaseReplicationFactor.Marshall(dist)
817
+ if err != nil {
818
+ return err
819
+ }
820
+ try := func(ctx context.Context) (bool, error) {
821
+ req, err := http.NewRequestWithContext(ctx, method, url.String(), bytes.NewReader(body))
822
+ if err != nil {
823
+ return false, fmt.Errorf("create http request: %w", err)
824
+ }
825
+
826
+ res, err := c.client.Do(req)
827
+ if err != nil {
828
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
829
+ }
830
+ defer res.Body.Close()
831
+
832
+ if code := res.StatusCode; code != http.StatusNoContent {
833
+ body, _ := io.ReadAll(res.Body)
834
+ return shouldRetry(code), fmt.Errorf("status code: %v body: (%s)", code, body)
835
+ }
836
+ return false, nil
837
+ }
838
+ return c.retry(ctx, 34, try)
839
+ }
adapters/clients/remote_index_test.go ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // _ _
13
+ //
14
+ // __ _____ __ ___ ___ __ _| |_ ___
15
+ //
16
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
17
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
18
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
19
+ //
20
+ // Copyright © 2016 - 2022 SeMI Technologies B.V. All rights reserved.
21
+ //
22
+ // CONTACT: [email protected]
23
+ package clients
24
+
25
+ import (
26
+ "context"
27
+ "fmt"
28
+ "io"
29
+ "net/http"
30
+ "net/http/httptest"
31
+ "strings"
32
+ "testing"
33
+ "time"
34
+
35
+ "github.com/stretchr/testify/assert"
36
+ "github.com/weaviate/weaviate/adapters/handlers/rest/clusterapi"
37
+ )
38
+
39
+ func TestRemoteIndexIncreaseRF(t *testing.T) {
40
+ t.Parallel()
41
+
42
+ ctx := context.Background()
43
+ path := "/replicas/indices/C1/replication-factor:increase"
44
+ fs := newFakeRemoteIndexServer(t, http.MethodPut, path)
45
+ ts := fs.server(t)
46
+ defer ts.Close()
47
+ client := newRemoteIndex(ts.Client())
48
+ t.Run("ConnectionError", func(t *testing.T) {
49
+ err := client.IncreaseReplicationFactor(ctx, "", "C1", nil)
50
+ assert.NotNil(t, err)
51
+ assert.Contains(t, err.Error(), "connect")
52
+ })
53
+ n := 0
54
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
55
+ if n == 0 {
56
+ w.WriteHeader(http.StatusInternalServerError)
57
+ } else if n == 1 {
58
+ w.WriteHeader(http.StatusTooManyRequests)
59
+ } else {
60
+ w.WriteHeader(http.StatusNoContent)
61
+ }
62
+ n++
63
+ }
64
+ t.Run("Success", func(t *testing.T) {
65
+ err := client.IncreaseReplicationFactor(ctx, fs.host, "C1", nil)
66
+ assert.Nil(t, err)
67
+ })
68
+ }
69
+
70
+ func TestRemoteIndexReInitShardIn(t *testing.T) {
71
+ t.Parallel()
72
+
73
+ ctx := context.Background()
74
+ path := "/indices/C1/shards/S1:reinit"
75
+ fs := newFakeRemoteIndexServer(t, http.MethodPut, path)
76
+ ts := fs.server(t)
77
+ defer ts.Close()
78
+ client := newRemoteIndex(ts.Client())
79
+ t.Run("ConnectionError", func(t *testing.T) {
80
+ err := client.ReInitShard(ctx, "", "C1", "S1")
81
+ assert.NotNil(t, err)
82
+ assert.Contains(t, err.Error(), "connect")
83
+ })
84
+ n := 0
85
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
86
+ if n == 0 {
87
+ w.WriteHeader(http.StatusInternalServerError)
88
+ } else if n == 1 {
89
+ w.WriteHeader(http.StatusTooManyRequests)
90
+ } else {
91
+ w.WriteHeader(http.StatusNoContent)
92
+ }
93
+ n++
94
+ }
95
+ t.Run("Success", func(t *testing.T) {
96
+ err := client.ReInitShard(ctx, fs.host, "C1", "S1")
97
+ assert.Nil(t, err)
98
+ })
99
+ }
100
+
101
+ func TestRemoteIndexCreateShard(t *testing.T) {
102
+ t.Parallel()
103
+
104
+ ctx := context.Background()
105
+ path := "/indices/C1/shards/S1"
106
+ fs := newFakeRemoteIndexServer(t, http.MethodPost, path)
107
+ ts := fs.server(t)
108
+ defer ts.Close()
109
+ client := newRemoteIndex(ts.Client())
110
+ t.Run("ConnectionError", func(t *testing.T) {
111
+ err := client.CreateShard(ctx, "", "C1", "S1")
112
+ assert.NotNil(t, err)
113
+ assert.Contains(t, err.Error(), "connect")
114
+ })
115
+ n := 0
116
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
117
+ if n == 0 {
118
+ w.WriteHeader(http.StatusInternalServerError)
119
+ } else if n == 1 {
120
+ w.WriteHeader(http.StatusTooManyRequests)
121
+ } else {
122
+ w.WriteHeader(http.StatusCreated)
123
+ }
124
+ n++
125
+ }
126
+ t.Run("Success", func(t *testing.T) {
127
+ err := client.CreateShard(ctx, fs.host, "C1", "S1")
128
+ assert.Nil(t, err)
129
+ })
130
+ }
131
+
132
+ func TestRemoteIndexUpdateShardStatus(t *testing.T) {
133
+ t.Parallel()
134
+
135
+ ctx := context.Background()
136
+ path := "/indices/C1/shards/S1/status"
137
+ fs := newFakeRemoteIndexServer(t, http.MethodPost, path)
138
+ ts := fs.server(t)
139
+ defer ts.Close()
140
+ client := newRemoteIndex(ts.Client())
141
+ t.Run("ConnectionError", func(t *testing.T) {
142
+ err := client.UpdateShardStatus(ctx, "", "C1", "S1", "NewStatus")
143
+ assert.NotNil(t, err)
144
+ assert.Contains(t, err.Error(), "connect")
145
+ })
146
+ n := 0
147
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
148
+ if n == 0 {
149
+ w.WriteHeader(http.StatusInternalServerError)
150
+ } else if n == 1 {
151
+ w.WriteHeader(http.StatusTooManyRequests)
152
+ }
153
+ n++
154
+ }
155
+ t.Run("Success", func(t *testing.T) {
156
+ err := client.UpdateShardStatus(ctx, fs.host, "C1", "S1", "NewStatus")
157
+ assert.Nil(t, err)
158
+ })
159
+ }
160
+
161
+ func TestRemoteIndexShardStatus(t *testing.T) {
162
+ t.Parallel()
163
+ var (
164
+ ctx = context.Background()
165
+ path = "/indices/C1/shards/S1/status"
166
+ fs = newFakeRemoteIndexServer(t, http.MethodGet, path)
167
+ Status = "READONLY"
168
+ )
169
+ ts := fs.server(t)
170
+ defer ts.Close()
171
+ client := newRemoteIndex(ts.Client())
172
+ t.Run("ConnectionError", func(t *testing.T) {
173
+ _, err := client.GetShardStatus(ctx, "", "C1", "S1")
174
+ assert.NotNil(t, err)
175
+ assert.Contains(t, err.Error(), "connect")
176
+ })
177
+ n := 0
178
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
179
+ if n == 0 {
180
+ w.WriteHeader(http.StatusInternalServerError)
181
+ } else if n == 1 {
182
+ w.WriteHeader(http.StatusTooManyRequests)
183
+ } else if n == 2 {
184
+ w.Header().Set("content-type", "any")
185
+ } else if n == 3 {
186
+ clusterapi.IndicesPayloads.GetShardStatusResults.SetContentTypeHeader(w)
187
+ } else {
188
+ clusterapi.IndicesPayloads.GetShardStatusResults.SetContentTypeHeader(w)
189
+ bytes, _ := clusterapi.IndicesPayloads.GetShardStatusResults.Marshal(Status)
190
+ w.Write(bytes)
191
+ }
192
+ n++
193
+ }
194
+
195
+ t.Run("ContentType", func(t *testing.T) {
196
+ _, err := client.GetShardStatus(ctx, fs.host, "C1", "S1")
197
+ assert.NotNil(t, err)
198
+ })
199
+ t.Run("Status", func(t *testing.T) {
200
+ _, err := client.GetShardStatus(ctx, fs.host, "C1", "S1")
201
+ assert.NotNil(t, err)
202
+ })
203
+ t.Run("Success", func(t *testing.T) {
204
+ st, err := client.GetShardStatus(ctx, fs.host, "C1", "S1")
205
+ assert.Nil(t, err)
206
+ assert.Equal(t, "READONLY", st)
207
+ })
208
+ }
209
+
210
+ func TestRemoteIndexPutFile(t *testing.T) {
211
+ t.Parallel()
212
+ var (
213
+ ctx = context.Background()
214
+ path = "/indices/C1/shards/S1/files/file1"
215
+ fs = newFakeRemoteIndexServer(t, http.MethodPost, path)
216
+ )
217
+ ts := fs.server(t)
218
+ defer ts.Close()
219
+ client := newRemoteIndex(ts.Client())
220
+
221
+ rsc := struct {
222
+ *strings.Reader
223
+ io.Closer
224
+ }{
225
+ strings.NewReader("hello, world"),
226
+ io.NopCloser(nil),
227
+ }
228
+ t.Run("ConnectionError", func(t *testing.T) {
229
+ err := client.PutFile(ctx, "", "C1", "S1", "file1", rsc)
230
+ assert.NotNil(t, err)
231
+ assert.Contains(t, err.Error(), "connect")
232
+ })
233
+ n := 0
234
+ fs.doAfter = func(w http.ResponseWriter, r *http.Request) {
235
+ if n == 0 {
236
+ w.WriteHeader(http.StatusInternalServerError)
237
+ } else if n == 1 {
238
+ w.WriteHeader(http.StatusTooManyRequests)
239
+ } else {
240
+ w.WriteHeader(http.StatusNoContent)
241
+ }
242
+ n++
243
+ }
244
+
245
+ t.Run("Success", func(t *testing.T) {
246
+ err := client.PutFile(ctx, fs.host, "C1", "S1", "file1", rsc)
247
+ assert.Nil(t, err)
248
+ })
249
+ }
250
+
251
+ func newRemoteIndex(httpClient *http.Client) *RemoteIndex {
252
+ ri := NewRemoteIndex(httpClient)
253
+ ri.minBackOff = time.Millisecond * 1
254
+ ri.maxBackOff = time.Millisecond * 10
255
+ ri.timeoutUnit = time.Millisecond * 20
256
+ return ri
257
+ }
258
+
259
+ type fakeRemoteIndexServer struct {
260
+ method string
261
+ path string
262
+ host string
263
+ doBefore func(w http.ResponseWriter, r *http.Request) error
264
+ doAfter func(w http.ResponseWriter, r *http.Request)
265
+ }
266
+
267
+ func newFakeRemoteIndexServer(t *testing.T, method, path string) *fakeRemoteIndexServer {
268
+ f := &fakeRemoteIndexServer{
269
+ method: method,
270
+ path: path,
271
+ }
272
+ f.doBefore = func(w http.ResponseWriter, r *http.Request) error {
273
+ if r.Method != f.method {
274
+ w.WriteHeader(http.StatusBadRequest)
275
+ return fmt.Errorf("method want %s got %s", method, r.Method)
276
+ }
277
+ if f.path != r.URL.Path {
278
+ w.WriteHeader(http.StatusBadRequest)
279
+ return fmt.Errorf("path want %s got %s", path, r.URL.Path)
280
+ }
281
+ return nil
282
+ }
283
+ return f
284
+ }
285
+
286
+ func (f *fakeRemoteIndexServer) server(t *testing.T) *httptest.Server {
287
+ if f.doBefore == nil {
288
+ f.doBefore = func(w http.ResponseWriter, r *http.Request) error {
289
+ if r.Method != f.method {
290
+ w.WriteHeader(http.StatusBadRequest)
291
+ return fmt.Errorf("method want %s got %s", f.method, r.Method)
292
+ }
293
+ if f.path != r.URL.Path {
294
+ w.WriteHeader(http.StatusBadRequest)
295
+ return fmt.Errorf("path want %s got %s", f.path, r.URL.Path)
296
+ }
297
+ return nil
298
+ }
299
+ }
300
+ handler := func(w http.ResponseWriter, r *http.Request) {
301
+ if err := f.doBefore(w, r); err != nil {
302
+ t.Error(err)
303
+ return
304
+ }
305
+ if f.doAfter != nil {
306
+ f.doAfter(w, r)
307
+ }
308
+ }
309
+ serv := httptest.NewServer(http.HandlerFunc(handler))
310
+ f.host = serv.URL[7:]
311
+ return serv
312
+ }
adapters/clients/replication.go ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "bytes"
16
+ "context"
17
+ "encoding/base64"
18
+ "encoding/json"
19
+ "fmt"
20
+ "io"
21
+ "math/rand"
22
+ "net/http"
23
+ "net/url"
24
+ "time"
25
+
26
+ "github.com/go-openapi/strfmt"
27
+ "github.com/weaviate/weaviate/adapters/handlers/rest/clusterapi"
28
+ "github.com/weaviate/weaviate/entities/additional"
29
+ "github.com/weaviate/weaviate/entities/search"
30
+ "github.com/weaviate/weaviate/entities/storobj"
31
+ "github.com/weaviate/weaviate/usecases/objects"
32
+ "github.com/weaviate/weaviate/usecases/replica"
33
+ )
34
+
35
+ // ReplicationClient is to coordinate operations among replicas
36
+
37
+ type replicationClient retryClient
38
+
39
+ func NewReplicationClient(httpClient *http.Client) replica.Client {
40
+ return &replicationClient{
41
+ client: httpClient,
42
+ retryer: newRetryer(),
43
+ }
44
+ }
45
+
46
+ // FetchObject fetches one object it exits
47
+ func (c *replicationClient) FetchObject(ctx context.Context, host, index,
48
+ shard string, id strfmt.UUID, selectProps search.SelectProperties,
49
+ additional additional.Properties,
50
+ ) (objects.Replica, error) {
51
+ resp := objects.Replica{}
52
+ req, err := newHttpReplicaRequest(ctx, http.MethodGet, host, index, shard, "", id.String(), nil)
53
+ if err != nil {
54
+ return resp, fmt.Errorf("create http request: %w", err)
55
+ }
56
+ err = c.doCustomUnmarshal(c.timeoutUnit*20, req, nil, resp.UnmarshalBinary)
57
+ return resp, err
58
+ }
59
+
60
+ func (c *replicationClient) DigestObjects(ctx context.Context,
61
+ host, index, shard string, ids []strfmt.UUID,
62
+ ) (result []replica.RepairResponse, err error) {
63
+ var resp []replica.RepairResponse
64
+ body, err := json.Marshal(ids)
65
+ if err != nil {
66
+ return nil, fmt.Errorf("marshal digest objects input: %w", err)
67
+ }
68
+ req, err := newHttpReplicaRequest(
69
+ ctx, http.MethodGet, host, index, shard,
70
+ "", "_digest", bytes.NewReader(body))
71
+ if err != nil {
72
+ return resp, fmt.Errorf("create http request: %w", err)
73
+ }
74
+ err = c.do(c.timeoutUnit*20, req, body, &resp)
75
+ return resp, err
76
+ }
77
+
78
+ func (c *replicationClient) OverwriteObjects(ctx context.Context,
79
+ host, index, shard string, vobjects []*objects.VObject,
80
+ ) ([]replica.RepairResponse, error) {
81
+ var resp []replica.RepairResponse
82
+ body, err := clusterapi.IndicesPayloads.VersionedObjectList.Marshal(vobjects)
83
+ if err != nil {
84
+ return nil, fmt.Errorf("encode request: %w", err)
85
+ }
86
+ req, err := newHttpReplicaRequest(
87
+ ctx, http.MethodPut, host, index, shard,
88
+ "", "_overwrite", bytes.NewReader(body))
89
+ if err != nil {
90
+ return resp, fmt.Errorf("create http request: %w", err)
91
+ }
92
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
93
+ return resp, err
94
+ }
95
+
96
+ func (c *replicationClient) FetchObjects(ctx context.Context, host,
97
+ index, shard string, ids []strfmt.UUID,
98
+ ) ([]objects.Replica, error) {
99
+ resp := make(objects.Replicas, len(ids))
100
+ idsBytes, err := json.Marshal(ids)
101
+ if err != nil {
102
+ return nil, fmt.Errorf("marshal ids: %w", err)
103
+ }
104
+
105
+ idsEncoded := base64.StdEncoding.EncodeToString(idsBytes)
106
+
107
+ req, err := newHttpReplicaRequest(ctx, http.MethodGet, host, index, shard, "", "", nil)
108
+ if err != nil {
109
+ return nil, fmt.Errorf("create http request: %w", err)
110
+ }
111
+
112
+ req.URL.RawQuery = url.Values{"ids": []string{idsEncoded}}.Encode()
113
+ err = c.doCustomUnmarshal(c.timeoutUnit*90, req, nil, resp.UnmarshalBinary)
114
+ return resp, err
115
+ }
116
+
117
+ func (c *replicationClient) PutObject(ctx context.Context, host, index,
118
+ shard, requestID string, obj *storobj.Object,
119
+ ) (replica.SimpleResponse, error) {
120
+ var resp replica.SimpleResponse
121
+ body, err := clusterapi.IndicesPayloads.SingleObject.Marshal(obj)
122
+ if err != nil {
123
+ return resp, fmt.Errorf("encode request: %w", err)
124
+ }
125
+
126
+ req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard, requestID, "", nil)
127
+ if err != nil {
128
+ return resp, fmt.Errorf("create http request: %w", err)
129
+ }
130
+
131
+ clusterapi.IndicesPayloads.SingleObject.SetContentTypeHeaderReq(req)
132
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
133
+ return resp, err
134
+ }
135
+
136
+ func (c *replicationClient) DeleteObject(ctx context.Context, host, index,
137
+ shard, requestID string, uuid strfmt.UUID,
138
+ ) (replica.SimpleResponse, error) {
139
+ var resp replica.SimpleResponse
140
+ req, err := newHttpReplicaRequest(ctx, http.MethodDelete, host, index, shard, requestID, uuid.String(), nil)
141
+ if err != nil {
142
+ return resp, fmt.Errorf("create http request: %w", err)
143
+ }
144
+
145
+ err = c.do(c.timeoutUnit*90, req, nil, &resp)
146
+ return resp, err
147
+ }
148
+
149
+ func (c *replicationClient) PutObjects(ctx context.Context, host, index,
150
+ shard, requestID string, objects []*storobj.Object,
151
+ ) (replica.SimpleResponse, error) {
152
+ var resp replica.SimpleResponse
153
+ body, err := clusterapi.IndicesPayloads.ObjectList.Marshal(objects)
154
+ if err != nil {
155
+ return resp, fmt.Errorf("encode request: %w", err)
156
+ }
157
+ req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard, requestID, "", nil)
158
+ if err != nil {
159
+ return resp, fmt.Errorf("create http request: %w", err)
160
+ }
161
+
162
+ clusterapi.IndicesPayloads.ObjectList.SetContentTypeHeaderReq(req)
163
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
164
+ return resp, err
165
+ }
166
+
167
+ func (c *replicationClient) MergeObject(ctx context.Context, host, index, shard, requestID string,
168
+ doc *objects.MergeDocument,
169
+ ) (replica.SimpleResponse, error) {
170
+ var resp replica.SimpleResponse
171
+ body, err := clusterapi.IndicesPayloads.MergeDoc.Marshal(*doc)
172
+ if err != nil {
173
+ return resp, fmt.Errorf("encode request: %w", err)
174
+ }
175
+
176
+ req, err := newHttpReplicaRequest(ctx, http.MethodPatch, host, index, shard,
177
+ requestID, doc.ID.String(), nil)
178
+ if err != nil {
179
+ return resp, fmt.Errorf("create http request: %w", err)
180
+ }
181
+
182
+ clusterapi.IndicesPayloads.MergeDoc.SetContentTypeHeaderReq(req)
183
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
184
+ return resp, err
185
+ }
186
+
187
+ func (c *replicationClient) AddReferences(ctx context.Context, host, index,
188
+ shard, requestID string, refs []objects.BatchReference,
189
+ ) (replica.SimpleResponse, error) {
190
+ var resp replica.SimpleResponse
191
+ body, err := clusterapi.IndicesPayloads.ReferenceList.Marshal(refs)
192
+ if err != nil {
193
+ return resp, fmt.Errorf("encode request: %w", err)
194
+ }
195
+ req, err := newHttpReplicaRequest(ctx, http.MethodPost, host, index, shard,
196
+ requestID, "references", nil)
197
+ if err != nil {
198
+ return resp, fmt.Errorf("create http request: %w", err)
199
+ }
200
+
201
+ clusterapi.IndicesPayloads.ReferenceList.SetContentTypeHeaderReq(req)
202
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
203
+ return resp, err
204
+ }
205
+
206
+ func (c *replicationClient) DeleteObjects(ctx context.Context, host, index, shard, requestID string,
207
+ uuids []strfmt.UUID, dryRun bool,
208
+ ) (resp replica.SimpleResponse, err error) {
209
+ body, err := clusterapi.IndicesPayloads.BatchDeleteParams.Marshal(uuids, dryRun)
210
+ if err != nil {
211
+ return resp, fmt.Errorf("encode request: %w", err)
212
+ }
213
+ req, err := newHttpReplicaRequest(ctx, http.MethodDelete, host, index, shard, requestID, "", nil)
214
+ if err != nil {
215
+ return resp, fmt.Errorf("create http request: %w", err)
216
+ }
217
+
218
+ clusterapi.IndicesPayloads.BatchDeleteParams.SetContentTypeHeaderReq(req)
219
+ err = c.do(c.timeoutUnit*90, req, body, &resp)
220
+ return resp, err
221
+ }
222
+
223
+ // Commit asks a host to commit and stores the response in the value pointed to by resp
224
+ func (c *replicationClient) Commit(ctx context.Context, host, index, shard string, requestID string, resp interface{}) error {
225
+ req, err := newHttpReplicaCMD(host, "commit", index, shard, requestID, nil)
226
+ if err != nil {
227
+ return fmt.Errorf("create http request: %w", err)
228
+ }
229
+
230
+ return c.do(c.timeoutUnit*90, req, nil, resp)
231
+ }
232
+
233
+ func (c *replicationClient) Abort(ctx context.Context, host, index, shard, requestID string) (
234
+ resp replica.SimpleResponse, err error,
235
+ ) {
236
+ req, err := newHttpReplicaCMD(host, "abort", index, shard, requestID, nil)
237
+ if err != nil {
238
+ return resp, fmt.Errorf("create http request: %w", err)
239
+ }
240
+
241
+ err = c.do(c.timeoutUnit*5, req, nil, &resp)
242
+ return resp, err
243
+ }
244
+
245
+ func newHttpReplicaRequest(ctx context.Context, method, host, index, shard, requestId, suffix string, body io.Reader) (*http.Request, error) {
246
+ path := fmt.Sprintf("/replicas/indices/%s/shards/%s/objects", index, shard)
247
+ if suffix != "" {
248
+ path = fmt.Sprintf("%s/%s", path, suffix)
249
+ }
250
+ u := url.URL{
251
+ Scheme: "http",
252
+ Host: host,
253
+ Path: path,
254
+ }
255
+
256
+ if requestId != "" {
257
+ u.RawQuery = url.Values{replica.RequestKey: []string{requestId}}.Encode()
258
+ }
259
+
260
+ return http.NewRequestWithContext(ctx, method, u.String(), body)
261
+ }
262
+
263
+ func newHttpReplicaCMD(host, cmd, index, shard, requestId string, body io.Reader) (*http.Request, error) {
264
+ path := fmt.Sprintf("/replicas/indices/%s/shards/%s:%s", index, shard, cmd)
265
+ q := url.Values{replica.RequestKey: []string{requestId}}.Encode()
266
+ url := url.URL{Scheme: "http", Host: host, Path: path, RawQuery: q}
267
+ return http.NewRequest(http.MethodPost, url.String(), body)
268
+ }
269
+
270
+ func (c *replicationClient) do(timeout time.Duration, req *http.Request, body []byte, resp interface{}) (err error) {
271
+ ctx, cancel := context.WithTimeout(req.Context(), timeout)
272
+ defer cancel()
273
+ try := func(ctx context.Context) (bool, error) {
274
+ if body != nil {
275
+ req.Body = io.NopCloser(bytes.NewReader(body))
276
+ }
277
+ res, err := c.client.Do(req)
278
+ if err != nil {
279
+ return ctx.Err() == nil, fmt.Errorf("connect: %w", err)
280
+ }
281
+ defer res.Body.Close()
282
+
283
+ if code := res.StatusCode; code != http.StatusOK {
284
+ b, _ := io.ReadAll(res.Body)
285
+ return shouldRetry(code), fmt.Errorf("status code: %v, error: %s", code, b)
286
+ }
287
+ if err := json.NewDecoder(res.Body).Decode(resp); err != nil {
288
+ return false, fmt.Errorf("decode response: %w", err)
289
+ }
290
+ return false, nil
291
+ }
292
+ return c.retry(ctx, 9, try)
293
+ }
294
+
295
+ func (c *replicationClient) doCustomUnmarshal(timeout time.Duration,
296
+ req *http.Request, body []byte, decode func([]byte) error,
297
+ ) (err error) {
298
+ return (*retryClient)(c).doWithCustomMarshaller(timeout, req, body, decode)
299
+ }
300
+
301
+ // backOff return a new random duration in the interval [d, 3d].
302
+ // It implements truncated exponential back-off with introduced jitter.
303
+ func backOff(d time.Duration) time.Duration {
304
+ return time.Duration(float64(d.Nanoseconds()*2) * (0.5 + rand.Float64()))
305
+ }
306
+
307
+ func shouldRetry(code int) bool {
308
+ return code == http.StatusInternalServerError ||
309
+ code == http.StatusTooManyRequests ||
310
+ code == http.StatusServiceUnavailable
311
+ }
adapters/clients/replication_test.go ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package clients
13
+
14
+ import (
15
+ "context"
16
+ "encoding/json"
17
+ "net/http"
18
+ "net/http/httptest"
19
+ "testing"
20
+ "time"
21
+
22
+ "github.com/go-openapi/strfmt"
23
+ "github.com/stretchr/testify/assert"
24
+ "github.com/stretchr/testify/require"
25
+ "github.com/weaviate/weaviate/entities/additional"
26
+ "github.com/weaviate/weaviate/entities/models"
27
+ "github.com/weaviate/weaviate/entities/storobj"
28
+ "github.com/weaviate/weaviate/usecases/objects"
29
+ "github.com/weaviate/weaviate/usecases/replica"
30
+ )
31
+
32
+ const (
33
+ RequestError = "RIDNotFound"
34
+ RequestSuccess = "RIDSuccess"
35
+ RequestInternalError = "RIDInternal"
36
+ RequestMalFormedResponse = "RIDMalFormed"
37
+ )
38
+
39
+ const (
40
+ UUID1 = strfmt.UUID("73f2eb5f-5abf-447a-81ca-74b1dd168241")
41
+ UUID2 = strfmt.UUID("73f2eb5f-5abf-447a-81ca-74b1dd168242")
42
+ )
43
+
44
+ type fakeServer struct {
45
+ method string
46
+ path string
47
+ RequestError replica.SimpleResponse
48
+ RequestSuccess replica.SimpleResponse
49
+ host string
50
+ }
51
+
52
+ func newFakeReplicationServer(t *testing.T, method, path string) *fakeServer {
53
+ return &fakeServer{
54
+ method: method,
55
+ path: path,
56
+ RequestError: replica.SimpleResponse{Errors: []replica.Error{{Msg: "error"}}},
57
+ RequestSuccess: replica.SimpleResponse{},
58
+ }
59
+ }
60
+
61
+ func (f *fakeServer) server(t *testing.T) *httptest.Server {
62
+ handler := func(w http.ResponseWriter, r *http.Request) {
63
+ if r.Method != f.method {
64
+ t.Errorf("method want %s got %s", f.method, r.Method)
65
+ w.WriteHeader(http.StatusBadRequest)
66
+ return
67
+ }
68
+ if f.path != r.URL.Path {
69
+ t.Errorf("path want %s got %s", f.path, r.URL.Path)
70
+ w.WriteHeader(http.StatusBadRequest)
71
+ return
72
+ }
73
+ requestID := r.URL.Query().Get(replica.RequestKey)
74
+ switch requestID {
75
+ case RequestInternalError:
76
+ w.WriteHeader(http.StatusInternalServerError)
77
+ case RequestError:
78
+ bytes, _ := json.Marshal(&f.RequestError)
79
+ w.Write(bytes)
80
+ case RequestSuccess:
81
+ bytes, _ := json.Marshal(&replica.SimpleResponse{})
82
+ w.Write(bytes)
83
+ case RequestMalFormedResponse:
84
+ w.Write([]byte(`mal formed`))
85
+ }
86
+ }
87
+ serv := httptest.NewServer(http.HandlerFunc(handler))
88
+ f.host = serv.URL[7:]
89
+ return serv
90
+ }
91
+
92
+ func anyObject(uuid strfmt.UUID) models.Object {
93
+ return models.Object{
94
+ Class: "C1",
95
+ CreationTimeUnix: 900000000001,
96
+ LastUpdateTimeUnix: 900000000002,
97
+ ID: uuid,
98
+ Properties: map[string]interface{}{
99
+ "stringProp": "string",
100
+ "textProp": "text",
101
+ "datePropArray": []string{"1980-01-01T00:00:00+02:00"},
102
+ },
103
+ }
104
+ }
105
+
106
+ func TestReplicationPutObject(t *testing.T) {
107
+ t.Parallel()
108
+
109
+ ctx := context.Background()
110
+ f := newFakeReplicationServer(t, http.MethodPost, "/replicas/indices/C1/shards/S1/objects")
111
+ ts := f.server(t)
112
+ defer ts.Close()
113
+
114
+ client := newReplicationClient(ts.Client())
115
+ t.Run("EncodeRequest", func(t *testing.T) {
116
+ obj := &storobj.Object{}
117
+ _, err := client.PutObject(ctx, "Node1", "C1", "S1", "RID", obj)
118
+ assert.NotNil(t, err)
119
+ assert.Contains(t, err.Error(), "encode")
120
+ })
121
+
122
+ obj := &storobj.Object{MarshallerVersion: 1, Object: anyObject(UUID1)}
123
+ t.Run("ConnectionError", func(t *testing.T) {
124
+ _, err := client.PutObject(ctx, "", "C1", "S1", "", obj)
125
+ assert.NotNil(t, err)
126
+ assert.Contains(t, err.Error(), "connect")
127
+ })
128
+
129
+ t.Run("Error", func(t *testing.T) {
130
+ resp, err := client.PutObject(ctx, f.host, "C1", "S1", RequestError, obj)
131
+ assert.Nil(t, err)
132
+ assert.Equal(t, replica.SimpleResponse{Errors: f.RequestError.Errors}, resp)
133
+ })
134
+
135
+ t.Run("DecodeResponse", func(t *testing.T) {
136
+ _, err := client.PutObject(ctx, f.host, "C1", "S1", RequestMalFormedResponse, obj)
137
+ assert.NotNil(t, err)
138
+ assert.Contains(t, err.Error(), "decode response")
139
+ })
140
+
141
+ t.Run("ServerInternalError", func(t *testing.T) {
142
+ _, err := client.PutObject(ctx, f.host, "C1", "S1", RequestInternalError, obj)
143
+ assert.NotNil(t, err)
144
+ assert.Contains(t, err.Error(), "status code")
145
+ })
146
+ }
147
+
148
+ func TestReplicationDeleteObject(t *testing.T) {
149
+ t.Parallel()
150
+
151
+ ctx := context.Background()
152
+ uuid := UUID1
153
+ path := "/replicas/indices/C1/shards/S1/objects/" + uuid.String()
154
+ fs := newFakeReplicationServer(t, http.MethodDelete, path)
155
+ ts := fs.server(t)
156
+ defer ts.Close()
157
+
158
+ client := newReplicationClient(ts.Client())
159
+ t.Run("ConnectionError", func(t *testing.T) {
160
+ _, err := client.DeleteObject(ctx, "", "C1", "S1", "", uuid)
161
+ assert.NotNil(t, err)
162
+ assert.Contains(t, err.Error(), "connect")
163
+ })
164
+
165
+ t.Run("Error", func(t *testing.T) {
166
+ resp, err := client.DeleteObject(ctx, fs.host, "C1", "S1", RequestError, uuid)
167
+ assert.Nil(t, err)
168
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
169
+ })
170
+
171
+ t.Run("DecodeResponse", func(t *testing.T) {
172
+ _, err := client.DeleteObject(ctx, fs.host, "C1", "S1", RequestMalFormedResponse, uuid)
173
+ assert.NotNil(t, err)
174
+ assert.Contains(t, err.Error(), "decode response")
175
+ })
176
+
177
+ t.Run("ServerInternalError", func(t *testing.T) {
178
+ _, err := client.DeleteObject(ctx, fs.host, "C1", "S1", RequestInternalError, uuid)
179
+ assert.NotNil(t, err)
180
+ assert.Contains(t, err.Error(), "status code")
181
+ })
182
+ }
183
+
184
+ func TestReplicationPutObjects(t *testing.T) {
185
+ t.Parallel()
186
+ ctx := context.Background()
187
+ fs := newFakeReplicationServer(t, http.MethodPost, "/replicas/indices/C1/shards/S1/objects")
188
+ fs.RequestError.Errors = append(fs.RequestError.Errors, replica.Error{Msg: "error2"})
189
+ ts := fs.server(t)
190
+ defer ts.Close()
191
+
192
+ client := newReplicationClient(ts.Client())
193
+ t.Run("EncodeRequest", func(t *testing.T) {
194
+ objs := []*storobj.Object{{}}
195
+ _, err := client.PutObjects(ctx, "Node1", "C1", "S1", "RID", objs)
196
+ assert.NotNil(t, err)
197
+ assert.Contains(t, err.Error(), "encode")
198
+ })
199
+
200
+ objects := []*storobj.Object{
201
+ {MarshallerVersion: 1, Object: anyObject(UUID1)},
202
+ {MarshallerVersion: 1, Object: anyObject(UUID2)},
203
+ }
204
+
205
+ t.Run("ConnectionError", func(t *testing.T) {
206
+ _, err := client.PutObjects(ctx, "", "C1", "S1", "", objects)
207
+ assert.NotNil(t, err)
208
+ assert.Contains(t, err.Error(), "connect")
209
+ })
210
+
211
+ t.Run("Error", func(t *testing.T) {
212
+ resp, err := client.PutObjects(ctx, fs.host, "C1", "S1", RequestError, objects)
213
+ assert.Nil(t, err)
214
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
215
+ })
216
+
217
+ t.Run("DecodeResponse", func(t *testing.T) {
218
+ _, err := client.PutObjects(ctx, fs.host, "C1", "S1", RequestMalFormedResponse, objects)
219
+ assert.NotNil(t, err)
220
+ assert.Contains(t, err.Error(), "decode response")
221
+ })
222
+
223
+ t.Run("ServerInternalError", func(t *testing.T) {
224
+ _, err := client.PutObjects(ctx, fs.host, "C1", "S1", RequestInternalError, objects)
225
+ assert.NotNil(t, err)
226
+ assert.Contains(t, err.Error(), "status code")
227
+ })
228
+ }
229
+
230
+ func TestReplicationMergeObject(t *testing.T) {
231
+ t.Parallel()
232
+ ctx := context.Background()
233
+ uuid := UUID1
234
+ f := newFakeReplicationServer(t, http.MethodPatch, "/replicas/indices/C1/shards/S1/objects/"+uuid.String())
235
+ ts := f.server(t)
236
+ defer ts.Close()
237
+
238
+ client := newReplicationClient(ts.Client())
239
+ doc := &objects.MergeDocument{ID: uuid}
240
+ t.Run("ConnectionError", func(t *testing.T) {
241
+ _, err := client.MergeObject(ctx, "", "C1", "S1", "", doc)
242
+ assert.NotNil(t, err)
243
+ assert.Contains(t, err.Error(), "connect")
244
+ })
245
+
246
+ t.Run("Error", func(t *testing.T) {
247
+ resp, err := client.MergeObject(ctx, f.host, "C1", "S1", RequestError, doc)
248
+ assert.Nil(t, err)
249
+ assert.Equal(t, replica.SimpleResponse{Errors: f.RequestError.Errors}, resp)
250
+ })
251
+
252
+ t.Run("DecodeResponse", func(t *testing.T) {
253
+ _, err := client.MergeObject(ctx, f.host, "C1", "S1", RequestMalFormedResponse, doc)
254
+ assert.NotNil(t, err)
255
+ assert.Contains(t, err.Error(), "decode response")
256
+ })
257
+
258
+ t.Run("ServerInternalError", func(t *testing.T) {
259
+ _, err := client.MergeObject(ctx, f.host, "C1", "S1", RequestInternalError, doc)
260
+ assert.NotNil(t, err)
261
+ assert.Contains(t, err.Error(), "status code")
262
+ })
263
+ }
264
+
265
+ func TestReplicationAddReferences(t *testing.T) {
266
+ t.Parallel()
267
+
268
+ ctx := context.Background()
269
+ fs := newFakeReplicationServer(t, http.MethodPost, "/replicas/indices/C1/shards/S1/objects/references")
270
+ fs.RequestError.Errors = append(fs.RequestError.Errors, replica.Error{Msg: "error2"})
271
+ ts := fs.server(t)
272
+ defer ts.Close()
273
+
274
+ client := newReplicationClient(ts.Client())
275
+ refs := []objects.BatchReference{{OriginalIndex: 1}, {OriginalIndex: 2}}
276
+ t.Run("ConnectionError", func(t *testing.T) {
277
+ _, err := client.AddReferences(ctx, "", "C1", "S1", "", refs)
278
+ assert.NotNil(t, err)
279
+ assert.Contains(t, err.Error(), "connect")
280
+ })
281
+
282
+ t.Run("Error", func(t *testing.T) {
283
+ resp, err := client.AddReferences(ctx, fs.host, "C1", "S1", RequestError, refs)
284
+ assert.Nil(t, err)
285
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
286
+ })
287
+
288
+ t.Run("DecodeResponse", func(t *testing.T) {
289
+ _, err := client.AddReferences(ctx, fs.host, "C1", "S1", RequestMalFormedResponse, refs)
290
+ assert.NotNil(t, err)
291
+ assert.Contains(t, err.Error(), "decode response")
292
+ })
293
+
294
+ t.Run("ServerInternalError", func(t *testing.T) {
295
+ _, err := client.AddReferences(ctx, fs.host, "C1", "S1", RequestInternalError, refs)
296
+ assert.NotNil(t, err)
297
+ assert.Contains(t, err.Error(), "status code")
298
+ })
299
+ }
300
+
301
+ func TestReplicationDeleteObjects(t *testing.T) {
302
+ t.Parallel()
303
+
304
+ ctx := context.Background()
305
+ fs := newFakeReplicationServer(t, http.MethodDelete, "/replicas/indices/C1/shards/S1/objects")
306
+ fs.RequestError.Errors = append(fs.RequestError.Errors, replica.Error{Msg: "error2"})
307
+ ts := fs.server(t)
308
+ defer ts.Close()
309
+ client := newReplicationClient(ts.Client())
310
+
311
+ uuids := []strfmt.UUID{strfmt.UUID("1"), strfmt.UUID("2")}
312
+ t.Run("ConnectionError", func(t *testing.T) {
313
+ _, err := client.DeleteObjects(ctx, "", "C1", "S1", "", uuids, false)
314
+ assert.NotNil(t, err)
315
+ assert.Contains(t, err.Error(), "connect")
316
+ })
317
+
318
+ t.Run("Error", func(t *testing.T) {
319
+ resp, err := client.DeleteObjects(ctx, fs.host, "C1", "S1", RequestError, uuids, false)
320
+ assert.Nil(t, err)
321
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
322
+ })
323
+
324
+ t.Run("DecodeResponse", func(t *testing.T) {
325
+ _, err := client.DeleteObjects(ctx, fs.host, "C1", "S1", RequestMalFormedResponse, uuids, false)
326
+ assert.NotNil(t, err)
327
+ assert.Contains(t, err.Error(), "decode response")
328
+ })
329
+
330
+ t.Run("ServerInternalError", func(t *testing.T) {
331
+ _, err := client.DeleteObjects(ctx, fs.host, "C1", "S1", RequestInternalError, uuids, false)
332
+ assert.NotNil(t, err)
333
+ assert.Contains(t, err.Error(), "status code")
334
+ })
335
+ }
336
+
337
+ func TestReplicationAbort(t *testing.T) {
338
+ t.Parallel()
339
+
340
+ ctx := context.Background()
341
+ path := "/replicas/indices/C1/shards/S1:abort"
342
+ fs := newFakeReplicationServer(t, http.MethodPost, path)
343
+ ts := fs.server(t)
344
+ defer ts.Close()
345
+ client := newReplicationClient(ts.Client())
346
+
347
+ t.Run("ConnectionError", func(t *testing.T) {
348
+ client := newReplicationClient(ts.Client())
349
+ client.maxBackOff = client.timeoutUnit * 20
350
+ _, err := client.Abort(ctx, "", "C1", "S1", "")
351
+ assert.NotNil(t, err)
352
+ assert.Contains(t, err.Error(), "connect")
353
+ })
354
+
355
+ t.Run("Error", func(t *testing.T) {
356
+ resp, err := client.Abort(ctx, fs.host, "C1", "S1", RequestError)
357
+ assert.Nil(t, err)
358
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
359
+ })
360
+
361
+ t.Run("DecodeResponse", func(t *testing.T) {
362
+ _, err := client.Abort(ctx, fs.host, "C1", "S1", RequestMalFormedResponse)
363
+ assert.NotNil(t, err)
364
+ assert.Contains(t, err.Error(), "decode response")
365
+ })
366
+ client.timeoutUnit = client.maxBackOff * 3
367
+ t.Run("ServerInternalError", func(t *testing.T) {
368
+ _, err := client.Abort(ctx, fs.host, "C1", "S1", RequestInternalError)
369
+ assert.NotNil(t, err)
370
+ assert.Contains(t, err.Error(), "status code")
371
+ })
372
+ }
373
+
374
+ func TestReplicationCommit(t *testing.T) {
375
+ t.Parallel()
376
+
377
+ ctx := context.Background()
378
+ path := "/replicas/indices/C1/shards/S1:commit"
379
+ fs := newFakeReplicationServer(t, http.MethodPost, path)
380
+ ts := fs.server(t)
381
+ defer ts.Close()
382
+ resp := replica.SimpleResponse{}
383
+ client := newReplicationClient(ts.Client())
384
+
385
+ t.Run("ConnectionError", func(t *testing.T) {
386
+ err := client.Commit(ctx, "", "C1", "S1", "", &resp)
387
+ assert.NotNil(t, err)
388
+ assert.Contains(t, err.Error(), "connect")
389
+ })
390
+
391
+ t.Run("Error", func(t *testing.T) {
392
+ err := client.Commit(ctx, fs.host, "C1", "S1", RequestError, &resp)
393
+ assert.Nil(t, err)
394
+ assert.Equal(t, replica.SimpleResponse{Errors: fs.RequestError.Errors}, resp)
395
+ })
396
+
397
+ t.Run("DecodeResponse", func(t *testing.T) {
398
+ err := client.Commit(ctx, fs.host, "C1", "S1", RequestMalFormedResponse, &resp)
399
+ assert.NotNil(t, err)
400
+ assert.Contains(t, err.Error(), "decode response")
401
+ })
402
+
403
+ t.Run("ServerInternalError", func(t *testing.T) {
404
+ err := client.Commit(ctx, fs.host, "C1", "S1", RequestInternalError, &resp)
405
+ assert.NotNil(t, err)
406
+ assert.Contains(t, err.Error(), "status code")
407
+ })
408
+ }
409
+
410
+ func TestReplicationFetchObject(t *testing.T) {
411
+ t.Parallel()
412
+
413
+ expected := objects.Replica{
414
+ ID: UUID1,
415
+ Object: &storobj.Object{
416
+ MarshallerVersion: 1,
417
+ Object: models.Object{
418
+ ID: UUID1,
419
+ Properties: map[string]interface{}{
420
+ "stringProp": "abc",
421
+ },
422
+ },
423
+ Vector: []float32{1, 2, 3, 4, 5},
424
+ VectorLen: 5,
425
+ },
426
+ }
427
+
428
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
429
+ b, _ := expected.MarshalBinary()
430
+ w.Write(b)
431
+ }))
432
+
433
+ c := newReplicationClient(server.Client())
434
+ resp, err := c.FetchObject(context.Background(), server.URL[7:],
435
+ "C1", "S1", expected.ID, nil, additional.Properties{})
436
+ require.Nil(t, err)
437
+ assert.Equal(t, expected.ID, resp.ID)
438
+ assert.Equal(t, expected.Deleted, resp.Deleted)
439
+ assert.EqualValues(t, expected.Object, resp.Object)
440
+ }
441
+
442
+ func TestReplicationFetchObjects(t *testing.T) {
443
+ t.Parallel()
444
+ expected := objects.Replicas{
445
+ {
446
+ ID: UUID1,
447
+ Object: &storobj.Object{
448
+ MarshallerVersion: 1,
449
+ Object: models.Object{
450
+ ID: UUID1,
451
+ Properties: map[string]interface{}{
452
+ "stringProp": "abc",
453
+ },
454
+ },
455
+ Vector: []float32{1, 2, 3, 4, 5},
456
+ VectorLen: 5,
457
+ },
458
+ },
459
+ {
460
+ ID: UUID2,
461
+ Object: &storobj.Object{
462
+ MarshallerVersion: 1,
463
+ Object: models.Object{
464
+ ID: UUID2,
465
+ Properties: map[string]interface{}{
466
+ "floatProp": float64(123),
467
+ },
468
+ },
469
+ Vector: []float32{10, 20, 30, 40, 50},
470
+ VectorLen: 5,
471
+ },
472
+ },
473
+ }
474
+
475
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
476
+ b, _ := expected.MarshalBinary()
477
+ w.Write(b)
478
+ }))
479
+
480
+ c := newReplicationClient(server.Client())
481
+ resp, err := c.FetchObjects(context.Background(), server.URL[7:], "C1", "S1", []strfmt.UUID{expected[0].ID})
482
+ require.Nil(t, err)
483
+ require.Len(t, resp, 2)
484
+ assert.Equal(t, expected[0].ID, resp[0].ID)
485
+ assert.Equal(t, expected[0].Deleted, resp[0].Deleted)
486
+ assert.EqualValues(t, expected[0].Object, resp[0].Object)
487
+ assert.Equal(t, expected[1].ID, resp[1].ID)
488
+ assert.Equal(t, expected[1].Deleted, resp[1].Deleted)
489
+ assert.EqualValues(t, expected[1].Object, resp[1].Object)
490
+ }
491
+
492
+ func TestReplicationDigestObjects(t *testing.T) {
493
+ t.Parallel()
494
+
495
+ now := time.Now()
496
+ expected := []replica.RepairResponse{
497
+ {
498
+ ID: UUID1.String(),
499
+ UpdateTime: now.UnixMilli(),
500
+ Version: 1,
501
+ },
502
+ {
503
+ ID: UUID2.String(),
504
+ UpdateTime: now.UnixMilli(),
505
+ Version: 1,
506
+ },
507
+ }
508
+
509
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
510
+ b, _ := json.Marshal(expected)
511
+ w.Write(b)
512
+ }))
513
+
514
+ c := newReplicationClient(server.Client())
515
+ resp, err := c.DigestObjects(context.Background(), server.URL[7:], "C1", "S1", []strfmt.UUID{
516
+ strfmt.UUID(expected[0].ID),
517
+ strfmt.UUID(expected[1].ID),
518
+ })
519
+ require.Nil(t, err)
520
+ require.Len(t, resp, 2)
521
+ assert.Equal(t, expected[0].ID, resp[0].ID)
522
+ assert.Equal(t, expected[0].Deleted, resp[0].Deleted)
523
+ assert.Equal(t, expected[0].UpdateTime, resp[0].UpdateTime)
524
+ assert.Equal(t, expected[0].Version, resp[0].Version)
525
+ assert.Equal(t, expected[1].ID, resp[1].ID)
526
+ assert.Equal(t, expected[1].Deleted, resp[1].Deleted)
527
+ assert.Equal(t, expected[1].UpdateTime, resp[1].UpdateTime)
528
+ assert.Equal(t, expected[1].Version, resp[1].Version)
529
+ }
530
+
531
+ func TestReplicationOverwriteObjects(t *testing.T) {
532
+ t.Parallel()
533
+
534
+ now := time.Now()
535
+ input := []*objects.VObject{
536
+ {
537
+ LatestObject: &models.Object{
538
+ ID: UUID1,
539
+ Class: "C1",
540
+ CreationTimeUnix: now.UnixMilli(),
541
+ LastUpdateTimeUnix: now.Add(time.Hour).UnixMilli(),
542
+ Properties: map[string]interface{}{
543
+ "stringProp": "abc",
544
+ },
545
+ Vector: []float32{1, 2, 3, 4, 5},
546
+ },
547
+ StaleUpdateTime: now.UnixMilli(),
548
+ Version: 0,
549
+ },
550
+ }
551
+ expected := []replica.RepairResponse{
552
+ {
553
+ ID: UUID1.String(),
554
+ Version: 1,
555
+ UpdateTime: now.Add(time.Hour).UnixMilli(),
556
+ },
557
+ }
558
+
559
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
560
+ b, _ := json.Marshal(expected)
561
+ w.Write(b)
562
+ }))
563
+
564
+ c := newReplicationClient(server.Client())
565
+ resp, err := c.OverwriteObjects(context.Background(), server.URL[7:], "C1", "S1", input)
566
+ require.Nil(t, err)
567
+ require.Len(t, resp, 1)
568
+ assert.Equal(t, expected[0].ID, resp[0].ID)
569
+ assert.Equal(t, expected[0].Version, resp[0].Version)
570
+ assert.Equal(t, expected[0].UpdateTime, resp[0].UpdateTime)
571
+ }
572
+
573
+ func TestExpBackOff(t *testing.T) {
574
+ N := 200
575
+ av := time.Duration(0)
576
+ delay := time.Nanosecond * 20
577
+ for i := 0; i < N; i++ {
578
+ av += backOff(delay)
579
+ }
580
+ av /= time.Duration(N)
581
+ if av < time.Nanosecond*30 || av > time.Nanosecond*50 {
582
+ t.Errorf("average time got %v", av)
583
+ }
584
+ }
585
+
586
+ func newReplicationClient(httpClient *http.Client) *replicationClient {
587
+ c := NewReplicationClient(httpClient).(*replicationClient)
588
+ c.minBackOff = time.Millisecond * 1
589
+ c.maxBackOff = time.Millisecond * 8
590
+ c.timeoutUnit = time.Millisecond * 20
591
+ return c
592
+ }
adapters/handlers/graphql/common/fetch/filter.go ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package fetch
13
+
14
+ import (
15
+ "fmt"
16
+
17
+ "github.com/tailor-inc/graphql"
18
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/descriptions"
19
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/local/common_filters"
20
+ )
21
+
22
+ // FilterBuilder can build where filters for both local and
23
+ type FilterBuilder struct {
24
+ prefix string
25
+ }
26
+
27
+ // NewFilterBuilder with kind and prefix
28
+ func NewFilterBuilder(prefix string) *FilterBuilder {
29
+ return &FilterBuilder{
30
+ prefix: prefix,
31
+ }
32
+ }
33
+
34
+ // Build a where filter ArgumentConfig
35
+ func (b *FilterBuilder) Build() *graphql.ArgumentConfig {
36
+ return &graphql.ArgumentConfig{
37
+ Description: descriptions.FetchWhereFilterFields,
38
+ Type: graphql.NewNonNull(graphql.NewInputObject(
39
+ graphql.InputObjectConfig{
40
+ Name: fmt.Sprintf("%sFetchObjectWhereInpObj", b.prefix),
41
+ Fields: b.fields(),
42
+ Description: descriptions.FetchWhereFilterFieldsInpObj,
43
+ },
44
+ )),
45
+ }
46
+ }
47
+
48
+ func (b *FilterBuilder) fields() graphql.InputObjectConfigFieldMap {
49
+ return graphql.InputObjectConfigFieldMap{
50
+ "class": &graphql.InputObjectFieldConfig{
51
+ Type: graphql.NewNonNull(b.class()),
52
+ Description: descriptions.WhereClass,
53
+ },
54
+ "properties": &graphql.InputObjectFieldConfig{
55
+ Type: graphql.NewNonNull(graphql.NewList(b.properties())),
56
+ Description: descriptions.WhereProperties,
57
+ },
58
+ "first": &graphql.InputObjectFieldConfig{
59
+ Type: graphql.Int,
60
+ Description: descriptions.First,
61
+ },
62
+ }
63
+ }
64
+
65
+ func (b *FilterBuilder) properties() *graphql.InputObject {
66
+ elements := common_filters.BuildNew(fmt.Sprintf("%sFetchObject", b.prefix))
67
+
68
+ // Remove path and operands fields as they are not required here
69
+ delete(elements, "path")
70
+ delete(elements, "operands")
71
+
72
+ // make operator required
73
+ elements["operator"].Type = graphql.NewNonNull(elements["operator"].Type)
74
+
75
+ elements["certainty"] = &graphql.InputObjectFieldConfig{
76
+ Type: graphql.NewNonNull(graphql.Float),
77
+ Description: descriptions.WhereCertainty,
78
+ }
79
+ elements["name"] = &graphql.InputObjectFieldConfig{
80
+ Type: graphql.NewNonNull(graphql.String),
81
+ Description: descriptions.WhereName,
82
+ }
83
+ elements["keywords"] = &graphql.InputObjectFieldConfig{
84
+ Type: graphql.NewList(b.keywordInpObj(fmt.Sprintf("%sFetchObjectWhereProperties", b.prefix))),
85
+ Description: descriptions.WhereKeywords,
86
+ }
87
+
88
+ networkFetchWhereInpObjPropertiesObj := graphql.NewInputObject(
89
+ graphql.InputObjectConfig{
90
+ Name: fmt.Sprintf("%sFetchObjectWhereInpObjProperties", b.prefix),
91
+ Fields: elements,
92
+ Description: descriptions.WhereProperties,
93
+ },
94
+ )
95
+
96
+ return networkFetchWhereInpObjPropertiesObj
97
+ }
98
+
99
+ func (b *FilterBuilder) keywordInpObj(prefix string) *graphql.InputObject {
100
+ return graphql.NewInputObject(
101
+ graphql.InputObjectConfig{
102
+ Name: fmt.Sprintf("%sKeywordsInpObj", prefix),
103
+ Fields: graphql.InputObjectConfigFieldMap{
104
+ "value": &graphql.InputObjectFieldConfig{
105
+ Type: graphql.String,
106
+ Description: descriptions.WhereKeywordsValue,
107
+ },
108
+ "weight": &graphql.InputObjectFieldConfig{
109
+ Type: graphql.Float,
110
+ Description: descriptions.WhereKeywordsWeight,
111
+ },
112
+ },
113
+ Description: descriptions.WhereKeywordsInpObj,
114
+ },
115
+ )
116
+ }
117
+
118
+ func (b *FilterBuilder) class() *graphql.InputObject {
119
+ filterClassElements := graphql.InputObjectConfigFieldMap{
120
+ "name": &graphql.InputObjectFieldConfig{
121
+ Type: graphql.String,
122
+ Description: descriptions.WhereName,
123
+ },
124
+ "certainty": &graphql.InputObjectFieldConfig{
125
+ Type: graphql.Float,
126
+ Description: descriptions.WhereCertainty,
127
+ },
128
+ "keywords": &graphql.InputObjectFieldConfig{
129
+ Type: graphql.NewList(b.keywordInpObj(fmt.Sprintf("%sFetchObjectWhereClass", b.prefix))),
130
+ Description: descriptions.WhereKeywords,
131
+ },
132
+ "first": &graphql.InputObjectFieldConfig{
133
+ Type: graphql.Int,
134
+ Description: descriptions.First,
135
+ },
136
+ }
137
+
138
+ networkFetchWhereInpObjClassInpObj := graphql.NewInputObject(
139
+ graphql.InputObjectConfig{
140
+ Name: fmt.Sprintf("%sFetchObjectWhereInpObjClassInpObj", b.prefix),
141
+ Fields: filterClassElements,
142
+ Description: descriptions.WhereClass,
143
+ },
144
+ )
145
+ return networkFetchWhereInpObjClassInpObj
146
+ }
adapters/handlers/graphql/common/json_number.go ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package common
13
+
14
+ import (
15
+ "encoding/json"
16
+ "fmt"
17
+
18
+ "github.com/tailor-inc/graphql"
19
+ "github.com/weaviate/weaviate/entities/aggregation"
20
+ )
21
+
22
+ // JSONNumberResolver turns json.Number types into number types usable by graphQL
23
+ func JSONNumberResolver(p graphql.ResolveParams) (interface{}, error) {
24
+ switch v := p.Source.(type) {
25
+ case map[string]interface{}:
26
+ field, ok := v[p.Info.FieldName]
27
+ if !ok {
28
+ return nil, nil
29
+ }
30
+
31
+ switch n := field.(type) {
32
+ case json.Number:
33
+ return n.Float64()
34
+ case int64:
35
+ return float64(n), nil
36
+ case int:
37
+ return float64(n), nil
38
+ case float64:
39
+ return n, nil
40
+ }
41
+
42
+ return nil, fmt.Errorf("unknown number type for %t", field)
43
+
44
+ case map[string]float64:
45
+ return v[p.Info.FieldName], nil
46
+
47
+ case aggregation.Text:
48
+ switch p.Info.FieldName {
49
+ // case "count":
50
+ // // TODO gh-974: Support Count in text aggregations
51
+ // return nil, nil
52
+
53
+ default:
54
+ return nil, fmt.Errorf("fieldName '%s' does not match text aggregation", p.Info.FieldName)
55
+ }
56
+
57
+ default:
58
+ return nil, fmt.Errorf("json number resolver: unusable type %T", p.Source)
59
+ }
60
+ }
adapters/handlers/graphql/common/json_number_test.go ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package common
13
+
14
+ import (
15
+ "encoding/json"
16
+ "fmt"
17
+ "testing"
18
+
19
+ "github.com/stretchr/testify/assert"
20
+ "github.com/tailor-inc/graphql"
21
+ )
22
+
23
+ type testCase struct {
24
+ input interface{}
25
+ expectedOutput float64
26
+ }
27
+
28
+ func TestJSONNumberResolver(t *testing.T) {
29
+ tests := []testCase{
30
+ {
31
+ input: json.Number("10"),
32
+ expectedOutput: 10.0,
33
+ },
34
+ {
35
+ input: int(10),
36
+ expectedOutput: 10.0,
37
+ },
38
+ {
39
+ input: int64(10),
40
+ expectedOutput: 10.0,
41
+ },
42
+ {
43
+ input: float64(10),
44
+ expectedOutput: 10.0,
45
+ },
46
+ }
47
+
48
+ for _, test := range tests {
49
+ name := fmt.Sprintf("%#v -> %#v", test.input, test.expectedOutput)
50
+ t.Run(name, func(t *testing.T) {
51
+ result, err := JSONNumberResolver(resolveParams(test.input))
52
+ assert.Nil(t, err, "should not error")
53
+ assert.Equal(t, test.expectedOutput, result)
54
+ })
55
+ }
56
+ }
57
+
58
+ func resolveParams(input interface{}) graphql.ResolveParams {
59
+ return graphql.ResolveParams{
60
+ Source: map[string]interface{}{
61
+ "myField": input,
62
+ },
63
+ Info: graphql.ResolveInfo{FieldName: "myField"},
64
+ }
65
+ }
66
+
67
+ func TestNumberFieldNotPresent(t *testing.T) {
68
+ // shouldn't return anything, but also not error. This can otherwise lead to
69
+ // odd behavior when no entries are present, yet we asked for int props and
70
+ // type, see https://github.com/weaviate/weaviate/issues/775
71
+ params := graphql.ResolveParams{
72
+ Source: map[string]interface{}{},
73
+ Info: graphql.ResolveInfo{FieldName: "myField"},
74
+ }
75
+
76
+ result, err := JSONNumberResolver(params)
77
+ assert.Nil(t, err)
78
+ assert.Equal(t, nil, result)
79
+ }
adapters/handlers/graphql/descriptions/aggregate.go ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // AGGREGATE
16
+ const (
17
+ AggregateProperty = "Aggregate this property"
18
+ AggregateObjects = "Aggregate Objects on a local Weaviate"
19
+ )
20
+
21
+ const GroupBy = "Specify which properties to group by"
22
+
23
+ const (
24
+ AggregatePropertyObject = "An object containing Aggregation information about this property"
25
+ )
26
+
27
+ const AggregateObjectsObj = "An object allowing Aggregation of %ss on a local Weaviate"
28
+
29
+ const (
30
+ AggregateMean = "Aggregate on the mean of numeric property values"
31
+ AggregateSum = "Aggregate on the sum of numeric property values"
32
+ AggregateMedian = "Aggregate on the median of numeric property values"
33
+ AggregateMode = "Aggregate on the mode of numeric property values"
34
+ AggregateMin = "Aggregate on the minimum of numeric property values"
35
+ AggregateMax = "Aggregate on the maximum of numeric property values"
36
+ AggregateCount = "Aggregate on the total amount of found property values"
37
+ AggregateGroupedBy = "Indicates the group of returned data"
38
+ )
39
+
40
+ const AggregateNumericObj = "An object containing the %s of numeric properties"
41
+
42
+ const AggregateCountObj = "An object containing countable properties"
43
+
44
+ const AggregateGroupedByObj = "An object containing the path and value of the grouped property"
45
+
46
+ const (
47
+ AggregateGroupedByGroupedByPath = "The path of the grouped property"
48
+ AggregateGroupedByGroupedByValue = "The value of the grouped property"
49
+ )
50
+
51
+ // NETWORK
52
+ const NetworkAggregateWeaviateObj = "An object containing Get Objects fields for network Weaviate instance: "
53
+
54
+ const NetworkAggregate = "Perform Aggregation of Objects"
55
+
56
+ const (
57
+ NetworkAggregateObj = "An object allowing Aggregation of Objects"
58
+ NetworkAggregatePropertyObject = "An object containing Aggregation information about this property"
59
+ )
60
+
61
+ const NetworkAggregateThingsActionsObj = "An object allowing Aggregation of %ss on a network Weaviate"
62
+
63
+ const (
64
+ NetworkAggregateMean = "Aggregate on the mean of numeric property values"
65
+ NetworkAggregateSum = "Aggregate on the sum of numeric property values"
66
+ NetworkAggregateMedian = "Aggregate on the median of numeric property values"
67
+ NetworkAggregateMode = "Aggregate on the mode of numeric property values"
68
+ NetworkAggregateMin = "Aggregate on the minimum of numeric property values"
69
+ NetworkAggregateMax = "Aggregate on the maximum of numeric property values"
70
+ NetworkAggregateCount = "Aggregate on the total amount of found property values"
71
+ NetworkAggregateGroupedBy = "Indicates the group of returned data"
72
+ )
73
+
74
+ const NetworkAggregateNumericObj = "An object containing the %s of numeric properties"
75
+
76
+ const NetworkAggregateCountObj = "An object containing countable properties"
77
+
78
+ const NetworkAggregateGroupedByObj = "An object containing the path and value of the grouped property"
79
+
80
+ const (
81
+ NetworkAggregateGroupedByGroupedByPath = "The path of the grouped property"
82
+ NetworkAggregateGroupedByGroupedByValue = "The value of the grouped property"
83
+ )
adapters/handlers/graphql/descriptions/explore.go ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ const (
16
+ LocalExplore = "Explore Concepts on a local weaviate with vector-aided search"
17
+ LocalExploreConcepts = "Explore Concepts on a local weaviate with vector-aided serach through keyword-based search terms"
18
+ VectorMovement = "Move your search term closer to or further away from another vector described by keywords"
19
+ Keywords = "Keywords are a list of search terms. Array type, e.g. [\"keyword 1\", \"keyword 2\"]"
20
+ Network = "Set to true, if the exploration should include remote peers"
21
+ Limit = "Limit the results set (usually fewer results mean faster queries)"
22
+ Offset = "Offset of the results set (usually fewer results mean faster queries)"
23
+ Certainty = "Normalized Distance between the result item and the search vector. Normalized to be between 0 (identical vectors) and 1 (perfect opposite)."
24
+ Distance = "The required degree of similarity between an object's characteristics and the provided filter values"
25
+ Vector = "Target vector to be used in kNN search"
26
+ Force = "The force to apply for a particular movements. Must be between 0 and 1 where 0 is equivalent to no movement and 1 is equivalent to largest movement possible"
27
+ ClassName = "Name of the Class"
28
+ ID = "Concept identifier in the uuid format"
29
+ Beacon = "Concept identifier in the beacon format, such as weaviate://<hostname>/<kind>/id"
30
+ )
adapters/handlers/graphql/descriptions/fetch.go ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // Local
16
+ const (
17
+ LocalFetch = "Fetch Beacons that are similar to a specified concept from the Objects subsets on a Weaviate network"
18
+ LocalFetchObj = "An object used to perform a Fuzzy Fetch to search for Objects and Actions similar to a specified concept on a Weaviate network"
19
+ )
20
+
21
+ const (
22
+ LocalFetchObjects = "Perform a Fuzzy Fetch to Fetch Beacons similar to a specified concept on a Weaviate network from the Objects subset"
23
+ LocalFetchFuzzy = "Perform a Fuzzy Fetch to Fetch Beacons similar to a specified concept on a Weaviate network from both the Objects subsets"
24
+ )
25
+
26
+ const (
27
+ LocalFetchBeacon = "A Beacon result from a local Weaviate Local Fetch query"
28
+ LocalFetchClassName = "The class name of the result from a local Weaviate Local Fetch query"
29
+ LocalFetchCertainty = "The degree of similarity on a scale of 0-1 between the Beacon's characteristics and the provided concept"
30
+ LocalFetchActionsObj = "An object used to Fetch Beacons from the Actions subset of the dataset"
31
+ )
32
+
33
+ const (
34
+ LocalFetchFuzzyBeacon = "A Beacon result from a local Weaviate Fetch Fuzzy query from both the Objects subsets"
35
+ LocalFetchFuzzyClassName = "Class name of the result from a local Weaviate Fetch Fuzzy query from both the Objects subsets"
36
+ LocalFetchFuzzyCertainty = "The degree of similarity on a scale of 0-1 between the Beacon's characteristics and the provided concept"
37
+ LocalFetchFuzzyObj = "An object used to Fetch Beacons from both the Objects subsets"
38
+ )
39
+
40
+ // NETWORK
41
+ const (
42
+ NetworkFetch = "Fetch Beacons that are similar to a specified concept from the Objects subsets on a Weaviate network"
43
+ NetworkFetchObj = "An object used to perform a Fuzzy Fetch to search for Objects similar to a specified concept on a Weaviate network"
44
+ )
45
+
46
+ const (
47
+ NetworkFetchFuzzy = "Perform a Fuzzy Fetch to Fetch Beacons similar to a specified concept on a Weaviate network from both the Objects subsets"
48
+ )
49
+
50
+ const (
51
+ NetworkFetchFuzzyClassName = "The class name of the result from a network Weaviate Fetch Fuzzy query from both the Objects subsets"
52
+ NetworkFetchFuzzyBeacon = "A Beacon result from a network Weaviate Fetch Fuzzy query from both the Objects subsets"
53
+ NetworkFetchFuzzyCertainty = "The degree of similarity on a scale of 0-1 between the Beacon's characteristics and the provided concept"
54
+ NetworkFetchFuzzyObj = "An object used to Fetch Beacons from both the Objects subsets"
55
+ )
adapters/handlers/graphql/descriptions/filters.go ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // Where filter elements
16
+ const (
17
+ GetWhere = "Filter options for a local Get query, used to convert the result to the specified filters"
18
+ GetWhereInpObj = "An object containing filter options for a local Get query, used to convert the result to the specified filters"
19
+ )
20
+
21
+ const (
22
+ LocalMetaWhere = "Filter options for a local Meta query, used to convert the result to the specified filters"
23
+ LocalMetaWhereInpObj = "An object containing filter options for a local Meta query, used to convert the result to the specified filters"
24
+ )
25
+
26
+ const (
27
+ AggregateWhere = "Filter options for a local Aggregate query, used to convert the result to the specified filters"
28
+ AggregateWhereInpObj = "An object containing filter options for a local Aggregate query, used to convert the result to the specified filters"
29
+ )
30
+
31
+ const (
32
+ NetworkGetWhere = "Filter options for a network Get query, used to convert the result to the specified filters"
33
+ NetworkGetWhereInpObj = "An object containing filter options for a network Get query, used to convert the result to the specified filters"
34
+ )
35
+
36
+ const (
37
+ NetworkMetaWhere = "Filter options for a network Meta query, used to convert the result to the specified filters"
38
+ NetworkMetaWhereInpObj = "An object containing filter options for a network Meta query, used to convert the result to the specified filters"
39
+ )
40
+
41
+ const (
42
+ NetworkAggregateWhere = "Filter options for a network Aggregate query, used to convert the result to the specified filters"
43
+ NetworkAggregateWhereInpObj = "An object containing filter options for a network Aggregate query, used to convert the result to the specified filters"
44
+ )
45
+
46
+ const (
47
+ WhereOperands = "Contains the Operands that can be applied to a 'where' filter"
48
+ WhereOperandsInpObj = "An object containing the Operands that can be applied to a 'where' filter"
49
+ )
50
+
51
+ const (
52
+ WhereOperator = "Contains the Operators that can be applied to a 'where' filter"
53
+ WhereOperatorEnum = "An object containing the Operators that can be applied to a 'where' filter"
54
+ )
55
+
56
+ const WherePath = "Specify the path from the Objects fields to the property name (e.g. ['Things', 'City', 'population'] leads to the 'population' property of a 'City' object)"
57
+
58
+ const (
59
+ WhereValueInt = "Specify an Integer value that the target property will be compared to"
60
+ WhereValueNumber = "Specify a Float value that the target property will be compared to"
61
+ WhereValueBoolean = "Specify a Boolean value that the target property will be compared to"
62
+ WhereValueString = "Specify a String value that the target property will be compared to"
63
+ WhereValueRange = "Specify both geo-coordinates (latitude and longitude as decimals) and a maximum distance from the described coordinates. The search will return any result which is located less than or equal to the specified maximum distance in km away from the specified point."
64
+ WhereValueRangeGeoCoordinates = "The geoCoordinates that form the center point of the search."
65
+ WhereValueRangeGeoCoordinatesLatitude = "The latitude (in decimal format) of the geoCoordinates to search around."
66
+ WhereValueRangeGeoCoordinatesLongitude = "The longitude (in decimal format) of the geoCoordinates to search around."
67
+ WhereValueRangeDistance = "The distance from the point specified via geoCoordinates."
68
+ WhereValueRangeDistanceMax = "The maximum distance from the point specified geoCoordinates."
69
+ WhereValueText = "Specify a Text value that the target property will be compared to"
70
+ WhereValueDate = "Specify a Date value that the target property will be compared to"
71
+ )
72
+
73
+ // Properties and Classes filter elements (used by Fetch and Introspect Where filters)
74
+ const (
75
+ WhereProperties = "Specify which properties to filter on"
76
+ WherePropertiesObj = "Specify which properties to filter on"
77
+ )
78
+
79
+ const (
80
+ WherePropertiesPropertyName = "Specify which property name to filter properties on"
81
+ WhereCertainty = "Specify the required degree of similarity between an object's characteristics and the provided filter values on a scale of 0-1"
82
+ WhereName = "Specify the name of the property to filter on"
83
+ )
84
+
85
+ const (
86
+ WhereKeywords = "Specify which keywords to filter on"
87
+ WhereKeywordsInpObj = "Specify the value and the weight of a keyword"
88
+ )
89
+
90
+ const (
91
+ WhereKeywordsValue = "Specify the value of the keyword"
92
+ WhereKeywordsWeight = "Specify the weight of the keyword"
93
+ )
94
+
95
+ const (
96
+ WhereClass = "Specify which classes to filter on"
97
+ WhereInpObj = "Specify which classes and properties to filter on"
98
+ )
99
+
100
+ // Unique Fetch filter elements
101
+ const (
102
+ FetchWhereFilterFields = "An object containing filter options for a network Fetch search, used to convert the result to the specified filters"
103
+ FetchWhereFilterFieldsInpObj = "Filter options for a network Fetch search, used to convert the result to the specified filters"
104
+ )
105
+
106
+ const (
107
+ FetchFuzzyValue = "Specify the concept that will be used to fetch Objects on the network (e.g. 'Airplane', or 'City')"
108
+ FetchFuzzyCertainty = "Specify how much a Beacon's characteristics must match the provided concept on a scale of 0 to 1"
109
+ )
110
+
111
+ // Unique Introspect filter elements
112
+ const (
113
+ IntrospectWhereFilterFields = "An object containing filter options for a network Fetch search, used to convert the result to the specified filters"
114
+ IntrospectWhereFilterFieldsInpObj = "Filter options for a network Fetch search, used to convert the result to the specified filters"
115
+ IntrospectBeaconId = "The id of the Beacon"
116
+ )
117
+
118
+ // GroupBy filter elements
119
+ const (
120
+ GroupByGroup = "Specify the property of the class to group by"
121
+ GroupByCount = "Get the number of instances of a property in a group"
122
+ GroupBySum = "Get the sum of the values of a property in a group"
123
+ GroupByMin = "Get the minimum occurring value of a property in a group"
124
+ GroupByMax = "Get the maximum occurring value of a property in a group"
125
+ GroupByMean = "Get the mean value of a property in a group"
126
+ GroupByMedian = "Get the median of a property in a group"
127
+ GroupByMode = "Get the mode of a property in a group"
128
+ )
129
+
130
+ // Request timeout filter elements
131
+ const NetworkTimeout = "Specify the time in seconds after which an unresolved request automatically fails"
132
+
133
+ // Pagination filter elements
134
+ const (
135
+ First = "Show the first x results (pagination option)"
136
+ After = "Show the results after the first x results (pagination option)"
137
+ )
138
+
139
+ // Cursor API
140
+ const (
141
+ AfterID = "Show the results after a given ID"
142
+ )
143
+
144
+ const (
145
+ SortPath = "Specify the path from the Objects fields to the property name (e.g. ['Get', 'City', 'population'] leads to the 'population' property of a 'City' object)"
146
+ SortOrder = "Specify the sort order, either ascending (asc) which is default or descending (desc)"
147
+ )
148
+
149
+ const (
150
+ GroupByFilter = "Specify the property of the class to group by"
151
+ GroupByPath = "Specify the path from the objects fields to the property name (e.g. ['Things', 'City', 'population'] leads to the 'population' property of a 'City' object)"
152
+ GroupByGroups = "Specify the number of groups to be created"
153
+ GroupByObjectsPerGroup = "Specify the number of max objects in group"
154
+ )
adapters/handlers/graphql/descriptions/get.go ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // Local
16
+ const (
17
+ GetObjects = "Get Objects on a local Weaviate"
18
+ )
19
+
20
+ const (
21
+ GetObj = "An object used to Get Objects on a local Weaviate"
22
+ Get = "Get Objects on a local Weaviate"
23
+ )
24
+
25
+ const GetObjectsActionsObj = "An object used to get %ss on a local Weaviate"
26
+
27
+ const GetClassUUID = "The UUID of a Object, assigned by its local Weaviate"
28
+
29
+ // Network
30
+ const (
31
+ NetworkGet = "Get Objects from a Weaviate in a network"
32
+ NetworkGetObj = "An object used to Get Objects from a Weaviate in a network"
33
+ )
34
+
35
+ const NetworkGetWeaviateObj = "An object containing Get Objects fields for network Weaviate instance: "
36
+
37
+ const (
38
+ NetworkGetObjects = "Get Objects from a Weaviate in a network"
39
+ )
40
+
41
+ const (
42
+ NetworkGetObjectsObj = "An object containing the Objects objects on this network Weaviate instance."
43
+ )
44
+
45
+ const NetworkGetClassUUID = "The UUID of a Object, assigned by the Weaviate network" // TODO check this with @lauraham
46
+
47
+ const ConsistencyLevel = "Determines how many replicas must acknowledge a request " +
48
+ "before it is considered successful. Can be 'ONE', 'QUORUM', or 'ALL'"
49
+
50
+ const Tenant = "The value by which a tenant is identified, specified in the class schema"
adapters/handlers/graphql/descriptions/getMeta.go ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // Local
16
+ const (
17
+ LocalMetaObj = "An object used to Get Meta information about Objects on a local Weaviate"
18
+ LocalMeta = "Get Meta information about Objects on a local Weaviate"
19
+ )
20
+
21
+ const (
22
+ MetaPropertyType = "The datatype of this property"
23
+ MetaPropertyCount = "The total amount of found instances for this property" // TODO check this with @lauraham
24
+ MetaPropertyTopOccurrences = "An object containing data about the most frequently occurring values for this property"
25
+ MetaPropertyTopOccurrencesValue = "The most frequently occurring value for this property"
26
+ MetaPropertyTopOccurrencesOccurs = "How often the most frequently occurring value for this property occurs" // TODO check this with @lauraham
27
+ MetaPropertyMinimum = "The minimum value for this property"
28
+ MetaPropertyMaximum = "The maximum value for this property"
29
+ MetaPropertyMean = "The mean of all values for this property"
30
+ MetaPropertySum = "The sum of all values for this property"
31
+ MetaPropertyObject = "An object containing meta information about this property"
32
+ )
33
+
34
+ const (
35
+ AggregatePropertyType = "The datatype of this property"
36
+ AggregatePropertyCount = "The total amount of found instances for this property" // TODO check this with @lauraham
37
+ AggregatePropertyTopOccurrences = "An object containing data about the most frequently occurring values for this property"
38
+ AggregatePropertyTopOccurrencesValue = "The most frequently occurring value for this property"
39
+ AggregatePropertyTopOccurrencesOccurs = "How often the most frequently occurring value for this property occurs" // TODO check this with @lauraham
40
+ AggregatePropertyMinimum = "The minimum value for this property"
41
+ AggregatePropertyMaximum = "The maximum value for this property"
42
+ AggregatePropertyMean = "The mean of all values for this property"
43
+ AggregatePropertySum = "The sum of all values for this property"
44
+ )
45
+
46
+ // Network
47
+ const (
48
+ NetworkMeta = "Get meta information about Objects from a Weaviate in a network"
49
+ NetworkMetaObj = "An object used to Get meta information about Objects from a Weaviate in a network"
50
+ NetworkMetaWeaviateObj = "An object containing the Meta Objects fields for network Weaviate instance: "
51
+ )
52
+
53
+ const (
54
+ MetaMetaProperty = "Meta information about the object"
55
+ MetaProperty = "Meta information about the property "
56
+ )
57
+
58
+ const (
59
+ MetaClassPropertyTotalTrue = "How often this boolean property's value is true in the dataset"
60
+ MetaClassPropertyPercentageTrue = "The percentage of true values for this boolean property in the dataset"
61
+ )
62
+
63
+ const (
64
+ MetaClassPropertyTotalFalse = "How often this boolean property's value is false in the dataset"
65
+ MetaClassPropertyPercentageFalse = "The percentage of false values for this boolean property in the dataset"
66
+ )
67
+
68
+ const (
69
+ MetaClassPropertyPointingTo = "The classes that this object contains a reference to"
70
+ MetaClassMetaCount = "The total amount of found instances for a class"
71
+ MetaClassMetaObj = "An object containing Meta information about a class"
72
+ )
73
+
74
+ const (
75
+ AggregateClassPropertyTotalTrue = "How often this boolean property's value is true in the dataset"
76
+ AggregateClassPropertyPercentageTrue = "The percentage of true values for this boolean property in the dataset"
77
+ )
78
+
79
+ const (
80
+ AggregateClassPropertyTotalFalse = "How often this boolean property's value is false in the dataset"
81
+ AggregateClassPropertyPercentageFalse = "The percentage of false values for this boolean property in the dataset"
82
+ )
83
+
84
+ const (
85
+ AggregateClassPropertyPointingTo = "The classes that this object contains a reference to"
86
+ AggregateClassAggregateCount = "The total amount of found instances for a class"
87
+ AggregateClassAggregateObj = "An object containing Aggregate information about a class"
88
+ )
adapters/handlers/graphql/descriptions/introspect.go ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // NETWORK
16
+ const (
17
+ NetworkIntrospect = "Get Introspection information about Objects and/or Beacons in a Weaviate network"
18
+ NetworkIntrospectObj = "An object used to perform an Introspection query on a Weaviate network"
19
+ )
20
+
21
+ const (
22
+ NetworkIntrospectWeaviate = "The Weaviate instance that the current Object or Beacon belongs to"
23
+ NetworkIntrospectClassName = "The name of the current Object or Beacon's class"
24
+ NetworkIntrospectCertainty = "The degree of similarity between a(n) Object or Beacon and the filter input"
25
+ )
26
+
27
+ const (
28
+ NetworkIntrospectBeaconProperties = "The properties of a Beacon"
29
+ NetworkIntrospectBeaconPropertiesPropertyName = "The names of the properties of a Beacon"
30
+ )
adapters/handlers/graphql/descriptions/merge.go ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // Local
16
+ const (
17
+ LocalMergeObj = "An object used to Merge Objects on a local Weaviate"
18
+ LocalMerge = "Merge Objects on a local Weaviate"
19
+ )
20
+
21
+ const LocalMergeClassUUID = "The UUID of a Object, assigned by its local Weaviate"
22
+
23
+ // Network
24
+ const (
25
+ NetworkMerge = "Merge Objects from a Weaviate in a network"
26
+ NetworkMergeObj = "An object used to Merge Objects from a Weaviate in a network"
27
+ )
28
+
29
+ const NetworkMergeWeaviateObj = "An object containing Merge Objects fields for network Weaviate instance: "
30
+
31
+ const NetworkMergeClassUUID = "The UUID of a Thing or Action, assigned by the Weaviate network" // TODO check this with @lauraham
adapters/handlers/graphql/descriptions/rootQuery.go ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Package descriptions provides the descriptions as used by the graphql endpoint for Weaviate
13
+ package descriptions
14
+
15
+ // ROOT
16
+ const (
17
+ WeaviateObj = "The location of the root query"
18
+ WeaviateNetwork = "Query a Weaviate network"
19
+ )
20
+
21
+ // LOCAL
22
+ const LocalObj = "A query on a local Weaviate"
23
+
24
+ // NETWORK
25
+ const (
26
+ NetworkWeaviate = "An object for the network Weaviate instance: "
27
+ NetworkObj = "An object used to perform queries on a Weaviate network"
28
+ )
adapters/handlers/graphql/graphiql/graphiql.go ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ // Based on `graphiql.go` from https://github.com/graphql-go/handler
13
+ // only made RenderGraphiQL a public function.
14
+ package graphiql
15
+
16
+ import (
17
+ "encoding/json"
18
+ "html/template"
19
+ "net/http"
20
+ "strings"
21
+ )
22
+
23
+ // graphiqlVersion is the current version of GraphiQL
24
+ const graphiqlVersion = "0.11.11"
25
+
26
+ // graphiqlData is the page data structure of the rendered GraphiQL page
27
+ type graphiqlData struct {
28
+ GraphiqlVersion string
29
+ QueryString string
30
+ Variables string
31
+ OperationName string
32
+ AuthKey string
33
+ AuthToken string
34
+ }
35
+
36
+ func AddMiddleware(next http.Handler) http.Handler {
37
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38
+ if strings.HasPrefix(r.URL.Path, "/v1/graphql") && r.Method == http.MethodGet {
39
+ renderGraphiQL(w, r)
40
+ } else {
41
+ next.ServeHTTP(w, r)
42
+ }
43
+ })
44
+ }
45
+
46
+ // renderGraphiQL renders the GraphiQL GUI
47
+ func renderGraphiQL(w http.ResponseWriter, r *http.Request) {
48
+ w.Header().Set("WWW-Authenticate", `Basic realm="Provide your key and token (as username as password respectively)"`)
49
+
50
+ user, password, authOk := r.BasicAuth()
51
+ if !authOk {
52
+ http.Error(w, "Not authorized", 401)
53
+ return
54
+ }
55
+
56
+ queryParams := r.URL.Query()
57
+
58
+ t := template.New("GraphiQL")
59
+ t, err := t.Parse(graphiqlTemplate)
60
+ if err != nil {
61
+ http.Error(w, err.Error(), http.StatusInternalServerError)
62
+ return
63
+ }
64
+
65
+ // Attempt to deserialize the 'variables' query key to something reasonable.
66
+ var queryVars interface{}
67
+ err = json.Unmarshal([]byte(queryParams.Get("variables")), &queryVars)
68
+
69
+ var varsString string
70
+ if err == nil {
71
+ vars, err := json.MarshalIndent(queryVars, "", " ")
72
+ if err != nil {
73
+ http.Error(w, err.Error(), http.StatusInternalServerError)
74
+ return
75
+ }
76
+ varsString = string(vars)
77
+ if varsString == "null" {
78
+ varsString = ""
79
+ }
80
+ }
81
+
82
+ // Create result string
83
+ d := graphiqlData{
84
+ GraphiqlVersion: graphiqlVersion,
85
+ QueryString: queryParams.Get("query"),
86
+ Variables: varsString,
87
+ OperationName: queryParams.Get("operationName"),
88
+ AuthKey: user,
89
+ AuthToken: password,
90
+ }
91
+ err = t.ExecuteTemplate(w, "index", d)
92
+ if err != nil {
93
+ http.Error(w, err.Error(), http.StatusInternalServerError)
94
+ }
95
+ }
96
+
97
+ // tmpl is the page template to render GraphiQL
98
+ const graphiqlTemplate = `
99
+ {{ define "index" }}
100
+ <!--
101
+ The request to this GraphQL server provided the header "Accept: text/html"
102
+ and as a result has been presented GraphiQL - an in-browser IDE for
103
+ exploring GraphQL.
104
+
105
+ If you wish to receive JSON, provide the header "Accept: application/json" or
106
+ add "&raw" to the end of the URL within a browser.
107
+ -->
108
+ <!DOCTYPE html>
109
+ <html>
110
+ <head>
111
+ <meta charset="utf-8" />
112
+ <title>GraphiQL</title>
113
+ <meta name="robots" content="noindex" />
114
+ <meta name="referrer" content="origin">
115
+ <style>
116
+ body {
117
+ height: 100%;
118
+ margin: 0;
119
+ overflow: hidden;
120
+ width: 100%;
121
+ }
122
+ #graphiql {
123
+ height: 100vh;
124
+ }
125
+ </style>
126
+ <link href="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.css" rel="stylesheet" />
127
+ <script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script>
128
+ <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
129
+ <script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script>
130
+ <script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script>
131
+ <script src="//cdn.jsdelivr.net/npm/graphiql@{{ .GraphiqlVersion }}/graphiql.min.js"></script>
132
+ </head>
133
+ <body>
134
+ <div id="graphiql">Loading...</div>
135
+ <script>
136
+ // Collect the URL parameters
137
+ var parameters = {};
138
+ window.location.search.substr(1).split('&').forEach(function (entry) {
139
+ var eq = entry.indexOf('=');
140
+ if (eq >= 0) {
141
+ parameters[decodeURIComponent(entry.slice(0, eq))] =
142
+ decodeURIComponent(entry.slice(eq + 1));
143
+ }
144
+ });
145
+
146
+ // Produce a Location query string from a parameter object.
147
+ function locationQuery(params) {
148
+ return '?' + Object.keys(params).filter(function (key) {
149
+ return Boolean(params[key]);
150
+ }).map(function (key) {
151
+ return encodeURIComponent(key) + '=' +
152
+ encodeURIComponent(params[key]);
153
+ }).join('&');
154
+ }
155
+
156
+ // Derive a fetch URL from the current URL, sans the GraphQL parameters.
157
+ var graphqlParamNames = {
158
+ query: true,
159
+ variables: true,
160
+ operationName: true
161
+ };
162
+
163
+ var otherParams = {};
164
+ for (var k in parameters) {
165
+ if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
166
+ otherParams[k] = parameters[k];
167
+ }
168
+ }
169
+ var fetchURL = locationQuery(otherParams);
170
+
171
+ // Defines a GraphQL fetcher using the fetch API.
172
+ function graphQLFetcher(graphQLParams) {
173
+ return fetch(fetchURL, {
174
+ method: 'post',
175
+ headers: {
176
+ 'Accept': 'application/json',
177
+ 'Content-Type': 'application/json',
178
+ 'X-API-KEY': {{ .AuthKey }},
179
+ 'X-API-TOKEN': {{ .AuthToken }}
180
+ },
181
+ body: JSON.stringify(graphQLParams),
182
+ credentials: 'include',
183
+ }).then(function (response) {
184
+ return response.text();
185
+ }).then(function (responseBody) {
186
+ try {
187
+ return JSON.parse(responseBody);
188
+ } catch (error) {
189
+ return responseBody;
190
+ }
191
+ });
192
+ }
193
+
194
+ // When the query and variables string is edited, update the URL bar so
195
+ // that it can be easily shared.
196
+ function onEditQuery(newQuery) {
197
+ parameters.query = newQuery;
198
+ updateURL();
199
+ }
200
+
201
+ function onEditVariables(newVariables) {
202
+ parameters.variables = newVariables;
203
+ updateURL();
204
+ }
205
+
206
+ function onEditOperationName(newOperationName) {
207
+ parameters.operationName = newOperationName;
208
+ updateURL();
209
+ }
210
+
211
+ function updateURL() {
212
+ history.replaceState(null, null, locationQuery(parameters));
213
+ }
214
+
215
+ // Render <GraphiQL /> into the body.
216
+ ReactDOM.render(
217
+ React.createElement(GraphiQL, {
218
+ fetcher: graphQLFetcher,
219
+ onEditQuery: onEditQuery,
220
+ onEditVariables: onEditVariables,
221
+ onEditOperationName: onEditOperationName,
222
+ query: {{ .QueryString }},
223
+ response: null,
224
+ variables: {{ .Variables }},
225
+ operationName: {{ .OperationName }},
226
+ }),
227
+ document.getElementById('graphiql')
228
+ );
229
+ </script>
230
+ </body>
231
+ </html>
232
+ {{ end }}
233
+ `
adapters/handlers/graphql/local/aggregate/aggregate.go ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package aggregate
13
+
14
+ import (
15
+ "fmt"
16
+
17
+ "github.com/tailor-inc/graphql"
18
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/descriptions"
19
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/local/common_filters"
20
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/utils"
21
+ "github.com/weaviate/weaviate/entities/aggregation"
22
+ "github.com/weaviate/weaviate/entities/models"
23
+ "github.com/weaviate/weaviate/entities/schema"
24
+ "github.com/weaviate/weaviate/usecases/config"
25
+ )
26
+
27
+ type ModulesProvider interface {
28
+ AggregateArguments(class *models.Class) map[string]*graphql.ArgumentConfig
29
+ ExtractSearchParams(arguments map[string]interface{}, className string) map[string]interface{}
30
+ }
31
+
32
+ // Build the Aggregate Kinds schema
33
+ func Build(dbSchema *schema.Schema, config config.Config,
34
+ modulesProvider ModulesProvider,
35
+ ) (*graphql.Field, error) {
36
+ if len(dbSchema.Objects.Classes) == 0 {
37
+ return nil, utils.ErrEmptySchema
38
+ }
39
+
40
+ var err error
41
+ var localAggregateObjects *graphql.Object
42
+ if len(dbSchema.Objects.Classes) > 0 {
43
+ localAggregateObjects, err = classFields(dbSchema.Objects.Classes, config, modulesProvider)
44
+ if err != nil {
45
+ return nil, err
46
+ }
47
+ }
48
+
49
+ field := graphql.Field{
50
+ Name: "Aggregate",
51
+ Description: descriptions.AggregateWhere,
52
+ Type: localAggregateObjects,
53
+ Resolve: passThroughResolver,
54
+ }
55
+
56
+ return &field, nil
57
+ }
58
+
59
+ func classFields(databaseSchema []*models.Class,
60
+ config config.Config, modulesProvider ModulesProvider,
61
+ ) (*graphql.Object, error) {
62
+ fields := graphql.Fields{}
63
+
64
+ for _, class := range databaseSchema {
65
+ field, err := classField(class, class.Description, config, modulesProvider)
66
+ if err != nil {
67
+ return nil, err
68
+ }
69
+
70
+ fields[class.Class] = field
71
+ }
72
+
73
+ return graphql.NewObject(graphql.ObjectConfig{
74
+ Name: "AggregateObjectsObj",
75
+ Fields: fields,
76
+ Description: descriptions.AggregateObjectsObj,
77
+ }), nil
78
+ }
79
+
80
+ func classField(class *models.Class, description string,
81
+ config config.Config, modulesProvider ModulesProvider,
82
+ ) (*graphql.Field, error) {
83
+ metaClassName := fmt.Sprintf("Aggregate%s", class.Class)
84
+
85
+ fields := graphql.ObjectConfig{
86
+ Name: metaClassName,
87
+ Fields: (graphql.FieldsThunk)(func() graphql.Fields {
88
+ fields, err := classPropertyFields(class)
89
+ if err != nil {
90
+ // we cannot return an error in this FieldsThunk and have to panic unfortunately
91
+ panic(fmt.Sprintf("Failed to assemble single Local Aggregate Class field: %s", err))
92
+ }
93
+
94
+ return fields
95
+ }),
96
+ Description: description,
97
+ }
98
+
99
+ fieldsObject := graphql.NewObject(fields)
100
+ fieldsField := &graphql.Field{
101
+ Type: graphql.NewList(fieldsObject),
102
+ Description: description,
103
+ Args: graphql.FieldConfigArgument{
104
+ "limit": &graphql.ArgumentConfig{
105
+ Description: descriptions.First,
106
+ Type: graphql.Int,
107
+ },
108
+ "where": &graphql.ArgumentConfig{
109
+ Description: descriptions.GetWhere,
110
+ Type: graphql.NewInputObject(
111
+ graphql.InputObjectConfig{
112
+ Name: fmt.Sprintf("AggregateObjects%sWhereInpObj", class.Class),
113
+ Fields: common_filters.BuildNew(fmt.Sprintf("AggregateObjects%s", class.Class)),
114
+ Description: descriptions.GetWhereInpObj,
115
+ },
116
+ ),
117
+ },
118
+ "groupBy": &graphql.ArgumentConfig{
119
+ Description: descriptions.GroupBy,
120
+ Type: graphql.NewList(graphql.String),
121
+ },
122
+ "nearVector": nearVectorArgument(class.Class),
123
+ "nearObject": nearObjectArgument(class.Class),
124
+ "objectLimit": &graphql.ArgumentConfig{
125
+ Description: descriptions.First,
126
+ Type: graphql.Int,
127
+ },
128
+ "hybrid": hybridArgument(fieldsObject, class, modulesProvider),
129
+ },
130
+ Resolve: makeResolveClass(modulesProvider, class),
131
+ }
132
+
133
+ if modulesProvider != nil {
134
+ for name, argument := range modulesProvider.AggregateArguments(class) {
135
+ fieldsField.Args[name] = argument
136
+ }
137
+ }
138
+
139
+ if schema.MultiTenancyEnabled(class) {
140
+ fieldsField.Args["tenant"] = tenantArgument()
141
+ }
142
+
143
+ return fieldsField, nil
144
+ }
145
+
146
+ func classPropertyFields(class *models.Class) (graphql.Fields, error) {
147
+ fields := graphql.Fields{}
148
+ for _, property := range class.Properties {
149
+ propertyType, err := schema.GetPropertyDataType(class, property.Name)
150
+ if err != nil {
151
+ return nil, fmt.Errorf("%s.%s: %s", class.Class, property.Name, err)
152
+ }
153
+
154
+ convertedDataType, err := classPropertyField(*propertyType, class, property)
155
+ if err != nil {
156
+ return nil, err
157
+ }
158
+
159
+ fields[property.Name] = convertedDataType
160
+ }
161
+
162
+ // Special case: meta { count } appended to all regular props
163
+ fields["meta"] = &graphql.Field{
164
+ Description: descriptions.LocalMetaObj,
165
+ Type: metaObject(fmt.Sprintf("Aggregate%s", class.Class)),
166
+ Resolve: func(p graphql.ResolveParams) (interface{}, error) {
167
+ // pass-through
168
+ return p.Source, nil
169
+ },
170
+ }
171
+
172
+ // Always append Grouped By field
173
+ fields["groupedBy"] = &graphql.Field{
174
+ Description: descriptions.AggregateGroupedBy,
175
+ Type: groupedByProperty(class),
176
+ Resolve: func(p graphql.ResolveParams) (interface{}, error) {
177
+ switch typed := p.Source.(type) {
178
+ case aggregation.Group:
179
+ return typed.GroupedBy, nil
180
+ case map[string]interface{}:
181
+ return typed["groupedBy"], nil
182
+ default:
183
+ return nil, fmt.Errorf("groupedBy: unsupported type %T", p.Source)
184
+ }
185
+ },
186
+ }
187
+
188
+ return fields, nil
189
+ }
190
+
191
+ func metaObject(prefix string) *graphql.Object {
192
+ return graphql.NewObject(graphql.ObjectConfig{
193
+ Name: fmt.Sprintf("%sMetaObject", prefix),
194
+ Fields: graphql.Fields{
195
+ "count": &graphql.Field{
196
+ Type: graphql.Int,
197
+ Resolve: func(p graphql.ResolveParams) (interface{}, error) {
198
+ group, ok := p.Source.(aggregation.Group)
199
+ if !ok {
200
+ return nil, fmt.Errorf("meta count: expected aggregation.Group, got %T", p.Source)
201
+ }
202
+
203
+ return group.Count, nil
204
+ },
205
+ },
206
+ },
207
+ })
208
+ }
209
+
210
+ func classPropertyField(dataType schema.DataType, class *models.Class, property *models.Property) (*graphql.Field, error) {
211
+ switch dataType {
212
+ case schema.DataTypeText:
213
+ return makePropertyField(class, property, stringPropertyFields)
214
+ case schema.DataTypeInt:
215
+ return makePropertyField(class, property, numericPropertyFields)
216
+ case schema.DataTypeNumber:
217
+ return makePropertyField(class, property, numericPropertyFields)
218
+ case schema.DataTypeBoolean:
219
+ return makePropertyField(class, property, booleanPropertyFields)
220
+ case schema.DataTypeDate:
221
+ return makePropertyField(class, property, datePropertyFields)
222
+ case schema.DataTypeCRef:
223
+ return makePropertyField(class, property, referencePropertyFields)
224
+ case schema.DataTypeGeoCoordinates:
225
+ // simply skip for now, see gh-729
226
+ return nil, nil
227
+ case schema.DataTypePhoneNumber:
228
+ // skipping for now, see gh-1088 where it was outscoped
229
+ return nil, nil
230
+ case schema.DataTypeBlob:
231
+ return makePropertyField(class, property, stringPropertyFields)
232
+ case schema.DataTypeTextArray:
233
+ return makePropertyField(class, property, stringPropertyFields)
234
+ case schema.DataTypeIntArray, schema.DataTypeNumberArray:
235
+ return makePropertyField(class, property, numericPropertyFields)
236
+ case schema.DataTypeBooleanArray:
237
+ return makePropertyField(class, property, booleanPropertyFields)
238
+ case schema.DataTypeDateArray:
239
+ return makePropertyField(class, property, datePropertyFields)
240
+ case schema.DataTypeUUID, schema.DataTypeUUIDArray:
241
+ // not aggregatable
242
+ return nil, nil
243
+ case schema.DataTypeObject, schema.DataTypeObjectArray:
244
+ // TODO: check if it's aggregable, skip for now
245
+ return nil, nil
246
+ default:
247
+ return nil, fmt.Errorf(schema.ErrorNoSuchDatatype+": %s", dataType)
248
+ }
249
+ }
250
+
251
+ type propertyFieldMaker func(class *models.Class,
252
+ property *models.Property, prefix string) *graphql.Object
253
+
254
+ func makePropertyField(class *models.Class, property *models.Property,
255
+ fieldMaker propertyFieldMaker,
256
+ ) (*graphql.Field, error) {
257
+ prefix := "Aggregate"
258
+ return &graphql.Field{
259
+ Description: fmt.Sprintf(`%s"%s"`, descriptions.AggregateProperty, property.Name),
260
+ Type: fieldMaker(class, property, prefix),
261
+ Resolve: func(p graphql.ResolveParams) (interface{}, error) {
262
+ switch typed := p.Source.(type) {
263
+ case aggregation.Group:
264
+ res, ok := typed.Properties[property.Name]
265
+ if !ok {
266
+ return nil, fmt.Errorf("missing property '%s'", property.Name)
267
+ }
268
+
269
+ return res, nil
270
+
271
+ default:
272
+ return nil, fmt.Errorf("property %s, unsupported type %T", property.Name, p.Source)
273
+ }
274
+ },
275
+ }, nil
276
+ }
277
+
278
+ func passThroughResolver(p graphql.ResolveParams) (interface{}, error) {
279
+ // bubble up root resolver
280
+ return p.Source, nil
281
+ }
adapters/handlers/graphql/local/aggregate/explore_argument.go ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package aggregate
13
+
14
+ import (
15
+ "github.com/tailor-inc/graphql"
16
+ "github.com/weaviate/weaviate/adapters/handlers/graphql/local/common_filters"
17
+ )
18
+
19
+ func nearVectorArgument(className string) *graphql.ArgumentConfig {
20
+ return common_filters.NearVectorArgument("AggregateObjects", className)
21
+ }
22
+
23
+ func nearObjectArgument(className string) *graphql.ArgumentConfig {
24
+ return common_filters.NearObjectArgument("AggregateObjects", className)
25
+ }
adapters/handlers/graphql/local/aggregate/helpers_for_test.go ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package aggregate
13
+
14
+ import (
15
+ "context"
16
+ "fmt"
17
+
18
+ testhelper "github.com/weaviate/weaviate/adapters/handlers/graphql/test/helper"
19
+ "github.com/weaviate/weaviate/entities/aggregation"
20
+ "github.com/weaviate/weaviate/entities/models"
21
+ "github.com/weaviate/weaviate/usecases/config"
22
+ )
23
+
24
+ type mockRequestsLog struct{}
25
+
26
+ func (m *mockRequestsLog) Register(first string, second string) {
27
+ }
28
+
29
+ type mockResolver struct {
30
+ testhelper.MockResolver
31
+ }
32
+
33
+ func newMockResolver(cfg config.Config) *mockResolver {
34
+ field, err := Build(&testhelper.CarSchema, cfg, nil)
35
+ if err != nil {
36
+ panic(fmt.Sprintf("could not build graphql test schema: %s", err))
37
+ }
38
+ mockLog := &mockRequestsLog{}
39
+ mocker := &mockResolver{}
40
+ mocker.RootFieldName = "Aggregate"
41
+ mocker.RootField = field
42
+ mocker.RootObject = map[string]interface{}{
43
+ "Resolver": Resolver(mocker),
44
+ "RequestsLog": mockLog,
45
+ "Config": cfg,
46
+ }
47
+
48
+ return mocker
49
+ }
50
+
51
+ func (m *mockResolver) Aggregate(ctx context.Context, principal *models.Principal,
52
+ params *aggregation.Params,
53
+ ) (interface{}, error) {
54
+ args := m.Called(params)
55
+ return args.Get(0), args.Error(1)
56
+ }
adapters/handlers/graphql/local/aggregate/hybrid_search.go ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // _ _
2
+ // __ _____ __ ___ ___ __ _| |_ ___
3
+ // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
4
+ // \ V V / __/ (_| |\ V /| | (_| | || __/
5
+ // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
6
+ //
7
+ // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
8
+ //
9
+ // CONTACT: [email protected]
10
+ //
11
+
12
+ package aggregate
13
+
14
+ import (
15
+ "fmt"
16
+ "os"
17
+
18
+ "github.com/tailor-inc/graphql"
19
+ "github.com/weaviate/weaviate/entities/models"
20
+ )
21
+
22
+ func hybridArgument(classObject *graphql.Object,
23
+ class *models.Class, modulesProvider ModulesProvider,
24
+ ) *graphql.ArgumentConfig {
25
+ prefix := fmt.Sprintf("AggregateObjects%s", class.Class)
26
+ return &graphql.ArgumentConfig{
27
+ Type: graphql.NewInputObject(
28
+ graphql.InputObjectConfig{
29
+ Name: fmt.Sprintf("%sHybridInpObj", prefix),
30
+ Fields: hybridOperands(classObject, class, modulesProvider),
31
+ Description: "Hybrid search",
32
+ },
33
+ ),
34
+ }
35
+ }
36
+
37
+ func hybridOperands(classObject *graphql.Object,
38
+ class *models.Class, modulesProvider ModulesProvider,
39
+ ) graphql.InputObjectConfigFieldMap {
40
+ ss := graphql.NewInputObject(graphql.InputObjectConfig{
41
+ Name: class.Class + "HybridSubSearch",
42
+ Fields: hybridSubSearch(classObject, class, modulesProvider),
43
+ })
44
+ fieldMap := graphql.InputObjectConfigFieldMap{
45
+ "query": &graphql.InputObjectFieldConfig{
46
+ Description: "Query string",
47
+ Type: graphql.String,
48
+ },
49
+ "alpha": &graphql.InputObjectFieldConfig{
50
+ Description: "Search weight",
51
+ Type: graphql.Float,
52
+ },
53
+ "vector": &graphql.InputObjectFieldConfig{
54
+ Description: "Vector search",
55
+ Type: graphql.NewList(graphql.Float),
56
+ },
57
+ }
58
+
59
+ if os.Getenv("ENABLE_EXPERIMENTAL_HYBRID_OPERANDS") != "" {
60
+ fieldMap["operands"] = &graphql.InputObjectFieldConfig{
61
+ Description: "Subsearch list",
62
+ Type: graphql.NewList(ss),
63
+ }
64
+ }
65
+
66
+ return fieldMap
67
+ }
68
+
69
+ func hybridSubSearch(classObject *graphql.Object,
70
+ class *models.Class, modulesProvider ModulesProvider,
71
+ ) graphql.InputObjectConfigFieldMap {
72
+ prefixName := class.Class + "SubSearch"
73
+
74
+ return graphql.InputObjectConfigFieldMap{
75
+ "weight": &graphql.InputObjectFieldConfig{
76
+ Description: "weight, 0 to 1",
77
+ Type: graphql.Float,
78
+ },
79
+ "sparseSearch": &graphql.InputObjectFieldConfig{
80
+ Description: "Sparse Search",
81
+ Type: graphql.NewInputObject(
82
+ graphql.InputObjectConfig{
83
+ Name: fmt.Sprintf("%sHybridAggregateBM25InpObj", prefixName),
84
+ Fields: bm25Fields(prefixName),
85
+ Description: "BM25f search",
86
+ },
87
+ ),
88
+ },
89
+ }
90
+ }