typesense-rails 1.0.0.rc4 → 1.0.0.rc8

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: '08f8f980f414278d0cec345234a113795cc07b7c84654a90b430a6063b28254b'
4
+ data.tar.gz: 0021006106d1b4e4eef8a31b1c3042cec64a4dc1f8f7ea132bc8b2d205239ce8
5
5
  SHA512:
6
- metadata.gz: 6004a8189a68987366577a1903404a22c2eb3fad4ac4226d6deeeacf6a34a7411d35ca6da0a9ec585278932b27b1c96a568fa192eeb69e3d1cb2b708df750768
7
- data.tar.gz: 10626d38245d494eff9b81d28d5f8c2e2d409fbdc69f6bc217acd0e78079504a9b3eaf2a38fc62fe4f5582742758e6c4f14206c04125953bca2c1690061fee31
6
+ metadata.gz: 850d72bfb78a1a3cf6da763ffeb686e740749005af7e8515514d267df1a78e696c8f17bfde67d319ef28e828716eaa7f8cb78395f83e0e223892df0db9d25c74
7
+ data.tar.gz: c5681aad41eee6ea3696e84d3afddf0881e79d5af83d717af42f0799c44e90161552e2d1bf6c1b88cafd561257ee35fef6027633551b86c3a70026297a122f4f
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.rc8"
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,
@@ -268,14 +270,16 @@ module Typesense
268
270
  end
269
271
 
270
272
  def collection_name_with_timestamp(options)
271
- "#{typesense_collection_name(options)}_#{Time.now.to_i}"
273
+ "#{typesense_collection_name(options)}_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
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,9 +634,12 @@ module Typesense
528
634
  typesense_configurations.each do |options, settings|
529
635
  next if typesense_indexing_disabled?(options)
530
636
 
637
+ existing_collection_resources = {}
638
+ old_collection_name = nil
531
639
  begin
532
640
  master_index = typesense_ensure_init(options, settings, false)
533
- delete_collection(master_index[:alias_name])
641
+ existing_collection_resources = typesense_collection_resources(master_index[:alias_name])
642
+ old_collection_name = master_index[:collection_name]
534
643
  rescue ArgumentError
535
644
  @typesense_indexes[settings] = { collection_name: "", alias_name: typesense_collection_name(options) }
536
645
  master_index = @typesense_indexes[settings]
@@ -542,7 +651,7 @@ module Typesense
542
651
  tmp_options.delete(:per_environment) # already included in the temporary index_name
543
652
  tmp_settings = settings.dup
544
653
 
545
- create_collection(src_index_name, settings)
654
+ create_collection(src_index_name, settings, existing_collection: existing_collection_resources)
546
655
 
547
656
  typesense_find_in_batches(batch_size) do |group|
548
657
  if typesense_conditional_index?(options)
@@ -557,6 +666,7 @@ module Typesense
557
666
  end
558
667
 
559
668
  upsert_alias(src_index_name, master_index[:alias_name])
669
+ delete_collection(old_collection_name) if old_collection_name.present? && old_collection_name != src_index_name
560
670
  master_index[:collection_name] = src_index_name
561
671
  end
562
672
  nil
@@ -847,11 +957,11 @@ module Typesense
847
957
  def typesense_full_const_get(name)
848
958
  list = name.split("::")
849
959
  list.shift if list.first.blank?
850
- obj = Object.const_defined?(:RUBY_VERSION) && RUBY_VERSION.to_f < 1.9 ? Object : self
960
+ obj = Object
851
961
  list.each do |x|
852
962
  # This is required because const_get tries to look for constants in the
853
963
  # ancestor chain, but we only want constants that are HERE
854
- obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
964
+ obj = obj.const_defined?(x, false) ? obj.const_get(x, false) : obj.const_missing(x)
855
965
  end
856
966
  obj
857
967
  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
@@ -139,6 +142,9 @@ ActiveRecord::Schema.define do
139
142
  create_table :misconfigured_blocks do |t|
140
143
  t.string :name
141
144
  end
145
+ create_table :reindex_alias_probes do |t|
146
+ t.string :name
147
+ end
142
148
  if defined?(ActiveModel::Serializer)
143
149
  create_table :serialized_objects do |t|
144
150
  t.string :name
@@ -173,6 +179,20 @@ end
173
179
  class Camera < Product
174
180
  end
175
181
 
182
+ class V30ResourceProduct < ActiveRecord::Base
183
+ self.table_name = "products"
184
+ self.inheritance_column = :_type_disabled
185
+
186
+ include Typesense
187
+
188
+ typesense auto_index: false,
189
+ index_name: safe_index_name("v30_resource_products") do
190
+ attribute :name
191
+ synonym_sets [safe_index_name("shared-product-synonyms")]
192
+ curation_sets [safe_index_name("shared-product-curations")]
193
+ end
194
+ end
195
+
176
196
  class Color < ActiveRecord::Base
177
197
  include Typesense
178
198
  attr_accessor :not_indexed
@@ -198,6 +218,14 @@ class Color < ActiveRecord::Base
198
218
  end
199
219
  end
200
220
 
221
+ class Collection < ActiveRecord::Base
222
+ include Typesense
223
+
224
+ typesense auto_index: false, index_name: safe_index_name("Collection") do
225
+ attribute :name
226
+ end
227
+ end
228
+
201
229
  class DisabledBoolean < ActiveRecord::Base
202
230
  include Typesense
203
231
 
@@ -223,6 +251,14 @@ class DisabledSymbol < ActiveRecord::Base
223
251
  end
224
252
  end
225
253
 
254
+ class ReindexAliasProbe < ActiveRecord::Base
255
+ include Typesense
256
+
257
+ typesense auto_index: false, index_name: safe_index_name("ReindexAliasProbe") do
258
+ attribute :name
259
+ end
260
+ end
261
+
226
262
  module Namespaced
227
263
  def self.table_name_prefix
228
264
  "namespaced_"
@@ -628,6 +664,35 @@ describe "Namespaced::Model" do
628
664
  end
629
665
  end
630
666
 
667
+ describe "Collection" do
668
+ it "resolves the model class instead of Typesense::Collection" do
669
+ expect(Collection.typesense_options[:type]).to eq(Collection)
670
+ end
671
+
672
+ it "uses the ActiveRecord model when loading search hits" do
673
+ record = Collection.create!(name: "Archive")
674
+
675
+ allow(Collection).to receive(:typesense_raw_search).and_return(
676
+ {
677
+ "hits" => [
678
+ {
679
+ "document" => { "id" => record.id.to_s },
680
+ "highlights" => []
681
+ }
682
+ ],
683
+ "found" => 1,
684
+ "page" => 1,
685
+ "request_params" => { "per_page" => 10 }
686
+ }
687
+ )
688
+
689
+ results = Collection.search("*", "name")
690
+
691
+ expect(results.length).to eq(1)
692
+ expect(results.first).to eq(record)
693
+ end
694
+ end
695
+
631
696
  describe "UniqUsers" do
632
697
  before(:all) do
633
698
  UniqUser.clear_index!
@@ -937,6 +1002,33 @@ describe "An imaginary store" do
937
1002
  expect(Product.search("*", "", { "per_page" => Typesense::IndexSettings::DEFAULT_BATCH_SIZE }).size).to eq(n)
938
1003
  end
939
1004
 
1005
+ it "does not delete the live alias target before swapping during reindex" do
1006
+ alias_name = Product.index_name
1007
+ old_collection_name = "#{alias_name}_old"
1008
+ new_collection_name = "#{alias_name}_new"
1009
+ previous_indexes = Product.instance_variable_get(:@typesense_indexes)
1010
+
1011
+ Product.instance_variable_set(:@typesense_indexes, {})
1012
+
1013
+ allow(Product).to receive(:get_collection).with(alias_name).and_return({ "name" => old_collection_name })
1014
+ allow(Product).to receive(:typesense_collection_resources).with(alias_name).and_return({})
1015
+ allow(Product).to receive(:collection_name_with_timestamp).and_return(new_collection_name)
1016
+ allow(Product).to receive(:typesense_find_in_batches)
1017
+
1018
+ expect(Product).not_to receive(:delete_collection).with(alias_name)
1019
+ expect(Product).to receive(:create_collection).with(
1020
+ new_collection_name,
1021
+ Product.typesense_settings,
1022
+ existing_collection: {}
1023
+ ).ordered
1024
+ expect(Product).to receive(:upsert_alias).with(new_collection_name, alias_name).ordered
1025
+ expect(Product).to receive(:delete_collection).with(old_collection_name).ordered
1026
+
1027
+ Product.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1028
+ ensure
1029
+ Product.instance_variable_set(:@typesense_indexes, previous_indexes)
1030
+ end
1031
+
940
1032
  it "should not return products that are not indexable" do
941
1033
  @sekrit.index!
942
1034
  @no_href.index!
@@ -1017,6 +1109,43 @@ describe "MongoObject" do
1017
1109
  end
1018
1110
  end
1019
1111
 
1112
+ describe "ReindexAliasProbe" do
1113
+ before(:each) do
1114
+ ReindexAliasProbe.delete_all
1115
+ ReindexAliasProbe.clear_index!
1116
+ rescue StandardError
1117
+ ArgumentError
1118
+ ensure
1119
+ ReindexAliasProbe.create!(name: "alpha")
1120
+ ReindexAliasProbe.create!(name: "beta")
1121
+ ReindexAliasProbe.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1122
+ end
1123
+
1124
+ after(:each) do
1125
+ ReindexAliasProbe.delete_all
1126
+ ReindexAliasProbe.clear_index!
1127
+ rescue StandardError
1128
+ ArgumentError
1129
+ end
1130
+
1131
+ it "keeps the alias searchable while reindex builds the replacement collection" do
1132
+ alias_name = ReindexAliasProbe.index_name
1133
+ search_params = { q: "alpha", query_by: "name" }
1134
+
1135
+ expect(ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]).to eq(1)
1136
+
1137
+ expect(ReindexAliasProbe).to receive(:create_collection).and_wrap_original do |original, *args, **kwargs|
1138
+ expect(
1139
+ ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]
1140
+ ).to eq(1)
1141
+ original.call(*args, **kwargs)
1142
+ end
1143
+
1144
+ ReindexAliasProbe.reindex(Typesense::IndexSettings::DEFAULT_BATCH_SIZE)
1145
+ expect(ReindexAliasProbe.typesense_client.collections[alias_name].documents.search(search_params)["found"]).to eq(1)
1146
+ end
1147
+ end
1148
+
1020
1149
  describe "Book" do
1021
1150
  before(:all) do
1022
1151
  Book.clear_index!
@@ -1049,6 +1178,199 @@ describe "Book" do
1049
1178
  end
1050
1179
  end
1051
1180
 
1181
+ describe "Typesense version compatibility" do
1182
+ let(:settings) { Product.typesense_settings }
1183
+
1184
+ it "uses collection-level synonyms on v29" do
1185
+ allow(Product).to receive(:typesense_server_major_version).and_return(29)
1186
+ expect(Product).to receive(:typesense_multi_way_synonyms)
1187
+ .with("products_v29", settings.get_setting(:multi_way_synonyms))
1188
+ expect(Product).to receive(:typesense_one_way_synonyms)
1189
+ .with("products_v29", settings.get_setting(:one_way_synonyms))
1190
+
1191
+ Product.apply_typesense_collection_resources(
1192
+ "products_v29",
1193
+ multi_way_synonyms: settings.get_setting(:multi_way_synonyms),
1194
+ one_way_synonyms: settings.get_setting(:one_way_synonyms)
1195
+ )
1196
+ end
1197
+
1198
+ it "creates and attaches synonym sets on v30+" do
1199
+ collection_resource = double("collection_resource")
1200
+ collections_resource = double("collections_resource")
1201
+ synonym_sets_resource = double("synonym_sets_resource")
1202
+ client = double("typesense_client", collections: collections_resource, synonym_sets: synonym_sets_resource)
1203
+
1204
+ allow(Product).to receive(:typesense_server_major_version).and_return(30)
1205
+ allow(Product).to receive(:typesense_client).and_return(client)
1206
+ expect(collections_resource).to receive(:[]).with("products_v30").and_return(collection_resource)
1207
+ expect(synonym_sets_resource).to receive(:upsert).with("products_v30_synonyms_index", { "items" => [] }).ordered
1208
+ expect(synonym_sets_resource).to receive(:upsert).with(
1209
+ "products_v30_synonyms_index",
1210
+ {
1211
+ "items" => [
1212
+ { "id" => "phone-synonym", "synonyms" => %w[galaxy samsung samsung_electronics] },
1213
+ { "id" => "smart-phone-synonym", "root" => "smartphone",
1214
+ "synonyms" => %w[nokia samsung motorola android] }
1215
+ ]
1216
+ }
1217
+ ).ordered
1218
+ expect(collection_resource).to receive(:update) do |payload|
1219
+ expect(payload["synonym_sets"]).to match_array(%w[existing-shared shared-synonyms products_v30_synonyms_index])
1220
+ expect(payload["curation_sets"]).to match_array(%w[existing-curation shared-curations])
1221
+ end
1222
+
1223
+ Product.apply_typesense_collection_resources(
1224
+ "products_v30",
1225
+ multi_way_synonyms: settings.get_setting(:multi_way_synonyms),
1226
+ one_way_synonyms: settings.get_setting(:one_way_synonyms),
1227
+ synonym_sets: ["shared-synonyms"],
1228
+ curation_sets: ["shared-curations"],
1229
+ existing_collection: {
1230
+ "synonym_sets" => ["existing-shared"],
1231
+ "curation_sets" => ["existing-curation"]
1232
+ }
1233
+ )
1234
+ end
1235
+
1236
+ it "rejects v30-only collection resources on v29" do
1237
+ allow(Product).to receive(:typesense_server_major_version).and_return(29)
1238
+
1239
+ expect do
1240
+ Product.apply_typesense_collection_resources(
1241
+ "products_v29",
1242
+ synonym_sets: ["shared-synonyms"]
1243
+ )
1244
+ end.to raise_error(Typesense::BadConfiguration, /synonym_sets require Typesense v30.0 or newer/)
1245
+ end
1246
+ end
1247
+
1248
+ describe "Typesense v30 resource integration" do
1249
+ before do
1250
+ skip("SynonymSets and CurationSets are only supported in Typesense v30+") unless typesense_v30_or_above?
1251
+ end
1252
+
1253
+ after do
1254
+ begin
1255
+ Typesense.client.synonym_sets.retrieve.each do |set|
1256
+ Typesense.client.synonym_sets[set["name"]].delete if set["name"].include?(SAFE_INDEX_PREFIX)
1257
+ end
1258
+ rescue StandardError
1259
+ nil
1260
+ end
1261
+
1262
+ begin
1263
+ Typesense.client.curation_sets.retrieve.each do |set|
1264
+ Typesense.client.curation_sets[set["name"]].delete if set["name"].include?(SAFE_INDEX_PREFIX)
1265
+ end
1266
+ rescue StandardError
1267
+ nil
1268
+ end
1269
+
1270
+ begin
1271
+ V30ResourceProduct.clear_index!
1272
+ rescue StandardError
1273
+ nil
1274
+ end
1275
+
1276
+ begin
1277
+ Product.clear_index!
1278
+ rescue StandardError
1279
+ nil
1280
+ end
1281
+ end
1282
+
1283
+ it "attaches generated synonym sets for inline synonym DSL" do
1284
+ begin
1285
+ Product.clear_index!
1286
+ rescue StandardError
1287
+ nil
1288
+ end
1289
+ collection_obj = Product.typesense_index
1290
+
1291
+ collection = Typesense.client.collections[collection_obj[:collection_name]].retrieve
1292
+ generated_synonym_set = "#{collection_obj[:collection_name]}_synonyms_index"
1293
+
1294
+ expect(collection["synonym_sets"]).to include(generated_synonym_set)
1295
+
1296
+ synonym_set = Typesense.client.synonym_sets[generated_synonym_set].retrieve
1297
+ item_ids = synonym_set.fetch("items").map { |item| item["id"] }
1298
+
1299
+ expect(item_ids).to include("phone-synonym", "smart-phone-synonym")
1300
+ end
1301
+
1302
+ it "attaches explicit synonym sets and curation sets to the collection" do
1303
+ synonym_set_name = safe_index_name("shared-product-synonyms")
1304
+ curation_set_name = safe_index_name("shared-product-curations")
1305
+
1306
+ Typesense.client.synonym_sets.upsert(
1307
+ synonym_set_name,
1308
+ {
1309
+ "items" => [
1310
+ { "id" => "shared-phone", "root" => "phone", "synonyms" => %w[handset mobile] }
1311
+ ]
1312
+ }
1313
+ )
1314
+ Typesense.client.curation_sets.upsert(
1315
+ curation_set_name,
1316
+ {
1317
+ "items" => [
1318
+ {
1319
+ "id" => "promote-phone",
1320
+ "rule" => { "query" => "phone", "match" => "exact" },
1321
+ "includes" => [{ "id" => "1", "position" => 1 }],
1322
+ "excludes" => [],
1323
+ "filter_curated_hits" => false,
1324
+ "remove_matched_tokens" => false,
1325
+ "stop_processing" => true
1326
+ }
1327
+ ]
1328
+ }
1329
+ )
1330
+
1331
+ V30ResourceProduct.typesense_index
1332
+
1333
+ collection = Typesense.client.collections[V30ResourceProduct.collection_name].retrieve
1334
+
1335
+ expect(collection["synonym_sets"]).to include(synonym_set_name)
1336
+ expect(collection["curation_sets"]).to include(curation_set_name)
1337
+ end
1338
+
1339
+ it "preserves attached synonym sets and curation sets through reindex" do
1340
+ synonym_set_name = safe_index_name("shared-product-synonyms")
1341
+ curation_set_name = safe_index_name("shared-product-curations")
1342
+
1343
+ Typesense.client.synonym_sets.upsert(
1344
+ synonym_set_name,
1345
+ { "items" => [{ "id" => "shared-phone", "root" => "phone", "synonyms" => %w[handset mobile] }] }
1346
+ )
1347
+ Typesense.client.curation_sets.upsert(
1348
+ curation_set_name,
1349
+ {
1350
+ "items" => [
1351
+ {
1352
+ "id" => "promote-phone",
1353
+ "rule" => { "query" => "phone", "match" => "exact" },
1354
+ "includes" => [{ "id" => "1", "position" => 1 }],
1355
+ "excludes" => [],
1356
+ "filter_curated_hits" => false,
1357
+ "remove_matched_tokens" => false,
1358
+ "stop_processing" => true
1359
+ }
1360
+ ]
1361
+ }
1362
+ )
1363
+
1364
+ V30ResourceProduct.typesense_index
1365
+ V30ResourceProduct.reindex
1366
+
1367
+ collection = Typesense.client.collections[V30ResourceProduct.collection_name].retrieve
1368
+
1369
+ expect(collection["synonym_sets"]).to include(synonym_set_name)
1370
+ expect(collection["curation_sets"]).to include(curation_set_name)
1371
+ end
1372
+ end
1373
+
1052
1374
  describe "Kaminari" do
1053
1375
  before(:all) do
1054
1376
  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.rc8
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-04-26 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