searchkick 6.0.2 → 6.1.0
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/CHANGELOG.md +11 -0
- data/LICENSE.txt +1 -1
- data/README.md +2 -26
- data/lib/searchkick/index.rb +1 -1
- data/lib/searchkick/index_options.rb +3 -3
- data/lib/searchkick/query.rb +62 -3
- data/lib/searchkick/relation.rb +74 -25
- data/lib/searchkick/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33be926c61ab632ecf611bce7ceecc0f23cec9eedde74758b66c297936e48293
|
|
4
|
+
data.tar.gz: c9dd60ae1fe20336066554d08c042a2fde153d7d1bb2e49bacc32219a3943915
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95ebb7c3cc4d511a7f75a1cef3f73f64e1bbf8242c18091cbacfe71bcd244840538c11f2c83db0a676f90c442668f4a1cb1c320d6e970a23bec00ee7ccec2935
|
|
7
|
+
data.tar.gz: eb7638d2cda8512314894dbcd205ccff57293b332f3e52e3fdbd4a8bb6359ea8cb309b2bd53b3b7c67515538e07422e16eca919092757106307abd22781df603
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 6.1.0 (2026-02-18)
|
|
2
|
+
|
|
3
|
+
- Added `per` method
|
|
4
|
+
- Fixed error with `aggs` method and non-hash arguments
|
|
5
|
+
- Fixed smart aggs behavior when multiple `where` calls
|
|
6
|
+
|
|
7
|
+
## 6.0.3 (2026-01-06)
|
|
8
|
+
|
|
9
|
+
- Fixed `inspect` method for `Relation`
|
|
10
|
+
|
|
1
11
|
## 6.0.2 (2025-10-24)
|
|
2
12
|
|
|
3
13
|
- Fixed `as_json` method for `HashWrapper`
|
|
@@ -22,6 +32,7 @@
|
|
|
22
32
|
- Removed default quantization for `knn` option for Elasticsearch 8.14+
|
|
23
33
|
- Removed `results` method (use `to_a` instead)
|
|
24
34
|
- Removed `execute` option and method (no longer needed)
|
|
35
|
+
- Removed `options` method (use individual methods instead)
|
|
25
36
|
- Removed dependency on Hashie
|
|
26
37
|
- Deprecated `conversions` option in favor of `conversions_v2`
|
|
27
38
|
- Dropped support for Elasticsearch 7 and OpenSearch 1
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1182,7 +1182,7 @@ And to search, use:
|
|
|
1182
1182
|
```ruby
|
|
1183
1183
|
Animal.search("*") # all animals
|
|
1184
1184
|
Dog.search("*") # just dogs
|
|
1185
|
-
Animal.search("*").type(
|
|
1185
|
+
Animal.search("*").type(Cat, Dog) # just cats and dogs
|
|
1186
1186
|
```
|
|
1187
1187
|
|
|
1188
1188
|
**Notes:**
|
|
@@ -1547,30 +1547,6 @@ See [Production Rails](https://github.com/ankane/production_rails) for other goo
|
|
|
1547
1547
|
|
|
1548
1548
|
## Performance
|
|
1549
1549
|
|
|
1550
|
-
### JSON Generation
|
|
1551
|
-
|
|
1552
|
-
Increase performance with faster JSON generation. Add to your Gemfile:
|
|
1553
|
-
|
|
1554
|
-
```ruby
|
|
1555
|
-
gem "json", ">= 2.10.2"
|
|
1556
|
-
```
|
|
1557
|
-
|
|
1558
|
-
And create an initializer with:
|
|
1559
|
-
|
|
1560
|
-
```ruby
|
|
1561
|
-
class SearchSerializer
|
|
1562
|
-
CODER = JSON::Coder.new { |v, _| v.is_a?(Time) ? v.as_json : v }
|
|
1563
|
-
|
|
1564
|
-
def dump(object)
|
|
1565
|
-
CODER.generate(object)
|
|
1566
|
-
end
|
|
1567
|
-
end
|
|
1568
|
-
|
|
1569
|
-
Elasticsearch::API.settings[:serializer] = SearchSerializer.new
|
|
1570
|
-
# or
|
|
1571
|
-
OpenSearch::API.settings[:serializer] = SearchSerializer.new
|
|
1572
|
-
```
|
|
1573
|
-
|
|
1574
1550
|
### Persistent HTTP Connections
|
|
1575
1551
|
|
|
1576
1552
|
Significantly increase performance with persistent HTTP connections. Add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile and it’ll automatically be used.
|
|
@@ -2311,7 +2287,7 @@ Product.search("apples").where(in_stock: true).limit(10).offset(50)
|
|
|
2311
2287
|
|
|
2312
2288
|
All existing options can be used as methods, or you can continue to use the existing API.
|
|
2313
2289
|
|
|
2314
|
-
This release also significantly improves the performance of searches when using conversions. To upgrade without downtime, add `conversions_v2` to your model and an additional field to `search_data`:
|
|
2290
|
+
This release also significantly improves the performance of searches when using conversions. To upgrade conversions without downtime, add `conversions_v2` to your model and an additional field to `search_data`:
|
|
2315
2291
|
|
|
2316
2292
|
```ruby
|
|
2317
2293
|
class Product < ApplicationRecord
|
data/lib/searchkick/index.rb
CHANGED
|
@@ -359,7 +359,7 @@ module Searchkick
|
|
|
359
359
|
end
|
|
360
360
|
|
|
361
361
|
# https://gist.github.com/jarosan/3124884
|
|
362
|
-
#
|
|
362
|
+
# https://www.elastic.co/blog/changing-mapping-with-zero-downtime/
|
|
363
363
|
def full_reindex(relation, import: true, resume: false, retain: false, mode: nil, refresh_interval: nil, scope: nil, wait: nil, job_options: nil)
|
|
364
364
|
raise ArgumentError, "wait only available in :async mode" if !wait.nil? && mode != :async
|
|
365
365
|
raise ArgumentError, "Full reindex does not support :queue mode - use :async mode instead" if mode == :queue
|
|
@@ -524,7 +524,7 @@ module Searchkick
|
|
|
524
524
|
|
|
525
525
|
dynamic_fields = {
|
|
526
526
|
# analyzed field must be the default field for include_in_all
|
|
527
|
-
#
|
|
527
|
+
# https://www.elastic.co/guide/reference/mapping/multi-field-type/
|
|
528
528
|
# however, we can include the not_analyzed field in _all
|
|
529
529
|
# and the _all index analyzer will take care of it
|
|
530
530
|
"{name}" => keyword_mapping
|
|
@@ -544,7 +544,7 @@ module Searchkick
|
|
|
544
544
|
end
|
|
545
545
|
end
|
|
546
546
|
|
|
547
|
-
#
|
|
547
|
+
# https://www.elastic.co/guide/reference/mapping/multi-field-type/
|
|
548
548
|
multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
|
|
549
549
|
|
|
550
550
|
mappings = {
|
|
@@ -578,7 +578,7 @@ module Searchkick
|
|
|
578
578
|
# https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
|
|
579
579
|
# TODO use a snowball stemmer on synonyms when creating the token filter
|
|
580
580
|
|
|
581
|
-
#
|
|
581
|
+
# https://discuss.elastic.co/t/synonym-multi-words-search/10964
|
|
582
582
|
# I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
|
|
583
583
|
# - Only apply the synonym expansion at index time
|
|
584
584
|
# - Don't have the synonym filter applied search
|
data/lib/searchkick/query.rb
CHANGED
|
@@ -872,10 +872,21 @@ module Searchkick
|
|
|
872
872
|
}
|
|
873
873
|
end
|
|
874
874
|
|
|
875
|
-
where = {}
|
|
876
|
-
where = ensure_permitted(options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
|
|
877
875
|
agg_where = ensure_permitted(agg_options[:where] || {})
|
|
878
|
-
|
|
876
|
+
if options[:smart_aggs] != false && options[:where]
|
|
877
|
+
where = ensure_permitted(options[:where])
|
|
878
|
+
where_without_field = where.reject { |k| k == field }
|
|
879
|
+
# where_without_field = where_without_field(where, field.to_s)
|
|
880
|
+
if where_without_field.any?
|
|
881
|
+
if agg_where.any?
|
|
882
|
+
agg_where = where.merge(agg_where)
|
|
883
|
+
# agg_where = combine_agg_where(agg_where, where_without_field)
|
|
884
|
+
else
|
|
885
|
+
agg_where = where_without_field
|
|
886
|
+
end
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
agg_filters = where_filters(agg_where)
|
|
879
890
|
|
|
880
891
|
# only do one level comparison for simplicity
|
|
881
892
|
filters.select! do |filter|
|
|
@@ -902,6 +913,53 @@ module Searchkick
|
|
|
902
913
|
end
|
|
903
914
|
end
|
|
904
915
|
|
|
916
|
+
def where_without_field(where, field)
|
|
917
|
+
result = {}
|
|
918
|
+
where.each do |f, v|
|
|
919
|
+
case f
|
|
920
|
+
when :_and
|
|
921
|
+
r = v.map { |v2| where_without_field(v2, field) }.reject(&:empty?)
|
|
922
|
+
result[f] = r unless r.empty?
|
|
923
|
+
when :_or
|
|
924
|
+
r = v.map { |v2| where_without_field(v2, field) }
|
|
925
|
+
result[f] = r unless r.any?(&:empty?)
|
|
926
|
+
when :or
|
|
927
|
+
r = v.map { |v2| v2.map { |v3| where_without_field(v3, field) }.reject { |v2| v2.any?(&:empty?) } }
|
|
928
|
+
result[f] = r unless r.empty?
|
|
929
|
+
when :_not
|
|
930
|
+
r = where_without_field(v, field)
|
|
931
|
+
result[f] = r unless r.empty?
|
|
932
|
+
when :_script
|
|
933
|
+
result[f] = v
|
|
934
|
+
else
|
|
935
|
+
if f.to_s != field
|
|
936
|
+
result[f] = v
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
result
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def combine_agg_where(agg_where, where)
|
|
944
|
+
result = agg_where.dup
|
|
945
|
+
field_keys = result.except(:_and, :_or, :or, :_not, :_script).transform_keys(&:to_s)
|
|
946
|
+
where.each do |f, v|
|
|
947
|
+
case f
|
|
948
|
+
when :_and, :_or, :or, :_not, :_script
|
|
949
|
+
if result.key?(f)
|
|
950
|
+
# combine with _and if needed
|
|
951
|
+
result[:_and] ||= []
|
|
952
|
+
result[:_and] += [{f => v}]
|
|
953
|
+
else
|
|
954
|
+
result[f] = v
|
|
955
|
+
end
|
|
956
|
+
else
|
|
957
|
+
result[f] = v unless field_keys.include?(f.to_s)
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
result
|
|
961
|
+
end
|
|
962
|
+
|
|
905
963
|
def set_knn(payload, knn, per_page, offset)
|
|
906
964
|
if term != "*"
|
|
907
965
|
raise ArgumentError, "Use Searchkick.multi_search for hybrid search"
|
|
@@ -1049,6 +1107,7 @@ module Searchkick
|
|
|
1049
1107
|
(where || {}).each do |field, value|
|
|
1050
1108
|
field = :_id if field.to_s == "id"
|
|
1051
1109
|
|
|
1110
|
+
# update smart aggs when adding new symbol
|
|
1052
1111
|
if field == :or
|
|
1053
1112
|
value.each do |or_clause|
|
|
1054
1113
|
filters << {bool: {should: or_clause.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
|
data/lib/searchkick/relation.rb
CHANGED
|
@@ -17,27 +17,36 @@ module Searchkick
|
|
|
17
17
|
@options = options
|
|
18
18
|
|
|
19
19
|
# generate query to validate options
|
|
20
|
-
query
|
|
20
|
+
query if options.any?
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# same as Active Record
|
|
24
24
|
def inspect
|
|
25
|
-
entries =
|
|
25
|
+
entries = private_execute.first(11).map!(&:inspect)
|
|
26
26
|
entries[10] = "..." if entries.size == 11
|
|
27
27
|
"#<#{self.class.name} [#{entries.join(', ')}]>"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def aggs(
|
|
31
|
-
if
|
|
30
|
+
def aggs(*args, **kwargs)
|
|
31
|
+
if args.empty? && kwargs.empty?
|
|
32
32
|
private_execute.aggs
|
|
33
33
|
else
|
|
34
|
-
clone.aggs!(
|
|
34
|
+
clone.aggs!(*args, **kwargs)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def aggs!(
|
|
38
|
+
def aggs!(*args, **kwargs)
|
|
39
39
|
check_loaded
|
|
40
|
-
|
|
40
|
+
aggs = {}
|
|
41
|
+
args.flatten.each do |arg|
|
|
42
|
+
if arg.is_a?(Hash)
|
|
43
|
+
aggs.merge!(arg)
|
|
44
|
+
else
|
|
45
|
+
aggs[arg] = {}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
aggs.merge!(kwargs)
|
|
49
|
+
merge_option(:aggs, aggs)
|
|
41
50
|
self
|
|
42
51
|
end
|
|
43
52
|
|
|
@@ -61,7 +70,7 @@ module Searchkick
|
|
|
61
70
|
|
|
62
71
|
def body_options!(value)
|
|
63
72
|
check_loaded
|
|
64
|
-
(
|
|
73
|
+
merge_option(:body_options, value)
|
|
65
74
|
self
|
|
66
75
|
end
|
|
67
76
|
|
|
@@ -86,7 +95,7 @@ module Searchkick
|
|
|
86
95
|
elsif !value.is_a?(Hash)
|
|
87
96
|
value = {value => {factor: 1}}
|
|
88
97
|
end
|
|
89
|
-
(
|
|
98
|
+
merge_option(:boost_by, value)
|
|
90
99
|
self
|
|
91
100
|
end
|
|
92
101
|
|
|
@@ -98,7 +107,7 @@ module Searchkick
|
|
|
98
107
|
check_loaded
|
|
99
108
|
# legacy format
|
|
100
109
|
value = {value[:field] => value.except(:field)} if value[:field]
|
|
101
|
-
(
|
|
110
|
+
merge_option(:boost_by_distance, value)
|
|
102
111
|
self
|
|
103
112
|
end
|
|
104
113
|
|
|
@@ -108,7 +117,7 @@ module Searchkick
|
|
|
108
117
|
|
|
109
118
|
def boost_by_recency!(value)
|
|
110
119
|
check_loaded
|
|
111
|
-
(
|
|
120
|
+
merge_option(:boost_by_recency, value)
|
|
112
121
|
self
|
|
113
122
|
end
|
|
114
123
|
|
|
@@ -119,7 +128,7 @@ module Searchkick
|
|
|
119
128
|
def boost_where!(value)
|
|
120
129
|
check_loaded
|
|
121
130
|
# TODO merge duplicate fields
|
|
122
|
-
(
|
|
131
|
+
merge_option(:boost_where, value)
|
|
123
132
|
self
|
|
124
133
|
end
|
|
125
134
|
|
|
@@ -189,7 +198,7 @@ module Searchkick
|
|
|
189
198
|
|
|
190
199
|
def exclude!(*values)
|
|
191
200
|
check_loaded
|
|
192
|
-
(
|
|
201
|
+
concat_option(:exclude, values.flatten)
|
|
193
202
|
self
|
|
194
203
|
end
|
|
195
204
|
|
|
@@ -209,7 +218,7 @@ module Searchkick
|
|
|
209
218
|
|
|
210
219
|
def fields!(*values)
|
|
211
220
|
check_loaded
|
|
212
|
-
(
|
|
221
|
+
concat_option(:fields, values.flatten)
|
|
213
222
|
self
|
|
214
223
|
end
|
|
215
224
|
|
|
@@ -229,7 +238,7 @@ module Searchkick
|
|
|
229
238
|
|
|
230
239
|
def includes!(*values)
|
|
231
240
|
check_loaded
|
|
232
|
-
(
|
|
241
|
+
concat_option(:includes, values.flatten)
|
|
233
242
|
self
|
|
234
243
|
end
|
|
235
244
|
|
|
@@ -243,7 +252,7 @@ module Searchkick
|
|
|
243
252
|
if values.all? { |v| v.respond_to?(:searchkick_index) }
|
|
244
253
|
models!(*values)
|
|
245
254
|
else
|
|
246
|
-
(
|
|
255
|
+
concat_option(:index_name, values)
|
|
247
256
|
self
|
|
248
257
|
end
|
|
249
258
|
end
|
|
@@ -254,7 +263,7 @@ module Searchkick
|
|
|
254
263
|
|
|
255
264
|
def indices_boost!(value)
|
|
256
265
|
check_loaded
|
|
257
|
-
(
|
|
266
|
+
merge_option(:indices_boost, value)
|
|
258
267
|
self
|
|
259
268
|
end
|
|
260
269
|
|
|
@@ -319,7 +328,7 @@ module Searchkick
|
|
|
319
328
|
|
|
320
329
|
def models!(*values)
|
|
321
330
|
check_loaded
|
|
322
|
-
(
|
|
331
|
+
concat_option(:models, values.flatten)
|
|
323
332
|
self
|
|
324
333
|
end
|
|
325
334
|
|
|
@@ -329,7 +338,7 @@ module Searchkick
|
|
|
329
338
|
|
|
330
339
|
def model_includes!(*values)
|
|
331
340
|
check_loaded
|
|
332
|
-
(
|
|
341
|
+
concat_option(:model_includes, values.flatten)
|
|
333
342
|
self
|
|
334
343
|
end
|
|
335
344
|
|
|
@@ -373,7 +382,7 @@ module Searchkick
|
|
|
373
382
|
|
|
374
383
|
def order!(*values)
|
|
375
384
|
check_loaded
|
|
376
|
-
(
|
|
385
|
+
concat_option(:order, values.flatten)
|
|
377
386
|
self
|
|
378
387
|
end
|
|
379
388
|
|
|
@@ -409,6 +418,10 @@ module Searchkick
|
|
|
409
418
|
end
|
|
410
419
|
end
|
|
411
420
|
|
|
421
|
+
def per(value)
|
|
422
|
+
per_page(value)
|
|
423
|
+
end
|
|
424
|
+
|
|
412
425
|
def per_page!(value)
|
|
413
426
|
check_loaded
|
|
414
427
|
# TODO set limit?
|
|
@@ -432,7 +445,7 @@ module Searchkick
|
|
|
432
445
|
|
|
433
446
|
def request_params!(value)
|
|
434
447
|
check_loaded
|
|
435
|
-
(
|
|
448
|
+
merge_option(:request_params, value)
|
|
436
449
|
self
|
|
437
450
|
end
|
|
438
451
|
|
|
@@ -482,7 +495,7 @@ module Searchkick
|
|
|
482
495
|
|
|
483
496
|
def select!(*values)
|
|
484
497
|
check_loaded
|
|
485
|
-
(
|
|
498
|
+
concat_option(:select, values.flatten)
|
|
486
499
|
self
|
|
487
500
|
end
|
|
488
501
|
|
|
@@ -546,7 +559,7 @@ module Searchkick
|
|
|
546
559
|
|
|
547
560
|
def type!(*values)
|
|
548
561
|
check_loaded
|
|
549
|
-
(
|
|
562
|
+
concat_option(:type, values.flatten)
|
|
550
563
|
self
|
|
551
564
|
end
|
|
552
565
|
|
|
@@ -560,10 +573,18 @@ module Searchkick
|
|
|
560
573
|
|
|
561
574
|
def where!(value)
|
|
562
575
|
check_loaded
|
|
576
|
+
value = ensure_permitted(value)
|
|
563
577
|
if @options[:where]
|
|
564
|
-
@options[:where]
|
|
578
|
+
if @options[:where][:_and].is_a?(Array)
|
|
579
|
+
merge_option(:where, {_and: @options[:where][:_and] + [value]})
|
|
580
|
+
# keep simple when possible for smart aggs
|
|
581
|
+
elsif !@options[:where].keys.intersect?(value.keys)
|
|
582
|
+
merge_option(:where, value)
|
|
583
|
+
else
|
|
584
|
+
@options[:where] = {_and: [@options[:where], value]}
|
|
585
|
+
end
|
|
565
586
|
else
|
|
566
|
-
@options[:where] =
|
|
587
|
+
@options[:where] = value
|
|
567
588
|
end
|
|
568
589
|
self
|
|
569
590
|
end
|
|
@@ -644,6 +665,16 @@ module Searchkick
|
|
|
644
665
|
Results.new(nil, nil, nil).respond_to?(...) || super
|
|
645
666
|
end
|
|
646
667
|
|
|
668
|
+
# TODO uncomment in 7.0
|
|
669
|
+
# def to_json(...)
|
|
670
|
+
# private_execute.to_a.to_json(...)
|
|
671
|
+
# end
|
|
672
|
+
|
|
673
|
+
# TODO uncomment in 7.0
|
|
674
|
+
# def as_json(...)
|
|
675
|
+
# private_execute.to_a.as_json(...)
|
|
676
|
+
# end
|
|
677
|
+
|
|
647
678
|
def to_yaml
|
|
648
679
|
private_execute.to_a.to_yaml
|
|
649
680
|
end
|
|
@@ -673,7 +704,25 @@ module Searchkick
|
|
|
673
704
|
|
|
674
705
|
def initialize_copy(other)
|
|
675
706
|
super
|
|
707
|
+
# shallow dup and avoid updating values in-place
|
|
708
|
+
@options = @options.dup
|
|
676
709
|
@execute = nil
|
|
677
710
|
end
|
|
711
|
+
|
|
712
|
+
def concat_option(key, value)
|
|
713
|
+
if @options[key]
|
|
714
|
+
@options[key] += value
|
|
715
|
+
else
|
|
716
|
+
@options[key] = value.to_ary
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def merge_option(key, value)
|
|
721
|
+
if @options[key]
|
|
722
|
+
@options[key] = @options[key].merge(value)
|
|
723
|
+
else
|
|
724
|
+
@options[key] = value.to_hash
|
|
725
|
+
end
|
|
726
|
+
end
|
|
678
727
|
end
|
|
679
728
|
end
|
data/lib/searchkick/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: searchkick
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.0
|
|
4
|
+
version: 6.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
- !ruby/object:Gem::Version
|
|
78
78
|
version: '0'
|
|
79
79
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
80
|
+
rubygems_version: 4.0.3
|
|
81
81
|
specification_version: 4
|
|
82
82
|
summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch
|
|
83
83
|
test_files: []
|