From 00f6fd321b4f13e41f0e3b0bad7b00049ae52cfe Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 17:43:43 +0200 Subject: [PATCH 1/9] ci: bump upload artifact version to 4 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 55e8919..33850d6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ 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 path: coverage/ From 80792269c70306f4b93bd64c4eb09bcd155be867 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:21:10 +0200 Subject: [PATCH 2/9] feat(stemming): add support for stemming dictionaries API - Add `Stemming`, `StemmingDictionaries` and `StemmingDictionary` classes - Add `stemming` attribute to `Client` class - Add integration tests for dictionary operations - Update `collections_spec.rb` with stem dictionary field --- lib/typesense.rb | 3 ++ lib/typesense/client.rb | 3 +- lib/typesense/stemming.rb | 14 ++++++++ lib/typesense/stemming_dictionaries.rb | 48 ++++++++++++++++++++++++++ lib/typesense/stemming_dictionary.rb | 20 +++++++++++ spec/typesense/collections_spec.rb | 3 ++ spec/typesense/stemming_spec.rb | 40 +++++++++++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 lib/typesense/stemming.rb create mode 100644 lib/typesense/stemming_dictionaries.rb create mode 100644 lib/typesense/stemming_dictionary.rb create mode 100644 spec/typesense/stemming_spec.rb diff --git a/lib/typesense.rb b/lib/typesense.rb index 81b163e..625a7cb 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' \ No newline at end of file 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..4efa31a --- /dev/null +++ b/lib/typesense/stemming.rb @@ -0,0 +1,14 @@ +# 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..d7a7e00 --- /dev/null +++ b/lib/typesense/stemming_dictionaries.rb @@ -0,0 +1,48 @@ +# 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 + @api_call.get(endpoint_path) + 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..21fdd17 --- /dev/null +++ b/spec/typesense/stemming_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require_relative "../spec_helper" + +describe "Stemming Dictionaries" 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 { WebMock.disable! } + after { WebMock.enable! } + + 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 From 6add5a03ffd9adf15c6286d3a93326a20d42067f Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:30:16 +0200 Subject: [PATCH 3/9] chore: lint --- lib/typesense.rb | 2 +- lib/typesense/stemming.rb | 3 ++- lib/typesense/stemming_dictionaries.rb | 20 ++++++++--------- spec/typesense/stemming_spec.rb | 31 +++++++++++++------------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/typesense.rb b/lib/typesense.rb index 625a7cb..07a74a1 100644 --- a/lib/typesense.rb +++ b/lib/typesense.rb @@ -34,4 +34,4 @@ module Typesense require_relative 'typesense/error' require_relative 'typesense/stemming' require_relative 'typesense/stemming_dictionaries' -require_relative 'typesense/stemming_dictionary' \ No newline at end of file +require_relative 'typesense/stemming_dictionary' diff --git a/lib/typesense/stemming.rb b/lib/typesense/stemming.rb index 4efa31a..71dc885 100644 --- a/lib/typesense/stemming.rb +++ b/lib/typesense/stemming.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true + module Typesense class Stemming - RESOURCE_PATH = "/stemming" + RESOURCE_PATH = '/stemming' def initialize(api_call) @api_call = api_call diff --git a/lib/typesense/stemming_dictionaries.rb b/lib/typesense/stemming_dictionaries.rb index d7a7e00..848ea95 100644 --- a/lib/typesense/stemming_dictionaries.rb +++ b/lib/typesense/stemming_dictionaries.rb @@ -2,7 +2,7 @@ module Typesense class StemmingDictionaries - RESOURCE_PATH = "/stemming/dictionaries" + RESOURCE_PATH = '/stemming/dictionaries' def initialize(api_call) @api_call = api_call @@ -11,21 +11,21 @@ def initialize(api_call) 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 + 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"), + 'post', + endpoint_path('import'), query_parameters: { id: dict_id }, body_parameters: words_and_roots_combinations_in_jsonl, - additional_headers: { "Content-Type" => "text/plain" }, + additional_headers: { 'Content-Type' => 'text/plain' } ) if words_and_roots_combinations.is_a?(Array) - result_in_jsonl.split("\n").map { |r| Oj.load(r) } + result_in_jsonl.split('\n').map { |r| Oj.load(r) } else result_in_jsonl end @@ -42,7 +42,7 @@ def [](dict_id) private def endpoint_path(operation = nil) - "#{StemmingDictionaries::RESOURCE_PATH}#{operation.nil? ? "" : "/#{URI.encode_www_form_component(operation)}"}" + "#{StemmingDictionaries::RESOURCE_PATH}#{operation.nil? ? '' : "/#{URI.encode_www_form_component(operation)}"}" end end end diff --git a/spec/typesense/stemming_spec.rb b/spec/typesense/stemming_spec.rb index 21fdd17..ed645af 100644 --- a/spec/typesense/stemming_spec.rb +++ b/spec/typesense/stemming_spec.rb @@ -1,40 +1,41 @@ # frozen_string_literal: true -require_relative "../spec_helper" -describe "Stemming Dictionaries" do +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, + nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }], + api_key: 'xyz', + connection_timeout_seconds: 10 ) end - let(:dictionary_id) { "test_dictionary" } + let(:dictionary_id) { 'test_dictionary' } let(:dictionary) do [ - { "root" => "exampleRoot1", "word" => "exampleWord1" }, - { "root" => "exampleRoot2", "word" => "exampleWord2" }, + { 'root' => 'exampleRoot1', 'word' => 'exampleWord1' }, + { 'root' => 'exampleRoot2', 'word' => 'exampleWord2' } ] end before { WebMock.disable! } after { WebMock.enable! } - it "can upsert a dictionary" do + 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 + 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) + expect(response['id']).to eq(dictionary_id) + expect(response['words']).to eq(dictionary) end - it "can retrieve all dictionaries" do + 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) + expect(response['dictionaries'].length).to eq(1) + expect(response['dictionaries'][0]).to eq(dictionary_id) end end From e65dc426e6a093e4462d3ac3f8986d61d5196d05 Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:31:32 +0200 Subject: [PATCH 4/9] ci: bump typesense version to v28.0.rc36 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33850d6..ed8fb93 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: ruby-version: ['2.7', '3.0', '3.2'] services: typesense: - image: typesense/typesense:27.1 + image: typesense/typesense:28.0.rc36 ports: - 8108:8108 volumes: From 281936e60785570e84c2b3affe8fc123316a4b6c Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:38:55 +0200 Subject: [PATCH 5/9] fix(stemming): use double quotes for newline characters - replace single-quoted `\n` with double-quoted `"\n"` in `jsonl` string processing - fix inconsistent line splitting behavior in `upsert` method --- lib/typesense/stemming_dictionaries.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/typesense/stemming_dictionaries.rb b/lib/typesense/stemming_dictionaries.rb index 848ea95..8fd0c36 100644 --- a/lib/typesense/stemming_dictionaries.rb +++ b/lib/typesense/stemming_dictionaries.rb @@ -11,7 +11,7 @@ def initialize(api_call) 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') + words_and_roots_combinations.map { |combo| Oj.dump(combo, mode: :compat) }.join("\n") else words_and_roots_combinations end @@ -25,7 +25,7 @@ def upsert(dict_id, words_and_roots_combinations) ) if words_and_roots_combinations.is_a?(Array) - result_in_jsonl.split('\n').map { |r| Oj.load(r) } + result_in_jsonl.split("\n").map { |r| Oj.load(r) } else result_in_jsonl end From 20a8a09ba3bfdf0dc58315d54995385b029bb46f Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:41:35 +0200 Subject: [PATCH 6/9] ci: remove 2.7 from matrix, introduce 3.3 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ed8fb93..b540300 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ 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:28.0.rc36 From beb14bcd2a9a00032feb633e6abb0d8f8f2adb8a Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Tue, 4 Feb 2025 18:44:40 +0200 Subject: [PATCH 7/9] ci: fix artifact naming conflict --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b540300..0d7d999 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,6 +39,6 @@ jobs: - run: bundle exec rspec --format documentation - uses: actions/upload-artifact@v4 with: - name: coverage + name: coverage-ruby-${{ matrix.ruby-version }} path: coverage/ retention-days: 1 From 52694c48d3649088d973e0699498376bf6c0f1ab Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Wed, 5 Feb 2025 10:18:29 +0200 Subject: [PATCH 8/9] chore: add base64 to dependencies --- typesense.gemspec | 1 + 1 file changed, 1 insertion(+) 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' From fb2029fb6f878efb194340ca4953da635401e39e Mon Sep 17 00:00:00 2001 From: Fanis Tharropoulos Date: Thu, 6 Feb 2025 10:37:09 +0200 Subject: [PATCH 9/9] fix(stemming): handle empty dictionary retrieval gracefully - add fallback response for empty dictionary retrieval in `StemmingDictionaries` - refactor test setup to ensure proper test isolation - fix webmock configuration in tests --- lib/typesense/stemming_dictionaries.rb | 3 ++- spec/typesense/stemming_spec.rb | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/typesense/stemming_dictionaries.rb b/lib/typesense/stemming_dictionaries.rb index 8fd0c36..0eee2c1 100644 --- a/lib/typesense/stemming_dictionaries.rb +++ b/lib/typesense/stemming_dictionaries.rb @@ -32,7 +32,8 @@ def upsert(dict_id, words_and_roots_combinations) end def retrieve - @api_call.get(endpoint_path) + response = @api_call.get(endpoint_path) + response || { 'dictionaries' => [] } end def [](dict_id) diff --git a/spec/typesense/stemming_spec.rb b/spec/typesense/stemming_spec.rb index ed645af..fbf6bff 100644 --- a/spec/typesense/stemming_spec.rb +++ b/spec/typesense/stemming_spec.rb @@ -19,8 +19,15 @@ ] end - before { WebMock.disable! } - after { WebMock.enable! } + 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)