typesense-rails 1.0.0.rc4 → 1.0.0.rc7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235c4761cba73de6042fe41ac751658c001b0a44fa2a7e2959fea7b398bf186c
4
- data.tar.gz: deaaded73d6ac588e47c0439f515ff4771f05ab063692d651cdb00ab60c7513e
3
+ metadata.gz: 4c0186b1b9d3de405d50d5d736725d4a93863e029e116544b7d3870b83dad9fa
4
+ data.tar.gz: 13c8237df903014ad8147f624f3d4ca7b99429023cf6311f438b9f6f5986f312
5
5
  SHA512:
6
- metadata.gz: 6004a8189a68987366577a1903404a22c2eb3fad4ac4226d6deeeacf6a34a7411d35ca6da0a9ec585278932b27b1c96a568fa192eeb69e3d1cb2b708df750768
7
- data.tar.gz: 10626d38245d494eff9b81d28d5f8c2e2d409fbdc69f6bc217acd0e78079504a9b3eaf2a38fc62fe4f5582742758e6c4f14206c04125953bca2c1690061fee31
6
+ metadata.gz: b61f66f7abcdefc86bbd3eb0ac5a668469d299f7d0265b984950bda1fdaa2a7b6ab9ab4ff9ef5a088925e92a96ec0ba11cedce05aab48ef4e6a9e9060afb351b
7
+ data.tar.gz: 3da63dd78087369681b89aa9e82c0b516348c4f9db9eb119ee51f6025ef9631484048b818bff9cca9c7d08c091eceffe720eaae11d76e3e02bab02f6bd9ba18f
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  gem 'json', '>= 1.5.1'
4
- gem "typesense", "~> 0.13.0"
4
+ gem "typesense", ">= 5.0.0", "< 6.0.0"
5
5
 
6
6
 
7
7
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
data/Gemfile.lock CHANGED
@@ -39,7 +39,7 @@ GEM
39
39
  erubi (~> 1.4)
40
40
  rails-dom-testing (~> 2.0)
41
41
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
42
- active_model_serializers (0.10.15)
42
+ active_model_serializers (0.10.16)
43
43
  actionpack (>= 4.1)
44
44
  activemodel (>= 4.1)
45
45
  case_transform (>= 0.2)
@@ -66,27 +66,29 @@ GEM
66
66
  tzinfo (~> 2.0)
67
67
  zeitwerk (~> 2.3)
68
68
  ast (2.4.3)
69
- base64 (0.3.0)
70
- bigdecimal (3.2.3)
69
+ base64 (0.2.0)
70
+ bigdecimal (4.0.1)
71
71
  builder (3.3.0)
72
72
  case_transform (0.2)
73
73
  activesupport
74
- cgi (0.5.0)
75
- concurrent-ruby (1.3.5)
74
+ cgi (0.5.1)
75
+ concurrent-ruby (1.3.6)
76
76
  crass (1.0.6)
77
- date (3.4.1)
77
+ date (3.5.1)
78
78
  diff-lcs (1.6.2)
79
79
  erb (4.0.4)
80
80
  cgi (>= 0.3.3)
81
81
  erubi (1.13.1)
82
- ethon (0.15.0)
83
- ffi (>= 1.15.0)
84
- ffi (1.17.2)
82
+ faraday (2.8.1)
83
+ base64
84
+ faraday-net_http (>= 2.0, < 3.1)
85
+ ruby2_keywords (>= 0.0.4)
86
+ faraday-net_http (3.0.2)
85
87
  globalid (1.3.0)
86
88
  activesupport (>= 6.1)
87
- i18n (1.14.7)
89
+ i18n (1.14.8)
88
90
  concurrent-ruby (~> 1.0)
89
- json (2.15.0)
91
+ json (2.19.3)
90
92
  jsonapi-renderer (0.2.2)
91
93
  kaminari (0.17.0)
92
94
  actionpack (>= 3.0.0)
@@ -95,10 +97,11 @@ GEM
95
97
  lefthook (0.7.7)
96
98
  lint_roller (1.1.0)
97
99
  logger (1.7.0)
98
- loofah (2.24.1)
100
+ loofah (2.25.1)
99
101
  crass (~> 1.0.2)
100
102
  nokogiri (>= 1.12.0)
101
- mail (2.8.1)
103
+ mail (2.9.0)
104
+ logger
102
105
  mini_mime (>= 0.1.1)
103
106
  net-imap
104
107
  net-pop
@@ -107,8 +110,8 @@ GEM
107
110
  method_source (1.1.0)
108
111
  mini_mime (1.1.5)
109
112
  mini_portile2 (2.8.9)
110
- minitest (5.25.5)
111
- net-imap (0.4.22)
113
+ minitest (5.26.1)
114
+ net-imap (0.4.23)
112
115
  date
113
116
  net-protocol
114
117
  net-pop (0.1.2)
@@ -117,25 +120,21 @@ GEM
117
120
  timeout
118
121
  net-smtp (0.5.1)
119
122
  net-protocol
120
- nio4r (2.7.4)
123
+ nio4r (2.7.5)
121
124
  nokogiri (1.15.7)
122
125
  mini_portile2 (~> 2.8.2)
123
126
  racc (~> 1.4)
124
- oj (3.16.11)
125
- bigdecimal (>= 3.0)
126
- ostruct (>= 0.2)
127
- ostruct (0.6.3)
128
127
  pagy (6.5.0)
129
128
  parallel (1.27.0)
130
- parser (3.3.9.0)
129
+ parser (3.3.10.2)
131
130
  ast (~> 2.4.1)
132
131
  racc
133
- prism (1.5.1)
134
- psych (5.2.6)
132
+ prism (1.9.0)
133
+ psych (5.3.1)
135
134
  date
136
135
  stringio
137
136
  racc (1.8.1)
138
- rack (2.2.18)
137
+ rack (2.2.22)
139
138
  rack-test (2.2.0)
140
139
  rack (>= 1.3)
141
140
  rails (6.1.7.10)
@@ -157,8 +156,8 @@ GEM
157
156
  activesupport (>= 5.0.0)
158
157
  minitest
159
158
  nokogiri (>= 1.6)
160
- rails-html-sanitizer (1.6.2)
161
- loofah (~> 2.21)
159
+ rails-html-sanitizer (1.7.0)
160
+ loofah (~> 2.25)
162
161
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
163
162
  railties (6.1.7.10)
164
163
  actionpack (= 6.1.7.10)
@@ -167,25 +166,26 @@ GEM
167
166
  rake (>= 12.2)
168
167
  thor (~> 1.0)
169
168
  rainbow (3.1.1)
170
- rake (13.3.0)
171
- rdoc (6.14.2)
169
+ rake (13.3.1)
170
+ rdoc (7.2.0)
172
171
  erb
173
172
  psych (>= 4.0.0)
173
+ tsort
174
174
  regexp_parser (2.11.3)
175
- rspec (3.13.1)
175
+ rspec (3.13.2)
176
176
  rspec-core (~> 3.13.0)
177
177
  rspec-expectations (~> 3.13.0)
178
178
  rspec-mocks (~> 3.13.0)
179
- rspec-core (3.13.5)
179
+ rspec-core (3.13.6)
180
180
  rspec-support (~> 3.13.0)
181
181
  rspec-expectations (3.13.5)
182
182
  diff-lcs (>= 1.2.0, < 2.0)
183
183
  rspec-support (~> 3.13.0)
184
- rspec-mocks (3.13.5)
184
+ rspec-mocks (3.13.8)
185
185
  diff-lcs (>= 1.2.0, < 2.0)
186
186
  rspec-support (~> 3.13.0)
187
- rspec-support (3.13.6)
188
- rubocop (1.81.1)
187
+ rspec-support (3.13.7)
188
+ rubocop (1.86.0)
189
189
  json (~> 2.3)
190
190
  language_server-protocol (~> 3.17.0.2)
191
191
  lint_roller (~> 1.1.0)
@@ -193,14 +193,15 @@ GEM
193
193
  parser (>= 3.3.0.2)
194
194
  rainbow (>= 2.2.2, < 4.0)
195
195
  regexp_parser (>= 2.9.3, < 3.0)
196
- rubocop-ast (>= 1.47.1, < 2.0)
196
+ rubocop-ast (>= 1.49.0, < 2.0)
197
197
  ruby-progressbar (~> 1.7)
198
198
  unicode-display_width (>= 2.4.0, < 4.0)
199
- rubocop-ast (1.47.1)
199
+ rubocop-ast (1.49.1)
200
200
  parser (>= 3.3.7.2)
201
- prism (~> 1.4)
201
+ prism (~> 1.7)
202
202
  ruby-progressbar (1.13.0)
203
- sequel (5.96.0)
203
+ ruby2_keywords (0.0.5)
204
+ sequel (5.102.0)
204
205
  bigdecimal
205
206
  sprockets (4.2.2)
206
207
  concurrent-ruby (~> 1.0)
@@ -212,19 +213,19 @@ GEM
212
213
  sprockets (>= 3.0.0)
213
214
  sqlite3 (1.6.9)
214
215
  mini_portile2 (~> 2.8.0)
215
- stringio (3.1.7)
216
- thor (1.4.0)
217
- timeout (0.4.3)
218
- typesense (0.13.0)
219
- oj (~> 3.11)
220
- typhoeus (~> 1.4)
221
- typhoeus (1.5.0)
222
- ethon (>= 0.9.0, < 0.16.0)
216
+ stringio (3.2.0)
217
+ thor (1.5.0)
218
+ timeout (0.6.1)
219
+ tsort (0.2.0)
220
+ typesense (5.0.0)
221
+ base64 (~> 0.2.0)
222
+ faraday (~> 2.8)
223
+ json (~> 2.9)
223
224
  tzinfo (2.0.6)
224
225
  concurrent-ruby (~> 1.0)
225
226
  unicode-display_width (3.2.0)
226
227
  unicode-emoji (~> 4.1)
227
- unicode-emoji (4.1.0)
228
+ unicode-emoji (4.2.0)
228
229
  websocket-driver (0.8.0)
229
230
  base64
230
231
  websocket-extensions (>= 0.1.0)
@@ -251,7 +252,7 @@ DEPENDENCIES
251
252
  rubocop (~> 1.19)
252
253
  sequel (>= 4.0)
253
254
  sqlite3 (~> 1.6.0)
254
- typesense (~> 0.13.0)
255
+ typesense (>= 5.0.0, < 6.0.0)
255
256
  will_paginate (>= 2.3.15)
256
257
 
257
258
  BUNDLED WITH
data/README.md CHANGED
@@ -18,7 +18,8 @@ Special thanks to the Algolia team for their original implementation, which prov
18
18
  - Environment-specific indexing
19
19
  - Support for faceted search and filtering
20
20
  - Customizable schema with predefined fields
21
- - Multi-way and one-way synonyms support
21
+ - Multi-way and one-way synonyms support across Typesense v29 and v30+
22
+ - Direct attachment of v30+ synonym sets and curation sets
22
23
  - Rake tasks for index management
23
24
 
24
25
  ## Installation
@@ -91,6 +92,10 @@ class Product < ApplicationRecord
91
92
  }
92
93
  ]
93
94
 
95
+ # Attach existing global resources on Typesense v30+
96
+ synonym_sets ["catalog-synonyms"]
97
+ curation_sets ["catalog-curations"]
98
+
94
99
  # Symbols to index
95
100
  symbols_to_index ["-", "_"]
96
101
 
@@ -103,6 +108,9 @@ class Product < ApplicationRecord
103
108
  end
104
109
  ```
105
110
 
111
+ For Typesense v29, `multi_way_synonyms` and `one_way_synonyms` continue to use the legacy collection-level synonym APIs.
112
+ For Typesense v30 and newer, this gem stores those DSL synonyms in a collection-specific synonym set and attaches it automatically, while also supporting explicit `synonym_sets` / `curation_sets`.
113
+
106
114
  ### Working with Relationships
107
115
 
108
116
  ```ruby
@@ -16,6 +16,8 @@ module Typesense
16
16
  @@pagination_backend = configuration[:pagination_backend] if configuration.key?(:pagination_backend)
17
17
  @@log_level = configuration[:log_level] if configuration.key?(:log_level)
18
18
  @@configuration = configuration
19
+ @client = nil
20
+ reset_server_version_cache!
19
21
  end
20
22
 
21
23
  def pagination_backend
@@ -66,5 +68,21 @@ module Typesense
66
68
  def setup_client
67
69
  @client = Typesense::Client.new(@@configuration)
68
70
  end
71
+
72
+ def server_major_version
73
+ @server_major_version ||= begin
74
+ version = debug_info.fetch("version", "")
75
+ version == "nightly" ? 30 : version.split(".").first.to_i
76
+ end
77
+ end
78
+
79
+ def debug_info
80
+ @debug_info ||= client.debug.retrieve
81
+ end
82
+
83
+ def reset_server_version_cache!
84
+ @server_major_version = nil
85
+ @debug_info = nil
86
+ end
69
87
  end
70
88
  end
@@ -14,7 +14,9 @@ module Typesense
14
14
  }
15
15
 
16
16
  pagy_version = Gem::Version.new(::Pagy::VERSION)
17
- pagy = if pagy_version >= Gem::Version.new('9.0')
17
+ pagy = if pagy_version >= Gem::Version.new("43.0.0.rc1")
18
+ ::Pagy::Offset.new(**vars)
19
+ elsif pagy_version >= Gem::Version.new("9.0")
18
20
  ::Pagy.new(**vars)
19
21
  else
20
22
  ::Pagy.new(vars)
@@ -1,3 +1,3 @@
1
1
  module Typesense
2
- GEM_VERSION = "1.0.0.rc4"
2
+ GEM_VERSION = "1.0.0.rc7"
3
3
  end
@@ -80,6 +80,8 @@ module Typesense
80
80
  OPTIONS = [
81
81
  :multi_way_synonyms,
82
82
  :one_way_synonyms,
83
+ :synonym_sets,
84
+ :curation_sets,
83
85
  :predefined_fields,
84
86
  :fields,
85
87
  :default_sorting_field,
@@ -271,11 +273,13 @@ module Typesense
271
273
  "#{typesense_collection_name(options)}_#{Time.now.to_i}"
272
274
  end
273
275
 
274
- def typesense_create_collection(collection_name, settings = nil)
276
+ def typesense_create_collection(collection_name, settings = nil, existing_collection: nil)
275
277
  fields = settings.get_setting(:predefined_fields) || settings.get_setting(:fields)
276
278
  default_sorting_field = settings.get_setting(:default_sorting_field)
277
279
  multi_way_synonyms = settings.get_setting(:multi_way_synonyms)
278
280
  one_way_synonyms = settings.get_setting(:one_way_synonyms)
281
+ synonym_sets = settings.get_setting(:synonym_sets)
282
+ curation_sets = settings.get_setting(:curation_sets)
279
283
  symbols_to_index = settings.get_setting(:symbols_to_index)
280
284
  token_separators = settings.get_setting(:token_separators)
281
285
  enable_nested_fields = settings.get_setting(:enable_nested_fields)
@@ -300,9 +304,14 @@ module Typesense
300
304
  )
301
305
  Typesense.log(:debug, "Collection '#{collection_name}' created!")
302
306
 
303
- typesense_multi_way_synonyms(collection_name, multi_way_synonyms) if multi_way_synonyms
304
-
305
- typesense_one_way_synonyms(collection_name, one_way_synonyms) if one_way_synonyms
307
+ apply_typesense_collection_resources(
308
+ collection_name,
309
+ multi_way_synonyms: multi_way_synonyms,
310
+ one_way_synonyms: one_way_synonyms,
311
+ synonym_sets: synonym_sets,
312
+ curation_sets: curation_sets,
313
+ existing_collection: existing_collection
314
+ )
306
315
  end
307
316
 
308
317
  def typesense_upsert_alias(collection_name, alias_name)
@@ -385,6 +394,103 @@ module Typesense
385
394
  end
386
395
  end
387
396
 
397
+ def apply_typesense_collection_resources(collection_name, multi_way_synonyms: nil, one_way_synonyms: nil, synonym_sets: nil, curation_sets: nil, existing_collection: nil)
398
+ if typesense_server_major_version >= 30
399
+ apply_v30_collection_resources(
400
+ collection_name,
401
+ multi_way_synonyms: multi_way_synonyms,
402
+ one_way_synonyms: one_way_synonyms,
403
+ synonym_sets: synonym_sets,
404
+ curation_sets: curation_sets,
405
+ existing_collection: existing_collection
406
+ )
407
+ else
408
+ ensure_v30_resource_support!(synonym_sets, curation_sets)
409
+ typesense_multi_way_synonyms(collection_name, multi_way_synonyms) if multi_way_synonyms
410
+ typesense_one_way_synonyms(collection_name, one_way_synonyms) if one_way_synonyms
411
+ end
412
+ end
413
+
414
+ def apply_v30_collection_resources(collection_name, multi_way_synonyms: nil, one_way_synonyms: nil, synonym_sets: nil, curation_sets: nil, existing_collection: nil)
415
+ inline_synonyms_present = multi_way_synonyms || one_way_synonyms
416
+ attached_synonym_sets = Array(existing_collection && existing_collection["synonym_sets"]) + Array(synonym_sets)
417
+ attached_curation_sets = Array(existing_collection && existing_collection["curation_sets"]) + Array(curation_sets)
418
+
419
+ if inline_synonyms_present
420
+ synonym_set_name = default_synonym_set_name(collection_name)
421
+ ensure_synonym_set_exists(synonym_set_name)
422
+ upsert_synonym_set_items(synonym_set_name, multi_way_synonyms, one_way_synonyms)
423
+ attached_synonym_sets << synonym_set_name
424
+ end
425
+
426
+ collection_patch = {}
427
+ collection_patch["synonym_sets"] = attached_synonym_sets.uniq if attached_synonym_sets.any?
428
+ collection_patch["curation_sets"] = attached_curation_sets.uniq if attached_curation_sets.any?
429
+
430
+ return if collection_patch.empty?
431
+
432
+ typesense_client.collections[collection_name].update(collection_patch)
433
+ end
434
+
435
+ def ensure_v30_resource_support!(synonym_sets, curation_sets)
436
+ unsupported = []
437
+ unsupported << "synonym_sets" if synonym_sets
438
+ unsupported << "curation_sets" if curation_sets
439
+ return if unsupported.empty?
440
+
441
+ raise Typesense::BadConfiguration, "#{unsupported.join(' and ')} require Typesense v30.0 or newer"
442
+ end
443
+
444
+ def default_synonym_set_name(collection_name)
445
+ "#{collection_name}_synonyms_index"
446
+ end
447
+
448
+ def ensure_synonym_set_exists(synonym_set_name)
449
+ typesense_client.synonym_sets.upsert(synonym_set_name, { "items" => [] })
450
+ end
451
+
452
+ def upsert_synonym_set_items(synonym_set_name, multi_way_synonyms, one_way_synonyms)
453
+ items = []
454
+
455
+ Array(multi_way_synonyms).each do |synonym_hash|
456
+ synonym_hash.each do |synonym_name, synonym|
457
+ items << { "id" => synonym_name, "synonyms" => synonym }
458
+ end
459
+ end
460
+
461
+ Array(one_way_synonyms).each do |synonym_hash|
462
+ synonym_hash.each do |synonym_name, synonym|
463
+ items << synonym.merge("id" => synonym_name)
464
+ end
465
+ end
466
+
467
+ typesense_client.synonym_sets.upsert(synonym_set_name, { "items" => items })
468
+ end
469
+
470
+ def typesense_server_major_version
471
+ Typesense.server_major_version
472
+ end
473
+
474
+ def reset_typesense_server_major_version!
475
+ Typesense.reset_server_version_cache!
476
+ end
477
+
478
+ def typesense_debug_info
479
+ Typesense.debug_info
480
+ end
481
+
482
+ def typesense_collection_resources(collection_name)
483
+ return {} if typesense_server_major_version < 30
484
+
485
+ collection = typesense_get_collection(collection_name)
486
+ return {} unless collection
487
+
488
+ {
489
+ "synonym_sets" => Array(collection["synonym_sets"]),
490
+ "curation_sets" => Array(collection["curation_sets"])
491
+ }
492
+ end
493
+
388
494
  def typesense(options = {}, &block)
389
495
  self.typesense_settings = IndexSettings.new(options, &block)
390
496
  self.typesense_options = { type: typesense_full_const_get(model_name.to_s) }.merge(options) # :per_page => typesense_settings.get_setting(:hitsPerPage) || 10, :page => 1
@@ -528,8 +634,10 @@ module Typesense
528
634
  typesense_configurations.each do |options, settings|
529
635
  next if typesense_indexing_disabled?(options)
530
636
 
637
+ existing_collection_resources = {}
531
638
  begin
532
639
  master_index = typesense_ensure_init(options, settings, false)
640
+ existing_collection_resources = typesense_collection_resources(master_index[:alias_name])
533
641
  delete_collection(master_index[:alias_name])
534
642
  rescue ArgumentError
535
643
  @typesense_indexes[settings] = { collection_name: "", alias_name: typesense_collection_name(options) }
@@ -542,7 +650,7 @@ module Typesense
542
650
  tmp_options.delete(:per_environment) # already included in the temporary index_name
543
651
  tmp_settings = settings.dup
544
652
 
545
- create_collection(src_index_name, settings)
653
+ create_collection(src_index_name, settings, existing_collection: existing_collection_resources)
546
654
 
547
655
  typesense_find_in_batches(batch_size) do |group|
548
656
  if typesense_conditional_index?(options)
@@ -847,11 +955,11 @@ module Typesense
847
955
  def typesense_full_const_get(name)
848
956
  list = name.split("::")
849
957
  list.shift if list.first.blank?
850
- obj = Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 ? Object : self
958
+ obj = Object
851
959
  list.each do |x|
852
960
  # This is required because const_get tries to look for constants in the
853
961
  # ancestor chain, but we only want constants that are HERE
854
- obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
962
+ obj = obj.const_defined?(x, false) ? obj.const_get(x, false) : obj.const_missing(x)
855
963
  end
856
964
  obj
857
965
  end
@@ -73,6 +73,9 @@ ActiveRecord::Schema.define do
73
73
  t.string :short_name
74
74
  t.integer :hex
75
75
  end
76
+ create_table :collections do |t|
77
+ t.string :name
78
+ end
76
79
  create_table :namespaced_models do |t|
77
80
  t.string :name
78
81
  t.integer :another_private_value
@@ -173,6 +176,20 @@ end
173
176
  class Camera < Product
174
177
  end
175
178
 
179
+ class V30ResourceProduct < ActiveRecord::Base
180
+ self.table_name = "products"
181
+ self.inheritance_column = :_type_disabled
182
+
183
+ include Typesense
184
+
185
+ typesense auto_index: false,
186
+ index_name: safe_index_name("v30_resource_products") do
187
+ attribute :name
188
+ synonym_sets [safe_index_name("shared-product-synonyms")]
189
+ curation_sets [safe_index_name("shared-product-curations")]
190
+ end
191
+ end
192
+
176
193
  class Color < ActiveRecord::Base
177
194
  include Typesense
178
195
  attr_accessor :not_indexed
@@ -198,6 +215,14 @@ class Color < ActiveRecord::Base
198
215
  end
199
216
  end
200
217
 
218
+ class Collection < ActiveRecord::Base
219
+ include Typesense
220
+
221
+ typesense auto_index: false, index_name: safe_index_name("Collection") do
222
+ attribute :name
223
+ end
224
+ end
225
+
201
226
  class DisabledBoolean < ActiveRecord::Base
202
227
  include Typesense
203
228
 
@@ -628,6 +653,35 @@ describe "Namespaced::Model" do
628
653
  end
629
654
  end
630
655
 
656
+ describe "Collection" do
657
+ it "resolves the model class instead of Typesense::Collection" do
658
+ expect(Collection.typesense_options[:type]).to eq(Collection)
659
+ end
660
+
661
+ it "uses the ActiveRecord model when loading search hits" do
662
+ record = Collection.create!(name: "Archive")
663
+
664
+ allow(Collection).to receive(:typesense_raw_search).and_return(
665
+ {
666
+ "hits" => [
667
+ {
668
+ "document" => { "id" => record.id.to_s },
669
+ "highlights" => []
670
+ }
671
+ ],
672
+ "found" => 1,
673
+ "page" => 1,
674
+ "request_params" => { "per_page" => 10 }
675
+ }
676
+ )
677
+
678
+ results = Collection.search("*", "name")
679
+
680
+ expect(results.length).to eq(1)
681
+ expect(results.first).to eq(record)
682
+ end
683
+ end
684
+
631
685
  describe "UniqUsers" do
632
686
  before(:all) do
633
687
  UniqUser.clear_index!
@@ -1049,6 +1103,199 @@ describe "Book" do
1049
1103
  end
1050
1104
  end
1051
1105
 
1106
+ describe "Typesense version compatibility" do
1107
+ let(:settings) { Product.typesense_settings }
1108
+
1109
+ it "uses collection-level synonyms on v29" do
1110
+ allow(Product).to receive(:typesense_server_major_version).and_return(29)
1111
+ expect(Product).to receive(:typesense_multi_way_synonyms)
1112
+ .with("products_v29", settings.get_setting(:multi_way_synonyms))
1113
+ expect(Product).to receive(:typesense_one_way_synonyms)
1114
+ .with("products_v29", settings.get_setting(:one_way_synonyms))
1115
+
1116
+ Product.apply_typesense_collection_resources(
1117
+ "products_v29",
1118
+ multi_way_synonyms: settings.get_setting(:multi_way_synonyms),
1119
+ one_way_synonyms: settings.get_setting(:one_way_synonyms)
1120
+ )
1121
+ end
1122
+
1123
+ it "creates and attaches synonym sets on v30+" do
1124
+ collection_resource = double("collection_resource")
1125
+ collections_resource = double("collections_resource")
1126
+ synonym_sets_resource = double("synonym_sets_resource")
1127
+ client = double("typesense_client", collections: collections_resource, synonym_sets: synonym_sets_resource)
1128
+
1129
+ allow(Product).to receive(:typesense_server_major_version).and_return(30)
1130
+ allow(Product).to receive(:typesense_client).and_return(client)
1131
+ expect(collections_resource).to receive(:[]).with("products_v30").and_return(collection_resource)
1132
+ expect(synonym_sets_resource).to receive(:upsert).with("products_v30_synonyms_index", { "items" => [] }).ordered
1133
+ expect(synonym_sets_resource).to receive(:upsert).with(
1134
+ "products_v30_synonyms_index",
1135
+ {
1136
+ "items" => [
1137
+ { "id" => "phone-synonym", "synonyms" => %w[galaxy samsung samsung_electronics] },
1138
+ { "id" => "smart-phone-synonym", "root" => "smartphone",
1139
+ "synonyms" => %w[nokia samsung motorola android] }
1140
+ ]
1141
+ }
1142
+ ).ordered
1143
+ expect(collection_resource).to receive(:update) do |payload|
1144
+ expect(payload["synonym_sets"]).to match_array(%w[existing-shared shared-synonyms products_v30_synonyms_index])
1145
+ expect(payload["curation_sets"]).to match_array(%w[existing-curation shared-curations])
1146
+ end
1147
+
1148
+ Product.apply_typesense_collection_resources(
1149
+ "products_v30",
1150
+ multi_way_synonyms: settings.get_setting(:multi_way_synonyms),
1151
+ one_way_synonyms: settings.get_setting(:one_way_synonyms),
1152
+ synonym_sets: ["shared-synonyms"],
1153
+ curation_sets: ["shared-curations"],
1154
+ existing_collection: {
1155
+ "synonym_sets" => ["existing-shared"],
1156
+ "curation_sets" => ["existing-curation"]
1157
+ }
1158
+ )
1159
+ end
1160
+
1161
+ it "rejects v30-only collection resources on v29" do
1162
+ allow(Product).to receive(:typesense_server_major_version).and_return(29)
1163
+
1164
+ expect do
1165
+ Product.apply_typesense_collection_resources(
1166
+ "products_v29",
1167
+ synonym_sets: ["shared-synonyms"]
1168
+ )
1169
+ end.to raise_error(Typesense::BadConfiguration, /synonym_sets require Typesense v30.0 or newer/)
1170
+ end
1171
+ end
1172
+
1173
+ describe "Typesense v30 resource integration" do
1174
+ before do
1175
+ skip("SynonymSets and CurationSets are only supported in Typesense v30+") unless typesense_v30_or_above?
1176
+ end
1177
+
1178
+ after do
1179
+ begin
1180
+ Typesense.client.synonym_sets.retrieve.each do |set|
1181
+ Typesense.client.synonym_sets[set["name"]].delete if set["name"].include?(SAFE_INDEX_PREFIX)
1182
+ end
1183
+ rescue StandardError
1184
+ nil
1185
+ end
1186
+
1187
+ begin
1188
+ Typesense.client.curation_sets.retrieve.each do |set|
1189
+ Typesense.client.curation_sets[set["name"]].delete if set["name"].include?(SAFE_INDEX_PREFIX)
1190
+ end
1191
+ rescue StandardError
1192
+ nil
1193
+ end
1194
+
1195
+ begin
1196
+ V30ResourceProduct.clear_index!
1197
+ rescue StandardError
1198
+ nil
1199
+ end
1200
+
1201
+ begin
1202
+ Product.clear_index!
1203
+ rescue StandardError
1204
+ nil
1205
+ end
1206
+ end
1207
+
1208
+ it "attaches generated synonym sets for inline synonym DSL" do
1209
+ begin
1210
+ Product.clear_index!
1211
+ rescue StandardError
1212
+ nil
1213
+ end
1214
+ collection_obj = Product.typesense_index
1215
+
1216
+ collection = Typesense.client.collections[collection_obj[:collection_name]].retrieve
1217
+ generated_synonym_set = "#{collection_obj[:collection_name]}_synonyms_index"
1218
+
1219
+ expect(collection["synonym_sets"]).to include(generated_synonym_set)
1220
+
1221
+ synonym_set = Typesense.client.synonym_sets[generated_synonym_set].retrieve
1222
+ item_ids = synonym_set.fetch("items").map { |item| item["id"] }
1223
+
1224
+ expect(item_ids).to include("phone-synonym", "smart-phone-synonym")
1225
+ end
1226
+
1227
+ it "attaches explicit synonym sets and curation sets to the collection" do
1228
+ synonym_set_name = safe_index_name("shared-product-synonyms")
1229
+ curation_set_name = safe_index_name("shared-product-curations")
1230
+
1231
+ Typesense.client.synonym_sets.upsert(
1232
+ synonym_set_name,
1233
+ {
1234
+ "items" => [
1235
+ { "id" => "shared-phone", "root" => "phone", "synonyms" => %w[handset mobile] }
1236
+ ]
1237
+ }
1238
+ )
1239
+ Typesense.client.curation_sets.upsert(
1240
+ curation_set_name,
1241
+ {
1242
+ "items" => [
1243
+ {
1244
+ "id" => "promote-phone",
1245
+ "rule" => { "query" => "phone", "match" => "exact" },
1246
+ "includes" => [{ "id" => "1", "position" => 1 }],
1247
+ "excludes" => [],
1248
+ "filter_curated_hits" => false,
1249
+ "remove_matched_tokens" => false,
1250
+ "stop_processing" => true
1251
+ }
1252
+ ]
1253
+ }
1254
+ )
1255
+
1256
+ V30ResourceProduct.typesense_index
1257
+
1258
+ collection = Typesense.client.collections[V30ResourceProduct.collection_name].retrieve
1259
+
1260
+ expect(collection["synonym_sets"]).to include(synonym_set_name)
1261
+ expect(collection["curation_sets"]).to include(curation_set_name)
1262
+ end
1263
+
1264
+ it "preserves attached synonym sets and curation sets through reindex" do
1265
+ synonym_set_name = safe_index_name("shared-product-synonyms")
1266
+ curation_set_name = safe_index_name("shared-product-curations")
1267
+
1268
+ Typesense.client.synonym_sets.upsert(
1269
+ synonym_set_name,
1270
+ { "items" => [{ "id" => "shared-phone", "root" => "phone", "synonyms" => %w[handset mobile] }] }
1271
+ )
1272
+ Typesense.client.curation_sets.upsert(
1273
+ curation_set_name,
1274
+ {
1275
+ "items" => [
1276
+ {
1277
+ "id" => "promote-phone",
1278
+ "rule" => { "query" => "phone", "match" => "exact" },
1279
+ "includes" => [{ "id" => "1", "position" => 1 }],
1280
+ "excludes" => [],
1281
+ "filter_curated_hits" => false,
1282
+ "remove_matched_tokens" => false,
1283
+ "stop_processing" => true
1284
+ }
1285
+ ]
1286
+ }
1287
+ )
1288
+
1289
+ V30ResourceProduct.typesense_index
1290
+ V30ResourceProduct.reindex
1291
+
1292
+ collection = Typesense.client.collections[V30ResourceProduct.collection_name].retrieve
1293
+
1294
+ expect(collection["synonym_sets"]).to include(synonym_set_name)
1295
+ expect(collection["curation_sets"]).to include(curation_set_name)
1296
+ end
1297
+ end
1298
+
1052
1299
  describe "Kaminari" do
1053
1300
  before(:all) do
1054
1301
  require "kaminari"
data/spec/spec_helper.rb CHANGED
@@ -52,3 +52,17 @@ def safe_index_list
52
52
  list = Typesense.client.collections.retrieve()
53
53
  list = list.select { |index| index["name"].include?(SAFE_INDEX_PREFIX) }
54
54
  end
55
+
56
+ def typesense_version
57
+ Typesense.client.debug.retrieve["version"]
58
+ rescue StandardError
59
+ nil
60
+ end
61
+
62
+ def typesense_v30_or_above?
63
+ version = typesense_version
64
+ return false unless version
65
+ return true if version == "nightly"
66
+
67
+ version.to_s[/\d+/].to_i >= 30
68
+ end
@@ -52,7 +52,7 @@ Gem::Specification.new do |s|
52
52
 
53
53
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new("1.2.0")
54
54
  s.add_runtime_dependency(%q<json>, [">= 1.5.1"])
55
- s.add_runtime_dependency(%q<typesense>, [">= 0.13.0"])
55
+ s.add_runtime_dependency(%q<typesense>, [">= 5.0.0", "< 6.0.0"])
56
56
  s.add_development_dependency(%q<will_paginate>, [">= 2.3.15"])
57
57
  s.add_development_dependency(%q<kaminari>, [">= 0"])
58
58
  s.add_development_dependency(%q<pagy>, [">= 0"])
@@ -60,10 +60,10 @@ Gem::Specification.new do |s|
60
60
  s.add_development_dependency "rdoc"
61
61
  else
62
62
  s.add_dependency(%q<json>, [">= 1.5.1"])
63
- s.add_dependency(%q<typesense>, [">= 0.13.0"])
63
+ s.add_dependency(%q<typesense>, [">= 5.0.0", "< 6.0.0"])
64
64
  end
65
65
  else
66
66
  s.add_dependency(%q<json>, [">= 1.5.1"])
67
- s.add_dependency(%q<typesense>, [">= 0.13.0"])
67
+ s.add_dependency(%q<typesense>, [">= 5.0.0", "< 6.0.0"])
68
68
  end
69
69
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typesense-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc4
4
+ version: 1.0.0.rc7
5
5
  platform: ruby
6
6
  authors:
7
7
  - typesense
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-26 00:00:00.000000000 Z
11
+ date: 2026-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -30,14 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.13.0
33
+ version: 5.0.0
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: 6.0.0
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
41
  - - ">="
39
42
  - !ruby/object:Gem::Version
40
- version: 0.13.0
43
+ version: 5.0.0
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: 6.0.0
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: will_paginate
43
49
  requirement: !ruby/object:Gem::Requirement