searchkick 4.0.0 → 5.0.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.
data/lib/searchkick.rb CHANGED
@@ -1,46 +1,64 @@
1
+ # dependencies
1
2
  require "active_support"
2
3
  require "active_support/core_ext/hash/deep_merge"
3
- require "elasticsearch"
4
+ require "active_support/core_ext/module/attr_internal"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "active_support/notifications"
4
7
  require "hashie"
5
8
 
6
- require "searchkick/bulk_indexer"
9
+ # stdlib
10
+ require "forwardable"
11
+
12
+ # modules
13
+ require "searchkick/controller_runtime"
7
14
  require "searchkick/index"
15
+ require "searchkick/index_cache"
16
+ require "searchkick/index_options"
8
17
  require "searchkick/indexer"
9
18
  require "searchkick/hash_wrapper"
10
- require "searchkick/middleware"
19
+ require "searchkick/log_subscriber"
11
20
  require "searchkick/model"
12
21
  require "searchkick/multi_search"
13
22
  require "searchkick/query"
14
23
  require "searchkick/reindex_queue"
15
24
  require "searchkick/record_data"
16
25
  require "searchkick/record_indexer"
26
+ require "searchkick/relation"
27
+ require "searchkick/relation_indexer"
17
28
  require "searchkick/results"
18
29
  require "searchkick/version"
19
30
 
31
+ # integrations
20
32
  require "searchkick/railtie" if defined?(Rails)
21
- require "searchkick/logging" if defined?(ActiveSupport::Notifications)
22
33
 
23
34
  module Searchkick
35
+ # requires faraday
36
+ autoload :Middleware, "searchkick/middleware"
37
+
24
38
  # background jobs
25
39
  autoload :BulkReindexJob, "searchkick/bulk_reindex_job"
26
40
  autoload :ProcessBatchJob, "searchkick/process_batch_job"
27
41
  autoload :ProcessQueueJob, "searchkick/process_queue_job"
28
42
  autoload :ReindexV2Job, "searchkick/reindex_v2_job"
29
43
 
44
+ # errors
30
45
  class Error < StandardError; end
31
46
  class MissingIndexError < Error; end
32
- class UnsupportedVersionError < Error; end
33
- class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end
47
+ class UnsupportedVersionError < Error
48
+ def message
49
+ "This version of Searchkick requires Elasticsearch 7+ or OpenSearch 1+"
50
+ end
51
+ end
52
+ class InvalidQueryError < Error; end
34
53
  class DangerousOperation < Error; end
35
54
  class ImportError < Error; end
36
55
 
37
56
  class << self
38
- attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options
57
+ attr_accessor :search_method_name, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name, :model_options, :client_type
39
58
  attr_writer :client, :env, :search_timeout
40
59
  attr_reader :aws_credentials
41
60
  end
42
61
  self.search_method_name = :search
43
- self.wordnet_path = "/var/lib/wn_s.pl"
44
62
  self.timeout = 10
45
63
  self.models = []
46
64
  self.client_options = {}
@@ -49,15 +67,45 @@ module Searchkick
49
67
 
50
68
  def self.client
51
69
  @client ||= begin
52
- require "typhoeus/adapters/faraday" if defined?(Typhoeus)
53
-
54
- Elasticsearch::Client.new({
55
- url: ENV["ELASTICSEARCH_URL"],
56
- transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
57
- retry_on_failure: 2
58
- }.deep_merge(client_options)) do |f|
59
- f.use Searchkick::Middleware
60
- f.request signer_middleware_key, signer_middleware_aws_params if aws_credentials
70
+ client_type =
71
+ if self.client_type
72
+ self.client_type
73
+ elsif defined?(OpenSearch::Client) && defined?(Elasticsearch::Client)
74
+ raise Error, "Multiple clients found - set Searchkick.client_type = :elasticsearch or :opensearch"
75
+ elsif defined?(OpenSearch::Client)
76
+ :opensearch
77
+ elsif defined?(Elasticsearch::Client)
78
+ :elasticsearch
79
+ else
80
+ raise Error, "No client found - install the `elasticsearch` or `opensearch-ruby` gem"
81
+ end
82
+
83
+ # check after client to ensure faraday is installed
84
+ # TODO remove in Searchkick 6
85
+ if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
86
+ require "typhoeus/adapters/faraday"
87
+ end
88
+
89
+ if client_type == :opensearch
90
+ OpenSearch::Client.new({
91
+ url: ENV["OPENSEARCH_URL"],
92
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
93
+ retry_on_failure: 2
94
+ }.deep_merge(client_options)) do |f|
95
+ f.use Searchkick::Middleware
96
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
97
+ end
98
+ else
99
+ raise Error, "The `elasticsearch` gem must be 7+" if Elasticsearch::VERSION.to_i < 7
100
+
101
+ Elasticsearch::Client.new({
102
+ url: ENV["ELASTICSEARCH_URL"],
103
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
104
+ retry_on_failure: 2
105
+ }.deep_merge(client_options)) do |f|
106
+ f.use Searchkick::Middleware
107
+ f.request :aws_sigv4, signer_middleware_aws_params if aws_credentials
108
+ end
61
109
  end
62
110
  end
63
111
  end
@@ -67,23 +115,28 @@ module Searchkick
67
115
  end
68
116
 
69
117
  def self.search_timeout
70
- @search_timeout || timeout
118
+ (defined?(@search_timeout) && @search_timeout) || timeout
71
119
  end
72
120
 
73
- def self.server_version
74
- @server_version ||= client.info["version"]["number"]
121
+ # private
122
+ def self.server_info
123
+ @server_info ||= client.info
75
124
  end
76
125
 
77
- def self.server_below?(version)
78
- Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
126
+ def self.server_version
127
+ @server_version ||= server_info["version"]["number"]
79
128
  end
80
129
 
81
- # memoize for performance
82
- def self.server_below7?
83
- unless defined?(@server_below7)
84
- @server_below7 = server_below?("7.0.0")
130
+ def self.opensearch?
131
+ unless defined?(@opensearch)
132
+ @opensearch = server_info["version"]["distribution"] == "opensearch"
85
133
  end
86
- @server_below7
134
+ @opensearch
135
+ end
136
+
137
+ def self.server_below?(version)
138
+ server_version = opensearch? ? "7.10.2" : self.server_version
139
+ Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
87
140
  end
88
141
 
89
142
  def self.search(term = "*", model: nil, **options, &block)
@@ -111,17 +164,27 @@ module Searchkick
111
164
  end
112
165
  end
113
166
 
114
- options = options.merge(block: block) if block
115
- query = Searchkick::Query.new(klass, term, options)
167
+ # TODO remove in Searchkick 6
116
168
  if options[:execute] == false
117
- query
118
- else
119
- query.execute
169
+ Searchkick.warn("The execute option is no longer needed")
170
+ options.delete(:execute)
120
171
  end
172
+
173
+ options = options.merge(block: block) if block
174
+ Searchkick::Relation.new(klass, term, **options)
121
175
  end
122
176
 
123
177
  def self.multi_search(queries)
124
- Searchkick::MultiSearch.new(queries).perform
178
+ return if queries.empty?
179
+
180
+ queries = queries.map { |q| q.send(:query) }
181
+ event = {
182
+ name: "Multi Search",
183
+ body: queries.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
184
+ }
185
+ ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
186
+ Searchkick::MultiSearch.new(queries).perform
187
+ end
125
188
  end
126
189
 
127
190
  # callbacks
@@ -142,13 +205,25 @@ module Searchkick
142
205
  end
143
206
  end
144
207
 
145
- def self.callbacks(value)
208
+ # message is private
209
+ def self.callbacks(value = nil, message: nil)
146
210
  if block_given?
147
211
  previous_value = callbacks_value
148
212
  begin
149
213
  self.callbacks_value = value
150
214
  result = yield
151
- indexer.perform if callbacks_value == :bulk
215
+ if callbacks_value == :bulk && indexer.queued_items.any?
216
+ event = {}
217
+ if message
218
+ message.call(event)
219
+ else
220
+ event[:name] = "Bulk"
221
+ event[:count] = indexer.queued_items.size
222
+ end
223
+ ActiveSupport::Notifications.instrument("request.searchkick", event) do
224
+ indexer.perform
225
+ end
226
+ end
152
227
  result
153
228
  ensure
154
229
  self.callbacks_value = previous_value
@@ -159,25 +234,20 @@ module Searchkick
159
234
  end
160
235
 
161
236
  def self.aws_credentials=(creds)
162
- begin
163
- require "faraday_middleware/aws_signers_v4"
164
- rescue LoadError
165
- require "faraday_middleware/aws_sigv4"
166
- end
237
+ require "faraday_middleware/aws_sigv4"
238
+
167
239
  @aws_credentials = creds
168
240
  @client = nil # reset client
169
241
  end
170
242
 
171
243
  def self.reindex_status(index_name)
172
- if redis
173
- batches_left = Searchkick::Index.new(index_name).batches_left
174
- {
175
- completed: batches_left == 0,
176
- batches_left: batches_left
177
- }
178
- else
179
- raise Searchkick::Error, "Redis not configured"
180
- end
244
+ raise Searchkick::Error, "Redis not configured" unless redis
245
+
246
+ batches_left = Searchkick::Index.new(index_name).batches_left
247
+ {
248
+ completed: batches_left == 0,
249
+ batches_left: batches_left
250
+ }
181
251
  end
182
252
 
183
253
  def self.with_redis
@@ -192,30 +262,46 @@ module Searchkick
192
262
  end
193
263
  end
194
264
 
265
+ def self.warn(message)
266
+ super("[searchkick] WARNING: #{message}")
267
+ end
268
+
195
269
  # private
196
- def self.load_records(records, ids)
197
- records =
198
- if records.respond_to?(:primary_key)
199
- # ActiveRecord
200
- records.where(records.primary_key => ids) if records.primary_key
201
- elsif records.respond_to?(:queryable)
202
- # Mongoid 3+
203
- records.queryable.for_ids(ids)
204
- elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
205
- # Nobrainer
206
- records.unscoped.where(:id.in => ids)
207
- elsif records.respond_to?(:key_column_names)
208
- records.where(records.key_column_names.first => ids)
270
+ def self.load_records(relation, ids)
271
+ relation =
272
+ if relation.respond_to?(:primary_key)
273
+ primary_key = relation.primary_key
274
+ raise Error, "Need primary key to load records" if !primary_key
275
+
276
+ relation.where(primary_key => ids)
277
+ elsif relation.respond_to?(:queryable)
278
+ relation.queryable.for_ids(ids)
209
279
  end
210
280
 
211
- raise Searchkick::Error, "Not sure how to load records" if !records
281
+ raise Error, "Not sure how to load records" if !relation
282
+
283
+ relation
284
+ end
212
285
 
213
- records
286
+ # private
287
+ def self.load_model(class_name, allow_child: false)
288
+ model = class_name.safe_constantize
289
+ raise Error, "Could not find class: #{class_name}" unless model
290
+ if allow_child
291
+ unless model.respond_to?(:searchkick_klass)
292
+ raise Error, "#{class_name} is not a searchkick model"
293
+ end
294
+ else
295
+ unless Searchkick.models.include?(model)
296
+ raise Error, "#{class_name} is not a searchkick model"
297
+ end
298
+ end
299
+ model
214
300
  end
215
301
 
216
302
  # private
217
303
  def self.indexer
218
- Thread.current[:searchkick_indexer] ||= Searchkick::Indexer.new
304
+ Thread.current[:searchkick_indexer] ||= Indexer.new
219
305
  end
220
306
 
221
307
  # private
@@ -229,28 +315,66 @@ module Searchkick
229
315
  end
230
316
 
231
317
  # private
232
- def self.signer_middleware_key
233
- defined?(FaradayMiddleware::AwsSignersV4) ? :aws_signers_v4 : :aws_sigv4
318
+ def self.signer_middleware_aws_params
319
+ {service: "es", region: "us-east-1"}.merge(aws_credentials)
234
320
  end
235
321
 
236
322
  # private
237
- def self.signer_middleware_aws_params
238
- if signer_middleware_key == :aws_sigv4
239
- {service: "es", region: "us-east-1"}.merge(aws_credentials)
323
+ # methods are forwarded to base class
324
+ # this check to see if scope exists on that class
325
+ # it's a bit tricky, but this seems to work
326
+ def self.relation?(klass)
327
+ if klass.respond_to?(:current_scope)
328
+ !klass.current_scope.nil?
240
329
  else
241
- {
242
- credentials: aws_credentials[:credentials] || Aws::Credentials.new(aws_credentials[:access_key_id], aws_credentials[:secret_access_key]),
243
- service_name: "es",
244
- region: aws_credentials[:region] || "us-east-1"
245
- }
330
+ klass.is_a?(Mongoid::Criteria) || !Mongoid::Threaded.current_scope(klass).nil?
246
331
  end
247
332
  end
248
- end
249
333
 
250
- # TODO find better ActiveModel hook
251
- require "active_model/callbacks"
252
- ActiveModel::Callbacks.include(Searchkick::Model)
334
+ # private
335
+ def self.scope(model)
336
+ # safety check to make sure used properly in code
337
+ raise Error, "Cannot scope relation" if relation?(model)
338
+
339
+ if model.searchkick_options[:unscope]
340
+ model.unscoped
341
+ else
342
+ model
343
+ end
344
+ end
345
+
346
+ # private
347
+ def self.not_found_error?(e)
348
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::NotFound)) ||
349
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
350
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::NotFound))
351
+ end
352
+
353
+ # private
354
+ def self.transport_error?(e)
355
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Error)) ||
356
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
357
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Error))
358
+ end
359
+
360
+ # private
361
+ def self.not_allowed_error?(e)
362
+ (defined?(Elastic::Transport) && e.is_a?(Elastic::Transport::Transport::Errors::MethodNotAllowed)) ||
363
+ (defined?(Elasticsearch::Transport) && e.is_a?(Elasticsearch::Transport::Transport::Errors::MethodNotAllowed)) ||
364
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::MethodNotAllowed))
365
+ end
366
+ end
253
367
 
254
368
  ActiveSupport.on_load(:active_record) do
255
369
  extend Searchkick::Model
256
370
  end
371
+
372
+ ActiveSupport.on_load(:mongoid) do
373
+ Mongoid::Document::ClassMethods.include Searchkick::Model
374
+ end
375
+
376
+ ActiveSupport.on_load(:action_controller) do
377
+ include Searchkick::ControllerRuntime
378
+ end
379
+
380
+ Searchkick::LogSubscriber.attach_to :searchkick
@@ -1,22 +1,32 @@
1
1
  namespace :searchkick do
2
- desc "reindex model"
2
+ desc "reindex a model (specify CLASS)"
3
3
  task reindex: :environment do
4
- if ENV["CLASS"]
5
- klass = ENV["CLASS"].constantize rescue nil
6
- if klass
7
- klass.reindex
8
- else
9
- abort "Could not find class: #{ENV['CLASS']}"
4
+ class_name = ENV["CLASS"]
5
+ abort "USAGE: rake searchkick:reindex CLASS=Product" unless class_name
6
+
7
+ model =
8
+ begin
9
+ Searchkick.load_model(class_name)
10
+ rescue Searchkick::Error => e
11
+ abort e.message
10
12
  end
11
- else
12
- abort "USAGE: rake searchkick:reindex CLASS=Product"
13
- end
13
+
14
+ puts "Reindexing #{model.name}..."
15
+ model.reindex
16
+ puts "Reindex successful"
14
17
  end
15
18
 
16
19
  namespace :reindex do
17
20
  desc "reindex all models"
18
21
  task all: :environment do
19
- Rails.application.eager_load!
22
+ # eager load models to populate Searchkick.models
23
+ if Rails.respond_to?(:autoloaders) && Rails.autoloaders.zeitwerk_enabled?
24
+ # fix for https://github.com/rails/rails/issues/37006
25
+ Zeitwerk::Loader.eager_load_all
26
+ else
27
+ Rails.application.eager_load!
28
+ end
29
+
20
30
  Searchkick.models.each do |model|
21
31
  puts "Reindexing #{model.name}..."
22
32
  model.reindex
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: 4.0.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-12 00:00:00.000000000 Z
11
+ date: 2022-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '5.2'
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'
27
- - !ruby/object:Gem::Dependency
28
- name: elasticsearch
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '6'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '6'
26
+ version: '5.2'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: hashie
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,66 +38,24 @@ dependencies:
52
38
  - - ">="
53
39
  - !ruby/object:Gem::Version
54
40
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: bundler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: minitest
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- description:
98
- email: andrew@chartkick.com
41
+ description:
42
+ email: andrew@ankane.org
99
43
  executables: []
100
44
  extensions: []
101
45
  extra_rdoc_files: []
102
46
  files:
103
47
  - CHANGELOG.md
104
- - CONTRIBUTING.md
105
48
  - LICENSE.txt
106
49
  - README.md
107
50
  - lib/searchkick.rb
108
- - lib/searchkick/bulk_indexer.rb
109
51
  - lib/searchkick/bulk_reindex_job.rb
52
+ - lib/searchkick/controller_runtime.rb
110
53
  - lib/searchkick/hash_wrapper.rb
111
54
  - lib/searchkick/index.rb
55
+ - lib/searchkick/index_cache.rb
112
56
  - lib/searchkick/index_options.rb
113
57
  - lib/searchkick/indexer.rb
114
- - lib/searchkick/logging.rb
58
+ - lib/searchkick/log_subscriber.rb
115
59
  - lib/searchkick/middleware.rb
116
60
  - lib/searchkick/model.rb
117
61
  - lib/searchkick/multi_search.rb
@@ -123,6 +67,8 @@ files:
123
67
  - lib/searchkick/record_indexer.rb
124
68
  - lib/searchkick/reindex_queue.rb
125
69
  - lib/searchkick/reindex_v2_job.rb
70
+ - lib/searchkick/relation.rb
71
+ - lib/searchkick/relation_indexer.rb
126
72
  - lib/searchkick/results.rb
127
73
  - lib/searchkick/version.rb
128
74
  - lib/tasks/searchkick.rake
@@ -130,7 +76,7 @@ homepage: https://github.com/ankane/searchkick
130
76
  licenses:
131
77
  - MIT
132
78
  metadata: {}
133
- post_install_message:
79
+ post_install_message:
134
80
  rdoc_options: []
135
81
  require_paths:
136
82
  - lib
@@ -138,15 +84,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
138
84
  requirements:
139
85
  - - ">="
140
86
  - !ruby/object:Gem::Version
141
- version: '2.4'
87
+ version: '2.6'
142
88
  required_rubygems_version: !ruby/object:Gem::Requirement
143
89
  requirements:
144
90
  - - ">="
145
91
  - !ruby/object:Gem::Version
146
92
  version: '0'
147
93
  requirements: []
148
- rubygems_version: 3.0.3
149
- signing_key:
94
+ rubygems_version: 3.3.3
95
+ signing_key:
150
96
  specification_version: 4
151
- summary: Intelligent search made easy with Rails and Elasticsearch
97
+ summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch
152
98
  test_files: []
data/CONTRIBUTING.md DELETED
@@ -1,53 +0,0 @@
1
- # Contributing
2
-
3
- First, thanks for wanting to contribute. You’re awesome! :heart:
4
-
5
- ## Help
6
-
7
- We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
8
-
9
- All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
10
-
11
- ## Bugs
12
-
13
- Think you’ve discovered a bug?
14
-
15
- 1. Search existing issues to see if it’s been reported.
16
- 2. Try the `master` branch to make sure it hasn’t been fixed.
17
-
18
- ```rb
19
- gem "searchkick", github: "ankane/searchkick"
20
- ```
21
-
22
- 3. Try the `debug` option when searching. This can reveal useful info.
23
-
24
- ```ruby
25
- Product.search("something", debug: true)
26
- ```
27
-
28
- If the above steps don’t help, create an issue.
29
-
30
- - Recreate the problem by forking [this gist](https://gist.github.com/ankane/f80b0923d9ae2c077f41997f7b704e5c). Include a link to your gist and the output in the issue.
31
- - For exceptions, include the complete backtrace.
32
-
33
- ## New Features
34
-
35
- If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
36
-
37
- ## Pull Requests
38
-
39
- Fork the project and create a pull request. A few tips:
40
-
41
- - Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
42
- - Follow the existing style. The code should read like it’s written by a single person.
43
- - Add one or more tests if possible. Make sure existing tests pass with:
44
-
45
- ```sh
46
- bundle exec rake test
47
- ```
48
-
49
- Feel free to open an issue to get feedback on your idea before spending too much time on it.
50
-
51
- ---
52
-
53
- This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.