diff --git a/lib/typesense.rb b/lib/typesense.rb index c05ccdb..d58cdcf 100644 --- a/lib/typesense.rb +++ b/lib/typesense.rb @@ -16,6 +16,8 @@ module Typesense require_relative 'typesense/override' require_relative 'typesense/synonyms' require_relative 'typesense/synonym' +require_relative 'typesense/synonym_sets' +require_relative 'typesense/synonym_set' require_relative 'typesense/aliases' require_relative 'typesense/alias' require_relative 'typesense/keys' diff --git a/lib/typesense/client.rb b/lib/typesense/client.rb index e7ef103..6e222aa 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, :nl_search_models + :multi_search, :analytics, :presets, :stemming, :nl_search_models, :synonym_sets def initialize(options = {}) @configuration = Configuration.new(options) @@ -21,6 +21,7 @@ def initialize(options = {}) @stemming = Stemming.new(@api_call) @presets = Presets.new(@api_call) @nl_search_models = NlSearchModels.new(@api_call) + @synonym_sets = SynonymSets.new(@api_call) end end end diff --git a/lib/typesense/synonym_set.rb b/lib/typesense/synonym_set.rb new file mode 100644 index 0000000..b28cbea --- /dev/null +++ b/lib/typesense/synonym_set.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Typesense + class SynonymSet + def initialize(synonym_set_name, api_call) + @synonym_set_name = synonym_set_name + @api_call = api_call + end + + def retrieve + @api_call.get(endpoint_path) + end + + def delete + @api_call.delete(endpoint_path) + end + + private + + def endpoint_path + "#{SynonymSets::RESOURCE_PATH}/#{URI.encode_www_form_component(@synonym_set_name)}" + end + end +end diff --git a/lib/typesense/synonym_sets.rb b/lib/typesense/synonym_sets.rb new file mode 100644 index 0000000..0a57e33 --- /dev/null +++ b/lib/typesense/synonym_sets.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Typesense + class SynonymSets + RESOURCE_PATH = '/synonym_sets' + + def initialize(api_call) + @api_call = api_call + @synonym_sets = {} + end + + def upsert(synonym_set_name, params) + @api_call.put(endpoint_path(synonym_set_name), params) + end + + def retrieve + @api_call.get(endpoint_path) + end + + def [](synonym_set_name) + @synonym_sets[synonym_set_name] ||= SynonymSet.new(synonym_set_name, @api_call) + end + + private + + def endpoint_path(operation = nil) + "#{RESOURCE_PATH}#{"/#{URI.encode_www_form_component(operation)}" unless operation.nil?}" + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 942b30f..42e2179 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -34,6 +34,37 @@ def typesense_healthy?(host = 'localhost', port = 8108) false end +def typesense_version + WebMock.allow_net_connect! + conn = Faraday.new('http://localhost:8108') + response = conn.get('/debug') do |req| + req.headers['X-TYPESENSE-API-KEY'] = 'xyz' + end + + if response.status == 200 && !response.body.empty? + debug_info = JSON.parse(response.body) + debug_info['version'] + end +rescue StandardError + nil +ensure + WebMock.disable_net_connect!(allow_localhost: true) +end + +def typesense_v30_or_above? + version = typesense_version + return false unless version + + return true if version == 'nightly' + + if version.match(/^v(\d+)/) + major_version = Regexp.last_match(1).to_i + return major_version >= 30 + end + + false +end + def ensure_typesense_running if typesense_healthy? puts '✅ Typesense is already running and healthy, ready for use in integration tests' @@ -96,7 +127,6 @@ def stop_typesense_if_started config.before(:suite) do ensure_typesense_running - WebMock.disable_net_connect! end config.after(:suite) do diff --git a/spec/typesense/analytics_rule_spec.rb b/spec/typesense/analytics_rule_spec.rb index 2ae99a7..ae0bf03 100644 --- a/spec/typesense/analytics_rule_spec.rb +++ b/spec/typesense/analytics_rule_spec.rb @@ -20,6 +20,10 @@ } end + before do + skip('Analytics is deprecated in Typesense v30+') if typesense_v30_or_above? + end + describe '#retrieve' do it 'returns the specified analytics rule' do stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules/search_suggestions', typesense.configuration.nodes[0])) diff --git a/spec/typesense/analytics_rules_spec.rb b/spec/typesense/analytics_rules_spec.rb index af72806..5e4ac65 100644 --- a/spec/typesense/analytics_rules_spec.rb +++ b/spec/typesense/analytics_rules_spec.rb @@ -20,6 +20,10 @@ } end + before do + skip('Analytics is deprecated in Typesense v30+') if typesense_v30_or_above? + end + describe '#upsert' do it 'creates a rule and returns it' do stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/analytics/rules/search_suggestions', typesense.configuration.nodes[0])) diff --git a/spec/typesense/synonym_set_spec.rb b/spec/typesense/synonym_set_spec.rb new file mode 100644 index 0000000..e4e1efe --- /dev/null +++ b/spec/typesense/synonym_set_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +describe Typesense::SynonymSet do + subject(:synonym_set) { typesense.synonym_sets['test-synonym-set'] } + + let(:typesense) do + Typesense::Client.new( + api_key: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ], + connection_timeout_seconds: 10, + retry_interval_seconds: 0.01 + ) + end + + let(:synonym_set_data) do + { + 'items' => [ + { + 'id' => 'dummy', + 'synonyms' => %w[foo bar baz], + 'root' => '' + } + ] + } + end + + before do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + # Create a test synonym set + typesense.synonym_sets.upsert('test-synonym-set', synonym_set_data) + end + + after do + # Clean up the test synonym set + typesense.synonym_sets['test-synonym-set'].delete + rescue StandardError + # Ignore errors if already deleted + end + + describe '#retrieve' do + it 'returns the specified synonym set' do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + result = synonym_set.retrieve + + expect(result['items']).to eq(synonym_set_data['items']) + end + end + + describe '#delete' do + it 'deletes the specified synonym set' do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + result = synonym_set.delete + + expect(result['name']).to eq('test-synonym-set') + + # Verify it's deleted by trying to retrieve it + expect { synonym_set.retrieve }.to raise_error(Typesense::Error::ObjectNotFound) + end + end +end diff --git a/spec/typesense/synonym_sets_spec.rb b/spec/typesense/synonym_sets_spec.rb new file mode 100644 index 0000000..0bccaf3 --- /dev/null +++ b/spec/typesense/synonym_sets_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +describe Typesense::SynonymSets do + subject(:synonym_sets) { typesense.synonym_sets } + + let(:typesense) do + Typesense::Client.new( + api_key: 'xyz', + nodes: [ + { + host: 'localhost', + port: 8108, + protocol: 'http' + } + ], + connection_timeout_seconds: 10, + retry_interval_seconds: 0.01 + ) + end + + let(:synonym_set_data) do + { + 'items' => [ + { + 'id' => 'dummy', + 'synonyms' => %w[foo bar baz], + 'root' => '' + } + ] + } + end + + before do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + end + + after do + next unless typesense_v30_or_above? + + # Clean up any created synonym sets + existing_sets = synonym_sets.retrieve + existing_sets.each do |set| + synonym_sets[set['name']].delete + end + rescue StandardError + # Ignore errors if no synonym sets exist + end + + describe '#upsert' do + it 'creates a synonym set and returns it' do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + result = synonym_sets.upsert('test-synonym-set', synonym_set_data) + + expect(result['items']).to eq(synonym_set_data['items']) + end + end + + describe '#retrieve' do + it 'retrieves all synonym sets' do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + # Create a synonym set first + synonym_sets.upsert('test-synonym-set', synonym_set_data) + + result = synonym_sets.retrieve + + expect(result).to be_an(Array) + expect(result.length).to be >= 1 + + # Find our test synonym set + test_set = result.find { |set| set['name'] == 'test-synonym-set' } + expect(test_set).not_to be_nil + expect(test_set['items']).to eq(synonym_set_data['items']) + end + end + + describe '#[]' do + it 'creates a synonym set object and returns it' do + skip('SynonymSets is only supported in Typesense v30+') unless typesense_v30_or_above? + + result = synonym_sets['test-synonym-set'] + + expect(result).to be_a(Typesense::SynonymSet) + expect(result.instance_variable_get(:@synonym_set_name)).to eq('test-synonym-set') + end + end +end diff --git a/spec/typesense/synonym_spec.rb b/spec/typesense/synonym_spec.rb index e3585f8..f7202ef 100644 --- a/spec/typesense/synonym_spec.rb +++ b/spec/typesense/synonym_spec.rb @@ -19,6 +19,10 @@ } end + before do + skip('Synonyms is deprecated in Typesense v30+, use SynonymSets instead') if typesense_v30_or_above? + end + describe '#retrieve' do it 'returns the specified synonym' do stub_request(:get, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/synonyms/synonym-set-1', typesense.configuration.nodes[0])) diff --git a/spec/typesense/synonyms_spec.rb b/spec/typesense/synonyms_spec.rb index 5df67ab..1ae650f 100644 --- a/spec/typesense/synonyms_spec.rb +++ b/spec/typesense/synonyms_spec.rb @@ -19,6 +19,10 @@ } end + before do + skip('Synonyms is deprecated in Typesense v30+, use SynonymSets instead') if typesense_v30_or_above? + end + describe '#upsert' do it 'creates an synonym rule and returns it' do stub_request(:put, Typesense::ApiCall.new(typesense.configuration).send(:uri_for, '/collections/companies/synonyms/synonym-set-1', typesense.configuration.nodes[0]))