diff --git a/README.md b/README.md index 0d60922..982bdb4 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,11 @@ client = Weaviate::Client.new( ) ``` -### Using the Schema endpoints +### Using the Collections endpoints ```ruby -# Creating a new data object class in the schema -client.schema.create( +# Creating a new collection +client.collections.create( class_name: 'Question', description: 'Information from a Jeopardy! question', properties: [ @@ -70,21 +70,21 @@ client.schema.create( vectorizer: "text2vec-openai" ) -# Get a single class from the schema -client.schema.get(class_name: 'Question') +# Get a single collection +client.collections.get(class_name: 'Question') -# Get the schema -client.schema.list() +# List all collections +client.collections.list() -# Update settings of an existing schema class. +# Update settings of an existing collection. # Does not support modifying existing properties. -client.schema.update( +client.collections.update( class_name: 'Question', description: 'Information from a Wheel of Fortune question' ) # Adding a new property -client.schema.add_property( +client.collections.add_property( class_name: 'Question', property: { "dataType": ["boolean"], @@ -92,14 +92,14 @@ client.schema.add_property( } ) -# Inspect the shards of a class -client.schema.shards(class_name: 'Question') +# Inspect the shards of a collection +client.collections.shards(class_name: 'Question') -# Remove a class (and all data in the instances) from the schema. -client.schema.delete(class_name: 'Question') +# Remove a collection (and all data in the instances). +client.collections.delete(class_name: 'Question') -# Creating a new data object class in the schema while configuring the vectorizer on the schema and on individual properties (Ollama example) -client.schema.create( +# Creating a new collection while configuring the vectorizer on the collection and on individual properties (Ollama example) +client.collections.create( class_name: 'Question', description: 'Information from a Jeopardy! question', properties: [ @@ -140,9 +140,9 @@ client.schema.create( }, ) -# Creating named schemas +# Creating collections with named vectors -client.schema.create( +client.collections.create( class_name: 'ArticleNV', description: 'Articles with named vectors', properties: [ @@ -403,10 +403,10 @@ client.ready? ### Tenants -Any schema can be multi-tenant by passing in these options for to `schema.create()`. +Any collection can be multi-tenant by passing in these options for to `collections.create()`. ```ruby -client.schema.create( +client.collections.create( # Other keys... multi_tenant: true, auto_tenant_creation: true, diff --git a/lib/weaviate.rb b/lib/weaviate.rb index f8290d0..7fe4e23 100644 --- a/lib/weaviate.rb +++ b/lib/weaviate.rb @@ -6,6 +6,7 @@ module Weaviate autoload :Base, "weaviate/base" autoload :Client, "weaviate/client" autoload :Error, "weaviate/error" + autoload :Collection, "weaviate/collection" autoload :Schema, "weaviate/schema" autoload :Meta, "weaviate/meta" autoload :Objects, "weaviate/objects" diff --git a/lib/weaviate/client.rb b/lib/weaviate/client.rb index ed8da57..e2c515a 100644 --- a/lib/weaviate/client.rb +++ b/lib/weaviate/client.rb @@ -40,8 +40,12 @@ def oidc Weaviate::OIDC.new(client: self).get end + def collections + @collections ||= Weaviate::Collection.new(client: self) + end + def schema - @schema ||= Weaviate::Schema.new(client: self) + @schema ||= collections end def meta diff --git a/lib/weaviate/collection.rb b/lib/weaviate/collection.rb new file mode 100644 index 0000000..1c7ba9c --- /dev/null +++ b/lib/weaviate/collection.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +module Weaviate + class Collection < Base + PATH = "schema" + + # Dumps the current Weaviate schema. The result contains an array of objects. + def list + response = client.connection.get(PATH) + response.body + end + + # Get a single class from the schema + def get(class_name:) + response = client.connection.get("#{PATH}/#{class_name}") + + if response.success? + response.body + elsif response.status == 404 + response.reason_phrase + end + end + + # Create a new data object class in the schema. + def create( + class_name:, + description: nil, + properties: nil, + multi_tenant: false, + auto_tenant_creation: false, + auto_tenant_activation: false, + vector_index_type: nil, + vector_index_config: nil, + vectorizer: nil, + module_config: nil, + inverted_index_config: nil, + replication_config: nil, + vector_config: nil # added for named vector support + ) + response = client.connection.post(PATH) do |req| + req.body = {} + req.body["class"] = class_name + req.body["description"] = description unless description.nil? + req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil? + req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil? + req.body["vectorizer"] = vectorizer unless vectorizer.nil? + req.body["moduleConfig"] = module_config unless module_config.nil? + req.body["properties"] = properties unless properties.nil? + + if multi_tenant + req.body["multiTenancyConfig"] = { + enabled: multi_tenant, + autoTenantCreation: auto_tenant_creation, + autoTenantActivation: auto_tenant_activation + } + end + + req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil? + req.body["replicationConfig"] = replication_config unless replication_config.nil? + req.body["vectorConfig"] = vector_config unless vector_config.nil? # Added for multi vector support + end + + response.body + end + + # Remove a class (and all data in the instances) from the schema. + def delete(class_name:) + response = client.connection.delete("#{PATH}/#{class_name}") + + if response.success? + response.body.empty? + else + response.body + end + end + + # Update settings of an existing schema class. + # TODO: Fix it. + # This endpoint keeps returning the following error: + # => {"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"}]} + def update( + class_name:, + description: nil, + vector_index_type: nil, + vector_index_config: nil, + vectorizer: nil, + module_config: nil, + properties: nil, + inverted_index_config: nil, + replication_config: nil + ) + response = client.connection.put("#{PATH}/#{class_name}") do |req| + req.body = {} + req.body["class"] = class_name unless class_name.nil? + req.body["description"] = description unless description.nil? + req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil? + req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil? + req.body["vectorizer"] = vectorizer unless vectorizer.nil? + req.body["moduleConfig"] = module_config unless module_config.nil? + req.body["properties"] = properties unless properties.nil? + req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil? + req.body["replicationConfig"] = replication_config unless replication_config.nil? + end + + if response.success? + end + response.body + end + + # Adds one or more tenants to a class. + def add_tenants( + class_name:, + tenants: + ) + response = client.connection.post("#{PATH}/#{class_name}/tenants") do |req| + tenants_str = tenants.map { |t| %({"name": "#{t}"}) }.join(", ") + req.body = "[#{tenants_str}]" + end + response.body + end + + # List tenants of a class. + def list_tenants(class_name:) + response = client.connection.get("#{PATH}/#{class_name}/tenants") + response.body + end + + # Remove one or more tenants from a class. + def remove_tenants( + class_name:, + tenants: + ) + response = client.connection.delete("#{PATH}/#{class_name}/tenants") do |req| + req.body = tenants + end + + if response.success? + end + + response.body + end + + # Add a property to an existing schema class. + def add_property( + class_name:, + property: + ) + response = client.connection.post("#{PATH}/#{class_name}/properties") do |req| + req.body = property + end + + if response.success? + end + response.body + end + + # Inspect the shards of a class + def shards(class_name:) + response = client.connection.get("#{PATH}/#{class_name}/shards") + response.body if response.success? + end + + # Update shard status + def update_shard_status(class_name:, shard_name:, status:) + validate_status!(status) + + response = client.connection.put("#{PATH}/#{class_name}/shards/#{shard_name}") do |req| + req.body = {} + req.body["status"] = status + end + response.body if response.success? + end + + private + + def validate_status!(status) + unless %w[READONLY READY].include?(status.to_s.upcase) + raise ArgumentError, 'status must be either "READONLY" or "READY"' + end + end + end +end diff --git a/lib/weaviate/schema.rb b/lib/weaviate/schema.rb index 8bf9bf9..f513291 100644 --- a/lib/weaviate/schema.rb +++ b/lib/weaviate/schema.rb @@ -1,182 +1,6 @@ # frozen_string_literal: true module Weaviate - class Schema < Base - PATH = "schema" - - # Dumps the current Weaviate schema. The result contains an array of objects. - def list - response = client.connection.get(PATH) - response.body - end - - # Get a single class from the schema - def get(class_name:) - response = client.connection.get("#{PATH}/#{class_name}") - - if response.success? - response.body - elsif response.status == 404 - response.reason_phrase - end - end - - # Create a new data object class in the schema. - def create( - class_name:, - description: nil, - properties: nil, - multi_tenant: false, - auto_tenant_creation: false, - auto_tenant_activation: false, - vector_index_type: nil, - vector_index_config: nil, - vectorizer: nil, - module_config: nil, - inverted_index_config: nil, - replication_config: nil, - vector_config: nil # added for named vector support - ) - response = client.connection.post(PATH) do |req| - req.body = {} - req.body["class"] = class_name - req.body["description"] = description unless description.nil? - req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil? - req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil? - req.body["vectorizer"] = vectorizer unless vectorizer.nil? - req.body["moduleConfig"] = module_config unless module_config.nil? - req.body["properties"] = properties unless properties.nil? - - if multi_tenant - req.body["multiTenancyConfig"] = { - enabled: multi_tenant, - autoTenantCreation: auto_tenant_creation, - autoTenantActivation: auto_tenant_activation - } - end - - req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil? - req.body["replicationConfig"] = replication_config unless replication_config.nil? - req.body["vectorConfig"] = vector_config unless vector_config.nil? # Added for multi vector support - end - - response.body - end - - # Remove a class (and all data in the instances) from the schema. - def delete(class_name:) - response = client.connection.delete("#{PATH}/#{class_name}") - - if response.success? - response.body.empty? - else - response.body - end - end - - # Update settings of an existing schema class. - # TODO: Fix it. - # This endpoint keeps returning the following error: - # => {"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"}]} - def update( - class_name:, - description: nil, - vector_index_type: nil, - vector_index_config: nil, - vectorizer: nil, - module_config: nil, - properties: nil, - inverted_index_config: nil, - replication_config: nil - ) - response = client.connection.put("#{PATH}/#{class_name}") do |req| - req.body = {} - req.body["class"] = class_name unless class_name.nil? - req.body["description"] = description unless description.nil? - req.body["vectorIndexType"] = vector_index_type unless vector_index_type.nil? - req.body["vectorIndexConfig"] = vector_index_config unless vector_index_config.nil? - req.body["vectorizer"] = vectorizer unless vectorizer.nil? - req.body["moduleConfig"] = module_config unless module_config.nil? - req.body["properties"] = properties unless properties.nil? - req.body["invertedIndexConfig"] = inverted_index_config unless inverted_index_config.nil? - req.body["replicationConfig"] = replication_config unless replication_config.nil? - end - - if response.success? - end - response.body - end - - # Adds one or more tenants to a class. - def add_tenants( - class_name:, - tenants: - ) - response = client.connection.post("#{PATH}/#{class_name}/tenants") do |req| - tenants_str = tenants.map { |t| %({"name": "#{t}"}) }.join(", ") - req.body = "[#{tenants_str}]" - end - response.body - end - - # List tenants of a class. - def list_tenants(class_name:) - response = client.connection.get("#{PATH}/#{class_name}/tenants") - response.body - end - - # Remove one or more tenants from a class. - def remove_tenants( - class_name:, - tenants: - ) - response = client.connection.delete("#{PATH}/#{class_name}/tenants") do |req| - req.body = tenants - end - - if response.success? - end - - response.body - end - - # Add a property to an existing schema class. - def add_property( - class_name:, - property: - ) - response = client.connection.post("#{PATH}/#{class_name}/properties") do |req| - req.body = property - end - - if response.success? - end - response.body - end - - # Inspect the shards of a class - def shards(class_name:) - response = client.connection.get("#{PATH}/#{class_name}/shards") - response.body if response.success? - end - - # Update shard status - def update_shard_status(class_name:, shard_name:, status:) - validate_status!(status) - - response = client.connection.put("#{PATH}/#{class_name}/shards/#{shard_name}") do |req| - req.body = {} - req.body["status"] = status - end - response.body if response.success? - end - - private - - def validate_status!(status) - unless %w[READONLY READY].include?(status.to_s.upcase) - raise ArgumentError, 'status must be either "READONLY" or "READY"' - end - end - end + # Schema is an alias for Collection, maintained for backward compatibility. + Schema = Collection end diff --git a/spec/weaviate/client_spec.rb b/spec/weaviate/client_spec.rb index 2f00c7c..2205431 100644 --- a/spec/weaviate/client_spec.rb +++ b/spec/weaviate/client_spec.rb @@ -27,9 +27,15 @@ end end + describe "#collections" do + it "returns a collection client" do + expect(client.collections).to be_a(Weaviate::Collection) + end + end + describe "#schema" do - it "returns a schema client" do - expect(client.schema).to be_a(Weaviate::Schema) + it "returns a collection client (backward compatibility)" do + expect(client.schema).to be_a(Weaviate::Collection) end end diff --git a/spec/weaviate/collection_spec.rb b/spec/weaviate/collection_spec.rb new file mode 100644 index 0000000..59874cc --- /dev/null +++ b/spec/weaviate/collection_spec.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Weaviate::Collection do + let(:client) { + Weaviate::Client.new( + url: "http://localhost:8080", + model_service: :openai, + model_service_api_key: "123" + ) + } + let(:collection) { client.collections } + let(:class_fixture) { JSON.parse(File.read("spec/fixtures/class.json")) } + let(:classes_fixture) { JSON.parse(File.read("spec/fixtures/classes.json")) } + let(:shard_fixture) { JSON.parse(File.read("spec/fixtures/shards.json")) } + + describe "#list" do + let(:response) { OpenStruct.new(body: classes_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:get) + .with("schema") + .and_return(response) + end + + it "returns collections" do + expect(collection.list.dig("classes").count).to eq(1) + end + end + + describe "#get" do + let(:response) { OpenStruct.new(success?: true, body: class_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:get) + .with("schema/Question") + .and_return(response) + end + + it "returns the collection" do + response = collection.get(class_name: "Question") + expect(response.dig("class")).to eq("Question") + end + end + + describe "#create" do + let(:response) { OpenStruct.new(success?: true, body: class_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:post) + .with("schema") + .and_return(response) + end + + it "returns the collection" do + response = collection.create( + class_name: "Question", + description: "Information from a Jeopardy! question", + multi_tenant: true, + properties: [ + { + dataType: ["text"], + description: "The question", + name: "question" + }, { + dataType: ["text"], + description: "The answer", + name: "answer" + }, { + dataType: ["text"], + description: "The category", + name: "category" + } + ] + ) + expect(response.dig("class")).to eq("Question") + end + + context "when auto_tenant_creation passed" do + before do + @captured_request = nil + allow_any_instance_of(Faraday::Connection).to receive(:post) do |_, path, &block| + expect(path).to eq("schema") + req = OpenStruct.new(body: {}) + block.call(req) + @captured_request = req + response + end + end + + it "sets up multiTenancyConfig with autoTenantCreation and autoTenantActivation enabled" do + collection.create( + class_name: "Question", + description: "Information from a Jeopardy! question", + multi_tenant: true, + auto_tenant_creation: true, + auto_tenant_activation: true + ) + + expect(@captured_request.body["multiTenancyConfig"]).to eq({enabled: true, autoTenantCreation: true, autoTenantActivation: true}) + end + end + + context "named vector config" do + before do + @captured_request = nil + allow_any_instance_of(Faraday::Connection).to receive(:post) do |_, path, &block| + expect(path).to eq("schema") + req = OpenStruct.new(body: {}) + block.call(req) + @captured_request = req + response + end + end + + it "sets up named vector config" do + collection.create( + class_name: "ArticleNV", + description: "Articles with named vectors", + properties: [ + { + dataType: ["text"], + name: "title" + }, + { + dataType: ["text"], + name: "body" + } + ], + vector_config: { + title: { + vectorizer: { + "text2vec-openai": { + properties: ["title"] + } + }, + vectorIndexType: "hnsw" + }, + body: { + vectorizer: { + "text2vec-openai": { + properties: ["body"] + } + }, + vectorIndexType: "hnsw" + } + } + ) + + expect(@captured_request.body["vectorConfig"]).to eq({title: {vectorizer: {"text2vec-openai": {properties: ["title"]}}, vectorIndexType: "hnsw"}, body: {vectorizer: {"text2vec-openai": {properties: ["body"]}}, vectorIndexType: "hnsw"}}) + end + end + end + + describe "#delete" do + let(:response) { OpenStruct.new(success?: true, body: "") } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:delete) + .with("schema/Question") + .and_return(response) + end + + it "returns true" do + expect(collection.delete( + class_name: "Question" + )).to be_equal(true) + end + end + + describe "#update" do + let(:response) { OpenStruct.new(success?: true, body: class_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:put) + .with("schema/Question") + .and_return(response) + end + + it "returns the collection" do + response = collection.update( + class_name: "Question", + description: "Information from a Wheel of Fortune question" + ) + expect(response.dig("class")).to eq("Question") + end + end + + describe "#add_property" + + describe "#add_tenants" do + let(:response) { OpenStruct.new(success?: true, body: class_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:post) + .with("schema/Question/tenants") + .and_return(response) + end + + it "returns the collection" do + response = collection.add_tenants( + class_name: "Question", + tenants: ["tenant1", "tenant2"] + ) + expect(response.dig("class")).to eq("Question") + end + end + + describe "#list_tenants" + + describe "#remove_tenants" + + describe "#shards" do + let(:response) { OpenStruct.new(success?: true, body: shard_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:get) + .with("schema/Question/shards") + .and_return(response) + end + + it "returns shards info" do + expect(collection.shards(class_name: "Question")).to be_equal(shard_fixture) + end + end + + describe "update_shard_status" do + let(:response) { OpenStruct.new(success?: true, body: shard_fixture) } + + before do + allow_any_instance_of(Faraday::Connection).to receive(:put) + .with("schema/Question/shards/xyz123") + .and_return(response) + end + + it "returns shards info" do + expect(collection.update_shard_status( + class_name: "Question", + shard_name: "xyz123", + status: "READONLY" + )).to be_equal(shard_fixture) + end + + it "raises the error if invalid status: is passed in" do + expect { + collection.update_shard_status( + class_name: "Question", + shard_name: "xyz123", + status: "NOTAVAILABLE" + ) + }.to raise_error(ArgumentError) + end + end +end