From ac284bc44436518179afd09aedb04510104aedc9 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 1 Jul 2025 17:05:39 +0300 Subject: [PATCH 1/4] feat(nl-search): add natural language search models support - add `NlSearchModels` class with CRUD operations for model management - add `NlSearchModel` class for individual model operations - integrate `nl_search_models` into main `Client` class - add comprehensive integration tests for all model operations - support create, retrieve, update, and delete operations for nl search models --- lib/typesense.rb | 2 + lib/typesense/client.rb | 3 +- lib/typesense/nl_search_model.rb | 28 ++++++ lib/typesense/nl_search_models.rb | 24 +++++ spec/typesense/nl_search_models_spec.rb | 118 ++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 lib/typesense/nl_search_model.rb create mode 100644 lib/typesense/nl_search_models.rb create mode 100644 spec/typesense/nl_search_models_spec.rb 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/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/nl_search_model.rb b/lib/typesense/nl_search_model.rb new file mode 100644 index 0000000..4abb804 --- /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 \ No newline at end of file diff --git a/lib/typesense/nl_search_models.rb b/lib/typesense/nl_search_models.rb new file mode 100644 index 0000000..56d408e --- /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 \ No newline at end of file diff --git a/spec/typesense/nl_search_models_spec.rb b/spec/typesense/nl_search_models_spec.rb new file mode 100644 index 0000000..acaf252 --- /dev/null +++ b/spec/typesense/nl_search_models_spec.rb @@ -0,0 +1,118 @@ +# 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['OPENAI_API_KEY'], + 'max_bytes' => 16000, + '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(16000) + 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 { + client.nl_search_models[model_schema['id']].retrieve + }.to raise_error(Typesense::Error::ObjectNotFound) + end +end \ No newline at end of file From aef3dfde9084bb5c8103e0eb7e32dcd7e11a204b Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 1 Jul 2025 17:16:37 +0300 Subject: [PATCH 2/4] chore: lint --- lib/typesense/nl_search_model.rb | 2 +- lib/typesense/nl_search_models.rb | 2 +- spec/typesense/nl_search_models_spec.rb | 35 ++++++++++++------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/typesense/nl_search_model.rb b/lib/typesense/nl_search_model.rb index 4abb804..c14e506 100644 --- a/lib/typesense/nl_search_model.rb +++ b/lib/typesense/nl_search_model.rb @@ -25,4 +25,4 @@ def endpoint_path "#{NlSearchModels::RESOURCE_PATH}/#{URI.encode_www_form_component(@model_id)}" end end -end \ No newline at end of file +end diff --git a/lib/typesense/nl_search_models.rb b/lib/typesense/nl_search_models.rb index 56d408e..ae4579f 100644 --- a/lib/typesense/nl_search_models.rb +++ b/lib/typesense/nl_search_models.rb @@ -21,4 +21,4 @@ def [](model_id) @nl_search_models[model_id] ||= NlSearchModel.new(model_id, @api_call) end end -end \ No newline at end of file +end diff --git a/spec/typesense/nl_search_models_spec.rb b/spec/typesense/nl_search_models_spec.rb index acaf252..51e9064 100644 --- a/spec/typesense/nl_search_models_spec.rb +++ b/spec/typesense/nl_search_models_spec.rb @@ -14,14 +14,13 @@ ) 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['OPENAI_API_KEY'], - 'max_bytes' => 16000, + 'api_key' => ENV.fetch('OPENAI_API_KEY', nil), + 'max_bytes' => 16_000, 'temperature' => 0.0 } end @@ -42,12 +41,12 @@ def cleanup_model(model_id) 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(16000) + expect(response['max_bytes']).to eq(16_000) expect(response['temperature']).to eq(0.0) ensure cleanup_model(model_schema['id']) @@ -56,7 +55,7 @@ def cleanup_model(model_id) 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 @@ -69,14 +68,14 @@ def cleanup_model(model_id) 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 @@ -86,15 +85,15 @@ def cleanup_model(model_id) 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') @@ -105,14 +104,14 @@ def cleanup_model(model_id) 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 { + + expect do client.nl_search_models[model_schema['id']].retrieve - }.to raise_error(Typesense::Error::ObjectNotFound) + end.to raise_error(Typesense::Error::ObjectNotFound) end -end \ No newline at end of file +end From c8a33231502337a0eb5cec13b2b097ff4e4d7ada Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 1 Jul 2025 17:39:39 +0300 Subject: [PATCH 3/4] chore: add vendor to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 12ee3e5..0eb6d81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.bundle/ +/vendor/ /.yardoc /_yardoc/ /coverage/ From 2949110a4f5db0def488ad666fad1f20ade06311 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 1 Jul 2025 17:42:26 +0300 Subject: [PATCH 4/4] chore: lint (unrelated) --- lib/typesense/analytics_rules.rb | 2 +- lib/typesense/documents.rb | 2 +- lib/typesense/overrides.rb | 2 +- lib/typesense/presets.rb | 2 +- lib/typesense/stemming_dictionaries.rb | 2 +- lib/typesense/synonyms.rb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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/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/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