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 +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +48 -47
- data/README.md +9 -1
- data/lib/typesense/config.rb +18 -0
- data/lib/typesense/pagination/pagy.rb +3 -1
- data/lib/typesense/version.rb +1 -1
- data/lib/typesense-rails.rb +115 -7
- data/spec/integration_spec.rb +247 -0
- data/spec/spec_helper.rb +14 -0
- data/typesense-rails.gemspec +3 -3
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c0186b1b9d3de405d50d5d736725d4a93863e029e116544b7d3870b83dad9fa
|
|
4
|
+
data.tar.gz: 13c8237df903014ad8147f624f3d4ca7b99429023cf6311f438b9f6f5986f312
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b61f66f7abcdefc86bbd3eb0ac5a668469d299f7d0265b984950bda1fdaa2a7b6ab9ab4ff9ef5a088925e92a96ec0ba11cedce05aab48ef4e6a9e9060afb351b
|
|
7
|
+
data.tar.gz: 3da63dd78087369681b89aa9e82c0b516348c4f9db9eb119ee51f6025ef9631484048b818bff9cca9c7d08c091eceffe720eaae11d76e3e02bab02f6bd9ba18f
|
data/Gemfile
CHANGED
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.
|
|
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.
|
|
70
|
-
bigdecimal (
|
|
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.
|
|
75
|
-
concurrent-ruby (1.3.
|
|
74
|
+
cgi (0.5.1)
|
|
75
|
+
concurrent-ruby (1.3.6)
|
|
76
76
|
crass (1.0.6)
|
|
77
|
-
date (3.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
89
|
+
i18n (1.14.8)
|
|
88
90
|
concurrent-ruby (~> 1.0)
|
|
89
|
-
json (2.
|
|
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.
|
|
100
|
+
loofah (2.25.1)
|
|
99
101
|
crass (~> 1.0.2)
|
|
100
102
|
nokogiri (>= 1.12.0)
|
|
101
|
-
mail (2.
|
|
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.
|
|
111
|
-
net-imap (0.4.
|
|
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.
|
|
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.
|
|
129
|
+
parser (3.3.10.2)
|
|
131
130
|
ast (~> 2.4.1)
|
|
132
131
|
racc
|
|
133
|
-
prism (1.
|
|
134
|
-
psych (5.
|
|
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.
|
|
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.
|
|
161
|
-
loofah (~> 2.
|
|
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.
|
|
171
|
-
rdoc (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
188
|
-
rubocop (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.
|
|
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.
|
|
199
|
+
rubocop-ast (1.49.1)
|
|
200
200
|
parser (>= 3.3.7.2)
|
|
201
|
-
prism (~> 1.
|
|
201
|
+
prism (~> 1.7)
|
|
202
202
|
ruby-progressbar (1.13.0)
|
|
203
|
-
|
|
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.
|
|
216
|
-
thor (1.
|
|
217
|
-
timeout (0.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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 (
|
|
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
|
data/lib/typesense/config.rb
CHANGED
|
@@ -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(
|
|
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)
|
data/lib/typesense/version.rb
CHANGED
data/lib/typesense-rails.rb
CHANGED
|
@@ -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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
data/spec/integration_spec.rb
CHANGED
|
@@ -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
|
data/typesense-rails.gemspec
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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
|