searchkick 5.0.2 → 5.4.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.
@@ -8,6 +8,9 @@ module Searchkick
8
8
  delegate :body, :params, to: :query
9
9
  delegate_missing_to :private_execute
10
10
 
11
+ attr_reader :model
12
+ alias_method :klass, :model
13
+
11
14
  def initialize(model, term = "*", **options)
12
15
  @model = model
13
16
  @term = term
@@ -26,20 +29,22 @@ module Searchkick
26
29
 
27
30
  def execute
28
31
  Searchkick.warn("The execute method is no longer needed")
29
- private_execute
30
- self
32
+ load
31
33
  end
32
34
 
35
+ # experimental
33
36
  def limit(value)
34
37
  clone.limit!(value)
35
38
  end
36
39
 
40
+ # experimental
37
41
  def limit!(value)
38
42
  check_loaded
39
43
  @options[:limit] = value
40
44
  self
41
45
  end
42
46
 
47
+ # experimental
43
48
  def offset(value = NO_DEFAULT_VALUE)
44
49
  # TODO remove in Searchkick 6
45
50
  if value == NO_DEFAULT_VALUE
@@ -49,22 +54,26 @@ module Searchkick
49
54
  end
50
55
  end
51
56
 
57
+ # experimental
52
58
  def offset!(value)
53
59
  check_loaded
54
60
  @options[:offset] = value
55
61
  self
56
62
  end
57
63
 
64
+ # experimental
58
65
  def page(value)
59
66
  clone.page!(value)
60
67
  end
61
68
 
69
+ # experimental
62
70
  def page!(value)
63
71
  check_loaded
64
72
  @options[:page] = value
65
73
  self
66
74
  end
67
75
 
76
+ # experimental
68
77
  def per_page(value = NO_DEFAULT_VALUE)
69
78
  # TODO remove in Searchkick 6
70
79
  if value == NO_DEFAULT_VALUE
@@ -74,24 +83,139 @@ module Searchkick
74
83
  end
75
84
  end
76
85
 
86
+ # experimental
77
87
  def per_page!(value)
78
88
  check_loaded
79
89
  @options[:per_page] = value
80
90
  self
81
91
  end
82
92
 
93
+ # experimental
94
+ def where(value = NO_DEFAULT_VALUE)
95
+ if value == NO_DEFAULT_VALUE
96
+ Where.new(self)
97
+ else
98
+ clone.where!(value)
99
+ end
100
+ end
101
+
102
+ # experimental
103
+ def where!(value)
104
+ check_loaded
105
+ if @options[:where]
106
+ @options[:where] = {_and: [@options[:where], ensure_permitted(value)]}
107
+ else
108
+ @options[:where] = ensure_permitted(value)
109
+ end
110
+ self
111
+ end
112
+
113
+ # experimental
114
+ def rewhere(value)
115
+ clone.rewhere!(value)
116
+ end
117
+
118
+ # experimental
119
+ def rewhere!(value)
120
+ check_loaded
121
+ @options[:where] = ensure_permitted(value)
122
+ self
123
+ end
124
+
125
+ # experimental
126
+ def order(*values)
127
+ clone.order!(*values)
128
+ end
129
+
130
+ # experimental
131
+ def order!(*values)
132
+ values = values.first if values.size == 1 && values.first.is_a?(Array)
133
+ check_loaded
134
+ (@options[:order] ||= []).concat(values)
135
+ self
136
+ end
137
+
138
+ # experimental
139
+ def reorder(*values)
140
+ clone.reorder!(*values)
141
+ end
142
+
143
+ # experimental
144
+ def reorder!(*values)
145
+ check_loaded
146
+ @options[:order] = values
147
+ self
148
+ end
149
+
150
+ # experimental
151
+ def select(*values, &block)
152
+ if block_given?
153
+ private_execute.select(*values, &block)
154
+ else
155
+ clone.select!(*values)
156
+ end
157
+ end
158
+
159
+ # experimental
160
+ def select!(*values)
161
+ check_loaded
162
+ (@options[:select] ||= []).concat(values)
163
+ self
164
+ end
165
+
166
+ # experimental
167
+ def reselect(*values)
168
+ clone.reselect!(*values)
169
+ end
170
+
171
+ # experimental
172
+ def reselect!(*values)
173
+ check_loaded
174
+ @options[:select] = values
175
+ self
176
+ end
177
+
178
+ # experimental
179
+ def includes(*values)
180
+ clone.includes!(*values)
181
+ end
182
+
183
+ # experimental
184
+ def includes!(*values)
185
+ check_loaded
186
+ (@options[:includes] ||= []).concat(values)
187
+ self
188
+ end
189
+
190
+ # experimental
83
191
  def only(*keys)
84
192
  Relation.new(@model, @term, **@options.slice(*keys))
85
193
  end
86
194
 
195
+ # experimental
87
196
  def except(*keys)
88
197
  Relation.new(@model, @term, **@options.except(*keys))
89
198
  end
90
199
 
200
+ # experimental
201
+ def load
202
+ private_execute
203
+ self
204
+ end
205
+
91
206
  def loaded?
92
207
  !@execute.nil?
93
208
  end
94
209
 
210
+ def respond_to_missing?(method_name, include_all)
211
+ Results.new(nil, nil, nil).respond_to?(method_name, include_all) || super
212
+ end
213
+
214
+ # TODO uncomment in 6.0
215
+ # def to_yaml
216
+ # private_execute.to_a.to_yaml
217
+ # end
218
+
95
219
  private
96
220
 
97
221
  def private_execute
@@ -108,5 +232,16 @@ module Searchkick
108
232
  # reset query since options will change
109
233
  @query = nil
110
234
  end
235
+
236
+ # provides *very* basic protection from unfiltered parameters
237
+ # this is not meant to be comprehensive and may be expanded in the future
238
+ def ensure_permitted(obj)
239
+ obj.to_h
240
+ end
241
+
242
+ def initialize_copy(other)
243
+ super
244
+ @execute = nil
245
+ end
111
246
  end
112
247
  end
@@ -14,12 +14,17 @@ module Searchkick
14
14
  relation = relation.search_import
15
15
  end
16
16
 
17
- # remove unneeded loading for async
18
- if mode == :async
17
+ # remove unneeded loading for async and queue
18
+ if mode == :async || mode == :queue
19
19
  if relation.respond_to?(:primary_key)
20
- relation = relation.select(relation.primary_key).except(:includes, :preload)
20
+ relation = relation.except(:includes, :preload)
21
+ unless mode == :queue && relation.klass.method_defined?(:search_routing)
22
+ relation = relation.except(:select).select(relation.primary_key)
23
+ end
21
24
  elsif relation.respond_to?(:only)
22
- relation = relation.only(:_id)
25
+ unless mode == :queue && relation.klass.method_defined?(:search_routing)
26
+ relation = relation.only(:_id)
27
+ end
23
28
  end
24
29
  end
25
30
 
@@ -42,11 +47,11 @@ module Searchkick
42
47
  end
43
48
 
44
49
  def batches_left
45
- Searchkick.with_redis { |r| r.scard(batches_key) }
50
+ Searchkick.with_redis { |r| r.call("SCARD", batches_key) }
46
51
  end
47
52
 
48
53
  def batch_completed(batch_id)
49
- Searchkick.with_redis { |r| r.srem(batches_key, batch_id) }
54
+ Searchkick.with_redis { |r| r.call("SREM", batches_key, [batch_id]) }
50
55
  end
51
56
 
52
57
  private
@@ -134,7 +139,7 @@ module Searchkick
134
139
  end
135
140
 
136
141
  def batch_job(class_name, batch_id, record_ids)
137
- Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
142
+ Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
138
143
  Searchkick::BulkReindexJob.perform_later(
139
144
  class_name: class_name,
140
145
  index_name: index.name,
@@ -0,0 +1,28 @@
1
+ module Searchkick
2
+ module Reranking
3
+ def self.rrf(first_ranking, *rankings, k: 60)
4
+ rankings.unshift(first_ranking)
5
+ rankings.map!(&:to_ary)
6
+
7
+ ranks = []
8
+ results = []
9
+ rankings.each do |ranking|
10
+ ranks << ranking.map.with_index.to_h { |v, i| [v, i + 1] }
11
+ results.concat(ranking)
12
+ end
13
+
14
+ results =
15
+ results.uniq.map do |result|
16
+ score =
17
+ ranks.sum do |rank|
18
+ r = rank[result]
19
+ r ? 1.0 / (k + r) : 0.0
20
+ end
21
+
22
+ {result: result, score: score}
23
+ end
24
+
25
+ results.sort_by { |v| -v[:score] }
26
+ end
27
+ end
28
+ end
@@ -3,6 +3,7 @@ module Searchkick
3
3
  include Enumerable
4
4
  extend Forwardable
5
5
 
6
+ # TODO remove klass and options in 6.0
6
7
  attr_reader :klass, :response, :options
7
8
 
8
9
  def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
@@ -13,6 +14,7 @@ module Searchkick
13
14
  @options = options
14
15
  end
15
16
 
17
+ # TODO make private in 6.0
16
18
  def results
17
19
  @results ||= with_hit.map(&:first)
18
20
  end
@@ -302,7 +304,7 @@ module Searchkick
302
304
  def build_hits
303
305
  @build_hits ||= begin
304
306
  if missing_records.any?
305
- Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| v[:id] }.join(", ")}")
307
+ Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| "#{Array(v[:model]).map(&:model_name).sort.join("/")} #{v[:id]}" }.join(", ")}")
306
308
  end
307
309
  with_hit_and_missing_records[0]
308
310
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Script
3
+ attr_reader :source, :lang, :params
4
+
5
+ def initialize(source, lang: "painless", params: {})
6
+ @source = source
7
+ @lang = lang
8
+ @params = params
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "5.0.2"
2
+ VERSION = "5.4.0"
3
3
  end
@@ -0,0 +1,11 @@
1
+ module Searchkick
2
+ class Where
3
+ def initialize(relation)
4
+ @relation = relation
5
+ end
6
+
7
+ def not(value)
8
+ @relation.where(_not: value)
9
+ end
10
+ end
11
+ end
data/lib/searchkick.rb CHANGED
@@ -10,26 +10,29 @@ require "hashie"
10
10
  require "forwardable"
11
11
 
12
12
  # modules
13
- require "searchkick/controller_runtime"
14
- require "searchkick/index"
15
- require "searchkick/index_cache"
16
- require "searchkick/index_options"
17
- require "searchkick/indexer"
18
- require "searchkick/hash_wrapper"
19
- require "searchkick/log_subscriber"
20
- require "searchkick/model"
21
- require "searchkick/multi_search"
22
- require "searchkick/query"
23
- require "searchkick/reindex_queue"
24
- require "searchkick/record_data"
25
- require "searchkick/record_indexer"
26
- require "searchkick/relation"
27
- require "searchkick/relation_indexer"
28
- require "searchkick/results"
29
- require "searchkick/version"
13
+ require_relative "searchkick/controller_runtime"
14
+ require_relative "searchkick/index"
15
+ require_relative "searchkick/index_cache"
16
+ require_relative "searchkick/index_options"
17
+ require_relative "searchkick/indexer"
18
+ require_relative "searchkick/hash_wrapper"
19
+ require_relative "searchkick/log_subscriber"
20
+ require_relative "searchkick/model"
21
+ require_relative "searchkick/multi_search"
22
+ require_relative "searchkick/query"
23
+ require_relative "searchkick/reindex_queue"
24
+ require_relative "searchkick/record_data"
25
+ require_relative "searchkick/record_indexer"
26
+ require_relative "searchkick/relation"
27
+ require_relative "searchkick/relation_indexer"
28
+ require_relative "searchkick/reranking"
29
+ require_relative "searchkick/results"
30
+ require_relative "searchkick/script"
31
+ require_relative "searchkick/version"
32
+ require_relative "searchkick/where"
30
33
 
31
34
  # integrations
32
- require "searchkick/railtie" if defined?(Rails)
35
+ require_relative "searchkick/railtie" if defined?(Rails)
33
36
 
34
37
  module Searchkick
35
38
  # requires faraday
@@ -134,11 +137,21 @@ module Searchkick
134
137
  @opensearch
135
138
  end
136
139
 
137
- def self.server_below?(version)
138
- server_version = opensearch? ? "7.10.2" : self.server_version
140
+ # TODO always check true version in Searchkick 6
141
+ def self.server_below?(version, true_version = false)
142
+ server_version = !true_version && opensearch? ? "7.10.2" : self.server_version
139
143
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
140
144
  end
141
145
 
146
+ # private
147
+ def self.knn_support?
148
+ if opensearch?
149
+ !server_below?("2.4.0", true)
150
+ else
151
+ !server_below?("8.6.0")
152
+ end
153
+ end
154
+
142
155
  def self.search(term = "*", model: nil, **options, &block)
143
156
  options = options.dup
144
157
  klass = model
@@ -180,13 +193,20 @@ module Searchkick
180
193
  queries = queries.map { |q| q.send(:query) }
181
194
  event = {
182
195
  name: "Multi Search",
183
- body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
196
+ body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
184
197
  }
185
198
  ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
186
199
  MultiSearch.new(queries).perform
187
200
  end
188
201
  end
189
202
 
203
+ # script
204
+
205
+ # experimental
206
+ def self.script(source, **options)
207
+ Script.new(source, **options)
208
+ end
209
+
190
210
  # callbacks
191
211
 
192
212
  def self.enable_callbacks
@@ -283,7 +303,7 @@ module Searchkick
283
303
  relation
284
304
  end
285
305
 
286
- # private
306
+ # public (for reindexing conversions)
287
307
  def self.load_model(class_name, allow_child: false)
288
308
  model = class_name.safe_constantize
289
309
  raise Error, "Could not find class: #{class_name}" unless model
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.2
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-03 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.2'
19
+ version: '6.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5.2'
26
+ version: '6.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: hashie
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -69,8 +69,11 @@ files:
69
69
  - lib/searchkick/reindex_v2_job.rb
70
70
  - lib/searchkick/relation.rb
71
71
  - lib/searchkick/relation_indexer.rb
72
+ - lib/searchkick/reranking.rb
72
73
  - lib/searchkick/results.rb
74
+ - lib/searchkick/script.rb
73
75
  - lib/searchkick/version.rb
76
+ - lib/searchkick/where.rb
74
77
  - lib/tasks/searchkick.rake
75
78
  homepage: https://github.com/ankane/searchkick
76
79
  licenses:
@@ -84,14 +87,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
87
  requirements:
85
88
  - - ">="
86
89
  - !ruby/object:Gem::Version
87
- version: '2.6'
90
+ version: '3.1'
88
91
  required_rubygems_version: !ruby/object:Gem::Requirement
89
92
  requirements:
90
93
  - - ">="
91
94
  - !ruby/object:Gem::Version
92
95
  version: '0'
93
96
  requirements: []
94
- rubygems_version: 3.3.3
97
+ rubygems_version: 3.5.11
95
98
  signing_key:
96
99
  specification_version: 4
97
100
  summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch