noiseless 0.7.2 → 0.9.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -2
  3. data/lib/noiseless/adapter.rb +29 -41
  4. data/lib/noiseless/adapters/elasticsearch.rb +0 -1
  5. data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +8 -6
  6. data/lib/noiseless/adapters/execution_modules/es_compatible_execution.rb +65 -17
  7. data/lib/noiseless/adapters/execution_modules/http_transport.rb +29 -18
  8. data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +102 -114
  9. data/lib/noiseless/adapters/execution_modules/pgvector_support.rb +42 -47
  10. data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +65 -327
  11. data/lib/noiseless/adapters/execution_modules/postgresql_query.rb +269 -0
  12. data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +172 -296
  13. data/lib/noiseless/adapters/execution_modules/typesense_query.rb +143 -0
  14. data/lib/noiseless/adapters/indices_api.rb +3 -1
  15. data/lib/noiseless/adapters/open_search.rb +30 -88
  16. data/lib/noiseless/adapters/postgresql.rb +6 -16
  17. data/lib/noiseless/adapters/typesense.rb +1 -2
  18. data/lib/noiseless/ast/aggregation.rb +0 -23
  19. data/lib/noiseless/ast/aggregation_builder.rb +28 -0
  20. data/lib/noiseless/ast/hybrid.rb +4 -2
  21. data/lib/noiseless/ast/join.rb +4 -5
  22. data/lib/noiseless/ast/vector.rb +25 -4
  23. data/lib/noiseless/bulk_importer.rb +17 -32
  24. data/lib/noiseless/callbacks.rb +4 -30
  25. data/lib/noiseless/connection_manager.rb +9 -3
  26. data/lib/noiseless/controller_runtime.rb +37 -0
  27. data/lib/noiseless/document_manager.rb +22 -41
  28. data/lib/noiseless/embedding.rb +33 -0
  29. data/lib/noiseless/instrumentation.rb +2 -136
  30. data/lib/noiseless/introspection/mermaid_diagrams.rb +143 -0
  31. data/lib/noiseless/introspection/query_visualizer.rb +3 -228
  32. data/lib/noiseless/introspection.rb +1 -1
  33. data/lib/noiseless/log_subscriber.rb +103 -0
  34. data/lib/noiseless/model.rb +19 -71
  35. data/lib/noiseless/model_registry.rb +1 -7
  36. data/lib/noiseless/multi_search.rb +23 -113
  37. data/lib/noiseless/pagination.rb +14 -40
  38. data/lib/noiseless/query_builder.rb +16 -16
  39. data/lib/noiseless/query_delegation.rb +15 -0
  40. data/lib/noiseless/railtie.rb +0 -2
  41. data/lib/noiseless/record_class_resolver.rb +15 -0
  42. data/lib/noiseless/response/records.rb +9 -3
  43. data/lib/noiseless/runtime_reset_middleware.rb +1 -1
  44. data/lib/noiseless/search_index_update_job.rb +13 -14
  45. data/lib/noiseless/test_case.rb +0 -1
  46. data/lib/noiseless/version.rb +1 -1
  47. data/lib/noiseless.rb +1 -10
  48. metadata +33 -13
  49. data/lib/tasks/benchmark.rake +0 -35
@@ -5,6 +5,12 @@ module Noiseless
5
5
  DEFAULT_PER_PAGE = 20
6
6
  MAX_PER_PAGE = 100
7
7
 
8
+ def self.query_options(paginate)
9
+ return { from: 0, size: DEFAULT_PER_PAGE } unless paginate
10
+
11
+ { from: (paginate.page - 1) * paginate.per_page, size: paginate.per_page }
12
+ end
13
+
8
14
  # Simple paginated array wrapper - no external dependencies
9
15
  class PaginatedArray < Array
10
16
  attr_accessor :current_page, :per_page, :total_count
@@ -75,16 +81,16 @@ module Noiseless
75
81
 
76
82
  # Encode cursor for API response
77
83
  def encode
78
- Base64.urlsafe_encode64(JSON.generate({ f: field, v: value, d: direction }))
84
+ [ActiveSupport::JSON.encode({ f: field, v: value, d: direction })].pack("m0").tr("+/", "-_")
79
85
  end
80
86
 
81
87
  # Decode cursor from API request
82
88
  def self.decode(encoded)
83
89
  return nil if encoded.blank?
84
90
 
85
- data = JSON.parse(Base64.urlsafe_decode64(encoded))
91
+ data = JSON.parse(encoded.tr("-_", "+/").unpack1("m0"))
86
92
  new(field: data["f"], value: data["v"], direction: data["d"]&.to_sym || :asc)
87
- rescue StandardError
93
+ rescue ArgumentError, JSON::ParserError
88
94
  nil
89
95
  end
90
96
 
@@ -131,6 +137,10 @@ module Noiseless
131
137
  # Search paginator - builds and executes paginated queries
132
138
  class SearchPaginator
133
139
  include Enumerable
140
+ extend QueryDelegation
141
+
142
+ delegate_query_methods :match, :multi_match, :filter, :sort, :aggregation, :geo_distance, :vector,
143
+ to: :@query_builder
134
144
 
135
145
  def initialize(model_class, page: 1, per_page: nil)
136
146
  @model_class = model_class
@@ -213,42 +223,6 @@ module Noiseless
213
223
  @results.to_a
214
224
  end
215
225
 
216
- # Query building delegation
217
- def match(field, value, **)
218
- @query_builder.match(field, value, **)
219
- self
220
- end
221
-
222
- def multi_match(query, fields, **)
223
- @query_builder.multi_match(query, fields, **)
224
- self
225
- end
226
-
227
- def filter(field, value, **)
228
- @query_builder.filter(field, value, **)
229
- self
230
- end
231
-
232
- def sort(field, direction = :asc, **)
233
- @query_builder.sort(field, direction, **)
234
- self
235
- end
236
-
237
- def aggregation(name, type, **)
238
- @query_builder.aggregation(name, type, **)
239
- self
240
- end
241
-
242
- def geo_distance(field, lat:, lon:, distance:, **)
243
- @query_builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
244
- self
245
- end
246
-
247
- def vector(field, embedding, **)
248
- @query_builder.vector(field, embedding, **)
249
- self
250
- end
251
-
252
226
  # Response access
253
227
  def results
254
228
  execute_search unless @executed
@@ -312,7 +286,7 @@ module Noiseless
312
286
 
313
287
  client = Noiseless.connections.client(@model_class.connection)
314
288
  ast = @query_builder.to_ast
315
- @results = client.search(ast, model_class: @model_class)
289
+ @results = Sync { client.search(ast, model_class: @model_class).wait }
316
290
  @total_count = @results.total
317
291
  @executed = true
318
292
  end
@@ -2,9 +2,19 @@
2
2
 
3
3
  module Noiseless
4
4
  class QueryBuilder
5
+ def self.indexes_for(model)
6
+ # Multi-search starts without a model and supplies its own indexes.
7
+ return [] if model.nil?
8
+
9
+ return Array(model.search_index) if model.respond_to?(:search_index) && model.search_index&.any?
10
+ return [model.index_name] if model.respond_to?(:index_name) && model.index_name
11
+
12
+ [model.name.demodulize.underscore.pluralize]
13
+ end
14
+
5
15
  def initialize(model)
6
16
  @model = model
7
- @indexes = determine_indexes(model)
17
+ @indexes = self.class.indexes_for(model)
8
18
  @nodes = []
9
19
  @aggregations = []
10
20
  @collapse = nil
@@ -143,12 +153,13 @@ module Noiseless
143
153
  # @param text_query [String] The text query for BM25 matching
144
154
  # @param embedding [Array<Float>] The query embedding vector
145
155
  # @param field [Symbol] The embedding field name
156
+ # @param fields [Array<Symbol>] Fields the text query runs against
146
157
  # @param text_weight [Float] Weight for text search score (default: 0.5)
147
158
  # @param vector_weight [Float] Weight for vector search score (default: 0.5)
148
159
  # @param k [Integer] Number of nearest neighbors (default: 10)
149
- def hybrid(text_query, embedding, field:, text_weight: 0.5, vector_weight: 0.5, k: 10)
160
+ def hybrid(text_query, embedding, field:, fields: [], text_weight: 0.5, vector_weight: 0.5, k: 10)
150
161
  vector_node = AST::Vector.new(field, embedding, k: k)
151
- @hybrid = AST::Hybrid.new(text_query, vector_node, text_weight: text_weight, vector_weight: vector_weight)
162
+ @hybrid = AST::Hybrid.new(text_query, vector_node, fields:, text_weight:, vector_weight:)
152
163
  self
153
164
  end
154
165
 
@@ -188,8 +199,8 @@ module Noiseless
188
199
  # @param on [Hash] Join conditions
189
200
  # @param include_fields [Array] Fields to include from joined collection
190
201
  # @param strategy [Symbol] Join strategy :left or :inner
191
- def join(collection, on:, include_fields: [], strategy: :left)
192
- @joins << AST::Join.new(collection, on: on, include_fields: include_fields, strategy: strategy)
202
+ def join(collection, include_fields: [], strategy: :left)
203
+ @joins << AST::Join.new(collection, include_fields:, strategy:)
193
204
  self
194
205
  end
195
206
 
@@ -269,16 +280,5 @@ module Noiseless
269
280
  raise ArgumentError, "pinned_hits must be a String, Hash, or Array of [id, position]"
270
281
  end
271
282
  end
272
-
273
- def determine_indexes(model)
274
- # Check for search_index (plural array) first
275
- return Array(model.search_index) if model.respond_to?(:search_index) && model.search_index&.any?
276
-
277
- # Check for index_name (singular string) next
278
- return [model.index_name] if model.respond_to?(:index_name) && model.index_name
279
-
280
- # Fallback to pluralized model name
281
- [model.name.demodulize.underscore.pluralize]
282
- end
283
283
  end
284
284
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noiseless
4
+ module QueryDelegation
5
+ # Unlike ordinary delegation, fluent query methods return their wrapper.
6
+ def delegate_query_methods(*names, to:)
7
+ names.each do |name|
8
+ define_method(name) do |*args, **options, &block|
9
+ instance_variable_get(to).public_send(name, *args, **options, &block)
10
+ self
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/railtie"
4
- require_relative "instrumentation"
5
- require_relative "runtime_reset_middleware"
6
4
 
7
5
  module Noiseless
8
6
  class Railtie < Rails::Railtie
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noiseless
4
+ module RecordClassResolver
5
+ # Search definitions and database models are different kinds of classes.
6
+ # Only a Noiseless::Model uses the explicit record_class mapping.
7
+ def self.resolve(model_class)
8
+ if model_class.is_a?(Class) && model_class <= Model
9
+ model_class.record_class
10
+ else
11
+ model_class
12
+ end
13
+ end
14
+ end
15
+ end
@@ -4,7 +4,12 @@ module Noiseless
4
4
  module Response
5
5
  class Records < Base
6
6
  def initialize(raw_response, model_class)
7
- super
7
+ record_class = RecordClassResolver.resolve(model_class)
8
+ unless record_class.respond_to?(:where)
9
+ raise ArgumentError, "#{model_class || 'nil'} cannot load records; configure record_class on your Noiseless::Model search class"
10
+ end
11
+
12
+ super(raw_response, record_class)
8
13
  @records = nil
9
14
  @record_hit_map = nil
10
15
  end
@@ -62,7 +67,7 @@ module Noiseless
62
67
  end
63
68
 
64
69
  def load_records
65
- return [] if hits.empty? || !model_class.respond_to?(:where)
70
+ return [] if hits.empty?
66
71
 
67
72
  # Extract IDs from hits
68
73
  ids = hits.map { |hit| hit["_id"] }
@@ -75,8 +80,9 @@ module Noiseless
75
80
 
76
81
  # Sort records by the order they appear in search results
77
82
  sorted_records = []
83
+ records_by_id = loaded_records.index_by { it.id.to_s }
78
84
  hits.each_with_index do |hit, hit_index|
79
- record = loaded_records.find { |r| r.id.to_s == hit["_id"].to_s }
85
+ record = records_by_id[hit["_id"].to_s]
80
86
  if record
81
87
  sorted_records << record
82
88
  @record_hit_map[record] = hit_index
@@ -8,7 +8,7 @@ module Noiseless
8
8
 
9
9
  def call(env)
10
10
  # Reset runtime tracking at the beginning of each request
11
- Thread.current[:noiseless_runtime] = 0
11
+ ActiveSupport::IsolatedExecutionState[:noiseless_runtime] = 0
12
12
  @app.call(env)
13
13
  end
14
14
  end
@@ -31,12 +31,9 @@ module Noiseless
31
31
  raise ArgumentError, "Unknown operation: #{operation}"
32
32
  end
33
33
  rescue StandardError => e
34
- if options[:raise_on_error]
35
- raise e
36
- elsif (logger = Rails.logger)
37
- # Log error silently
38
- logger.error "Noiseless: Background job failed for #{model_class_name}##{record_id}: #{e.message}"
39
- end
34
+ raise if options[:raise_on_error]
35
+
36
+ Rails.logger&.error "Noiseless: Background job failed for #{model_class_name}##{record_id}: #{e.message}"
40
37
  end
41
38
 
42
39
  # Minimal object for deleted records
@@ -58,16 +55,21 @@ module Noiseless
58
55
  nil
59
56
  end
60
57
  end
58
+
59
+ # Queued runs raise so the job backend retries instead of recording success.
60
+ module Queued
61
+ def perform(model_class_name, record_id, operation, options = {})
62
+ SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options.merge(raise_on_error: true))
63
+ end
64
+ end
61
65
  end
62
66
 
63
67
  # ActiveJob integration
64
68
  if defined?(ActiveJob::Base)
65
69
  class ActiveJobSearchIndexUpdateJob < ActiveJob::Base
66
- queue_as :default
70
+ include SearchIndexUpdateJob::Queued
67
71
 
68
- def perform(model_class_name, record_id, operation, options = {})
69
- SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options)
70
- end
72
+ queue_as :default
71
73
  end
72
74
  end
73
75
 
@@ -75,10 +77,7 @@ module Noiseless
75
77
  if defined?(Sidekiq)
76
78
  class SidekiqSearchIndexUpdateJob
77
79
  include Sidekiq::Worker
78
-
79
- def perform(model_class_name, record_id, operation, options = {})
80
- SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options)
81
- end
80
+ include SearchIndexUpdateJob::Queued
82
81
  end
83
82
  end
84
83
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "minitest"
4
4
  require "async"
5
- require "set" # rubocop:disable Lint/RedundantRequireStatement
6
5
  require_relative "test_helper"
7
6
 
8
7
  module Noiseless
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Noiseless
4
- VERSION = "0.7.2"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/noiseless.rb CHANGED
@@ -11,7 +11,6 @@ require "singleton"
11
11
  require "async"
12
12
  require "async/http/endpoint"
13
13
  require "async/http/client"
14
- require "async/pool"
15
14
  require_relative "noiseless/version"
16
15
 
17
16
  module Noiseless
@@ -96,11 +95,7 @@ module Noiseless
96
95
 
97
96
  # Register all connections statically from YAML - no runtime registration
98
97
  config.connections_config.each do |name, params|
99
- adapter_name = params[:adapter]
100
- hosts = params[:hosts] || []
101
- register_params = { adapter: adapter_name, hosts: hosts, timeout: params[:timeout] }
102
- register_params[:request_timeout] = params[:request_timeout] if params.key?(:request_timeout)
103
- connections.register(name, **register_params)
98
+ connections.register(name, **params, hosts: params[:hosts] || [])
104
99
  end
105
100
  end
106
101
 
@@ -123,10 +118,6 @@ module Noiseless
123
118
  loader.setup
124
119
  loader.eager_load if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
125
120
 
126
- # Manually require response classes since they're in a subdirectory
127
- require_relative "noiseless/response"
128
- require_relative "noiseless/response_factory"
129
-
130
121
  # Global registry instance
131
122
  def self.registry
132
123
  ModelRegistry.instance
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noiseless
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '8.1'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 8.1.4
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,48 +26,51 @@ dependencies:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
28
  version: '8.1'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 8.1.4
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: async
28
34
  requirement: !ruby/object:Gem::Requirement
29
35
  requirements:
30
36
  - - "~>"
31
37
  - !ruby/object:Gem::Version
32
- version: '2.45'
38
+ version: '2.46'
33
39
  type: :runtime
34
40
  prerelease: false
35
41
  version_requirements: !ruby/object:Gem::Requirement
36
42
  requirements:
37
43
  - - "~>"
38
44
  - !ruby/object:Gem::Version
39
- version: '2.45'
45
+ version: '2.46'
40
46
  - !ruby/object:Gem::Dependency
41
47
  name: async-http
42
48
  requirement: !ruby/object:Gem::Requirement
43
49
  requirements:
44
50
  - - "~>"
45
51
  - !ruby/object:Gem::Version
46
- version: '0.103'
52
+ version: '0.105'
47
53
  type: :runtime
48
54
  prerelease: false
49
55
  version_requirements: !ruby/object:Gem::Requirement
50
56
  requirements:
51
57
  - - "~>"
52
58
  - !ruby/object:Gem::Version
53
- version: '0.103'
59
+ version: '0.105'
54
60
  - !ruby/object:Gem::Dependency
55
- name: async-pool
61
+ name: io-event
56
62
  requirement: !ruby/object:Gem::Requirement
57
63
  requirements:
58
- - - "~>"
64
+ - - ">="
59
65
  - !ruby/object:Gem::Version
60
- version: '0.11'
66
+ version: 1.22.1
61
67
  type: :runtime
62
68
  prerelease: false
63
69
  version_requirements: !ruby/object:Gem::Requirement
64
70
  requirements:
65
- - - "~>"
71
+ - - ">="
66
72
  - !ruby/object:Gem::Version
67
- version: '0.11'
73
+ version: 1.22.1
68
74
  - !ruby/object:Gem::Dependency
69
75
  name: railties
70
76
  requirement: !ruby/object:Gem::Requirement
@@ -72,6 +78,9 @@ dependencies:
72
78
  - - "~>"
73
79
  - !ruby/object:Gem::Version
74
80
  version: '8.1'
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 8.1.4
75
84
  type: :runtime
76
85
  prerelease: false
77
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -79,6 +88,9 @@ dependencies:
79
88
  - - "~>"
80
89
  - !ruby/object:Gem::Version
81
90
  version: '8.1'
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 8.1.4
82
94
  - !ruby/object:Gem::Dependency
83
95
  name: zeitwerk
84
96
  requirement: !ruby/object:Gem::Requirement
@@ -228,13 +240,16 @@ files:
228
240
  - lib/noiseless/adapters/execution_modules/opensearch_execution.rb
229
241
  - lib/noiseless/adapters/execution_modules/pgvector_support.rb
230
242
  - lib/noiseless/adapters/execution_modules/postgresql_execution.rb
243
+ - lib/noiseless/adapters/execution_modules/postgresql_query.rb
231
244
  - lib/noiseless/adapters/execution_modules/typesense_execution.rb
245
+ - lib/noiseless/adapters/execution_modules/typesense_query.rb
232
246
  - lib/noiseless/adapters/indices_api.rb
233
247
  - lib/noiseless/adapters/open_search.rb
234
248
  - lib/noiseless/adapters/postgresql.rb
235
249
  - lib/noiseless/adapters/typesense.rb
236
250
  - lib/noiseless/ast.rb
237
251
  - lib/noiseless/ast/aggregation.rb
252
+ - lib/noiseless/ast/aggregation_builder.rb
238
253
  - lib/noiseless/ast/bool.rb
239
254
  - lib/noiseless/ast/bulk.rb
240
255
  - lib/noiseless/ast/collapse.rb
@@ -258,13 +273,17 @@ files:
258
273
  - lib/noiseless/bulk_importer.rb
259
274
  - lib/noiseless/callbacks.rb
260
275
  - lib/noiseless/connection_manager.rb
276
+ - lib/noiseless/controller_runtime.rb
261
277
  - lib/noiseless/document_manager.rb
262
278
  - lib/noiseless/dsl.rb
279
+ - lib/noiseless/embedding.rb
263
280
  - lib/noiseless/generators/application_search_generator.rb
264
281
  - lib/noiseless/instrumentation.rb
265
282
  - lib/noiseless/introspection.rb
266
283
  - lib/noiseless/introspection/console.rb
284
+ - lib/noiseless/introspection/mermaid_diagrams.rb
267
285
  - lib/noiseless/introspection/query_visualizer.rb
286
+ - lib/noiseless/log_subscriber.rb
268
287
  - lib/noiseless/mapping.rb
269
288
  - lib/noiseless/mapping_definition_processor.rb
270
289
  - lib/noiseless/model.rb
@@ -272,7 +291,9 @@ files:
272
291
  - lib/noiseless/multi_search.rb
273
292
  - lib/noiseless/pagination.rb
274
293
  - lib/noiseless/query_builder.rb
294
+ - lib/noiseless/query_delegation.rb
275
295
  - lib/noiseless/railtie.rb
296
+ - lib/noiseless/record_class_resolver.rb
276
297
  - lib/noiseless/response.rb
277
298
  - lib/noiseless/response/aggregations.rb
278
299
  - lib/noiseless/response/empty.rb
@@ -285,7 +306,6 @@ files:
285
306
  - lib/noiseless/test_case.rb
286
307
  - lib/noiseless/test_helper.rb
287
308
  - lib/noiseless/version.rb
288
- - lib/tasks/benchmark.rake
289
309
  - lib/tasks/release.rake
290
310
  - lib/tasks/test.rake
291
311
  homepage: https://github.com/seuros/noiseless
@@ -301,14 +321,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
301
321
  requirements:
302
322
  - - ">="
303
323
  - !ruby/object:Gem::Version
304
- version: '3.4'
324
+ version: '4.0'
305
325
  required_rubygems_version: !ruby/object:Gem::Requirement
306
326
  requirements:
307
327
  - - ">="
308
328
  - !ruby/object:Gem::Version
309
329
  version: '0'
310
330
  requirements: []
311
- rubygems_version: 4.0.10
331
+ rubygems_version: 4.0.20
312
332
  specification_version: 4
313
333
  summary: Async-first Rails search abstraction with multi-backend support
314
334
  test_files: []
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- namespace :benchmark do
4
- desc "Benchmark search performance across Elasticsearch, OpenSearch, and PostgreSQL"
5
- task search: :environment do
6
- require "benchmark/ips"
7
- require "async"
8
-
9
- # Use configured connections from Rails
10
- engines = {
11
- elasticsearch: :primary,
12
- opensearch: :secondary,
13
- postgresql: :postgresql
14
- }
15
-
16
- puts "=" * 80
17
- puts "MULTI-ENGINE SEARCH BENCHMARK"
18
- puts "=" * 80
19
- puts "Records: 10k benchmark articles"
20
- puts "Engines: #{engines.keys.join(', ')}"
21
- puts "=" * 80
22
-
23
- # Load PostgreSQL data
24
- puts "\nšŸ“Š Loading PostgreSQL..."
25
- Article.delete_all
26
- fixtures_path = Rails.root.join("../../test/fixtures")
27
- ActiveRecord::FixtureSet.create_fixtures(fixtures_path, ["benchmark_articles"])
28
- puts "āœ… Loaded #{Article.count} records"
29
-
30
- # Register PostgreSQL model
31
- Noiseless.connections.client(:postgresql).register_model(Article, index_name: "articles")
32
-
33
- puts "\nšŸš€ Benchmark ready. Run with: bundle exec rake benchmark:search"
34
- end
35
- end