diff --git a/.gitignore b/.gitignore index 12ee3e5..0eb6d81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.bundle/ +/vendor/ /.yardoc /_yardoc/ /coverage/ diff --git a/lib/typesense.rb b/lib/typesense.rb index 07a74a1..c05ccdb 100644 --- a/lib/typesense.rb +++ b/lib/typesense.rb @@ -35,3 +35,5 @@ module Typesense require_relative 'typesense/stemming' require_relative 'typesense/stemming_dictionaries' require_relative 'typesense/stemming_dictionary' +require_relative 'typesense/nl_search_models' +require_relative 'typesense/nl_search_model' diff --git a/lib/typesense/analytics_rules.rb b/lib/typesense/analytics_rules.rb index 45e65d8..d54a3de 100644 --- a/lib/typesense/analytics_rules.rb +++ b/lib/typesense/analytics_rules.rb @@ -24,7 +24,7 @@ def [](rule_name) private def endpoint_path(operation = nil) - "#{AnalyticsRules::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + "#{AnalyticsRules::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" end end end diff --git a/lib/typesense/client.rb b/lib/typesense/client.rb index ca70491..e7ef103 100644 --- a/lib/typesense/client.rb +++ b/lib/typesense/client.rb @@ -3,7 +3,7 @@ module Typesense class Client attr_reader :configuration, :collections, :aliases, :keys, :debug, :health, :metrics, :stats, :operations, - :multi_search, :analytics, :presets, :stemming + :multi_search, :analytics, :presets, :stemming, :nl_search_models def initialize(options = {}) @configuration = Configuration.new(options) @@ -20,6 +20,7 @@ def initialize(options = {}) @analytics = Analytics.new(@api_call) @stemming = Stemming.new(@api_call) @presets = Presets.new(@api_call) + @nl_search_models = NlSearchModels.new(@api_call) end end end diff --git a/lib/typesense/documents.rb b/lib/typesense/documents.rb index a5423f7..fdc6530 100644 --- a/lib/typesense/documents.rb +++ b/lib/typesense/documents.rb @@ -88,7 +88,7 @@ def truncate private def endpoint_path(operation = nil) - "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Documents::RESOURCE_PATH}#{operation.nil? ? '' : "/#{operation}"}" + "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Documents::RESOURCE_PATH}#{"/#{operation}" unless operation.nil?}" end end end diff --git a/lib/typesense/nl_search_model.rb b/lib/typesense/nl_search_model.rb new file mode 100644 index 0000000..c14e506 --- /dev/null +++ b/lib/typesense/nl_search_model.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Typesense + class NlSearchModel + def initialize(model_id, api_call) + @model_id = model_id + @api_call = api_call + end + + def retrieve + @api_call.get(endpoint_path) + end + + def update(update_schema) + @api_call.put(endpoint_path, update_schema) + end + + def delete + @api_call.delete(endpoint_path) + end + + private + + def endpoint_path + "#{NlSearchModels::RESOURCE_PATH}/#{URI.encode_www_form_component(@model_id)}" + end + end +end diff --git a/lib/typesense/nl_search_models.rb b/lib/typesense/nl_search_models.rb new file mode 100644 index 0000000..ae4579f --- /dev/null +++ b/lib/typesense/nl_search_models.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Typesense + class NlSearchModels + RESOURCE_PATH = '/nl_search_models' + + def initialize(api_call) + @api_call = api_call + @nl_search_models = {} + end + + def create(schema) + @api_call.post(RESOURCE_PATH, schema) + end + + def retrieve + @api_call.get(RESOURCE_PATH) + end + + def [](model_id) + @nl_search_models[model_id] ||= NlSearchModel.new(model_id, @api_call) + end + end +end diff --git a/lib/typesense/overrides.rb b/lib/typesense/overrides.rb index 61957c5..d07f342 100644 --- a/lib/typesense/overrides.rb +++ b/lib/typesense/overrides.rb @@ -25,7 +25,7 @@ def [](override_id) private def endpoint_path(operation = nil) - "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Overrides::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Overrides::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" end end end diff --git a/lib/typesense/presets.rb b/lib/typesense/presets.rb index 83a2841..03548d6 100644 --- a/lib/typesense/presets.rb +++ b/lib/typesense/presets.rb @@ -24,7 +24,7 @@ def [](preset_name) private def endpoint_path(operation = nil) - "#{Presets::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + "#{Presets::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" end end end diff --git a/lib/typesense/stemming_dictionaries.rb b/lib/typesense/stemming_dictionaries.rb index 0eee2c1..e71702d 100644 --- a/lib/typesense/stemming_dictionaries.rb +++ b/lib/typesense/stemming_dictionaries.rb @@ -43,7 +43,7 @@ def [](dict_id) private def endpoint_path(operation = nil) - "#{StemmingDictionaries::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + "#{StemmingDictionaries::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" end end end diff --git a/lib/typesense/synonyms.rb b/lib/typesense/synonyms.rb index 993e0ff..6f36a56 100644 --- a/lib/typesense/synonyms.rb +++ b/lib/typesense/synonyms.rb @@ -25,7 +25,7 @@ def [](synonym_id) private def endpoint_path(operation = nil) - "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Synonyms::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + "#{Collections::RESOURCE_PATH}/#{URI.encode_www_form_component(@collection_name)}#{Synonyms::RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" end end end diff --git a/spec/typesense/nl_search_models_spec.rb b/spec/typesense/nl_search_models_spec.rb new file mode 100644 index 0000000..51e9064 --- /dev/null +++ b/spec/typesense/nl_search_models_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +describe 'NlSearchModels', :integration do + # These tests require external API access and should not run on CI by default + next unless ENV['OPENAI_API_KEY'] + + let(:client) do + Typesense::Client.new( + nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + def create_model_schema(id_suffix = nil) + model_id = id_suffix ? "test_openai_model_#{id_suffix}" : "test_openai_model_#{Time.now.to_i}_#{rand(1000)}" + { + 'id' => model_id, + 'model_name' => 'openai/gpt-4.1', + 'api_key' => ENV.fetch('OPENAI_API_KEY', nil), + 'max_bytes' => 16_000, + 'temperature' => 0.0 + } + end + + def cleanup_model(model_id) + client.nl_search_models[model_id].delete + rescue Typesense::Error::ObjectNotFound + # Model doesn't exist, that's fine + end + + before do + WebMock.disable! + end + + after do + WebMock.enable! + end + + it 'can create a nl search model' do + model_schema = create_model_schema('create_test') + + begin + response = client.nl_search_models.create(model_schema) + expect(response['id']).to eq(model_schema['id']) + expect(response['model_name']).to eq('openai/gpt-4.1') + expect(response['max_bytes']).to eq(16_000) + expect(response['temperature']).to eq(0.0) + ensure + cleanup_model(model_schema['id']) + end + end + + it 'can retrieve a specific nl search model' do + model_schema = create_model_schema('retrieve_test') + + begin + client.nl_search_models.create(model_schema) + response = client.nl_search_models[model_schema['id']].retrieve + expect(response['id']).to eq(model_schema['id']) + expect(response['model_name']).to eq('openai/gpt-4.1') + ensure + cleanup_model(model_schema['id']) + end + end + + it 'can retrieve all nl search models' do + model_schema = create_model_schema('list_test') + + begin + client.nl_search_models.create(model_schema) + + response = client.nl_search_models.retrieve + expect(response).to be_an(Array) + expect(response.length).to be >= 1 + + model_ids = response.map { |model| model['id'] } + expect(model_ids).to include(model_schema['id']) + ensure + cleanup_model(model_schema['id']) + end + end + + it 'can update a nl search model' do + model_schema = create_model_schema('update_test') + + begin + client.nl_search_models.create(model_schema) + + update_schema = { + 'temperature' => 0.5, + 'system_prompt' => 'Updated system prompt for electronics search' + } + + response = client.nl_search_models[model_schema['id']].update(update_schema) + expect(response['temperature']).to eq(0.5) + expect(response['system_prompt']).to eq('Updated system prompt for electronics search') + ensure + cleanup_model(model_schema['id']) + end + end + + it 'can delete a nl search model' do + model_schema = create_model_schema('delete_test') + + client.nl_search_models.create(model_schema) + + response = client.nl_search_models[model_schema['id']].delete + expect(response['id']).to eq(model_schema['id']) + + expect do + client.nl_search_models[model_schema['id']].retrieve + end.to raise_error(Typesense::Error::ObjectNotFound) + end +end