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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +47 -13
- data/lib/search_engine/active_record_syncable.rb +85 -197
- data/lib/search_engine/ast/node.rb +3 -6
- data/lib/search_engine/async_partition_coordinator.rb +5 -3
- data/lib/search_engine/client/services/search.rb +2 -0
- data/lib/search_engine/client.rb +9 -3
- data/lib/search_engine/compiler.rb +33 -128
- data/lib/search_engine/deletion.rb +12 -9
- data/lib/search_engine/dsl/parser.rb +51 -71
- data/lib/search_engine/engine.rb +25 -16
- data/lib/search_engine/filters/sanitizer.rb +162 -126
- data/lib/search_engine/hydration/materializers.rb +53 -118
- data/{app/search_engine → lib}/search_engine/index_partition_job.rb +1 -1
- data/lib/search_engine/indexing_run_store/rails_cache.rb +44 -33
- data/lib/search_engine/indexing_run_store.rb +10 -0
- data/lib/search_engine/joins/guard.rb +14 -8
- data/lib/search_engine/multi.rb +92 -6
- data/lib/search_engine/multi_result.rb +40 -23
- data/lib/search_engine/notifications/compact_logger.rb +2 -2
- data/lib/search_engine/observability.rb +6 -6
- data/lib/search_engine/postgres_outbox/drainer.rb +7 -12
- data/lib/search_engine/postgres_outbox/event.rb +12 -0
- data/lib/search_engine/postgres_outbox/repository.rb +48 -102
- data/lib/search_engine/postgres_outbox.rb +1 -0
- data/lib/search_engine/registry.rb +27 -28
- data/lib/search_engine/relation/compiler.rb +18 -33
- data/lib/search_engine/relation/dsl/filters.rb +71 -175
- data/lib/search_engine/relation/dx.rb +3 -1
- data/lib/search_engine/relation/materializers.rb +6 -3
- data/lib/search_engine/relation/options.rb +5 -26
- data/lib/search_engine/relation/state.rb +3 -2
- data/lib/search_engine/relation.rb +5 -4
- data/lib/search_engine/result.rb +33 -2
- data/lib/search_engine/schema.rb +23 -24
- data/lib/search_engine/test/offline_client.rb +2 -2
- data/lib/search_engine/test/stub_client.rb +9 -16
- data/lib/search_engine/update.rb +1 -1
- data/lib/search_engine/version.rb +1 -1
- data/lib/search_engine.rb +11 -149
- data/lib/tasks/search_engine.rake +3 -5
- metadata +17 -3
- /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
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
data/lib/search_engine/update.rb
CHANGED
|
@@ -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(
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
File without changes
|