├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .rspec
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── lib
├── weaviate.rb
└── weaviate
│ ├── backups.rb
│ ├── base.rb
│ ├── classifications.rb
│ ├── client.rb
│ ├── error.rb
│ ├── health.rb
│ ├── meta.rb
│ ├── nodes.rb
│ ├── objects.rb
│ ├── oidc.rb
│ ├── query.rb
│ ├── schema.rb
│ └── version.rb
├── sig
└── weaviate.rbs
├── spec
├── fixtures
│ ├── backup.json
│ ├── batch_delete_object.json
│ ├── class.json
│ ├── classes.json
│ ├── classification.json
│ ├── meta.json
│ ├── nodes.json
│ ├── object.json
│ ├── objects.json
│ ├── oidc.json
│ └── shards.json
├── spec_helper.rb
├── weaviate
│ ├── backups_spec.rb
│ ├── classifications_spec.rb
│ ├── client_spec.rb
│ ├── health_spec.rb
│ ├── nodes_spec.rb
│ ├── objects_spec.rb
│ ├── query_spec.rb
│ └── schema_spec.rb
└── weaviate_spec.rb
└── weaviate.gemspec
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - "*"
7 | push:
8 | branches:
9 | - master
10 | jobs:
11 | tests:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
16 |
17 | steps:
18 | - uses: actions/checkout@master
19 |
20 | - name: Set up Ruby
21 | uses: ruby/setup-ruby@v1
22 | with:
23 | ruby-version: ${{ matrix.ruby }}
24 | bundler: default
25 | bundler-cache: true
26 |
27 | - name: StandardRb check
28 | run: bundle exec standardrb --format progress --format github --color
29 |
30 | - name: Run tests
31 | run: |
32 | bundle exec rspec
33 | docs:
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@master
37 | - name: Set up Ruby
38 | uses: ruby/setup-ruby@v1
39 | with:
40 | ruby-version: 3.3
41 | bundler: default
42 | bundler-cache: true
43 | - name: Build docs
44 | run: bundle exec rake yard
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 | .rubocop.yml
13 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [Unreleased]
2 |
3 | ## [0.9.2] - 2024-10-01
4 | - Weaviate::Client constructor accepts customer logger: to be passed in
5 |
6 | ## [0.9.1] - 2024-09-19
7 |
8 | ## [0.9.0] - 2024-07-08
9 |
10 | - Add object.replace method which uses PUT which performs a complete object replacement
11 |
12 | ### Breaking
13 | - Change the object.update method to use PATCH which only performs a partial update(previously performed a replacement)
14 |
15 | ## [0.8.11] - 2024-07-02
16 | - Allow the user to specify any options they want for multi-tenancy when creating a schema using their own hash
17 | - Allow Ollama vectorizer
18 |
19 | ## [0.8.10] - 2024-01-25
20 |
21 | ## [0.8.9] - 2023-10-10
22 |
23 | ## [0.8.8] - 2023-10-10
24 |
25 | ## [0.8.7] - 2023-09-11
26 |
27 | ## [0.8.6] - 2023-08-10
28 |
29 | ## [0.8.5] - 2023-07-19
30 | - Add multi-tenancy support
31 |
32 | ## [0.8.4] - 2023-06-30
33 | - Adding yard
34 |
35 | ## [0.8.3] - 2023-06-25
36 | - Add Google PaLM support
37 |
38 | ## [0.8.2] - 2023-05-18
39 |
40 | ## [0.8.1] - 2023-05-10
41 |
42 | ## [0.8.0] - 2023-04-18
43 |
44 | ### Breaking
45 | - Initializing the weaviate client requires the single `url:` key instead of separate `host:` and `schema:`
46 |
47 | ## [0.1.0] - 2023-03-24
48 |
49 | - Initial release
50 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in weaviate.gemspec
6 | gemspec
7 |
8 | gem "rake", "~> 13.0"
9 |
10 | gem "rspec", "~> 3.0"
11 | gem "standard", "~> 1.25.0"
12 | gem "graphql-client", "~> 0.19.0"
13 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | weaviate-ruby (0.9.2)
5 | faraday (>= 2.0.1, < 3.0)
6 | graphlient (>= 0.7.0, < 0.9.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | activesupport (7.1.3)
12 | base64
13 | bigdecimal
14 | concurrent-ruby (~> 1.0, >= 1.0.2)
15 | connection_pool (>= 2.2.5)
16 | drb
17 | i18n (>= 1.6, < 2)
18 | minitest (>= 5.1)
19 | mutex_m
20 | tzinfo (~> 2.0)
21 | ast (2.4.2)
22 | base64 (0.2.0)
23 | bigdecimal (3.1.6)
24 | byebug (11.1.3)
25 | coderay (1.1.3)
26 | concurrent-ruby (1.2.3)
27 | connection_pool (2.4.1)
28 | diff-lcs (1.5.0)
29 | drb (2.2.0)
30 | ruby2_keywords
31 | faraday (2.7.10)
32 | faraday-net_http (>= 2.0, < 3.1)
33 | ruby2_keywords (>= 0.0.4)
34 | faraday-net_http (3.0.2)
35 | graphlient (0.7.0)
36 | faraday (~> 2.0)
37 | graphql-client
38 | graphql (2.2.6)
39 | racc (~> 1.4)
40 | graphql-client (0.19.0)
41 | activesupport (>= 3.0)
42 | graphql
43 | i18n (1.14.1)
44 | concurrent-ruby (~> 1.0)
45 | json (2.6.3)
46 | language_server-protocol (3.17.0.3)
47 | method_source (1.0.0)
48 | minitest (5.21.2)
49 | mutex_m (0.2.0)
50 | parallel (1.22.1)
51 | parser (3.2.1.1)
52 | ast (~> 2.4.1)
53 | pry (0.14.2)
54 | coderay (~> 1.1)
55 | method_source (~> 1.0)
56 | pry-byebug (3.10.1)
57 | byebug (~> 11.0)
58 | pry (>= 0.13, < 0.15)
59 | racc (1.7.3)
60 | rainbow (3.1.1)
61 | rake (13.0.6)
62 | rdiscount (2.2.7.1)
63 | regexp_parser (2.7.0)
64 | rexml (3.3.9)
65 | rspec (3.11.0)
66 | rspec-core (~> 3.11.0)
67 | rspec-expectations (~> 3.11.0)
68 | rspec-mocks (~> 3.11.0)
69 | rspec-core (3.11.0)
70 | rspec-support (~> 3.11.0)
71 | rspec-expectations (3.11.0)
72 | diff-lcs (>= 1.2.0, < 2.0)
73 | rspec-support (~> 3.11.0)
74 | rspec-mocks (3.11.1)
75 | diff-lcs (>= 1.2.0, < 2.0)
76 | rspec-support (~> 3.11.0)
77 | rspec-support (3.11.0)
78 | rubocop (1.48.1)
79 | json (~> 2.3)
80 | parallel (~> 1.10)
81 | parser (>= 3.2.0.0)
82 | rainbow (>= 2.2.2, < 4.0)
83 | regexp_parser (>= 1.8, < 3.0)
84 | rexml (>= 3.2.5, < 4.0)
85 | rubocop-ast (>= 1.26.0, < 2.0)
86 | ruby-progressbar (~> 1.7)
87 | unicode-display_width (>= 2.4.0, < 3.0)
88 | rubocop-ast (1.28.0)
89 | parser (>= 3.2.1.0)
90 | rubocop-performance (1.16.0)
91 | rubocop (>= 1.7.0, < 2.0)
92 | rubocop-ast (>= 0.4.0)
93 | ruby-progressbar (1.13.0)
94 | ruby2_keywords (0.0.5)
95 | standard (1.25.3)
96 | language_server-protocol (~> 3.17.0.2)
97 | rubocop (~> 1.48.1)
98 | rubocop-performance (~> 1.16.0)
99 | tzinfo (2.0.6)
100 | concurrent-ruby (~> 1.0)
101 | unicode-display_width (2.4.2)
102 | yard (0.9.37)
103 |
104 | PLATFORMS
105 | arm64-darwin-21
106 | arm64-darwin-23
107 | x86_64-darwin-19
108 | x86_64-darwin-21
109 | x86_64-linux
110 |
111 | DEPENDENCIES
112 | graphql-client (~> 0.19.0)
113 | pry-byebug (~> 3.9)
114 | rake (~> 13.0)
115 | rdiscount
116 | rspec (~> 3.0)
117 | standard (~> 1.25.0)
118 | weaviate-ruby!
119 | yard
120 |
121 | BUNDLED WITH
122 | 2.4.0
123 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Andrei Bondarev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Weaviate
2 |
3 |
4 |
5 | +
6 |
7 |
8 |
9 | Ruby wrapper for the Weaviate.io API.
10 |
11 | Part of the [Langchain.rb](https://github.com/andreibondarev/langchainrb) stack.
12 |
13 | Available for paid consulting engagements! [Email me](mailto:andrei@sourcelabs.io).
14 |
15 | 
16 | [](https://badge.fury.io/rb/weaviate-ruby)
17 | [](http://rubydoc.info/gems/weaviate-ruby)
18 | [](https://github.com/andreibondarev/weaviate-ruby/blob/main/LICENSE.txt)
19 | [](https://discord.gg/WDARp7J2n8)
20 | [](https://twitter.com/rushing_andrei)
21 |
22 | ## Installation
23 |
24 | Install the gem and add to the application's Gemfile by executing:
25 |
26 | $ bundle add weaviate-ruby
27 |
28 | If bundler is not being used to manage dependencies, install the gem by executing:
29 |
30 | $ gem install weaviate-ruby
31 |
32 | ## Usage
33 |
34 | ### Instantiating API client
35 |
36 | ```ruby
37 | require 'weaviate'
38 |
39 | client = Weaviate::Client.new(
40 | url: 'https://some-endpoint.weaviate.network', # Replace with your endpoint
41 | api_key: '', # Weaviate API key
42 | model_service: :openai, # Service that will be used to generate vectors. Possible values: :openai, :azure_openai, :cohere, :huggingface, :google_palm
43 | model_service_api_key: 'xxxxxxx' # Either OpenAI, Azure OpenAI, Cohere, Hugging Face or Google PaLM api key
44 | )
45 | ```
46 |
47 | ### Using the Schema endpoints
48 |
49 | ```ruby
50 | # Creating a new data object class in the schema
51 | client.schema.create(
52 | class_name: 'Question',
53 | description: 'Information from a Jeopardy! question',
54 | properties: [
55 | {
56 | "dataType": ["text"],
57 | "description": "The question",
58 | "name": "question"
59 | }, {
60 | "dataType": ["text"],
61 | "description": "The answer",
62 | "name": "answer"
63 | }, {
64 | "dataType": ["text"],
65 | "description": "The category",
66 | "name": "category"
67 | }
68 | ],
69 | # Possible values: 'text2vec-cohere', 'text2vec-ollama', 'text2vec-openai', 'text2vec-huggingface', 'text2vec-transformers', 'text2vec-contextionary', 'img2vec-neural', 'multi2vec-clip', 'ref2vec-centroid'
70 | vectorizer: "text2vec-openai"
71 | )
72 |
73 | # Get a single class from the schema
74 | client.schema.get(class_name: 'Question')
75 |
76 | # Get the schema
77 | client.schema.list()
78 |
79 | # Update settings of an existing schema class.
80 | # Does not support modifying existing properties.
81 | client.schema.update(
82 | class_name: 'Question',
83 | description: 'Information from a Wheel of Fortune question'
84 | )
85 |
86 | # Adding a new property
87 | client.schema.add_property(
88 | class_name: 'Question',
89 | property: {
90 | "dataType": ["boolean"],
91 | "name": "homepage"
92 | }
93 | )
94 |
95 | # Inspect the shards of a class
96 | client.schema.shards(class_name: 'Question')
97 |
98 | # Remove a class (and all data in the instances) from the schema.
99 | client.schema.delete(class_name: 'Question')
100 |
101 | # Creating a new data object class in the schema while configuring the vectorizer on the schema and on individual properties (Ollama example)
102 | client.schema.create(
103 | class_name: 'Question',
104 | description: 'Information from a Jeopardy! question',
105 | properties: [
106 | {
107 | "dataType": ["text"],
108 | "description": "The question",
109 | "name": "question"
110 | # By default all properties are included in the vector
111 | }, {
112 | "dataType": ["text"],
113 | "description": "The answer",
114 | "name": "answer",
115 | "moduleConfig": {
116 | "text2vec-ollama": {
117 | "skip": false,
118 | "vectorizePropertyName": true,
119 | },
120 | },
121 | }, {
122 | "dataType": ["text"],
123 | "description": "The category",
124 | "name": "category",
125 | "indexFilterable": true,
126 | "indexSearchable": false,
127 | "moduleConfig": {
128 | "text2vec-ollama": {
129 | "skip": true, # Don't include in the vector
130 | },
131 | },
132 | }
133 | ],
134 | vectorizer: "text2vec-ollama",
135 | module_config: {
136 | "text2vec-ollama": {
137 | apiEndpoint: "http://localhost:11434",
138 | model: "mxbai-embed-large",
139 | },
140 | },
141 | )
142 |
143 | # Creating named schemas
144 |
145 | client.schema.create(
146 | class_name: 'ArticleNV',
147 | description: 'Articles with named vectors',
148 | properties: [
149 | {
150 | "dataType": ["text"],
151 | "name": "title"
152 | },
153 | {
154 | "dataType": ["text"],
155 | "name": "body"
156 | }
157 | ],
158 | # cannot use vectorizer and vector_config at the same time
159 | # will need to specify for each property
160 | vector_config: {
161 | "title": {
162 | "vectorizer": {
163 | "text2vec-openai": {
164 | "properties": ["title"]
165 | }
166 | },
167 | "vectorIndexType": "hnsw", # This is the default
168 | },
169 | "body": {
170 | "vectorizer": {
171 | "text2vec-openai": {
172 | "properties": ["body"]
173 | }
174 | },
175 | "vectorIndexType": "hnsw", # This is the default
176 |
177 | }
178 | }
179 | )
180 | ```
181 |
182 | ### Using the Objects endpoint
183 | ```ruby
184 | # Create a new data object.
185 | output = client.objects.create(
186 | class_name: 'Question',
187 | properties: {
188 | answer: '42',
189 | question: 'What is the meaning of life?',
190 | category: 'philosophy'
191 | }
192 | )
193 | uuid = output["id"]
194 |
195 | # Lists all data objects in reverse order of creation.
196 | client.objects.list()
197 |
198 | # Get a single data object.
199 | client.objects.get(
200 | class_name: "Question",
201 | id: uuid
202 | )
203 |
204 | # Check if a data object exists.
205 | client.objects.exists?(
206 | class_name: "Question",
207 | id: uuid
208 | )
209 |
210 | # Perform a partial update on an object based on its uuid.
211 | client.objects.update(
212 | class_name: "Question",
213 | id: uuid,
214 | properties: {
215 | category: "simple-math"
216 | }
217 | )
218 |
219 | # Replace an object based on its uuid.
220 | client.objects.replace(
221 | class_name: "Question",
222 | id: uuid,
223 | properties: {
224 | question: "What does 6 times 7 equal to?",
225 | category: "math",
226 | answer: "42"
227 | }
228 | )
229 |
230 | # Delete a single data object from Weaviate.
231 | client.objects.delete(
232 | class_name: "Question",
233 | id: uuid
234 | )
235 |
236 | # Batch create objects
237 | output = client.objects.batch_create(objects: [
238 | {
239 | class: "Question",
240 | properties: {
241 | answer: "42",
242 | question: "What is the meaning of life?",
243 | category: "philosophy"
244 | }
245 | }, {
246 | class: "Question",
247 | properties: {
248 | answer: "42",
249 | question: "What does 6 times 7 equal to?",
250 | category: "math"
251 | }
252 | }
253 | ])
254 | uuids = output.pluck("id")
255 |
256 | # Batch delete objects
257 | client.objects.batch_delete(
258 | class_name: "Question",
259 | where: {
260 | valueStringArray: uuids,
261 | operator: "ContainsAny",
262 | path: ["id"]
263 | }
264 | )
265 | ```
266 |
267 | ### Querying
268 |
269 | #### Get{}
270 | ```ruby
271 | near_text = '{ concepts: ["biology"] }'
272 | near_vector = '{ vector: [0.1, 0.2, ...] }'
273 | sort_obj = '{ path: ["category"], order: desc }'
274 | where_obj = '{ path: ["id"], operator: Equal, valueString: "..." }'
275 | with_hybrid = '{ query: "Sweets", alpha: 0.5 }'
276 |
277 | client.query.get(
278 | class_name: 'Question',
279 | fields: "question answer category _additional { answer { result hasAnswer property startPosition endPosition } }",
280 | limit: "1",
281 | offset: "1",
282 | after: "id",
283 | sort: sort_obj,
284 | where: where_obj,
285 |
286 | # To use this parameter you must have created your schema by setting the `vectorizer:` property to
287 | # either 'text2vec-transformers', 'text2vec-contextionary', 'text2vec-openai', 'multi2vec-clip', 'text2vec-huggingface' or 'text2vec-cohere'
288 | near_text: near_text,
289 |
290 | # To use this parameter you must have created your schema by setting the `vectorizer:` property to 'multi2vec-clip' or 'img2vec-neural'
291 | near_image: near_image,
292 |
293 | near_vector: near_vector,
294 |
295 | with_hybrid: with_hybrid,
296 |
297 | bm25: bm25,
298 |
299 | near_object: near_object,
300 |
301 | ask: '{ question: "your-question?" }'
302 | )
303 |
304 | # Example queries:
305 | client.query.get class_name: 'Question', where: '{ operator: Like, valueText: "SCIENCE", path: ["category"] }', fields: 'answer question category', limit: "2"
306 |
307 | client.query.get class_name: 'Question', fields: 'answer question category _additional { id }', after: "3c5f7039-37f3-4244-b3e2-8f4a083e448d", limit: "1"
308 |
309 | # Named vector query - uses targetVectors
310 | query_title = client.query.get(
311 | class_name: 'ArticleNV',
312 | fields: 'title body _additional { id }',
313 | near_text: '{
314 | targetVectors: ["title"],
315 | concepts: ["quantum computers advances"]
316 | }',
317 | limit: "2"
318 | )
319 |
320 |
321 |
322 | ```
323 |
324 | #### Aggs{}
325 | ```ruby
326 | client.query.aggs(
327 | class_name: "Question",
328 | fields: 'meta { count }',
329 | group_by: ["category"],
330 | object_limit: "10",
331 | near_text: "{ concepts: [\"knowledge\"] }"
332 | )
333 | ```
334 |
335 | #### Explore{}
336 | ```ruby
337 | client.query.explore(
338 | fields: 'className',
339 | near_text: "{ concepts: [\"science\"] }",
340 | limit: "1"
341 | )
342 | ```
343 |
344 | ### Classification
345 | ```ruby
346 | # Start a classification
347 | client.classifications.create(
348 | type: "zeroshot",
349 | class_name: "Posts",
350 | classify_properties: ["hasColor"],
351 | based_on_properties: ["text"]
352 | )
353 |
354 | # Get the status, results and metadata of a previously created classification
355 | client.classifications.get(
356 | id: ""
357 | )
358 | ```
359 |
360 | ### Backups
361 | ```ruby
362 | # Create backup
363 | client.backups.create(
364 | backend: "filesystem",
365 | id: "my-first-backup",
366 | include: ["Question"]
367 | )
368 |
369 | # Get the backup
370 | client.backups.get(
371 | backend: "filesystem",
372 | id: "my-first-backup"
373 | )
374 |
375 | # Restore backup
376 | client.backups.restore(
377 | backend: "filesystem",
378 | id: "my-first-backup"
379 | )
380 |
381 | # Check the backup restore status
382 | client.backups.restore_status(
383 | backend: "filesystem",
384 | id: "my-first-backup"
385 | )
386 | ```
387 |
388 | ### Nodes
389 | ```ruby
390 | client.nodes
391 | ```
392 |
393 | ### Health
394 | ```ruby
395 | # Live determines whether the application is alive. It can be used for Kubernetes liveness probe.
396 | client.live?
397 | ```
398 |
399 | ```ruby
400 | # Live determines whether the application is ready to receive traffic. It can be used for Kubernetes readiness probe.
401 | client.ready?
402 | ```
403 |
404 | ### Tenants
405 |
406 | Any schema can be multi-tenant by passing in these options for to `schema.create()`.
407 |
408 | ```ruby
409 | client.schema.create(
410 | # Other keys...
411 | multi_tenant: true,
412 | auto_tenant_creation: true,
413 | auto_tenant_activation: true
414 | )
415 | ```
416 |
417 | See [Weaviate Multi-tenancy operations](https://weaviate.io/developers/weaviate/manage-data/multi-tenancy). Note that the mix of snake case(used by Ruby) and lower camel case(used by Weaviate) is intentional as that hash is passed directly to Weaviate.
418 |
419 | All data methods in this library support an optional `tenant` argument which must be passed if multi-tenancy is enabled on the related collection
420 |
421 | ## Development
422 |
423 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
424 |
425 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
426 |
427 | ## Contributing
428 |
429 | Bug reports and pull requests are welcome on GitHub at https://github.com/andreibondarev/weaviate.
430 |
431 | ## License
432 |
433 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
434 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 | require "rspec/core/rake_task"
5 | require "yard"
6 |
7 | RSpec::Core::RakeTask.new(:spec)
8 |
9 | task default: :spec
10 |
11 | YARD::Rake::YardocTask.new do |t|
12 | t.options = ["--fail-on-warning"]
13 | end
14 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "weaviate"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | client = Weaviate::Client.new(
15 | url: ENV["WEAVIATE_URL"],
16 | api_key: ENV["WEAVIATE_API_KEY"],
17 | model_service: :openai,
18 | model_service_api_key: ENV["MODEL_SERVICE_API_KEY"]
19 | )
20 |
21 | require "irb"
22 | IRB.start(__FILE__)
23 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/weaviate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "weaviate/version"
4 |
5 | module Weaviate
6 | autoload :Base, "weaviate/base"
7 | autoload :Client, "weaviate/client"
8 | autoload :Error, "weaviate/error"
9 | autoload :Schema, "weaviate/schema"
10 | autoload :Meta, "weaviate/meta"
11 | autoload :Objects, "weaviate/objects"
12 | autoload :OIDC, "weaviate/oidc"
13 | autoload :Query, "weaviate/query"
14 | autoload :Nodes, "weaviate/nodes"
15 | autoload :Health, "weaviate/health"
16 | autoload :Classifications, "weaviate/classifications"
17 | autoload :Backups, "weaviate/backups"
18 | end
19 |
--------------------------------------------------------------------------------
/lib/weaviate/backups.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Backups < Base
5 | PATH = "backups"
6 |
7 | def create(
8 | backend:,
9 | id:,
10 | include: nil,
11 | exclude: nil
12 | )
13 | response = client.connection.post("#{PATH}/#{backend}") do |req|
14 | req.body = {}
15 | req.body["id"] = id
16 | req.body["include"] = include if include
17 | req.body["exclude"] = exclude if exclude
18 | end
19 |
20 | response.body
21 | end
22 |
23 | def get(
24 | backend:,
25 | id:
26 | )
27 | response = client.connection.get("#{PATH}/#{backend}/#{id}")
28 | response.body
29 | end
30 |
31 | def restore(
32 | backend:,
33 | id:,
34 | include: nil,
35 | exclude: nil
36 | )
37 | response = client.connection.post("#{PATH}/#{backend}/#{id}/restore") do |req|
38 | req.body = {}
39 | req.body["include"] = include if include
40 | req.body["exclude"] = exclude if exclude
41 | end
42 |
43 | response.body
44 | end
45 |
46 | def restore_status(
47 | backend:,
48 | id:
49 | )
50 | response = client.connection.get("#{PATH}/#{backend}/#{id}/restore")
51 | response.body
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/weaviate/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Base
5 | attr_reader :client
6 |
7 | def initialize(client:)
8 | @client = client
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/weaviate/classifications.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Classifications < Base
5 | PATH = "classifications"
6 |
7 | def get(id:)
8 | response = client.connection.get("#{PATH}/#{id}")
9 | response.body
10 | end
11 |
12 | def create(
13 | class_name:,
14 | type:,
15 | classify_properties: nil,
16 | based_on_properties: nil,
17 | settings: nil,
18 | filters: nil
19 | )
20 | response = client.connection.post(PATH) do |req|
21 | req.body = {}
22 | req.body["class"] = class_name
23 | req.body["type"] = type
24 | req.body["classifyProperties"] = classify_properties if classify_properties
25 | req.body["basedOnProperties"] = based_on_properties if based_on_properties
26 | req.body["settings"] = settings if settings
27 | req.body["filters"] = filters if filters
28 | end
29 |
30 | if response.success?
31 | response.body
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/weaviate/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "faraday"
4 | require "graphlient"
5 |
6 | module Weaviate
7 | class Client
8 | attr_reader :url, :api_key, :model_service, :model_service_api_key, :adapter, :logger
9 |
10 | API_VERSION = "v1"
11 |
12 | API_KEY_HEADERS = {
13 | ollama: "X-Ollama-Not-Used",
14 | openai: "X-OpenAI-Api-Key",
15 | azure_openai: "X-Azure-Api-Key",
16 | cohere: "X-Cohere-Api-Key",
17 | huggingface: "X-HuggingFace-Api-Key",
18 | google_palm: "X-Palm-Api-Key"
19 | }
20 |
21 | def initialize(
22 | url:,
23 | api_key: nil,
24 | model_service: nil,
25 | model_service_api_key: nil,
26 | adapter: Faraday.default_adapter,
27 | logger: nil
28 | )
29 | validate_model_service!(model_service) unless model_service.nil?
30 |
31 | @url = url
32 | @api_key = api_key
33 | @model_service = model_service
34 | @model_service_api_key = model_service_api_key
35 | @adapter = adapter
36 | @logger = logger || Logger.new($stdout)
37 | end
38 |
39 | def oidc
40 | Weaviate::OIDC.new(client: self).get
41 | end
42 |
43 | def schema
44 | @schema ||= Weaviate::Schema.new(client: self)
45 | end
46 |
47 | def meta
48 | @meta ||= Weaviate::Meta.new(client: self)
49 | @meta.get
50 | end
51 |
52 | def nodes
53 | @nodes ||= Weaviate::Nodes.new(client: self)
54 | @nodes.list
55 | end
56 |
57 | def live?
58 | @health ||= Weaviate::Health.new(client: self)
59 | @health.live?
60 | end
61 |
62 | def ready?
63 | @health ||= Weaviate::Health.new(client: self)
64 | @health.ready?
65 | end
66 |
67 | def backups
68 | @backups ||= Weaviate::Backups.new(client: self)
69 | end
70 |
71 | def classifications
72 | @classifications ||= Weaviate::Classifications.new(client: self)
73 | end
74 |
75 | def objects
76 | @objects ||= Weaviate::Objects.new(client: self)
77 | end
78 |
79 | def query
80 | @query ||= Weaviate::Query.new(client: self)
81 | end
82 |
83 | def graphql
84 | headers = {}
85 |
86 | if model_service && model_service_api_key
87 | headers[API_KEY_HEADERS[model_service]] = model_service_api_key
88 | end
89 |
90 | if api_key
91 | headers["Authorization"] = "Bearer #{api_key}"
92 | end
93 |
94 | @graphql ||= Graphlient::Client.new(
95 | "#{url}/#{API_VERSION}/graphql",
96 | headers: headers,
97 | http_options: {
98 | read_timeout: 20,
99 | write_timeout: 30
100 | }
101 | )
102 | end
103 |
104 | def connection
105 | @connection ||= Faraday.new(url: "#{url}/#{API_VERSION}/") do |faraday|
106 | if api_key
107 | faraday.request :authorization, :Bearer, api_key
108 | end
109 | faraday.request :json
110 | faraday.response :logger, logger, {headers: true, bodies: true, errors: true}
111 | faraday.response :json, content_type: /\bjson$/
112 | faraday.response :raise_error
113 | faraday.adapter adapter
114 |
115 | faraday.headers[API_KEY_HEADERS[model_service]] = model_service_api_key if model_service && model_service_api_key
116 | end
117 | end
118 |
119 | private
120 |
121 | def validate_model_service!(model_service)
122 | unless API_KEY_HEADERS.key?(model_service)
123 | raise ArgumentError, "Invalid model service: #{model_service}. Acceptable values are: #{API_KEY_HEADERS.keys.join(", ")}"
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/lib/weaviate/error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Error < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/weaviate/health.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Health < Base
5 | PATH = ".well-known"
6 |
7 | def live?
8 | response = client.connection.get("#{PATH}/live")
9 | response.status == 200
10 | end
11 |
12 | def ready?
13 | response = client.connection.get("#{PATH}/ready")
14 | response.status == 200
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/weaviate/meta.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Meta < Base
5 | PATH = "meta"
6 |
7 | def get
8 | response = client.connection.get(PATH)
9 | response.body
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/weaviate/nodes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Nodes < Base
5 | PATH = "nodes"
6 |
7 | def list
8 | response = client.connection.get(PATH)
9 | response.body
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/weaviate/objects.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Objects < Base
5 | PATH = "objects"
6 |
7 | # Lists all data objects in reverse order of creation. The data will be returned as an array of objects.
8 | def list(
9 | class_name: nil,
10 | limit: nil,
11 | tenant: nil,
12 | offset: nil,
13 | after: nil,
14 | include: nil,
15 | sort: nil,
16 | order: nil
17 | )
18 | response = client.connection.get(PATH) do |req|
19 | req.params["class"] = class_name unless class_name.nil?
20 | req.params["tenant"] = tenant unless tenant.nil?
21 | req.params["limit"] = limit unless limit.nil?
22 | req.params["offset"] = offset unless offset.nil?
23 | req.params["after"] = after unless after.nil?
24 | req.params["include"] = include unless include.nil?
25 | req.params["sort"] = sort unless sort.nil?
26 | req.params["order"] = order unless order.nil?
27 | end
28 |
29 | response.body
30 | end
31 |
32 | # Create a new data object. The provided meta-data and schema values are validated.
33 | def create(
34 | class_name:,
35 | properties:,
36 | tenant: nil,
37 | consistency_level: nil,
38 | id: nil,
39 | vector: nil
40 | )
41 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
42 |
43 | response = client.connection.post(PATH) do |req|
44 | unless consistency_level.nil?
45 | req.params = {
46 | consistency_level: consistency_level.to_s.upcase
47 | }
48 | end
49 |
50 | req.body = {}
51 | req.body["class"] = class_name
52 | req.body["properties"] = properties
53 | req.body["tenant"] = tenant unless tenant.nil?
54 | req.body["id"] = id unless id.nil?
55 | req.body["vector"] = vector unless vector.nil?
56 | end
57 |
58 | response.body
59 | end
60 |
61 | # Batch create objects
62 | def batch_create(
63 | objects:,
64 | consistency_level: nil,
65 | tenant: nil
66 | )
67 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
68 |
69 | response = client.connection.post("batch/#{PATH}") do |req|
70 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
71 | req.body = {objects: objects}
72 | req.body["tenant"] = tenant unless tenant.nil?
73 | end
74 |
75 | response.body
76 | end
77 |
78 | # Get a single data object.
79 | def get(
80 | class_name:,
81 | id:,
82 | include: nil,
83 | consistency_level: nil,
84 | tenant: nil
85 | )
86 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
87 |
88 | response = client.connection.get("#{PATH}/#{class_name}/#{id}") do |req|
89 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
90 | req.params["tenant"] = tenant unless tenant.nil?
91 | req.params["include"] = include unless include.nil?
92 | end
93 |
94 | response.body
95 | end
96 |
97 | # Check if a data object exists
98 | def exists?(
99 | class_name:,
100 | id:,
101 | consistency_level: nil,
102 | tenant: nil
103 | )
104 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
105 |
106 | response = client.connection.head("#{PATH}/#{class_name}/#{id}") do |req|
107 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
108 | req.params["tenant"] = tenant unless tenant.nil?
109 | end
110 |
111 | response.status == 204
112 | end
113 |
114 | # Update an individual data object based on its uuid.
115 | def update(
116 | class_name:,
117 | id:,
118 | properties:,
119 | vector: nil,
120 | tenant: nil,
121 | consistency_level: nil
122 | )
123 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
124 |
125 | response = client.connection.patch("#{PATH}/#{class_name}/#{id}") do |req|
126 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
127 |
128 | req.body = {}
129 | req.body["id"] = id
130 | req.body["class"] = class_name
131 | req.body["properties"] = properties
132 | req.body["vector"] = vector unless vector.nil?
133 | req.body["tenant"] = tenant unless tenant.nil?
134 | end
135 |
136 | response.body
137 | end
138 |
139 | # Replace an individual data object based on its uuid.
140 | def replace(
141 | class_name:,
142 | id:,
143 | properties:,
144 | vector: nil,
145 | tenant: nil,
146 | consistency_level: nil
147 | )
148 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
149 |
150 | response = client.connection.put("#{PATH}/#{class_name}/#{id}") do |req|
151 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
152 |
153 | req.body = {}
154 | req.body["id"] = id
155 | req.body["class"] = class_name
156 | req.body["properties"] = properties
157 | req.body["vector"] = vector unless vector.nil?
158 | req.body["tenant"] = tenant unless tenant.nil?
159 | end
160 |
161 | response.body
162 | end
163 |
164 | # Delete an individual data object from Weaviate.
165 | def delete(
166 | class_name:,
167 | id:,
168 | consistency_level: nil,
169 | tenant: nil
170 | )
171 | validate_consistency_level!(consistency_level) unless consistency_level.nil?
172 |
173 | response = client.connection.delete("#{PATH}/#{class_name}/#{id}") do |req|
174 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
175 | req.params["tenant"] = tenant unless tenant.nil?
176 | end
177 |
178 | if response.success?
179 | response.body.empty?
180 | else
181 | response.body
182 | end
183 | end
184 |
185 | def batch_delete(
186 | class_name:,
187 | where:,
188 | consistency_level: nil,
189 | output: nil,
190 | dry_run: nil,
191 | tenant: nil
192 | )
193 | path = "batch/#{PATH}"
194 |
195 | unless consistency_level.nil?
196 | validate_consistency_level!(consistency_level)
197 | end
198 |
199 | response = client.connection.delete(path) do |req|
200 | req.body = {
201 | match: {
202 | class: class_name,
203 | where: where
204 | }
205 | }
206 | req.body["output"] = output unless output.nil?
207 | req.body["dryRun"] = dry_run unless dry_run.nil?
208 | req.params["consistency_level"] = consistency_level.to_s.upcase unless consistency_level.nil?
209 | req.params["tenant"] = tenant unless tenant.nil?
210 | end
211 |
212 | response.body
213 | end
214 |
215 | # Validate a data object
216 | def validate(
217 | class_name:,
218 | properties:,
219 | id: nil
220 | )
221 | response = client.connection.post("#{PATH}/validate") do |req|
222 | req.body = {}
223 | req.body["class"] = class_name
224 | req.body["properties"] = properties
225 | req.body["id"] = id unless id.nil?
226 | end
227 |
228 | if response.success?
229 | response.body.empty?
230 | else
231 | response.body
232 | end
233 | end
234 |
235 | private
236 |
237 | def validate_consistency_level!(consistency_level)
238 | unless %w[ONE QUORUM ALL].include?(consistency_level.to_s.upcase)
239 | raise ArgumentError, 'consistency_level must be either "ONE" or "QUORUM" OR "ALL"'
240 | end
241 | end
242 | end
243 | end
244 |
--------------------------------------------------------------------------------
/lib/weaviate/oidc.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class OIDC < Base
5 | PATH = ".well-known/openid-configuration"
6 |
7 | def get
8 | response = client.connection.get(PATH)
9 | response.body
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/weaviate/query.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Query < Base
5 | def get(
6 | class_name:,
7 | fields:,
8 | after: nil,
9 | tenant: nil,
10 | limit: nil,
11 | autocut: nil,
12 | offset: nil,
13 | sort: nil,
14 | where: nil,
15 | near_text: nil,
16 | near_vector: nil,
17 | near_image: nil,
18 | near_object: nil,
19 | with_hybrid: nil,
20 | bm25: nil,
21 | ask: nil
22 | )
23 | response = client.graphql.execute(
24 | get_query(
25 | class_name: class_name,
26 | tenant: tenant,
27 | fields: fields,
28 | autocut: autocut,
29 | sort: sort,
30 | where: where,
31 | near_text: near_text,
32 | near_vector: near_vector,
33 | near_image: near_image,
34 | near_object: near_object,
35 | with_hybrid: with_hybrid,
36 | bm25: bm25,
37 | ask: ask
38 | ),
39 | after: after,
40 | limit: limit,
41 | offset: offset
42 | )
43 | response.original_hash.dig("data", "Get", class_name)
44 | rescue Graphlient::Errors::ExecutionError => error
45 | raise Weaviate::Error.new(error.response.data.get.errors.messages.to_h)
46 | end
47 |
48 | def aggs(
49 | class_name:,
50 | fields: nil,
51 | tenant: nil,
52 | where: nil,
53 | object_limit: nil,
54 | near_text: nil,
55 | near_vector: nil,
56 | near_image: nil,
57 | near_object: nil,
58 | group_by: nil
59 | )
60 | response = client.graphql.execute(
61 | aggs_query(
62 | class_name: class_name,
63 | tenant: tenant,
64 | fields: fields,
65 | where: where,
66 | near_text: near_text,
67 | near_vector: near_vector,
68 | near_image: near_image,
69 | near_object: near_object
70 | ),
71 | group_by: group_by,
72 | object_limit: object_limit
73 | )
74 | response.original_hash.dig("data", "Aggregate", class_name)
75 | rescue Graphlient::Errors::ExecutionError => error
76 | raise Weaviate::Error.new(error.response.data.aggregate.errors.messages.to_h)
77 | end
78 |
79 | def explore(
80 | fields:,
81 | after: nil,
82 | limit: nil,
83 | offset: nil,
84 | sort: nil,
85 | where: nil,
86 | near_text: nil,
87 | near_vector: nil,
88 | near_image: nil,
89 | near_object: nil
90 | )
91 | response = client.graphql.execute(
92 | explore_query(
93 | fields: fields,
94 | sort: sort,
95 | where: where,
96 | near_text: near_text,
97 | near_vector: near_vector,
98 | near_image: near_image,
99 | near_object: near_object
100 | ),
101 | after: after,
102 | limit: limit,
103 | offset: offset
104 | )
105 | response.original_hash.dig("data", "Explore")
106 | rescue Graphlient::Errors::ExecutionError => error
107 | raise Weaviate::Error.new(error.to_s)
108 | end
109 |
110 | private
111 |
112 | def explore_query(
113 | fields:,
114 | where: nil,
115 | near_text: nil,
116 | near_vector: nil,
117 | near_image: nil,
118 | near_object: nil,
119 | sort: nil
120 | )
121 | client.graphql.parse <<~GRAPHQL
122 | query(
123 | $limit: Int,
124 | $offset: Int
125 | ) {
126 | Explore (
127 | limit: $limit,
128 | offset: $offset,
129 | #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
130 | #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
131 | #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
132 | #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
133 | #{(!where.nil?) ? "where: #{where}" : ""},
134 | #{(!sort.nil?) ? "sort: #{sort}" : ""}
135 | ) {
136 | #{fields}
137 | }
138 | }
139 | GRAPHQL
140 | end
141 |
142 | def get_query(
143 | class_name:,
144 | fields:,
145 | autocut: nil,
146 | tenant: nil,
147 | where: nil,
148 | near_text: nil,
149 | near_vector: nil,
150 | near_image: nil,
151 | near_object: nil,
152 | with_hybrid: nil,
153 | bm25: nil,
154 | ask: nil,
155 | sort: nil
156 | )
157 | client.graphql.parse <<~GRAPHQL
158 | query(
159 | $after: String,
160 | $limit: Int,
161 | $offset: Int,
162 | ) {
163 | Get {
164 | #{class_name}(
165 | after: $after,
166 | limit: $limit,
167 | offset: $offset,
168 | #{(!autocut.nil?) ? "autocut: #{autocut}" : ""},
169 | #{(!tenant.nil?) ? "tenant: \"#{tenant}\"" : ""},
170 | #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
171 | #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
172 | #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
173 | #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
174 | #{(!with_hybrid.nil?) ? "hybrid: #{with_hybrid}" : ""},
175 | #{(!bm25.nil?) ? "bm25: #{bm25}" : ""},
176 | #{(!ask.nil?) ? "ask: #{ask}" : ""},
177 | #{(!where.nil?) ? "where: #{where}" : ""},
178 | #{(!sort.nil?) ? "sort: #{sort}" : ""}
179 | ) {
180 | #{fields}
181 | }
182 | }
183 | }
184 | GRAPHQL
185 | end
186 |
187 | def aggs_query(
188 | class_name:,
189 | fields:,
190 | tenant: nil,
191 | where: nil,
192 | near_text: nil,
193 | near_vector: nil,
194 | near_image: nil,
195 | near_object: nil
196 | )
197 | client.graphql.parse <<~GRAPHQL
198 | query(
199 | $group_by: [String],
200 | $object_limit: Int,
201 | ) {
202 | Aggregate {
203 | #{class_name}(
204 | objectLimit: $object_limit,
205 | groupBy: $group_by,
206 | #{(!tenant.nil?) ? "tenant: \"#{tenant}\"" : ""},
207 | #{(!near_text.nil?) ? "nearText: #{near_text}" : ""},
208 | #{(!near_vector.nil?) ? "nearVector: #{near_vector}" : ""},
209 | #{(!near_image.nil?) ? "nearImage: #{near_image}" : ""},
210 | #{(!near_object.nil?) ? "nearObject: #{near_object}" : ""},
211 | #{(!where.nil?) ? "where: #{where}" : ""}
212 | ) {
213 | #{fields}
214 | }
215 | }
216 | }
217 | GRAPHQL
218 | end
219 | end
220 | end
221 |
--------------------------------------------------------------------------------
/lib/weaviate/schema.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | class Schema < Base
5 | PATH = "schema"
6 |
7 | # Dumps the current Weaviate schema. The result contains an array of objects.
8 | def list
9 | response = client.connection.get(PATH)
10 | response.body
11 | end
12 |
13 | # Get a single class from the schema
14 | def get(class_name:)
15 | response = client.connection.get("#{PATH}/#{class_name}")
16 |
17 | if response.success?
18 | response.body
19 | elsif response.status == 404
20 | response.reason_phrase
21 | end
22 | end
23 |
24 | # Create a new data object class in the schema.
25 | def create(
26 | class_name:,
27 | description: nil,
28 | properties: nil,
29 | multi_tenant: false,
30 | auto_tenant_creation: false,
31 | auto_tenant_activation: false,
32 | vector_index_type: nil,
33 | vector_index_config: nil,
34 | vectorizer: nil,
35 | module_config: nil,
36 | inverted_index_config: nil,
37 | replication_config: nil,
38 | vector_config: nil # added for named vector support
39 | )
40 | response = client.connection.post(PATH) do |req|
41 | req.body = {}
42 | req.body["class"] = class_name
43 | req.body["description"] = description unless description.nil?
44 | req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil?
45 | req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil?
46 | req.body["vectorizer"] = vectorizer unless vectorizer.nil?
47 | req.body["moduleConfig"] = module_config unless module_config.nil?
48 | req.body["properties"] = properties unless properties.nil?
49 |
50 | if multi_tenant
51 | req.body["multiTenancyConfig"] = {
52 | enabled: multi_tenant,
53 | autoTenantCreation: auto_tenant_creation,
54 | autoTenantActivation: auto_tenant_activation
55 | }
56 | end
57 |
58 | req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil?
59 | req.body["replicationConfig"] = replication_config unless replication_config.nil?
60 | req.body["vectorConfig"] = vector_config unless vector_config.nil? # Added for multi vector support
61 | end
62 |
63 | response.body
64 | end
65 |
66 | # Remove a class (and all data in the instances) from the schema.
67 | def delete(class_name:)
68 | response = client.connection.delete("#{PATH}/#{class_name}")
69 |
70 | if response.success?
71 | response.body.empty?
72 | else
73 | response.body
74 | end
75 | end
76 |
77 | # Update settings of an existing schema class.
78 | # TODO: Fix it.
79 | # This endpoint keeps returning the following error:
80 | # => {"error"=>[{"message"=>"properties cannot be updated through updating the class. Use the add property feature (e.g. \"POST /v1/schema/{className}/properties\") to add additional properties"}]}
81 | def update(
82 | class_name:,
83 | description: nil,
84 | vector_index_type: nil,
85 | vector_index_config: nil,
86 | vectorizer: nil,
87 | module_config: nil,
88 | properties: nil,
89 | inverted_index_config: nil,
90 | replication_config: nil
91 | )
92 | response = client.connection.put("#{PATH}/#{class_name}") do |req|
93 | req.body = {}
94 | req.body["class"] = class_name unless class_name.nil?
95 | req.body["description"] = description unless description.nil?
96 | req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil?
97 | req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil?
98 | req.body["vectorizer"] = vectorizer unless vectorizer.nil?
99 | req.body["moduleConfig"] = module_config unless module_config.nil?
100 | req.body["properties"] = properties unless properties.nil?
101 | req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil?
102 | req.body["replicationConfig"] = replication_config unless replication_config.nil?
103 | end
104 |
105 | if response.success?
106 | end
107 | response.body
108 | end
109 |
110 | # Adds one or more tenants to a class.
111 | def add_tenants(
112 | class_name:,
113 | tenants:
114 | )
115 | response = client.connection.post("#{PATH}/#{class_name}/tenants") do |req|
116 | tenants_str = tenants.map { |t| %({"name": "#{t}"}) }.join(", ")
117 | req.body = "[#{tenants_str}]"
118 | end
119 | response.body
120 | end
121 |
122 | # List tenants of a class.
123 | def list_tenants(class_name:)
124 | response = client.connection.get("#{PATH}/#{class_name}/tenants")
125 | response.body
126 | end
127 |
128 | # Remove one or more tenants from a class.
129 | def remove_tenants(
130 | class_name:,
131 | tenants:
132 | )
133 | response = client.connection.delete("#{PATH}/#{class_name}/tenants") do |req|
134 | req.body = tenants
135 | end
136 |
137 | if response.success?
138 | end
139 |
140 | response.body
141 | end
142 |
143 | # Add a property to an existing schema class.
144 | def add_property(
145 | class_name:,
146 | property:
147 | )
148 | response = client.connection.post("#{PATH}/#{class_name}/properties") do |req|
149 | req.body = property
150 | end
151 |
152 | if response.success?
153 | end
154 | response.body
155 | end
156 |
157 | # Inspect the shards of a class
158 | def shards(class_name:)
159 | response = client.connection.get("#{PATH}/#{class_name}/shards")
160 | response.body if response.success?
161 | end
162 |
163 | # Update shard status
164 | def update_shard_status(class_name:, shard_name:, status:)
165 | validate_status!(status)
166 |
167 | response = client.connection.put("#{PATH}/#{class_name}/shards/#{shard_name}") do |req|
168 | req.body = {}
169 | req.body["status"] = status
170 | end
171 | response.body if response.success?
172 | end
173 |
174 | private
175 |
176 | def validate_status!(status)
177 | unless %w[READONLY READY].include?(status.to_s.upcase)
178 | raise ArgumentError, 'status must be either "READONLY" or "READY"'
179 | end
180 | end
181 | end
182 | end
183 |
--------------------------------------------------------------------------------
/lib/weaviate/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Weaviate
4 | VERSION = "0.9.2"
5 | end
6 |
--------------------------------------------------------------------------------
/sig/weaviate.rbs:
--------------------------------------------------------------------------------
1 | module Weaviate
2 | VERSION: String
3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4 | end
5 |
--------------------------------------------------------------------------------
/spec/fixtures/backup.json:
--------------------------------------------------------------------------------
1 | {
2 | "backend": "filesystem",
3 | "classes": ["Question"],
4 | "id": "my-first-backup",
5 | "path": "/var/lib/weaviate/backups/my-first-backup",
6 | "status": "STARTED"
7 | }
8 |
--------------------------------------------------------------------------------
/spec/fixtures/batch_delete_object.json:
--------------------------------------------------------------------------------
1 | {
2 | "dryRun": false,
3 | "match": {
4 | "class": "Question",
5 | "where": {
6 | "operands": null,
7 | "operator": "Equal",
8 | "path": ["id"],
9 | "valueString": "1"
10 | }
11 | },
12 | "output": "minimal",
13 | "results": {
14 | "failed": 0,
15 | "limit": 10000,
16 | "matches": 1,
17 | "objects": null,
18 | "successful": 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/spec/fixtures/class.json:
--------------------------------------------------------------------------------
1 | {
2 | "class": "Question",
3 | "description": "Information from a Jeopardy! question",
4 | "properties": [
5 | {
6 | "dataType": ["text"],
7 | "description": "The question",
8 | "name": "question",
9 | "tokenization": "word"
10 | },
11 | {
12 | "dataType": ["text"],
13 | "description": "The answer",
14 | "name": "answer",
15 | "tokenization": "word"
16 | },
17 | {
18 | "dataType": ["text"],
19 | "description": "The category",
20 | "name": "category",
21 | "tokenization": "word"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/spec/fixtures/classes.json:
--------------------------------------------------------------------------------
1 | {
2 | "classes": [
3 | {
4 | "class": "Question",
5 | "description": "Information from a Jeopardy! question",
6 | "properties": [
7 | {
8 | "dataType": ["text"],
9 | "description": "The question",
10 | "name": "question",
11 | "tokenization": "word"
12 | },
13 | {
14 | "dataType": ["text"],
15 | "description": "The answer",
16 | "name": "answer",
17 | "tokenization": "word"
18 | },
19 | {
20 | "dataType": ["text"],
21 | "description": "The category",
22 | "name": "category",
23 | "tokenization": "word"
24 | }
25 | ]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/spec/fixtures/classification.json:
--------------------------------------------------------------------------------
1 | {
2 | "basedOnProperties": ["text"],
3 | "class": "Posts",
4 | "classifyProperties": ["hasColor"],
5 | "id": "1",
6 | "meta": {
7 | "completed": "0001-01-01T00:00:00.000Z",
8 | "started": "2023-04-03T19:58:13.965Z"
9 | },
10 | "status": "running",
11 | "type": "zeroshot"
12 | }
13 |
--------------------------------------------------------------------------------
/spec/fixtures/meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "hostname": "http://[::]:8080",
3 | "modules": {
4 | "generative-openai": {
5 | "documentationHref": "https://beta.openai.com/docs/api-reference/completions",
6 | "name": "Generative Search - OpenAI"
7 | },
8 | "qna-openai": {
9 | "documentationHref": "https://beta.openai.com/docs/api-reference/completions",
10 | "name": "OpenAI Question & Answering Module"
11 | },
12 | "text2vec-cohere": {
13 | "documentationHref": "https://docs.cohere.ai/embedding-wiki/",
14 | "name": "Cohere Module"
15 | },
16 | "text2vec-huggingface": {
17 | "documentationHref": "https://huggingface.co/docs/api-inference/detailed_parameters#feature-extraction-task",
18 | "name": "Hugging Face Module"
19 | },
20 | "text2vec-openai": {
21 | "documentationHref": "https://beta.openai.com/docs/guides/embeddings/what-are-embeddings",
22 | "name": "OpenAI Module"
23 | }
24 | },
25 | "version": "1.18.1"
26 | }
--------------------------------------------------------------------------------
/spec/fixtures/nodes.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "gitHash": "00c9d31",
5 | "name": "weaviate-0",
6 | "shards": [
7 | {
8 | "class": "Question",
9 | "name": "G1SJoaYlFnm4",
10 | "objectCount": 10
11 | }
12 | ],
13 | "stats": {
14 | "objectCount": 10,
15 | "shardCount": 1
16 | },
17 | "status": "HEALTHY",
18 | "version": "1.18.1"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/spec/fixtures/object.json:
--------------------------------------------------------------------------------
1 | {
2 | "class": "Question",
3 | "creationTimeUnix": 1679757150769,
4 | "id": "123",
5 | "lastUpdateTimeUnix": 1679757150769,
6 | "properties": {
7 | "answer": "42",
8 | "category": "philosophy",
9 | "question": "What is the meaning of life?"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/spec/fixtures/objects.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "class": "Question",
4 | "creationTimeUnix": 1680733926988,
5 | "id": "fe9c02d8-b186-406b-8902-47c4654a04f1",
6 | "lastUpdateTimeUnix": 1680733926988,
7 | "properties": {
8 | "answer": "42",
9 | "category": "philosophy",
10 | "question": "What is the meaning of life?"
11 | }
12 | },
13 | {
14 | "class": "Question",
15 | "creationTimeUnix": 1680733926988,
16 | "id": "a295e49b-de17-44ec-950f-88400377ee83",
17 | "lastUpdateTimeUnix": 1680733926988,
18 | "properties": {
19 | "answer": "42",
20 | "category": "math",
21 | "question": "What does 6 times 7 equal to?"
22 | }
23 | }
24 | ]
25 |
--------------------------------------------------------------------------------
/spec/fixtures/oidc.json:
--------------------------------------------------------------------------------
1 | {
2 | "href": "http://my-token-issuer/auth/realms/my-weaviate-usecase",
3 | "cliendID": "my-weaviate-client"
4 | }
--------------------------------------------------------------------------------
/spec/fixtures/shards.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "xyz123",
4 | "status": "READY"
5 | }
6 | ]
7 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "weaviate"
4 |
5 | RSpec.configure do |config|
6 | # Enable flags like --only-failures and --next-failure
7 | config.example_status_persistence_file_path = ".rspec_status"
8 |
9 | # Disable RSpec exposing methods globally on `Module` and `main`
10 | config.disable_monkey_patching!
11 |
12 | config.expect_with :rspec do |c|
13 | c.syntax = :expect
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/weaviate/backups_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Backups do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080"
9 | )
10 | }
11 |
12 | let(:backups) { client.backups }
13 | let(:backup_fixture) { JSON.parse(File.read("spec/fixtures/backup.json")) }
14 |
15 | describe "#create" do
16 | let(:response) { OpenStruct.new(success?: true, body: backup_fixture) }
17 |
18 | before do
19 | allow_any_instance_of(Faraday::Connection).to receive(:post)
20 | .with("backups/filesystem")
21 | .and_return(response)
22 | end
23 |
24 | it "creates the backup" do
25 | response = backups.create(
26 | backend: "filesystem",
27 | id: "my-first-backup",
28 | include: ["Question"]
29 | )
30 | expect(response["id"]).to eq("my-first-backup")
31 | expect(response["status"]).to eq("STARTED")
32 | end
33 | end
34 |
35 | describe "#get" do
36 | let(:response) { OpenStruct.new(success?: true, body: backup_fixture) }
37 |
38 | before do
39 | allow_any_instance_of(Faraday::Connection).to receive(:get)
40 | .with("backups/filesystem/my-first-backup")
41 | .and_return(response)
42 | end
43 |
44 | it "returns the backup" do
45 | response = backups.get(
46 | backend: "filesystem",
47 | id: "my-first-backup"
48 | )
49 | expect(response["id"]).to eq("my-first-backup")
50 | end
51 | end
52 |
53 | describe "#restore" do
54 | let(:response) { OpenStruct.new(success?: true, body: backup_fixture) }
55 |
56 | before do
57 | allow_any_instance_of(Faraday::Connection).to receive(:post)
58 | .with("backups/filesystem/my-first-backup/restore")
59 | .and_return(response)
60 | end
61 |
62 | it "restores the backup" do
63 | response = backups.restore(
64 | backend: "filesystem",
65 | id: "my-first-backup",
66 | include: ["Question"]
67 | )
68 | expect(response["id"]).to eq("my-first-backup")
69 | expect(response["status"]).to eq("STARTED")
70 | end
71 | end
72 |
73 | describe "#restore_status" do
74 | let(:response) { OpenStruct.new(success?: true, body: backup_fixture) }
75 |
76 | before do
77 | allow_any_instance_of(Faraday::Connection).to receive(:get)
78 | .with("backups/filesystem/my-first-backup/restore")
79 | .and_return(response)
80 | end
81 |
82 | it "returns the restore status" do
83 | response = backups.restore_status(
84 | backend: "filesystem",
85 | id: "my-first-backup"
86 | )
87 | expect(response["id"]).to eq("my-first-backup")
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/spec/weaviate/classifications_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Classifications do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080"
9 | )
10 | }
11 |
12 | let(:classifications) { client.classifications }
13 | let(:classification_fixture) { JSON.parse(File.read("spec/fixtures/classification.json")) }
14 |
15 | describe "#create" do
16 | let(:response) { OpenStruct.new(success?: true, body: classification_fixture) }
17 |
18 | before do
19 | allow_any_instance_of(Faraday::Connection).to receive(:post)
20 | .with("classifications")
21 | .and_return(response)
22 | end
23 |
24 | it "creates the classification" do
25 | response = classifications.create(
26 | class_name: "Posts",
27 | type: "zeroshot",
28 | classify_properties: ["hasColor"],
29 | based_on_properties: ["text"]
30 | )
31 | expect(response["type"]).to eq("zeroshot")
32 | expect(response["status"]).to eq("running")
33 | end
34 | end
35 |
36 | describe "#get" do
37 | let(:response) { OpenStruct.new(success?: true, body: classification_fixture) }
38 |
39 | before do
40 | allow_any_instance_of(Faraday::Connection).to receive(:get)
41 | .with("classifications/1")
42 | .and_return(response)
43 | end
44 |
45 | it "returns the classification" do
46 | response = classifications.get(id: "1")
47 | expect(response["id"]).to eq("1")
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/weaviate/client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Client do
6 | let(:client) {
7 | described_class.new(
8 | url: "http://localhost:8080",
9 | model_service: :openai,
10 | model_service_api_key: "123"
11 | )
12 | }
13 |
14 | describe "#initialize" do
15 | it "creates a client" do
16 | expect(client).to be_a(Weaviate::Client)
17 | end
18 |
19 | it "accepts a custom logger" do
20 | logger = Logger.new($stdout)
21 | client = described_class.new(
22 | url: "localhost:8080",
23 | api_key: "123",
24 | logger: logger
25 | )
26 | expect(client.logger).to eq(logger)
27 | end
28 | end
29 |
30 | describe "#schema" do
31 | it "returns a schema client" do
32 | expect(client.schema).to be_a(Weaviate::Schema)
33 | end
34 | end
35 |
36 | describe "#meta" do
37 | let(:fixture) { JSON.parse(File.read("spec/fixtures/meta.json")) }
38 | let(:response) { OpenStruct.new(body: fixture) }
39 |
40 | before do
41 | allow_any_instance_of(Faraday::Connection).to receive(:get)
42 | .with("meta")
43 | .and_return(response)
44 | end
45 |
46 | it "returns meta information" do
47 | response = client.meta
48 | expect(response).to be_a(Hash)
49 | expect(response["hostname"]).to eq("http://[::]:8080")
50 | end
51 | end
52 |
53 | describe "#objects" do
54 | it "returns an objects client" do
55 | expect(client.objects).to be_a(Weaviate::Objects)
56 | end
57 | end
58 |
59 | describe "#query" do
60 | it "returns a query client" do
61 | expect(client.query).to be_a(Weaviate::Query)
62 | end
63 | end
64 |
65 | describe "#ready?" do
66 | let(:response) { OpenStruct.new(status: 200) }
67 |
68 | before do
69 | allow_any_instance_of(Faraday::Connection).to receive(:get)
70 | .with(".well-known/ready")
71 | .and_return(response)
72 | end
73 |
74 | it "returns a query client" do
75 | expect(client.ready?).to eq(true)
76 | end
77 | end
78 |
79 | describe "#live?" do
80 | let(:response) { OpenStruct.new(status: 200) }
81 |
82 | before do
83 | allow_any_instance_of(Faraday::Connection).to receive(:get)
84 | .with(".well-known/live")
85 | .and_return(response)
86 | end
87 |
88 | it "returns a query client" do
89 | expect(client.live?).to eq(true)
90 | end
91 | end
92 |
93 | describe "#classifications" do
94 | it "returns a classifications client" do
95 | expect(client.classifications).to be_a(Weaviate::Classifications)
96 | end
97 | end
98 |
99 | describe "#backups" do
100 | it "returns a backups client" do
101 | expect(client.backups).to be_a(Weaviate::Backups)
102 | end
103 | end
104 |
105 | describe "#oidc" do
106 | let(:fixture) { JSON.parse(File.read("spec/fixtures/oidc.json")) }
107 | let(:response) { OpenStruct.new(body: fixture) }
108 |
109 | before do
110 | allow_any_instance_of(Faraday::Connection).to receive(:get)
111 | .with(".well-known/openid-configuration")
112 | .and_return(response)
113 | end
114 |
115 | it "returns an oidc client" do
116 | response = client.oidc
117 | expect(response).to be_a(Hash)
118 | expect(response["cliendID"]).to eq("my-weaviate-client")
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/spec/weaviate/health_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Health do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080"
9 | )
10 | }
11 |
12 | let(:response) {
13 | OpenStruct.new(status: 200)
14 | }
15 |
16 | describe "#live" do
17 | before do
18 | allow_any_instance_of(Faraday::Connection).to receive(:get)
19 | .with(".well-known/live")
20 | .and_return(response)
21 | end
22 |
23 | it "return 200" do
24 | expect(client.live?).to eq(true)
25 | end
26 | end
27 |
28 | describe "#ready" do
29 | before do
30 | allow_any_instance_of(Faraday::Connection).to receive(:get)
31 | .with(".well-known/ready")
32 | .and_return(response)
33 | end
34 |
35 | it "return 200" do
36 | expect(client.ready?).to eq(true)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/weaviate/nodes_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Nodes do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080"
9 | )
10 | }
11 |
12 | let(:nodes_fixture) { JSON.parse(File.read("spec/fixtures/nodes.json")) }
13 |
14 | describe "#list" do
15 | let(:response) {
16 | OpenStruct.new(body: nodes_fixture)
17 | }
18 |
19 | before do
20 | allow_any_instance_of(Faraday::Connection).to receive(:get)
21 | .with("nodes")
22 | .and_return(response)
23 | end
24 |
25 | it "return the nodes info" do
26 | expect(client.nodes).to be_a(Hash)
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/weaviate/objects_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Objects do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080",
9 | model_service: :openai,
10 | model_service_api_key: "123"
11 | )
12 | }
13 | let(:objects) { client.objects }
14 | let(:object_fixture) { JSON.parse(File.read("spec/fixtures/object.json")) }
15 | let(:objects_fixture) { JSON.parse(File.read("spec/fixtures/objects.json")) }
16 |
17 | describe "#create" do
18 | let(:response) { OpenStruct.new(success?: true, body: object_fixture) }
19 |
20 | before do
21 | allow_any_instance_of(Faraday::Connection).to receive(:post)
22 | .with("objects")
23 | .and_return(response)
24 | end
25 |
26 | it "creates an object" do
27 | response = objects.create(
28 | class_name: "Question",
29 | tenant: "tenant_name",
30 | properties: {
31 | answer: "42",
32 | question: "What is the meaning of life?",
33 | category: "philosophy"
34 | }
35 | )
36 | expect(response.dig("class")).to eq("Question")
37 | end
38 | end
39 |
40 | describe "#list" do
41 | let(:response) { OpenStruct.new(body: {"objects" => objects_fixture}) }
42 |
43 | before do
44 | allow_any_instance_of(Faraday::Connection).to receive(:get)
45 | .with("objects")
46 | .and_return(response)
47 | end
48 |
49 | it "returns objects" do
50 | response = objects.list
51 | expect(response.count).to eq(1)
52 | end
53 | end
54 |
55 | describe "#exists?" do
56 | let(:response) { OpenStruct.new(success?: true, status: 204, body: "") }
57 |
58 | before do
59 | allow_any_instance_of(Faraday::Connection).to receive(:head)
60 | .with("objects/Question/123")
61 | .and_return(response)
62 | end
63 |
64 | it "gets an object" do
65 | response = objects.exists?(
66 | class_name: "Question",
67 | id: "123"
68 | )
69 | expect(response).to eq(true)
70 | end
71 | end
72 |
73 | describe "#get" do
74 | let(:response) { OpenStruct.new(success?: true, body: object_fixture) }
75 |
76 | before do
77 | allow_any_instance_of(Faraday::Connection).to receive(:get)
78 | .with("objects/Question/123")
79 | .and_return(response)
80 | end
81 |
82 | it "gets an object" do
83 | response = objects.get(
84 | class_name: "Question",
85 | id: "123"
86 | )
87 | expect(response.dig("class")).to eq("Question")
88 | end
89 | end
90 |
91 | describe "#delete" do
92 | let(:response) { OpenStruct.new(success?: true, body: "") }
93 |
94 | before do
95 | allow_any_instance_of(Faraday::Connection).to receive(:delete)
96 | .with("objects/Question/123")
97 | .and_return(response)
98 | end
99 |
100 | it "deletes an object" do
101 | expect(objects.delete(
102 | class_name: "Question",
103 | id: "123"
104 | )).to be_equal(true)
105 | end
106 | end
107 |
108 | describe "#batch_delete" do
109 | let(:batch_delete_fixture) { JSON.parse(File.read("spec/fixtures/batch_delete_object.json")) }
110 | let(:response) { OpenStruct.new(success?: true, body: batch_delete_fixture) }
111 |
112 | before do
113 | allow_any_instance_of(Faraday::Connection).to receive(:delete)
114 | .with("batch/objects")
115 | .and_return(response)
116 | end
117 |
118 | it "returns the correct response" do
119 | response = objects.batch_delete(
120 | class_name: "Question",
121 | where: {
122 | valueString: "1",
123 | operator: "Equal",
124 | path: ["id"]
125 | }
126 | )
127 | expect(response.dig("results", "successful")).to eq(1)
128 | end
129 | end
130 |
131 | describe "#update" do
132 | let(:response) { OpenStruct.new(success?: true, body: object_fixture) }
133 |
134 | before do
135 | allow_any_instance_of(Faraday::Connection).to receive(:patch)
136 | .with("objects/Question/123")
137 | .and_return(response)
138 | end
139 |
140 | it "returns the schema" do
141 | response = objects.update(
142 | class_name: "Question",
143 | id: "123",
144 | properties: {
145 | question: "What does 6 times 7 equal to?",
146 | category: "math",
147 | answer: "42"
148 | }
149 | )
150 | expect(response.dig("class")).to eq("Question")
151 | end
152 | end
153 |
154 | describe "#replace" do
155 | let(:response) { OpenStruct.new(success?: true, body: object_fixture) }
156 |
157 | before do
158 | allow_any_instance_of(Faraday::Connection).to receive(:put)
159 | .with("objects/Question/123")
160 | .and_return(response)
161 | end
162 |
163 | it "returns the schema" do
164 | response = objects.replace(
165 | class_name: "Question",
166 | id: "123",
167 | properties: {
168 | question: "What does 6 times 7 equal to?",
169 | category: "math",
170 | answer: "42"
171 | }
172 | )
173 | expect(response.dig("class")).to eq("Question")
174 | end
175 | end
176 |
177 | describe "#batch_create" do
178 | let(:response) { OpenStruct.new(success?: true, body: objects_fixture) }
179 |
180 | before do
181 | allow_any_instance_of(Faraday::Connection).to receive(:post)
182 | .with("batch/objects")
183 | .and_return(response)
184 | end
185 |
186 | it "batch creates objects" do
187 | response = objects.batch_create(objects: [
188 | {
189 | class_name: "Question",
190 | properties: {
191 | answer: "42",
192 | question: "What is the meaning of life?",
193 | category: "philosophy"
194 | }
195 | }, {
196 | class_name: "Question",
197 | properties: {
198 | answer: "42",
199 | question: "What does 6 times 7 equal to?",
200 | category: "math"
201 | }
202 | }
203 | ])
204 | expect(response.count).to eq(2)
205 | end
206 | end
207 | end
208 |
--------------------------------------------------------------------------------
/spec/weaviate/query_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 | require "graphlient"
5 |
6 | RSpec.describe Weaviate::Query do
7 | let(:client) {
8 | Weaviate::Client.new(
9 | url: "http://localhost:8080",
10 | model_service: :openai,
11 | model_service_api_key: "123"
12 | )
13 | }
14 | let(:query) { client.query }
15 |
16 | describe "#get" do
17 | let(:response) {
18 | double(original_hash: {
19 | "data" => {
20 | "Get" => {
21 | "Question" => [
22 | {
23 | "category" => "SCIENCE",
24 | "question" => "In 1953 Watson & Crick built a model of the molecular structure of this, the gene-carrying substance"
25 | }
26 | ]
27 | }
28 | }
29 | })
30 | }
31 |
32 | let(:graphql_query) {
33 | <<-GRAPHQL
34 | query {
35 | Get {
36 | Question(nearText: { concepts: ["biology"] }, limit: 1) {
37 | question
38 | category
39 | }
40 | }
41 | }
42 | GRAPHQL
43 | }
44 |
45 | before do
46 | allow_any_instance_of(Graphlient::Client).to receive(:parse)
47 | .and_return(graphql_query)
48 |
49 | allow_any_instance_of(Graphlient::Client).to receive(:execute)
50 | .and_return(response)
51 | end
52 |
53 | it "returns the query" do
54 | data = query.get(
55 | class_name: "Question",
56 | fields: "question, category",
57 | near_text: "{ concepts: [\"biology\"] }",
58 | tenant: "tenant_name",
59 | limit: "1"
60 | )
61 |
62 | expect(data.count).to eq(1)
63 | expect(data.first["category"]).to eq("SCIENCE")
64 | expect(data.first["question"]).to eq("In 1953 Watson & Crick built a model of the molecular structure of this, the gene-carrying substance")
65 | end
66 | end
67 |
68 | describe "#aggs" do
69 | let(:response) {
70 | double(original_hash: {
71 | "data" => {
72 | "Aggregate" => {
73 | "Question" => [
74 | {
75 | "category" => "SCIENCE",
76 | "question" => "In 1953 Watson & Crick built a model of the molecular structure of this, the gene-carrying substance"
77 | }
78 | ]
79 | }
80 | }
81 | })
82 | }
83 |
84 | let(:graphql_query) {
85 | <<-GRAPHQL
86 | query {
87 | Aggregate {
88 | Question(nearText: { concepts: ["biology"] }) {
89 | meta {
90 | count
91 | }
92 | }
93 | }
94 | }
95 | GRAPHQL
96 | }
97 |
98 | before do
99 | allow_any_instance_of(Graphlient::Client).to receive(:parse)
100 | .and_return(graphql_query)
101 |
102 | allow_any_instance_of(Graphlient::Client).to receive(:execute)
103 | .and_return(response)
104 | end
105 |
106 | it "returns the query" do
107 | data = query.aggs(
108 | class_name: "Question",
109 | fields: "question, category",
110 | near_text: "{ concepts: [\"biology\"] }"
111 | )
112 |
113 | expect(data.count).to eq(1)
114 | expect(data.first["category"]).to eq("SCIENCE")
115 | expect(data.first["question"]).to eq("In 1953 Watson & Crick built a model of the molecular structure of this, the gene-carrying substance")
116 | end
117 | end
118 |
119 | describe "#explore" do
120 | let(:response) {
121 | double(original_hash: {
122 | "data" => {
123 | "Explore" => [{"certainty" => "0.9999", "class_name" => "Question"}]
124 | }
125 | })
126 | }
127 |
128 | let(:graphql_query) {
129 | <<-GRAPHQL
130 | query {
131 | Explore(
132 | limit: 1
133 | nearText: { concepts: ["biology"] }
134 | ) {
135 | certainty
136 | className
137 | }
138 | }
139 | GRAPHQL
140 | }
141 |
142 | before do
143 | allow_any_instance_of(Graphlient::Client).to receive(:parse)
144 | .and_return(graphql_query)
145 |
146 | allow_any_instance_of(Graphlient::Client).to receive(:execute)
147 | .and_return(response)
148 | end
149 |
150 | it "returns the query" do
151 | response = query.explore(
152 | fields: "certainty className",
153 | near_text: "{ concepts: [\"biology\"] }",
154 | limit: "1"
155 | )
156 |
157 | expect(response.count).to eq(1)
158 | expect(response.first["certainty"]).to eq("0.9999")
159 | expect(response.first["class_name"]).to eq("Question")
160 | end
161 | end
162 | end
163 |
--------------------------------------------------------------------------------
/spec/weaviate/schema_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | RSpec.describe Weaviate::Schema do
6 | let(:client) {
7 | Weaviate::Client.new(
8 | url: "http://localhost:8080",
9 | model_service: :openai,
10 | model_service_api_key: "123"
11 | )
12 | }
13 | let(:schema) { client.schema }
14 | let(:class_fixture) { JSON.parse(File.read("spec/fixtures/class.json")) }
15 | let(:classes_fixture) { JSON.parse(File.read("spec/fixtures/classes.json")) }
16 | let(:shard_fixture) { JSON.parse(File.read("spec/fixtures/shards.json")) }
17 |
18 | describe "#list" do
19 | let(:response) { OpenStruct.new(body: classes_fixture) }
20 |
21 | before do
22 | allow_any_instance_of(Faraday::Connection).to receive(:get)
23 | .with("schema")
24 | .and_return(response)
25 | end
26 |
27 | it "returns schemas" do
28 | expect(schema.list.dig("classes").count).to eq(1)
29 | end
30 | end
31 |
32 | describe "#get" do
33 | let(:response) { OpenStruct.new(success?: true, body: class_fixture) }
34 |
35 | before do
36 | allow_any_instance_of(Faraday::Connection).to receive(:get)
37 | .with("schema/Question")
38 | .and_return(response)
39 | end
40 |
41 | it "returns the schema" do
42 | response = schema.get(class_name: "Question")
43 | expect(response.dig("class")).to eq("Question")
44 | end
45 | end
46 |
47 | describe "#create" do
48 | let(:response) { OpenStruct.new(success?: true, body: class_fixture) }
49 |
50 | before do
51 | allow_any_instance_of(Faraday::Connection).to receive(:post)
52 | .with("schema")
53 | .and_return(response)
54 | end
55 |
56 | it "returns the schema" do
57 | response = schema.create(
58 | class_name: "Question",
59 | description: "Information from a Jeopardy! question",
60 | multi_tenant: true,
61 | properties: [
62 | {
63 | dataType: ["text"],
64 | description: "The question",
65 | name: "question"
66 | }, {
67 | dataType: ["text"],
68 | description: "The answer",
69 | name: "answer"
70 | }, {
71 | dataType: ["text"],
72 | description: "The category",
73 | name: "category"
74 | }
75 | ]
76 | )
77 | expect(response.dig("class")).to eq("Question")
78 | end
79 |
80 | context "when auto_tenant_creation passed" do
81 | before do
82 | @captured_request = nil
83 | allow_any_instance_of(Faraday::Connection).to receive(:post) do |_, path, &block|
84 | expect(path).to eq("schema")
85 | req = OpenStruct.new(body: {})
86 | block.call(req)
87 | @captured_request = req
88 | response
89 | end
90 | end
91 |
92 | it "sets up multiTenancyConfig with autoTenantCreation and autoTenantActivation enabled" do
93 | schema.create(
94 | class_name: "Question",
95 | description: "Information from a Jeopardy! question",
96 | multi_tenant: true,
97 | auto_tenant_creation: true,
98 | auto_tenant_activation: true
99 | )
100 |
101 | expect(@captured_request.body["multiTenancyConfig"]).to eq({enabled: true, autoTenantCreation: true, autoTenantActivation: true})
102 | end
103 | end
104 |
105 | context "named vector config" do
106 | before do
107 | @captured_request = nil
108 | allow_any_instance_of(Faraday::Connection).to receive(:post) do |_, path, &block|
109 | expect(path).to eq("schema")
110 | req = OpenStruct.new(body: {})
111 | block.call(req)
112 | @captured_request = req
113 | response
114 | end
115 | end
116 |
117 | it "sets up named vector config" do
118 | schema.create(
119 | class_name: "ArticleNV",
120 | description: "Articles with named vectors",
121 | properties: [
122 | {
123 | dataType: ["text"],
124 | name: "title"
125 | },
126 | {
127 | dataType: ["text"],
128 | name: "body"
129 | }
130 | ],
131 | vector_config: {
132 | title: {
133 | vectorizer: {
134 | "text2vec-openai": {
135 | properties: ["title"]
136 | }
137 | },
138 | vectorIndexType: "hnsw" # Adding vector index type
139 | },
140 | body: {
141 | vectorizer: {
142 | "text2vec-openai": {
143 | properties: ["body"]
144 | }
145 | },
146 | vectorIndexType: "hnsw" # Adding vector index type
147 | }
148 | }
149 | )
150 |
151 | expect(@captured_request.body["vectorConfig"]).to eq({title: {vectorizer: {"text2vec-openai": {properties: ["title"]}}, vectorIndexType: "hnsw"}, body: {vectorizer: {"text2vec-openai": {properties: ["body"]}}, vectorIndexType: "hnsw"}})
152 | end
153 | end
154 | end
155 |
156 | describe "#delete" do
157 | let(:response) { OpenStruct.new(success?: true, body: "") }
158 |
159 | before do
160 | allow_any_instance_of(Faraday::Connection).to receive(:delete)
161 | .with("schema/Question")
162 | .and_return(response)
163 | end
164 |
165 | it "returns the schema" do
166 | expect(schema.delete(
167 | class_name: "Question"
168 | )).to be_equal(true)
169 | end
170 | end
171 |
172 | describe "#update" do
173 | let(:response) { OpenStruct.new(success?: true, body: class_fixture) }
174 |
175 | before do
176 | allow_any_instance_of(Faraday::Connection).to receive(:put)
177 | .with("schema/Question")
178 | .and_return(response)
179 | end
180 |
181 | it "returns the schema" do
182 | response = schema.update(
183 | class_name: "Question",
184 | description: "Information from a Wheel of Fortune question"
185 | )
186 | expect(response.dig("class")).to eq("Question")
187 | end
188 | end
189 |
190 | describe "#add_property"
191 |
192 | describe "#add_tenants" do
193 | let(:response) { OpenStruct.new(success?: true, body: class_fixture) }
194 |
195 | before do
196 | allow_any_instance_of(Faraday::Connection).to receive(:post)
197 | .with("schema/Question/tenants")
198 | .and_return(response)
199 | end
200 |
201 | it "returns the schema" do
202 | response = schema.add_tenants(
203 | class_name: "Question",
204 | tenants: ["tenant1", "tenant2"]
205 | )
206 | expect(response.dig("class")).to eq("Question")
207 | end
208 | end
209 |
210 | describe "#list_tenants"
211 |
212 | describe "#remove_tenants"
213 |
214 | describe "#shards" do
215 | let(:response) { OpenStruct.new(success?: true, body: shard_fixture) }
216 |
217 | before do
218 | allow_any_instance_of(Faraday::Connection).to receive(:get)
219 | .with("schema/Question/shards")
220 | .and_return(response)
221 | end
222 |
223 | it "returns shards info" do
224 | expect(schema.shards(class_name: "Question")).to be_equal(shard_fixture)
225 | end
226 | end
227 |
228 | describe "update_shard_status" do
229 | let(:response) { OpenStruct.new(success?: true, body: shard_fixture) }
230 |
231 | before do
232 | allow_any_instance_of(Faraday::Connection).to receive(:put)
233 | .with("schema/Question/shards/xyz123")
234 | .and_return(response)
235 | end
236 |
237 | it "returns shards info" do
238 | expect(schema.update_shard_status(
239 | class_name: "Question",
240 | shard_name: "xyz123",
241 | status: "READONLY"
242 | )).to be_equal(shard_fixture)
243 | end
244 |
245 | it "raises the error if invalid status: is passed in" do
246 | expect {
247 | schema.update_shard_status(
248 | class_name: "Question",
249 | shard_name: "xyz123",
250 | status: "NOTAVAILABLE"
251 | )
252 | }.to raise_error(ArgumentError)
253 | end
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/spec/weaviate_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Weaviate do
4 | it "has a version number" do
5 | expect(Weaviate::VERSION).not_to be nil
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/weaviate.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/weaviate/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "weaviate-ruby"
7 | spec.version = Weaviate::VERSION
8 | spec.authors = ["Andrei Bondarev"]
9 | spec.email = ["andrei@sourcelabs.io", "andrei.bondarev13@gmail.com"]
10 |
11 | spec.summary = "Ruby wrapper for the Weaviate.io API"
12 | spec.description = "Ruby wrapper for the Weaviate.io API"
13 | spec.homepage = "https://github.com/andreibondarev/weaviate-ruby"
14 | spec.license = "MIT"
15 | spec.required_ruby_version = ">= 2.6.0"
16 |
17 | spec.metadata["homepage_uri"] = spec.homepage
18 | spec.metadata["source_code_uri"] = "https://github.com/andreibondarev/weaviate-ruby"
19 | spec.metadata["changelog_uri"] = "https://github.com/andreibondarev/weaviate-ruby/CHANGELOG.md"
20 |
21 | # Specify which files should be added to the gem when it is released.
22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23 | spec.files = Dir.chdir(__dir__) do
24 | `git ls-files -z`.split("\x0").reject do |f|
25 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
26 | end
27 | end
28 | spec.bindir = "exe"
29 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30 | spec.require_paths = ["lib"]
31 |
32 | # Uncomment to register a new dependency of your gem
33 | # spec.add_dependency "example-gem", "~> 1.0"
34 |
35 | # For more information and examples about making a new gem, check out our
36 | # guide at: https://bundler.io/guides/creating_gem.html
37 |
38 | spec.add_dependency "faraday", ">= 2.0.1", "< 3.0"
39 | spec.add_dependency "graphlient", ">= 0.7.0", "< 0.9.0"
40 | spec.add_development_dependency "pry-byebug", "~> 3.9"
41 | spec.add_development_dependency "yard"
42 | spec.add_development_dependency "rdiscount" # for github-flavored markdown in yard
43 | end
44 |
--------------------------------------------------------------------------------