search-engine-for-typesense 30.9.2 → 30.9.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +47 -13
  4. data/lib/search_engine/active_record_syncable.rb +85 -197
  5. data/lib/search_engine/ast/node.rb +3 -6
  6. data/lib/search_engine/async_partition_coordinator.rb +5 -3
  7. data/lib/search_engine/client/services/search.rb +2 -0
  8. data/lib/search_engine/client.rb +9 -3
  9. data/lib/search_engine/compiler.rb +33 -128
  10. data/lib/search_engine/deletion.rb +12 -9
  11. data/lib/search_engine/dsl/parser.rb +51 -71
  12. data/lib/search_engine/engine.rb +25 -16
  13. data/lib/search_engine/filters/sanitizer.rb +162 -126
  14. data/lib/search_engine/hydration/materializers.rb +53 -118
  15. data/{app/search_engine → lib}/search_engine/index_partition_job.rb +1 -1
  16. data/lib/search_engine/indexing_run_store/rails_cache.rb +44 -33
  17. data/lib/search_engine/indexing_run_store.rb +10 -0
  18. data/lib/search_engine/joins/guard.rb +14 -8
  19. data/lib/search_engine/multi.rb +92 -6
  20. data/lib/search_engine/multi_result.rb +40 -23
  21. data/lib/search_engine/notifications/compact_logger.rb +2 -2
  22. data/lib/search_engine/observability.rb +6 -6
  23. data/lib/search_engine/postgres_outbox/drainer.rb +7 -12
  24. data/lib/search_engine/postgres_outbox/event.rb +12 -0
  25. data/lib/search_engine/postgres_outbox/repository.rb +48 -102
  26. data/lib/search_engine/postgres_outbox.rb +1 -0
  27. data/lib/search_engine/registry.rb +27 -28
  28. data/lib/search_engine/relation/compiler.rb +18 -33
  29. data/lib/search_engine/relation/dsl/filters.rb +71 -175
  30. data/lib/search_engine/relation/dx.rb +3 -1
  31. data/lib/search_engine/relation/materializers.rb +6 -3
  32. data/lib/search_engine/relation/options.rb +5 -26
  33. data/lib/search_engine/relation/state.rb +3 -2
  34. data/lib/search_engine/relation.rb +5 -4
  35. data/lib/search_engine/result.rb +33 -2
  36. data/lib/search_engine/schema.rb +23 -24
  37. data/lib/search_engine/test/offline_client.rb +2 -2
  38. data/lib/search_engine/test/stub_client.rb +9 -16
  39. data/lib/search_engine/update.rb +1 -1
  40. data/lib/search_engine/version.rb +1 -1
  41. data/lib/search_engine.rb +11 -149
  42. data/lib/tasks/search_engine.rake +3 -5
  43. metadata +17 -3
  44. /data/{app/search_engine → lib}/search_engine/postgres_outbox/drain_job.rb +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'monitor'
4
4
  require 'json'
5
+ require 'search_engine/test/offline_client'
5
6
 
6
7
  module SearchEngine
7
8
  module Test
@@ -102,7 +103,7 @@ module SearchEngine
102
103
 
103
104
  entry = capture(:search, url: compiled_url(collection), params: params_hash, url_opts: url_opts)
104
105
  payload = dequeue_or_default(:search, entry)
105
- wrap_single(payload)
106
+ block_given? ? yield(payload) : SearchEngine::Result.new(payload)
106
107
  end
107
108
 
108
109
  # Public API: multi search. Mirrors top-level helper client usage: returns raw Hash from Typesense.
@@ -117,9 +118,7 @@ module SearchEngine
117
118
 
118
119
  # Record a synthetic URL for multi endpoint; individual bodies are not posted here
119
120
  entry = capture(:multi_search, url: compiled_multi_url, params: searches, url_opts: url_opts)
120
- dequeue_or_default(:multi_search, entry).tap do |raw|
121
- return raw
122
- end
121
+ dequeue_or_default(:multi_search, entry)
123
122
  end
124
123
 
125
124
  private
@@ -128,7 +127,7 @@ module SearchEngine
128
127
  val = @lock.synchronize { queue_for(method).shift }
129
128
  case val
130
129
  when nil
131
- default_payload(method)
130
+ default_payload(entry)
132
131
  when Proc
133
132
  val.call(entry)
134
133
  when Exception
@@ -138,12 +137,11 @@ module SearchEngine
138
137
  end
139
138
  end
140
139
 
141
- def default_payload(method)
142
- if method == :search
143
- { 'hits' => [], 'found' => 0, 'out_of' => 0 }
144
- else
145
- { 'results' => [] }
146
- end
140
+ def default_payload(entry)
141
+ empty = SearchEngine::Test::OfflineClient::EMPTY_SEARCH
142
+ return empty if entry.verb == :search
143
+
144
+ { 'results' => entry.body.map { empty } }
147
145
  end
148
146
 
149
147
  def queue_for(method)
@@ -191,11 +189,6 @@ module SearchEngine
191
189
  @lock.synchronize { @calls[method] << entry }
192
190
  entry
193
191
  end
194
-
195
- def wrap_single(payload)
196
- # Mirror Client#search returning SearchEngine::Result
197
- SearchEngine::Result.new(payload, klass: nil)
198
- end
199
192
  end
200
193
  end
201
194
  end
@@ -20,7 +20,7 @@ module SearchEngine
20
20
  def update_by(klass:, attributes:, filter: nil, hash: nil, into: nil, partition: nil, timeout_ms: nil)
21
21
  raise ArgumentError, 'attributes must be a non-empty Hash' unless attributes.is_a?(Hash) && !attributes.empty?
22
22
 
23
- filter_str = SearchEngine::Deletion.build_filter(filter, hash)
23
+ filter_str = SearchEngine::Deletion.build_filter(filter, hash, klass: klass)
24
24
  collection = SearchEngine::Deletion.resolve_into(klass: klass, partition: partition, into: into)
25
25
 
26
26
  resp = SearchEngine.client.update_documents_by_filter(
@@ -3,5 +3,5 @@
3
3
  module SearchEngine
4
4
  # Current gem version.
5
5
  # @return [String]
6
- VERSION = '30.9.2'
6
+ VERSION = '30.9.3'
7
7
  end
data/lib/search_engine.rb CHANGED
@@ -56,6 +56,8 @@ require 'search_engine/active_record_syncable'
56
56
  # Top-level namespace for the SearchEngine gem.
57
57
  # Provides Typesense integration points for Rails applications.
58
58
  module SearchEngine
59
+ autoload :IndexPartitionJob, 'search_engine/index_partition_job'
60
+
59
61
  class << self
60
62
  # Access the singleton configuration instance.
61
63
  # @return [SearchEngine::Config]
@@ -122,21 +124,8 @@ module SearchEngine
122
124
  # res[:products].found
123
125
  # @note Emits "search_engine.multi_search" via ActiveSupport::Notifications with
124
126
  # payload: { searches_count, labels, http_status, source: :multi }.
125
- def multi_search(common: {})
126
- raise ArgumentError, 'block required' unless block_given?
127
-
128
- builder = SearchEngine::Multi.new
129
- yield builder
130
-
131
- labels = builder.labels
132
- raw = execute_multi_search_internal(builder: builder, common: common, labels: labels, use_custom_client: false)
133
-
134
- # Typesense client returns symbolized keys; be resilient to both forms.
135
- # Expect: { results: [ { ... }, ... ] }
136
- list = Array(raw && (raw[:results] || raw['results']))
137
- pairs = build_label_result_pairs(list, labels, builder)
138
-
139
- SearchEngine::Multi::ResultSet.new(pairs)
127
+ def multi_search(common: {}, &block)
128
+ multi_search_result(common: common, &block).result_set
140
129
  end
141
130
 
142
131
  # Execute a federated multi-search and return a MultiResult wrapper.
@@ -157,17 +146,8 @@ module SearchEngine
157
146
  # mr[:products].found
158
147
  # @note Emits "search_engine.multi_search" via ActiveSupport::Notifications with
159
148
  # payload: { searches_count, labels, http_status, source: :multi }.
160
- def multi_search_result(common: {})
161
- raise ArgumentError, 'block required' unless block_given?
162
-
163
- builder = SearchEngine::Multi.new
164
- yield builder
165
-
166
- labels = builder.labels
167
- raw = execute_multi_search_internal(builder: builder, common: common, labels: labels, use_custom_client: false)
168
-
169
- list = Array(raw && (raw[:results] || raw['results']))
170
- SearchEngine::MultiResult.new(labels: labels, raw_results: list, klasses: builder.klasses)
149
+ def multi_search_result(common: {}, &block)
150
+ multi_search_builder(&block).execute(common: common)
171
151
  end
172
152
 
173
153
  # Execute a federated multi-search and return the raw response.
@@ -183,16 +163,8 @@ module SearchEngine
183
163
  # @raise [SearchEngine::Errors::Api] when Typesense returns a non-2xx status
184
164
  # @note Emits "search_engine.multi_search" via ActiveSupport::Notifications with
185
165
  # payload: { searches_count, labels, http_status, source: :multi }.
186
- def multi_search_raw(common: {})
187
- raise ArgumentError, 'block required' unless block_given?
188
-
189
- builder = SearchEngine::Multi.new
190
- yield builder
191
-
192
- labels = builder.labels
193
- execute_multi_search_internal(builder: builder, common: common, labels: labels, use_custom_client: true)
194
- rescue Errors::Api => error
195
- raise augment_multi_api_error(error, labels)
166
+ def multi_search_raw(common: {}, &block)
167
+ multi_search_builder(&block).execute(common: common, raw: true)
196
168
  end
197
169
 
198
170
  # Build and render a graph of Typesense collections and their interconnections.
@@ -220,120 +192,10 @@ module SearchEngine
220
192
  @config_mutex ||= Mutex.new
221
193
  end
222
194
 
223
- # Internal method that executes multi-search request with common setup and error handling.
224
- # Returns the raw Typesense response.
225
- #
226
- # @param builder [SearchEngine::Multi] configured builder instance
227
- # @param common [Hash] optional params merged into each per-search payload
228
- # @param labels [Array<Symbol>] ordered labels for the search list
229
- # @param use_custom_client [Boolean] whether to check config.client for custom client instance
230
- # @return [Hash] raw Typesense multi-search response
231
- # @raise [ArgumentError] when the number of searches exceeds the configured limit
232
- # @raise [SearchEngine::Errors::Api] when Typesense returns a non-2xx status
233
- def execute_multi_search_internal(builder:, common:, labels:, use_custom_client:)
234
- count = builder.labels.size
235
- limit = SearchEngine.config.multi_search_limit
236
- enforce_multi_limit!(count, limit)
237
-
238
- payloads = builder.to_payloads(common: common)
239
- url_opts = SearchEngine::ClientOptions.url_options_from_config(SearchEngine.config)
240
-
241
- client_obj = SearchEngine.client(use_config_client: use_custom_client)
242
-
243
- if defined?(ActiveSupport::Notifications)
244
- se_payload = build_multi_event_payload(count, labels, url_opts)
245
- begin
246
- SearchEngine::Instrumentation.instrument('search_engine.multi_search', se_payload) do |ctx|
247
- client_obj.multi_search(searches: payloads, url_opts: url_opts).tap do
248
- ctx[:http_status] = 200
249
- end
250
- rescue Errors::Api => error
251
- ctx[:http_status] = error.status
252
- raise
253
- end
254
- rescue Errors::Api => error
255
- raise augment_multi_api_error(error, labels)
256
- end
257
- else
258
- begin
259
- client_obj.multi_search(searches: payloads, url_opts: url_opts)
260
- rescue Errors::Api => error
261
- raise augment_multi_api_error(error, labels)
262
- end
263
- end
264
- end
265
-
266
- def enforce_multi_limit!(count, limit)
267
- return unless count > limit
268
-
269
- raise ArgumentError,
270
- "multi_search: #{count} searches exceed limit (#{limit}). " \
271
- 'Increase `SearchEngine.config.multi_search_limit` if necessary.'
272
- end
273
-
274
- def build_multi_event_payload(count, labels, url_opts)
275
- {
276
- searches_count: count,
277
- labels: labels.map(&:to_s),
278
- http_status: nil,
279
- source: :multi,
280
- url_opts: Observability.filtered_url_opts(url_opts)
281
- }
282
- end
283
-
284
- def build_label_result_pairs(list, labels, builder)
285
- pairs = []
286
- list.each_with_index do |item, idx|
287
- label = labels[idx]
288
- klass = builder.klasses[idx]
289
- result = SearchEngine::Result.new(item, klass: klass)
290
- pairs << [label, result]
291
- end
292
- pairs
293
- end
294
-
295
- # Build an API error with additional label context when possible.
296
- # @param error [SearchEngine::Errors::Api]
297
- # @param labels [Array<Symbol>] ordered labels for the search list
298
- # @return [SearchEngine::Errors::Api]
299
- def augment_multi_api_error(error, labels)
300
- body = error.body
301
- failing_index = nil
302
- failing_status = nil
195
+ def multi_search_builder(&block)
196
+ raise ArgumentError, 'block required' unless block
303
197
 
304
- if body.is_a?(Hash)
305
- results = body['results'] || body[:results]
306
- if results.is_a?(Array)
307
- results.each_with_index do |item, idx|
308
- status = item.is_a?(Hash) ? (item['status'] || item[:status] || item['code'] || item[:code]) : nil
309
- next if status.nil? || status.to_i == 200
310
-
311
- failing_index = idx
312
- failing_status = status.to_i
313
- break
314
- end
315
- end
316
- end
317
-
318
- if failing_index && labels[failing_index]
319
- label = labels[failing_index]
320
- msg = "Multi-search failed for label :#{label} (status #{failing_status})."
321
- return Errors::Api.new(msg, status: error.status, body: error.body)
322
- end
323
-
324
- # Fallback: summarize
325
- codes = []
326
- if body.is_a?(Hash)
327
- results = body['results'] || body[:results]
328
- if results.is_a?(Array)
329
- results.each do |item|
330
- codes << (item['status'] || item[:status] || item['code'] || item[:code])
331
- end
332
- end
333
- end
334
- Errors::Api.new("Multi-search failed (statuses: #{codes.compact.join(', ')})", status: error.status,
335
- body: error.body
336
- )
198
+ SearchEngine::Multi.new.tap(&block)
337
199
  end
338
200
  end
339
201
  end
@@ -75,7 +75,8 @@ namespace :search_engine do
75
75
  parts = SearchEngine::Cli.partitions_for(klass)
76
76
  parts = [nil] if parts.nil? || parts.respond_to?(:empty?) && parts.empty?
77
77
  parts.each do |part|
78
- SearchEngine::Indexer.rebuild_partition!(klass, partition: part, into: physical)
78
+ imported = SearchEngine::Indexer.rebuild_partition!(klass, partition: part, into: physical)
79
+ raise SearchEngine::Errors::PartitionImportFailed, imported unless imported.status == :ok
79
80
  end
80
81
  end
81
82
  end
@@ -90,9 +91,6 @@ namespace :search_engine do
90
91
  end
91
92
 
92
93
  Kernel.exit(0)
93
- rescue ArgumentError => error
94
- warn("schema:apply failed: #{error.message}")
95
- Kernel.exit(1)
96
94
  rescue StandardError => error
97
95
  warn("schema:apply failed: #{error.message}")
98
96
  Kernel.exit(1)
@@ -523,7 +521,7 @@ namespace :search_engine do
523
521
 
524
522
  Tips:
525
523
  - FORCE_REBUILD=true forces the guarded physical create/reindex/swap lifecycle even when schema is current.
526
- - Explicit rollback targets are compare-and-swap safe when expected_current is supplied.
524
+ - expected_current requires a schema.around_rebuild lock shared by all alias writers.
527
525
  - Repeating a successful rollback is a no-op; use an explicit target when newer orphaned physicals exist.
528
526
  - Quote rake tasks with brackets to avoid shell globbing (e.g., zsh):
529
527
  rails 'search_engine:index:rebuild_partition[SearchEngine::Product,42]'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search-engine-for-typesense
3
3
  version: !ruby/object:Gem::Version
4
- version: 30.9.2
4
+ version: 30.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shkoda
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: 5.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.6.2
55
69
  description: Rails::Engine providing a thin wrapper around Typesense with idiomatic
56
70
  Rails integration.
57
71
  email:
@@ -64,8 +78,6 @@ files:
64
78
  - LICENSE
65
79
  - README.md
66
80
  - app/search_engine/search_engine/app_info.rb
67
- - app/search_engine/search_engine/index_partition_job.rb
68
- - app/search_engine/search_engine/postgres_outbox/drain_job.rb
69
81
  - lib/generators/search_engine/install/install_generator.rb
70
82
  - lib/generators/search_engine/install/templates/initializer.rb.tt
71
83
  - lib/generators/search_engine/model/model_generator.rb
@@ -151,6 +163,7 @@ files:
151
163
  - lib/search_engine/filters/sanitizer.rb
152
164
  - lib/search_engine/hydration/materializers.rb
153
165
  - lib/search_engine/hydration/selection_context.rb
166
+ - lib/search_engine/index_partition_job.rb
154
167
  - lib/search_engine/indexer.rb
155
168
  - lib/search_engine/indexer/batch_planner.rb
156
169
  - lib/search_engine/indexer/bulk_import.rb
@@ -186,6 +199,7 @@ files:
186
199
  - lib/search_engine/postgres_outbox/database_queries.rb
187
200
  - lib/search_engine/postgres_outbox/delivery_target.rb
188
201
  - lib/search_engine/postgres_outbox/drain_enqueuer.rb
202
+ - lib/search_engine/postgres_outbox/drain_job.rb
189
203
  - lib/search_engine/postgres_outbox/drainer.rb
190
204
  - lib/search_engine/postgres_outbox/event.rb
191
205
  - lib/search_engine/postgres_outbox/event_processor.rb