search-engine-for-typesense 1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/app/search_engine/search_engine/app_info.rb +11 -0
- data/app/search_engine/search_engine/index_partition_job.rb +170 -0
- data/lib/generators/search_engine/install/install_generator.rb +20 -0
- data/lib/generators/search_engine/install/templates/initializer.rb.tt +230 -0
- data/lib/generators/search_engine/model/model_generator.rb +86 -0
- data/lib/generators/search_engine/model/templates/model.rb.tt +12 -0
- data/lib/search-engine-for-typesense.rb +12 -0
- data/lib/search_engine/active_record_syncable.rb +247 -0
- data/lib/search_engine/admin/stopwords.rb +125 -0
- data/lib/search_engine/admin/synonyms.rb +125 -0
- data/lib/search_engine/admin.rb +12 -0
- data/lib/search_engine/ast/and.rb +52 -0
- data/lib/search_engine/ast/binary_op.rb +75 -0
- data/lib/search_engine/ast/eq.rb +19 -0
- data/lib/search_engine/ast/group.rb +18 -0
- data/lib/search_engine/ast/gt.rb +12 -0
- data/lib/search_engine/ast/gte.rb +12 -0
- data/lib/search_engine/ast/in.rb +28 -0
- data/lib/search_engine/ast/lt.rb +12 -0
- data/lib/search_engine/ast/lte.rb +12 -0
- data/lib/search_engine/ast/matches.rb +55 -0
- data/lib/search_engine/ast/node.rb +176 -0
- data/lib/search_engine/ast/not_eq.rb +13 -0
- data/lib/search_engine/ast/not_in.rb +24 -0
- data/lib/search_engine/ast/or.rb +52 -0
- data/lib/search_engine/ast/prefix.rb +51 -0
- data/lib/search_engine/ast/raw.rb +41 -0
- data/lib/search_engine/ast/unary_op.rb +43 -0
- data/lib/search_engine/ast.rb +101 -0
- data/lib/search_engine/base/creation.rb +727 -0
- data/lib/search_engine/base/deletion.rb +80 -0
- data/lib/search_engine/base/display_coercions.rb +36 -0
- data/lib/search_engine/base/hydration.rb +312 -0
- data/lib/search_engine/base/index_maintenance/cleanup.rb +202 -0
- data/lib/search_engine/base/index_maintenance/lifecycle.rb +251 -0
- data/lib/search_engine/base/index_maintenance/schema.rb +117 -0
- data/lib/search_engine/base/index_maintenance.rb +459 -0
- data/lib/search_engine/base/indexing_dsl.rb +255 -0
- data/lib/search_engine/base/joins.rb +479 -0
- data/lib/search_engine/base/model_dsl.rb +472 -0
- data/lib/search_engine/base/presets.rb +43 -0
- data/lib/search_engine/base/pretty_printer.rb +315 -0
- data/lib/search_engine/base/relation_delegation.rb +42 -0
- data/lib/search_engine/base/scopes.rb +113 -0
- data/lib/search_engine/base/updating.rb +92 -0
- data/lib/search_engine/base.rb +38 -0
- data/lib/search_engine/bulk.rb +284 -0
- data/lib/search_engine/cache.rb +33 -0
- data/lib/search_engine/cascade.rb +531 -0
- data/lib/search_engine/cli/doctor.rb +631 -0
- data/lib/search_engine/cli/support.rb +217 -0
- data/lib/search_engine/cli.rb +222 -0
- data/lib/search_engine/client/http_adapter.rb +63 -0
- data/lib/search_engine/client/request_builder.rb +92 -0
- data/lib/search_engine/client/services/base.rb +74 -0
- data/lib/search_engine/client/services/collections.rb +161 -0
- data/lib/search_engine/client/services/documents.rb +214 -0
- data/lib/search_engine/client/services/operations.rb +152 -0
- data/lib/search_engine/client/services/search.rb +190 -0
- data/lib/search_engine/client/services.rb +29 -0
- data/lib/search_engine/client.rb +765 -0
- data/lib/search_engine/client_options.rb +20 -0
- data/lib/search_engine/collection_resolver.rb +191 -0
- data/lib/search_engine/collections_graph.rb +330 -0
- data/lib/search_engine/compiled_params.rb +143 -0
- data/lib/search_engine/compiler.rb +383 -0
- data/lib/search_engine/config/observability.rb +27 -0
- data/lib/search_engine/config/presets.rb +92 -0
- data/lib/search_engine/config/selection.rb +16 -0
- data/lib/search_engine/config/typesense.rb +48 -0
- data/lib/search_engine/config/validators.rb +97 -0
- data/lib/search_engine/config.rb +917 -0
- data/lib/search_engine/console_helpers.rb +130 -0
- data/lib/search_engine/deletion.rb +103 -0
- data/lib/search_engine/dispatcher.rb +125 -0
- data/lib/search_engine/dsl/parser.rb +582 -0
- data/lib/search_engine/engine.rb +167 -0
- data/lib/search_engine/errors.rb +290 -0
- data/lib/search_engine/filters/sanitizer.rb +189 -0
- data/lib/search_engine/hydration/materializers.rb +808 -0
- data/lib/search_engine/hydration/selection_context.rb +96 -0
- data/lib/search_engine/indexer/batch_planner.rb +76 -0
- data/lib/search_engine/indexer/bulk_import.rb +626 -0
- data/lib/search_engine/indexer/import_dispatcher.rb +198 -0
- data/lib/search_engine/indexer/retry_policy.rb +103 -0
- data/lib/search_engine/indexer.rb +747 -0
- data/lib/search_engine/instrumentation.rb +308 -0
- data/lib/search_engine/joins/guard.rb +202 -0
- data/lib/search_engine/joins/resolver.rb +95 -0
- data/lib/search_engine/logging/color.rb +78 -0
- data/lib/search_engine/logging/format_helpers.rb +92 -0
- data/lib/search_engine/logging/partition_progress.rb +53 -0
- data/lib/search_engine/logging_subscriber.rb +388 -0
- data/lib/search_engine/mapper.rb +785 -0
- data/lib/search_engine/multi.rb +286 -0
- data/lib/search_engine/multi_result.rb +186 -0
- data/lib/search_engine/notifications/compact_logger.rb +675 -0
- data/lib/search_engine/observability.rb +162 -0
- data/lib/search_engine/operations.rb +58 -0
- data/lib/search_engine/otel.rb +227 -0
- data/lib/search_engine/partitioner.rb +128 -0
- data/lib/search_engine/ranking_plan.rb +118 -0
- data/lib/search_engine/registry.rb +158 -0
- data/lib/search_engine/relation/compiler.rb +711 -0
- data/lib/search_engine/relation/deletion.rb +37 -0
- data/lib/search_engine/relation/dsl/filters.rb +624 -0
- data/lib/search_engine/relation/dsl/selection.rb +240 -0
- data/lib/search_engine/relation/dsl.rb +903 -0
- data/lib/search_engine/relation/dx/dry_run.rb +59 -0
- data/lib/search_engine/relation/dx/friendly_where.rb +24 -0
- data/lib/search_engine/relation/dx.rb +231 -0
- data/lib/search_engine/relation/materializers.rb +118 -0
- data/lib/search_engine/relation/options.rb +138 -0
- data/lib/search_engine/relation/state.rb +274 -0
- data/lib/search_engine/relation/updating.rb +44 -0
- data/lib/search_engine/relation.rb +623 -0
- data/lib/search_engine/result.rb +664 -0
- data/lib/search_engine/schema.rb +1083 -0
- data/lib/search_engine/sources/active_record_source.rb +185 -0
- data/lib/search_engine/sources/base.rb +62 -0
- data/lib/search_engine/sources/lambda_source.rb +55 -0
- data/lib/search_engine/sources/sql_source.rb +196 -0
- data/lib/search_engine/sources.rb +71 -0
- data/lib/search_engine/stale_rules.rb +160 -0
- data/lib/search_engine/test/minitest_assertions.rb +57 -0
- data/lib/search_engine/test/offline_client.rb +134 -0
- data/lib/search_engine/test/rspec_matchers.rb +77 -0
- data/lib/search_engine/test/stub_client.rb +201 -0
- data/lib/search_engine/test.rb +66 -0
- data/lib/search_engine/test_autoload.rb +8 -0
- data/lib/search_engine/update.rb +35 -0
- data/lib/search_engine/version.rb +7 -0
- data/lib/search_engine.rb +332 -0
- data/lib/tasks/search_engine.rake +501 -0
- data/lib/tasks/search_engine_doctor.rake +16 -0
- metadata +225 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'search_engine/cli/support'
|
|
5
|
+
|
|
6
|
+
module SearchEngine
|
|
7
|
+
module Cli
|
|
8
|
+
# Orchestrates diagnostics checks and renders output for the doctor task.
|
|
9
|
+
#
|
|
10
|
+
# Public API: SearchEngine::Cli::Doctor.run
|
|
11
|
+
#
|
|
12
|
+
# Supports FORMAT env var (table/json) and redaction-aware details.
|
|
13
|
+
#
|
|
14
|
+
# @since M8
|
|
15
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/cli#doctor
|
|
16
|
+
module Doctor
|
|
17
|
+
DOCS_BASE = 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/'
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Build a link to the docs site (pass slug without .md, anchors allowed)
|
|
22
|
+
def wiki(slug)
|
|
23
|
+
return nil if slug.nil? || slug.to_s.strip.empty?
|
|
24
|
+
|
|
25
|
+
DOCS_BASE + slug.to_s.tr('_', '-')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Run all checks and print output to STDOUT.
|
|
30
|
+
# Returns exit code (0 success, 1 failure).
|
|
31
|
+
# @return [Integer]
|
|
32
|
+
# @since M8
|
|
33
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/cli#doctor
|
|
34
|
+
def run
|
|
35
|
+
puts 'Executing doctor checks...'
|
|
36
|
+
|
|
37
|
+
runner = Runner.new
|
|
38
|
+
result = runner.execute
|
|
39
|
+
|
|
40
|
+
if runner.json?
|
|
41
|
+
puts(JSON.generate(result))
|
|
42
|
+
else
|
|
43
|
+
puts(Renderers::Table.render(result, verbose: runner.verbose?))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
result[:ok] ? 0 : 1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# --- Internals -----------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
# Lightweight struct-like result builder keeping stable key ordering in JSON
|
|
53
|
+
module Builder
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
def new_summary
|
|
57
|
+
{ passed: 0, warned: 0, failed: 0, duration_ms_total: 0.0 }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def new_result
|
|
61
|
+
{ ok: true, summary: new_summary, checks: [] }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def result_for_check(name:, ok:, severity:, duration_ms:, details:, hint:, doc:, error_class:, error_message:)
|
|
65
|
+
{
|
|
66
|
+
name: name.to_s,
|
|
67
|
+
ok: ok ? true : false,
|
|
68
|
+
severity: severity&.to_sym,
|
|
69
|
+
duration_ms: duration_ms.to_f.round(1),
|
|
70
|
+
details: details,
|
|
71
|
+
hint: hint,
|
|
72
|
+
doc: doc,
|
|
73
|
+
error_class: error_class,
|
|
74
|
+
error_message: error_message && SearchEngine::Observability.truncate_message(error_message, 200)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Runner executes all doctor checks and aggregates a summary.
|
|
80
|
+
class Runner
|
|
81
|
+
def initialize(env = ENV)
|
|
82
|
+
@env = env
|
|
83
|
+
@started_ms = monotonic_ms
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def execute
|
|
87
|
+
results = Builder.new_result
|
|
88
|
+
|
|
89
|
+
checks = [
|
|
90
|
+
method(:check_config_presence),
|
|
91
|
+
method(:check_connectivity_health),
|
|
92
|
+
method(:check_api_key_validity),
|
|
93
|
+
method(:check_alias_resolution),
|
|
94
|
+
method(:check_dry_run_single),
|
|
95
|
+
method(:check_dry_run_multi),
|
|
96
|
+
method(:check_logging_mode),
|
|
97
|
+
method(:check_opentelemetry)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
checks.each do |chk|
|
|
101
|
+
res = safely_time(chk)
|
|
102
|
+
bump_summary!(results[:summary], res)
|
|
103
|
+
results[:checks] << res
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results[:ok] = results[:summary][:failed].zero?
|
|
107
|
+
results[:summary][:duration_ms_total] = (monotonic_ms - @started_ms).round(1)
|
|
108
|
+
|
|
109
|
+
results
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# --- Flags -----------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def json?
|
|
115
|
+
SearchEngine::Cli::Support.json_output?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def verbose?
|
|
119
|
+
SearchEngine::Cli.boolean_env?('VERBOSE')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- Checks ----------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def check_config_presence
|
|
125
|
+
started = monotonic_ms
|
|
126
|
+
cfg = SearchEngine.config
|
|
127
|
+
|
|
128
|
+
missing = []
|
|
129
|
+
missing << 'host' if cfg.host.to_s.strip.empty?
|
|
130
|
+
missing << 'port' unless cfg.port.is_a?(Integer) && cfg.port.positive?
|
|
131
|
+
missing << 'protocol' unless %w[http https].include?(cfg.protocol.to_s.strip.presence)
|
|
132
|
+
missing << 'api_key' if cfg.api_key.to_s.strip.empty?
|
|
133
|
+
missing << 'timeout_ms' unless cfg.timeout_ms.is_a?(Integer)
|
|
134
|
+
missing << 'open_timeout_ms' unless cfg.open_timeout_ms.is_a?(Integer)
|
|
135
|
+
|
|
136
|
+
ok = missing.empty?
|
|
137
|
+
hint = unless ok
|
|
138
|
+
'Set TYPESENSE_* envs or configure in an initializer. ' \
|
|
139
|
+
'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/installation#configuration'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
details = cfg.to_h_redacted
|
|
143
|
+
duration = monotonic_ms - started
|
|
144
|
+
Builder.result_for_check(
|
|
145
|
+
name: 'config_presence',
|
|
146
|
+
ok: ok,
|
|
147
|
+
severity: ok ? :info : :error,
|
|
148
|
+
duration_ms: duration,
|
|
149
|
+
details: details,
|
|
150
|
+
hint: hint,
|
|
151
|
+
doc: Doctor.wiki('installation#configuration'),
|
|
152
|
+
error_class: nil,
|
|
153
|
+
error_message: nil
|
|
154
|
+
)
|
|
155
|
+
rescue StandardError => error
|
|
156
|
+
failure(
|
|
157
|
+
'config_presence',
|
|
158
|
+
started,
|
|
159
|
+
error,
|
|
160
|
+
hint: 'Unexpected error reading configuration',
|
|
161
|
+
doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/configuration'
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def check_connectivity_health
|
|
166
|
+
started = monotonic_ms
|
|
167
|
+
client = client_with_overrides
|
|
168
|
+
health = client.health
|
|
169
|
+
ok = !(health && (health[:ok] == true || health['ok'] == true)).nil?
|
|
170
|
+
details = { response: redacted_value(health) }
|
|
171
|
+
hint = ok ? nil : 'Verify host/port/protocol and network reachability to Typesense.'
|
|
172
|
+
|
|
173
|
+
Builder.result_for_check(
|
|
174
|
+
name: 'health_check',
|
|
175
|
+
ok: ok,
|
|
176
|
+
severity: ok ? :info : :error,
|
|
177
|
+
duration_ms: monotonic_ms - started,
|
|
178
|
+
details: details,
|
|
179
|
+
hint: hint,
|
|
180
|
+
doc: Doctor.wiki('configuration#typesense-connection'),
|
|
181
|
+
error_class: nil,
|
|
182
|
+
error_message: nil
|
|
183
|
+
)
|
|
184
|
+
rescue SearchEngine::Errors::Error => error
|
|
185
|
+
failure(
|
|
186
|
+
'health_check',
|
|
187
|
+
started,
|
|
188
|
+
error,
|
|
189
|
+
hint: 'Check host/port/protocol and ingress/firewall settings.',
|
|
190
|
+
doc: Doctor.wiki('cli#doctor')
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def check_api_key_validity
|
|
195
|
+
started = monotonic_ms
|
|
196
|
+
client = client_with_overrides
|
|
197
|
+
list = client.list_collections
|
|
198
|
+
count = Array(list).size
|
|
199
|
+
details = { collections_count: count }
|
|
200
|
+
Builder.result_for_check(
|
|
201
|
+
name: 'api_key_valid',
|
|
202
|
+
ok: true,
|
|
203
|
+
severity: :info,
|
|
204
|
+
duration_ms: monotonic_ms - started,
|
|
205
|
+
details: details,
|
|
206
|
+
hint: nil,
|
|
207
|
+
doc: Doctor.wiki('configuration#typesense-connection'),
|
|
208
|
+
error_class: nil,
|
|
209
|
+
error_message: nil
|
|
210
|
+
)
|
|
211
|
+
rescue SearchEngine::Errors::Api => error
|
|
212
|
+
status = error.respond_to?(:status) ? error.status.to_i : nil
|
|
213
|
+
if [401, 403].include?(status)
|
|
214
|
+
Builder.result_for_check(
|
|
215
|
+
name: 'api_key_valid',
|
|
216
|
+
ok: false,
|
|
217
|
+
severity: :error,
|
|
218
|
+
duration_ms: monotonic_ms - started,
|
|
219
|
+
details: { http_status: status },
|
|
220
|
+
hint: 'Your Typesense API key is invalid or lacks permissions. Verify TYPESENSE_API_KEY.',
|
|
221
|
+
doc: Doctor.wiki('configuration#typesense-connection'),
|
|
222
|
+
error_class: error.class.name,
|
|
223
|
+
error_message: error.message
|
|
224
|
+
)
|
|
225
|
+
else
|
|
226
|
+
failure(
|
|
227
|
+
'api_key_valid',
|
|
228
|
+
started,
|
|
229
|
+
error,
|
|
230
|
+
hint: 'Unexpected API error while verifying key.',
|
|
231
|
+
doc: Doctor.wiki('client#errors')
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
rescue SearchEngine::Errors::Error => error
|
|
235
|
+
failure(
|
|
236
|
+
'api_key_valid',
|
|
237
|
+
started,
|
|
238
|
+
error,
|
|
239
|
+
hint: 'Connectivity problem while verifying key.',
|
|
240
|
+
doc: Doctor.wiki('client#errors')
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def check_alias_resolution
|
|
245
|
+
started = monotonic_ms
|
|
246
|
+
client = client_with_overrides
|
|
247
|
+
mapping = SearchEngine::Registry.mapping
|
|
248
|
+
if mapping.empty?
|
|
249
|
+
return Builder.result_for_check(
|
|
250
|
+
name: 'alias_check',
|
|
251
|
+
ok: true,
|
|
252
|
+
severity: :info,
|
|
253
|
+
duration_ms: monotonic_ms - started,
|
|
254
|
+
details: { note: 'no registered collections' },
|
|
255
|
+
hint: 'Define at least one model inheriting from SearchEngine::Base and declare collection name.',
|
|
256
|
+
doc: Doctor.wiki('schema'),
|
|
257
|
+
error_class: nil,
|
|
258
|
+
error_message: nil
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
missing = []
|
|
263
|
+
resolved = {}
|
|
264
|
+
mapping.each_key do |logical|
|
|
265
|
+
target = client.resolve_alias(logical)
|
|
266
|
+
if target.nil?
|
|
267
|
+
missing << logical
|
|
268
|
+
else
|
|
269
|
+
resolved[logical] = target
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
ok = missing.empty?
|
|
274
|
+
hint = if ok
|
|
275
|
+
nil
|
|
276
|
+
else
|
|
277
|
+
'Run schema lifecycle to create physical collections and swap alias. ' \
|
|
278
|
+
'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema#lifecycle'
|
|
279
|
+
end
|
|
280
|
+
details = { resolved: resolved, missing: missing }
|
|
281
|
+
Builder.result_for_check(
|
|
282
|
+
name: 'alias_check',
|
|
283
|
+
ok: ok,
|
|
284
|
+
severity: ok ? :info : :error,
|
|
285
|
+
duration_ms: monotonic_ms - started,
|
|
286
|
+
details: details,
|
|
287
|
+
hint: hint,
|
|
288
|
+
doc: Doctor.wiki('schema#lifecycle'),
|
|
289
|
+
error_class: nil,
|
|
290
|
+
error_message: nil
|
|
291
|
+
)
|
|
292
|
+
rescue SearchEngine::Errors::Error => error
|
|
293
|
+
failure(
|
|
294
|
+
'alias_check',
|
|
295
|
+
started,
|
|
296
|
+
error,
|
|
297
|
+
hint: 'Failed to check aliases due to API/connectivity error.',
|
|
298
|
+
doc: Doctor.wiki('schema')
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def check_dry_run_single
|
|
303
|
+
started = monotonic_ms
|
|
304
|
+
klass = first_registered_class
|
|
305
|
+
if klass.nil?
|
|
306
|
+
return Builder.result_for_check(
|
|
307
|
+
name: 'dry_run_single',
|
|
308
|
+
ok: true,
|
|
309
|
+
severity: :info,
|
|
310
|
+
duration_ms: monotonic_ms - started,
|
|
311
|
+
details: { note: 'no registered collections' },
|
|
312
|
+
hint: 'Add a model inheriting from SearchEngine::Base to preview compile output.',
|
|
313
|
+
doc: Doctor.wiki('dx'),
|
|
314
|
+
error_class: nil,
|
|
315
|
+
error_message: nil
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
rel = klass.all
|
|
320
|
+
preview = rel.dry_run!
|
|
321
|
+
details = {
|
|
322
|
+
url: preview[:url],
|
|
323
|
+
url_opts: SearchEngine::Observability.filtered_url_opts(preview[:url_opts] || {}),
|
|
324
|
+
body_preview: SearchEngine::Cli::Support.parse_json_or_string(preview[:body])
|
|
325
|
+
}
|
|
326
|
+
Builder.result_for_check(
|
|
327
|
+
name: 'dry_run_single',
|
|
328
|
+
ok: true,
|
|
329
|
+
severity: :info,
|
|
330
|
+
duration_ms: monotonic_ms - started,
|
|
331
|
+
details: details,
|
|
332
|
+
hint: nil,
|
|
333
|
+
doc: Doctor.wiki('dx'),
|
|
334
|
+
error_class: nil,
|
|
335
|
+
error_message: nil
|
|
336
|
+
)
|
|
337
|
+
rescue StandardError => error
|
|
338
|
+
failure(
|
|
339
|
+
'dry_run_single',
|
|
340
|
+
started,
|
|
341
|
+
error,
|
|
342
|
+
hint: 'Compile failed. Check DSL, selection, and joins configuration.',
|
|
343
|
+
doc: Doctor.wiki('query_dsl')
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def check_dry_run_multi
|
|
348
|
+
started = monotonic_ms
|
|
349
|
+
klass = first_registered_class
|
|
350
|
+
if klass.nil?
|
|
351
|
+
return Builder.result_for_check(
|
|
352
|
+
name: 'dry_run_multi',
|
|
353
|
+
ok: true,
|
|
354
|
+
severity: :info,
|
|
355
|
+
duration_ms: monotonic_ms - started,
|
|
356
|
+
details: { note: 'no registered collections' },
|
|
357
|
+
hint: 'Add at least one model to preview multi-search compile.',
|
|
358
|
+
doc: Doctor.wiki('multi_search'),
|
|
359
|
+
error_class: nil,
|
|
360
|
+
error_message: nil
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
m = SearchEngine::Multi.new
|
|
365
|
+
m.add(:a, klass.all)
|
|
366
|
+
m.add(:b, klass.all)
|
|
367
|
+
payloads = m.to_payloads(common: {})
|
|
368
|
+
red = payloads.map { |p| SearchEngine::Observability.redact(p) }
|
|
369
|
+
details = { searches_count: red.size, payloads_preview: red }
|
|
370
|
+
Builder.result_for_check(
|
|
371
|
+
name: 'dry_run_multi',
|
|
372
|
+
ok: true,
|
|
373
|
+
severity: :info,
|
|
374
|
+
duration_ms: monotonic_ms - started,
|
|
375
|
+
details: details,
|
|
376
|
+
hint: nil,
|
|
377
|
+
doc: Doctor.wiki('multi_search'),
|
|
378
|
+
error_class: nil,
|
|
379
|
+
error_message: nil
|
|
380
|
+
)
|
|
381
|
+
rescue StandardError => error
|
|
382
|
+
failure(
|
|
383
|
+
'dry_run_multi',
|
|
384
|
+
started,
|
|
385
|
+
error,
|
|
386
|
+
hint: 'Multi-search compile failed. Verify relations.',
|
|
387
|
+
doc: Doctor.wiki('multi_search')
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def check_logging_mode
|
|
392
|
+
started = monotonic_ms
|
|
393
|
+
log_cfg = SearchEngine.config.logging
|
|
394
|
+
details = {
|
|
395
|
+
mode: (log_cfg.respond_to?(:mode) ? log_cfg.mode : nil),
|
|
396
|
+
level: (log_cfg.respond_to?(:level) ? log_cfg.level : nil),
|
|
397
|
+
sample: (log_cfg.respond_to?(:sample) ? log_cfg.sample : nil),
|
|
398
|
+
logger_present: !(log_cfg.respond_to?(:logger) ? log_cfg.logger : SearchEngine.config.logger).nil?
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
Builder.result_for_check(
|
|
402
|
+
name: 'logging_mode',
|
|
403
|
+
ok: true,
|
|
404
|
+
severity: :info,
|
|
405
|
+
duration_ms: monotonic_ms - started,
|
|
406
|
+
details: details,
|
|
407
|
+
hint: nil,
|
|
408
|
+
doc: Doctor.wiki('observability'),
|
|
409
|
+
error_class: nil,
|
|
410
|
+
error_message: nil
|
|
411
|
+
)
|
|
412
|
+
rescue StandardError => error
|
|
413
|
+
failure(
|
|
414
|
+
'logging_mode',
|
|
415
|
+
started,
|
|
416
|
+
error,
|
|
417
|
+
hint: 'Unable to read logging configuration.',
|
|
418
|
+
doc: Doctor.wiki('observability')
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def check_opentelemetry
|
|
423
|
+
started = monotonic_ms
|
|
424
|
+
installed = SearchEngine::Otel.installed?
|
|
425
|
+
enabled = begin
|
|
426
|
+
SearchEngine::Otel.enabled?
|
|
427
|
+
rescue StandardError
|
|
428
|
+
false
|
|
429
|
+
end
|
|
430
|
+
svc = begin
|
|
431
|
+
SearchEngine.config.opentelemetry.service_name
|
|
432
|
+
rescue StandardError
|
|
433
|
+
nil
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
if installed && !enabled
|
|
437
|
+
Builder.result_for_check(
|
|
438
|
+
name: 'otel_status',
|
|
439
|
+
ok: true,
|
|
440
|
+
severity: :warning,
|
|
441
|
+
duration_ms: monotonic_ms - started,
|
|
442
|
+
details: { installed: installed, enabled: enabled, service_name: svc },
|
|
443
|
+
hint: 'OpenTelemetry installed but disabled. Enable via ' \
|
|
444
|
+
'SearchEngine.config.opentelemetry.enabled = true.',
|
|
445
|
+
doc: Doctor.wiki('observability#opentelemetry'),
|
|
446
|
+
error_class: nil,
|
|
447
|
+
error_message: nil
|
|
448
|
+
)
|
|
449
|
+
else
|
|
450
|
+
Builder.result_for_check(
|
|
451
|
+
name: 'otel_status',
|
|
452
|
+
ok: true,
|
|
453
|
+
severity: :info,
|
|
454
|
+
duration_ms: monotonic_ms - started,
|
|
455
|
+
details: { installed: installed, enabled: enabled, service_name: svc },
|
|
456
|
+
hint: nil,
|
|
457
|
+
doc: Doctor.wiki('observability#opentelemetry'),
|
|
458
|
+
error_class: nil,
|
|
459
|
+
error_message: nil
|
|
460
|
+
)
|
|
461
|
+
end
|
|
462
|
+
rescue StandardError => error
|
|
463
|
+
failure(
|
|
464
|
+
'otel_status',
|
|
465
|
+
started,
|
|
466
|
+
error,
|
|
467
|
+
hint: 'Unable to determine OpenTelemetry status.',
|
|
468
|
+
doc: Doctor.wiki('observability')
|
|
469
|
+
)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# --- Helpers ---------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def client_with_overrides
|
|
475
|
+
base = SearchEngine.config
|
|
476
|
+
cfg = SearchEngine::Config.new
|
|
477
|
+
cfg.host = override_or(base.host, ENV['TYPESENSE_HOST'])
|
|
478
|
+
cfg.port = override_or(base.port, env_int('TYPESENSE_PORT'))
|
|
479
|
+
cfg.protocol = base.protocol.to_s.strip || '' # nil is allowed
|
|
480
|
+
cfg.api_key = base.api_key
|
|
481
|
+
|
|
482
|
+
timeout_s = env_int('TYPESENSE_TIMEOUT')
|
|
483
|
+
cfg.timeout_ms = (timeout_s ? Integer(timeout_s) * 1000 : base.timeout_ms)
|
|
484
|
+
cfg.open_timeout_ms = base.open_timeout_ms
|
|
485
|
+
cfg.retries = base.retries
|
|
486
|
+
cfg.logger = base.logger
|
|
487
|
+
|
|
488
|
+
SearchEngine.client(config: cfg)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def env_int(name)
|
|
492
|
+
val = ENV[name]
|
|
493
|
+
return nil if val.nil? || val.to_s.strip.empty?
|
|
494
|
+
|
|
495
|
+
Integer(val)
|
|
496
|
+
rescue ArgumentError, TypeError
|
|
497
|
+
nil
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def override_or(base_value, override)
|
|
501
|
+
return base_value if override.nil? || override.to_s.strip.empty?
|
|
502
|
+
|
|
503
|
+
override
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def redacted_value(value)
|
|
507
|
+
case value
|
|
508
|
+
when Hash, Array
|
|
509
|
+
SearchEngine::Observability.redact(value)
|
|
510
|
+
else
|
|
511
|
+
value
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def failure(name, started_ms, error, hint:, doc: nil)
|
|
516
|
+
Builder.result_for_check(
|
|
517
|
+
name: name,
|
|
518
|
+
ok: false,
|
|
519
|
+
severity: :error,
|
|
520
|
+
duration_ms: (monotonic_ms - started_ms),
|
|
521
|
+
details: {},
|
|
522
|
+
hint: hint,
|
|
523
|
+
doc: doc,
|
|
524
|
+
error_class: error.class.name,
|
|
525
|
+
error_message: error.message
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def bump_summary!(summary, check)
|
|
530
|
+
if check[:ok]
|
|
531
|
+
if check[:severity] == :warning
|
|
532
|
+
summary[:warned] += 1
|
|
533
|
+
else
|
|
534
|
+
summary[:passed] += 1
|
|
535
|
+
end
|
|
536
|
+
else
|
|
537
|
+
summary[:failed] += 1
|
|
538
|
+
end
|
|
539
|
+
summary[:duration_ms_total] = (summary[:duration_ms_total] + check[:duration_ms].to_f).round(1)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def first_registered_class
|
|
543
|
+
pair = SearchEngine::Registry.mapping.first
|
|
544
|
+
pair&.last
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def monotonic_ms
|
|
548
|
+
SearchEngine::Instrumentation.monotonic_ms
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Time a check and handle unexpected errors uniformly.
|
|
552
|
+
def safely_time(callable)
|
|
553
|
+
started = monotonic_ms
|
|
554
|
+
callable.call
|
|
555
|
+
rescue StandardError => error
|
|
556
|
+
puts "error: #{error.backtrace.join("\n")}"
|
|
557
|
+
puts '--------------------------------'
|
|
558
|
+
puts '--------------------------------'
|
|
559
|
+
puts '--------------------------------'
|
|
560
|
+
failure(callable.name, started, error, hint: 'Unexpected error', doc: nil)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
module Renderers
|
|
565
|
+
# Table renderer for human-friendly output of doctor results.
|
|
566
|
+
module Table
|
|
567
|
+
module_function
|
|
568
|
+
|
|
569
|
+
def render(result, verbose: false)
|
|
570
|
+
rows = result[:checks].map { |c| to_row(c, verbose: verbose) }
|
|
571
|
+
header = %w[NAME STATUS DURATION HINT DOC]
|
|
572
|
+
"#{render_table([header] + rows)}\n\n#{summary_line(result)}"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def to_row(check, verbose: false)
|
|
576
|
+
name = check[:name]
|
|
577
|
+
status = status_str(check)
|
|
578
|
+
dur = format('%.1fms', check[:duration_ms].to_f)
|
|
579
|
+
hint = truncate(check[:hint].to_s, verbose: verbose)
|
|
580
|
+
doc = (check[:doc] || '').to_s
|
|
581
|
+
[name, status, dur, hint, doc]
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def status_str(check)
|
|
585
|
+
return 'FAIL' unless check[:ok]
|
|
586
|
+
return 'WARN' if check[:severity] == :warning
|
|
587
|
+
|
|
588
|
+
'OK'
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def truncate(text, verbose: false, max: 80)
|
|
592
|
+
return text if verbose || text.length <= max
|
|
593
|
+
|
|
594
|
+
"#{text[0, max]}..."
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def render_table(rows)
|
|
598
|
+
widths = column_widths(rows)
|
|
599
|
+
lines = rows.map.with_index do |row, idx|
|
|
600
|
+
line = row.each_with_index.map { |cell, i| pad(cell.to_s, widths[i]) }.join(' | ')
|
|
601
|
+
idx.zero? ? "#{line}\n#{separator(widths)}" : line
|
|
602
|
+
end
|
|
603
|
+
lines.join("\n")
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def column_widths(rows)
|
|
607
|
+
cols = rows.first.size
|
|
608
|
+
(0...cols).map do |i|
|
|
609
|
+
rows.map { |r| r[i].to_s.length }.max
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def pad(str, width)
|
|
614
|
+
str.ljust(width)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def separator(widths)
|
|
618
|
+
widths.map { |w| '-' * w }.join('-+-')
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def summary_line(result)
|
|
622
|
+
s = result[:summary]
|
|
623
|
+
ok = result[:ok]
|
|
624
|
+
"Summary: passed=#{s[:passed]} warned=#{s[:warned]} failed=#{s[:failed]} " \
|
|
625
|
+
"exit_code=#{ok ? 0 : 1}"
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
end
|