diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55e8919..0d7d999 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.7', '3.0', '3.2'] + ruby-version: ['3.0', '3.2', '3.3'] services: typesense: - image: typesense/typesense:27.1 + image: typesense/typesense:28.0.rc36 ports: - 8108:8108 volumes: @@ -37,8 +37,8 @@ jobs: bundler-cache: true # runs 'bundle install' and caches installed gems automatically - run: bundle exec rubocop - run: bundle exec rspec --format documentation - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: coverage + name: coverage-ruby-${{ matrix.ruby-version }} path: coverage/ retention-days: 1 diff --git a/lib/typesense.rb b/lib/typesense.rb index 81b163e..07a74a1 100644 --- a/lib/typesense.rb +++ b/lib/typesense.rb @@ -32,3 +32,6 @@ module Typesense require_relative 'typesense/stats' require_relative 'typesense/operations' require_relative 'typesense/error' +require_relative 'typesense/stemming' +require_relative 'typesense/stemming_dictionaries' +require_relative 'typesense/stemming_dictionary' diff --git a/lib/typesense/client.rb b/lib/typesense/client.rb index 9bb4df2..ca70491 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 + :multi_search, :analytics, :presets, :stemming def initialize(options = {}) @configuration = Configuration.new(options) @@ -18,6 +18,7 @@ def initialize(options = {}) @stats = Stats.new(@api_call) @operations = Operations.new(@api_call) @analytics = Analytics.new(@api_call) + @stemming = Stemming.new(@api_call) @presets = Presets.new(@api_call) end end diff --git a/lib/typesense/stemming.rb b/lib/typesense/stemming.rb new file mode 100644 index 0000000..71dc885 --- /dev/null +++ b/lib/typesense/stemming.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Typesense + class Stemming + RESOURCE_PATH = '/stemming' + + def initialize(api_call) + @api_call = api_call + end + + def dictionaries + @dictionaries ||= StemmingDictionaries.new(@api_call) + end + end +end diff --git a/lib/typesense/stemming_dictionaries.rb b/lib/typesense/stemming_dictionaries.rb new file mode 100644 index 0000000..0eee2c1 --- /dev/null +++ b/lib/typesense/stemming_dictionaries.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Typesense + class StemmingDictionaries + RESOURCE_PATH = '/stemming/dictionaries' + + def initialize(api_call) + @api_call = api_call + @dictionaries = {} + end + + def upsert(dict_id, words_and_roots_combinations) + words_and_roots_combinations_in_jsonl = if words_and_roots_combinations.is_a?(Array) + words_and_roots_combinations.map { |combo| Oj.dump(combo, mode: :compat) }.join("\n") + else + words_and_roots_combinations + end + + result_in_jsonl = @api_call.perform_request( + 'post', + endpoint_path('import'), + query_parameters: { id: dict_id }, + body_parameters: words_and_roots_combinations_in_jsonl, + additional_headers: { 'Content-Type' => 'text/plain' } + ) + + if words_and_roots_combinations.is_a?(Array) + result_in_jsonl.split("\n").map { |r| Oj.load(r) } + else + result_in_jsonl + end + end + + def retrieve + response = @api_call.get(endpoint_path) + response || { 'dictionaries' => [] } + end + + def [](dict_id) + @dictionaries[dict_id] ||= StemmingDictionary.new(dict_id, @api_call) + end + + private + + def endpoint_path(operation = nil) + "#{StemmingDictionaries::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" + end + end +end diff --git a/lib/typesense/stemming_dictionary.rb b/lib/typesense/stemming_dictionary.rb new file mode 100644 index 0000000..3107517 --- /dev/null +++ b/lib/typesense/stemming_dictionary.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Typesense + class StemmingDictionary + def initialize(id, api_call) + @dict_id = id + @api_call = api_call + end + + def retrieve + @api_call.get(endpoint_path) + end + + private + + def endpoint_path + "#{StemmingDictionaries::RESOURCE_PATH}/#{URI.encode_www_form_component(@dict_id)}" + end + end +end diff --git a/spec/typesense/collections_spec.rb b/spec/typesense/collections_spec.rb index ded43c6..8f0c5e7 100644 --- a/spec/typesense/collections_spec.rb +++ b/spec/typesense/collections_spec.rb @@ -100,6 +100,7 @@ 'optional' => false, 'sort' => false, 'stem' => false, + 'stem_dictionary' => '', 'store' => true }, { @@ -112,6 +113,7 @@ 'optional' => false, 'sort' => true, 'stem' => false, + 'stem_dictionary' => '', 'store' => true }, { @@ -124,6 +126,7 @@ 'optional' => false, 'sort' => false, 'stem' => false, + 'stem_dictionary' => '', 'store' => true } ] diff --git a/spec/typesense/stemming_spec.rb b/spec/typesense/stemming_spec.rb new file mode 100644 index 0000000..fbf6bff --- /dev/null +++ b/spec/typesense/stemming_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative '../spec_helper' + +describe 'StemmingDictionaries' do + let(:client) do + Typesense::Client.new( + nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }], + api_key: 'xyz', + connection_timeout_seconds: 10 + ) + end + + let(:dictionary_id) { 'test_dictionary' } + let(:dictionary) do + [ + { 'root' => 'exampleRoot1', 'word' => 'exampleWord1' }, + { 'root' => 'exampleRoot2', 'word' => 'exampleWord2' } + ] + end + + before do + WebMock.disable! + # Create the dictionary at the start of each test + client.stemming.dictionaries.upsert(dictionary_id, dictionary) + end + + after do + WebMock.enable! + end + + it 'can upsert a dictionary' do + response = client.stemming.dictionaries.upsert(dictionary_id, dictionary) + expect(response).to eq(dictionary) + end + + it 'can retrieve a dictionary' do + response = client.stemming.dictionaries[dictionary_id].retrieve + expect(response['id']).to eq(dictionary_id) + expect(response['words']).to eq(dictionary) + end + + it 'can retrieve all dictionaries' do + response = client.stemming.dictionaries.retrieve + expect(response['dictionaries'].length).to eq(1) + expect(response['dictionaries'][0]).to eq(dictionary_id) + end +end diff --git a/typesense.gemspec b/typesense.gemspec index ea507aa..e4e139f 100644 --- a/typesense.gemspec +++ b/typesense.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + spec.add_dependency 'base64', '~> 0.2.0' spec.add_dependency 'faraday', '~> 2.8' spec.add_dependency 'oj', '~> 3.16' spec.metadata['rubygems_mfa_required'] = 'true'