├── .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 | Weaviate logo 5 | +   6 | Ruby logo 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 | ![Tests status](https://github.com/andreibondarev/weaviate-ruby/actions/workflows/ci.yml/badge.svg) 16 | [![Gem Version](https://badge.fury.io/rb/weaviate-ruby.svg)](https://badge.fury.io/rb/weaviate-ruby) 17 | [![Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/gems/weaviate-ruby) 18 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/andreibondarev/weaviate-ruby/blob/main/LICENSE.txt) 19 | [![](https://dcbadge.vercel.app/api/server/WDARp7J2n8?compact=true&style=flat)](https://discord.gg/WDARp7J2n8) 20 | [![X](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40rushing_andrei)](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 | --------------------------------------------------------------------------------