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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
module Logging
|
|
5
|
+
# Pure helpers for formatting compact log lines and fixed-width tables.
|
|
6
|
+
# All methods are side-effect free and return strings/arrays only.
|
|
7
|
+
module FormatHelpers
|
|
8
|
+
DASH = '—'
|
|
9
|
+
ELLIPSIS = '…'
|
|
10
|
+
SPACE = ' '
|
|
11
|
+
KEY_VAL_SEP = '='
|
|
12
|
+
PAIR_SEP = ' '
|
|
13
|
+
PIPE = '|'
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Return EM dash when value is nil or empty?; otherwise return value as-is.
|
|
18
|
+
def value_or_dash(value)
|
|
19
|
+
return DASH if value.nil?
|
|
20
|
+
return DASH if value.respond_to?(:empty?) && value.empty?
|
|
21
|
+
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return EM dash unless payload contains the key; when present, return the value or EM dash if nil.
|
|
26
|
+
def display_or_dash(payload, key)
|
|
27
|
+
return DASH unless payload.is_a?(Hash) && payload.key?(key)
|
|
28
|
+
|
|
29
|
+
val = payload[key]
|
|
30
|
+
val.nil? ? DASH : val
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Treats false and nil as missing; returns EM dash in these cases.
|
|
34
|
+
def presence_or_dash(value)
|
|
35
|
+
value || DASH
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Truncate text to width using a single-character ellipsis when overflowing.
|
|
39
|
+
# Pads with spaces to reach width when shorter.
|
|
40
|
+
def fixed_width(text, width, align: :left)
|
|
41
|
+
s = text.to_s
|
|
42
|
+
return s if width.nil? || width <= 0
|
|
43
|
+
|
|
44
|
+
return s[0, [width - 1, 0].max] + ELLIPSIS if s.length > width
|
|
45
|
+
|
|
46
|
+
pad_len = width - s.length
|
|
47
|
+
case align
|
|
48
|
+
when :right
|
|
49
|
+
(' ' * pad_len) + s
|
|
50
|
+
when :center
|
|
51
|
+
left = pad_len / 2
|
|
52
|
+
right = pad_len - left
|
|
53
|
+
(' ' * left) + s + (' ' * right)
|
|
54
|
+
else
|
|
55
|
+
s + (' ' * pad_len)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Build a fixed-width table from rows (Array<Array<String>>)
|
|
60
|
+
# widths: Array<Integer>; aligns: Array<Symbol> (:left, :right, :center)
|
|
61
|
+
# Returns a single String with newline separators.
|
|
62
|
+
def build_table(rows, widths, aligns: nil)
|
|
63
|
+
return '' if rows.nil? || widths.nil?
|
|
64
|
+
|
|
65
|
+
aligns ||= Array.new(widths.length, :left)
|
|
66
|
+
|
|
67
|
+
lines = rows.map do |row|
|
|
68
|
+
cols = row.each_with_index.map do |col, idx|
|
|
69
|
+
fixed_width(col, widths[idx], align: aligns[idx])
|
|
70
|
+
end
|
|
71
|
+
cols.join(SPACE)
|
|
72
|
+
end
|
|
73
|
+
lines.join("\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Serialize a Hash of key=>value pairs into a compact "k=v" line.
|
|
77
|
+
# - Preserves insertion order
|
|
78
|
+
# - Omits nil values
|
|
79
|
+
def kv_compact(hash)
|
|
80
|
+
return '' unless hash.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
parts = []
|
|
83
|
+
hash.each do |k, v|
|
|
84
|
+
next if v.nil?
|
|
85
|
+
|
|
86
|
+
parts << "#{k}#{KEY_VAL_SEP}#{v}"
|
|
87
|
+
end
|
|
88
|
+
parts.join(PAIR_SEP)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
module Logging
|
|
5
|
+
# Shared helpers for rendering standardized partition progress log lines.
|
|
6
|
+
#
|
|
7
|
+
# Produces the same compact line used during regular indexation and cascade
|
|
8
|
+
# flows to keep output consistent and DRY.
|
|
9
|
+
module PartitionProgress
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Build a compact log line for a finished partition import.
|
|
13
|
+
#
|
|
14
|
+
# @param partition [Object] opaque partition token
|
|
15
|
+
# @param summary [SearchEngine::Indexer::Summary] result of the import
|
|
16
|
+
# @return [String]
|
|
17
|
+
def line(partition, summary)
|
|
18
|
+
require 'search_engine/logging/color'
|
|
19
|
+
|
|
20
|
+
sample_err = extract_sample_error(summary)
|
|
21
|
+
|
|
22
|
+
status_val = summary.status
|
|
23
|
+
failed_total = summary.failed_total.to_i
|
|
24
|
+
success_total = summary.success_total.to_i
|
|
25
|
+
status_color = SearchEngine::Logging::Color.for_partition_status(failed_total, success_total)
|
|
26
|
+
|
|
27
|
+
parts = []
|
|
28
|
+
parts << " #{SearchEngine::Logging::Color.apply("partition=#{partition.inspect}", status_color)} " \
|
|
29
|
+
"→ #{SearchEngine::Logging::Color.apply("status=#{status_val}", status_color)}"
|
|
30
|
+
parts << "docs=#{summary.docs_total}"
|
|
31
|
+
success_str = "success=#{success_total}"
|
|
32
|
+
parts << (success_total.positive? ? SearchEngine::Logging::Color.bold(success_str) : success_str)
|
|
33
|
+
failed_str = "failed=#{failed_total}"
|
|
34
|
+
parts << (failed_total.positive? ? SearchEngine::Logging::Color.apply(failed_str, :red) : failed_str)
|
|
35
|
+
parts << "batches=#{summary.batches_total}"
|
|
36
|
+
parts << "duration_ms=#{summary.duration_ms_total}"
|
|
37
|
+
parts << "sample_error=#{sample_err.inspect}" if sample_err
|
|
38
|
+
parts.join(' ')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Extract one sample error message from the summary, if present.
|
|
42
|
+
# Delegates to the internal helper on {SearchEngine::Base}.
|
|
43
|
+
#
|
|
44
|
+
# @param summary [SearchEngine::Indexer::Summary]
|
|
45
|
+
# @return [String, nil]
|
|
46
|
+
def extract_sample_error(summary)
|
|
47
|
+
SearchEngine::Base.__se_extract_sample_error(summary)
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
require 'search_engine/logging/format_helpers'
|
|
6
|
+
|
|
7
|
+
module SearchEngine
|
|
8
|
+
# Structured logging subscriber for unified instrumentation events.
|
|
9
|
+
#
|
|
10
|
+
# Consumes events emitted via {SearchEngine::Instrumentation.instrument}
|
|
11
|
+
# and writes either a compact single-line entry or a JSON object per event.
|
|
12
|
+
#
|
|
13
|
+
# Configuration:
|
|
14
|
+
#
|
|
15
|
+
# SearchEngine.configure do |c|
|
|
16
|
+
# c.logging = OpenStruct.new(mode: :compact, level: :info, sample: 1.0, logger: Rails.logger)
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Modes: :compact (default) or :json. Supports sampling and opt-out via
|
|
20
|
+
# sample: 0.0 or mode: nil.
|
|
21
|
+
#
|
|
22
|
+
# @since M8
|
|
23
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/observability#logging
|
|
24
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/observability`
|
|
25
|
+
module LoggingSubscriber
|
|
26
|
+
class << self
|
|
27
|
+
# Install the subscriber in a reloader-safe and idempotent way.
|
|
28
|
+
#
|
|
29
|
+
# @param config [#mode,#level,#sample,#logger,nil]
|
|
30
|
+
# @return [Object, nil] subscription handle
|
|
31
|
+
# @since M8
|
|
32
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/observability#logging
|
|
33
|
+
def install!(config = nil)
|
|
34
|
+
uninstall!
|
|
35
|
+
|
|
36
|
+
cfg = normalize_config(config)
|
|
37
|
+
return nil if cfg[:mode].nil? || cfg[:sample] <= 0.0
|
|
38
|
+
return nil unless defined?(ActiveSupport::Notifications)
|
|
39
|
+
|
|
40
|
+
@mode = cfg[:mode]
|
|
41
|
+
@level = cfg[:level]
|
|
42
|
+
@logger = cfg[:logger]
|
|
43
|
+
@sample = cfg[:sample]
|
|
44
|
+
|
|
45
|
+
@handle = ActiveSupport::Notifications.subscribe(/^search_engine\./) do |*args|
|
|
46
|
+
# Fast sampling decision before formatting or allocations beyond Event
|
|
47
|
+
next unless sampled?(@sample)
|
|
48
|
+
|
|
49
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
50
|
+
begin
|
|
51
|
+
log_line = (@mode == :json ? format_json(event) : format_compact(event))
|
|
52
|
+
emit(@logger, @level, log_line)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
# Never raise from logging
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Uninstall previously installed subscriber.
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
# @since M8
|
|
63
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/observability#logging
|
|
64
|
+
def uninstall!
|
|
65
|
+
return false unless defined?(ActiveSupport::Notifications)
|
|
66
|
+
return false unless @handle
|
|
67
|
+
|
|
68
|
+
ActiveSupport::Notifications.unsubscribe(@handle)
|
|
69
|
+
@handle = nil
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def normalize_config(config)
|
|
76
|
+
c = config || (SearchEngine.respond_to?(:config) ? SearchEngine.config.logging : nil)
|
|
77
|
+
logger =
|
|
78
|
+
if c.respond_to?(:logger) && c.logger
|
|
79
|
+
c.logger
|
|
80
|
+
elsif SearchEngine.respond_to?(:config) && SearchEngine.config&.logger
|
|
81
|
+
SearchEngine.config.logger
|
|
82
|
+
else
|
|
83
|
+
require 'logger'
|
|
84
|
+
Logger.new($stdout)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
mode = c&.mode || :compact
|
|
88
|
+
level = c&.level || :info
|
|
89
|
+
sample = begin
|
|
90
|
+
val = c&.sample
|
|
91
|
+
val = 1.0 if val.nil?
|
|
92
|
+
val = 0.0 if val.respond_to?(:to_f) && val.to_f.negative?
|
|
93
|
+
val = 1.0 if val.respond_to?(:to_f) && val.to_f > 1.0
|
|
94
|
+
val.to_f
|
|
95
|
+
rescue StandardError
|
|
96
|
+
1.0
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{ mode: mode&.to_sym, level: level.to_sym, sample: sample, logger: logger }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def emit(logger, level, line)
|
|
103
|
+
return unless logger && level && line
|
|
104
|
+
|
|
105
|
+
if logger.respond_to?(level)
|
|
106
|
+
logger.public_send(level, line)
|
|
107
|
+
elsif logger.respond_to?(:add)
|
|
108
|
+
# Fallback: map to Logger::Severity if available
|
|
109
|
+
sev = severity_map[level] || severity_map[:info]
|
|
110
|
+
logger.add(sev, line)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def severity_map
|
|
115
|
+
@severity_map ||= if defined?(::Logger)
|
|
116
|
+
{
|
|
117
|
+
debug: ::Logger::DEBUG,
|
|
118
|
+
info: ::Logger::INFO,
|
|
119
|
+
warn: ::Logger::WARN,
|
|
120
|
+
error: ::Logger::ERROR,
|
|
121
|
+
fatal: ::Logger::FATAL
|
|
122
|
+
}
|
|
123
|
+
else
|
|
124
|
+
{ debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def sampled?(rate)
|
|
129
|
+
return false if rate <= 0.0
|
|
130
|
+
return true if rate >= 1.0
|
|
131
|
+
|
|
132
|
+
rng = (Thread.current[:__se_log_rng__] ||= Random.new)
|
|
133
|
+
rng.rand < rate
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# --- Formatting helpers ---
|
|
137
|
+
|
|
138
|
+
def format_compact(event)
|
|
139
|
+
p = event.payload || {}
|
|
140
|
+
name = event.name
|
|
141
|
+
short = name.sub(/^search_engine\./, 'se.')
|
|
142
|
+
cid = short_correlation_id(p[:correlation_id])
|
|
143
|
+
duration = safe_duration(event, p)
|
|
144
|
+
status = pick_status(p)
|
|
145
|
+
collection = p[:collection] || p[:logical]
|
|
146
|
+
|
|
147
|
+
# Specialized single-line renderers (return String or nil)
|
|
148
|
+
specialized = format_compact_event(name: name, short: short, cid: cid, collection: collection,
|
|
149
|
+
duration: duration, status: status, payload: p
|
|
150
|
+
)
|
|
151
|
+
return specialized if specialized
|
|
152
|
+
|
|
153
|
+
parts = compact_base_parts(short, cid, collection, status, duration, p)
|
|
154
|
+
parts.join(' ')
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Build base compact parts for generic events (single-line, allocation-light)
|
|
158
|
+
def compact_base_parts(short, cid, collection, status, duration, p)
|
|
159
|
+
groups = SearchEngine::Logging::FormatHelpers.value_or_dash(p[:groups_count])
|
|
160
|
+
preset = p[:preset_name] || SearchEngine::Logging::FormatHelpers.value_or_dash(nil)
|
|
161
|
+
pinned = p[:curation_pinned_count] || p[:pinned_count] || 0
|
|
162
|
+
hidden = p[:curation_hidden_count] || p[:hidden_count] || 0
|
|
163
|
+
|
|
164
|
+
parts = []
|
|
165
|
+
parts << "[#{short}]"
|
|
166
|
+
parts << "id=#{cid}"
|
|
167
|
+
parts << "coll=#{collection}" if collection
|
|
168
|
+
parts << "status=#{status}"
|
|
169
|
+
parts << "dur=#{duration}ms"
|
|
170
|
+
parts << "groups=#{groups}"
|
|
171
|
+
parts << "preset=#{preset}"
|
|
172
|
+
parts << "cur=#{pinned}/#{hidden}"
|
|
173
|
+
parts
|
|
174
|
+
end
|
|
175
|
+
private :compact_base_parts
|
|
176
|
+
|
|
177
|
+
def format_json(event)
|
|
178
|
+
p = event.payload || {}
|
|
179
|
+
base = base_json_fields(event, p)
|
|
180
|
+
merge_event_extras!(base, event.name, p)
|
|
181
|
+
attach_params_preview_count!(base, p)
|
|
182
|
+
JSON.generate(safe_jsonable(base))
|
|
183
|
+
rescue StandardError
|
|
184
|
+
# As a last resort, log a minimal JSON line
|
|
185
|
+
duration = safe_duration(event, p)
|
|
186
|
+
cid = short_correlation_id(p[:correlation_id])
|
|
187
|
+
status = pick_status(p)
|
|
188
|
+
JSON.generate({ 'event' => event.name, 'cid' => cid, 'status' => status, 'duration_ms' => duration })
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def base_json_fields(event, p)
|
|
192
|
+
duration = safe_duration(event, p)
|
|
193
|
+
cid = short_correlation_id(p[:correlation_id])
|
|
194
|
+
status = pick_status(p)
|
|
195
|
+
h = {}
|
|
196
|
+
h['event'] = event.name
|
|
197
|
+
h['cid'] = cid
|
|
198
|
+
h['collection'] = (p[:collection] || p[:logical]) if p[:collection] || p[:logical]
|
|
199
|
+
h['status'] = status
|
|
200
|
+
h['duration_ms'] = duration
|
|
201
|
+
h['group_count'] = p[:groups_count] if p.key?(:groups_count)
|
|
202
|
+
h['preset_mode'] = p[:preset_mode] if p.key?(:preset_mode)
|
|
203
|
+
if p.key?(:curation_pinned_count) || p.key?(:pinned_count)
|
|
204
|
+
h['curation_pinned_count'] = (p[:curation_pinned_count] || p[:pinned_count])
|
|
205
|
+
end
|
|
206
|
+
if p.key?(:curation_hidden_count) || p.key?(:hidden_count)
|
|
207
|
+
h['curation_hidden_count'] = (p[:curation_hidden_count] || p[:hidden_count])
|
|
208
|
+
end
|
|
209
|
+
h
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def merge_event_extras!(h, name, p)
|
|
213
|
+
extras = build_event_json_extras(name, p)
|
|
214
|
+
extras.each { |k, v| h[k] = v } unless extras.empty?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def attach_params_preview_count!(h, p)
|
|
218
|
+
return unless p.key?(:params_preview)
|
|
219
|
+
|
|
220
|
+
preview = p[:params_preview]
|
|
221
|
+
preview = SearchEngine::Instrumentation.redact(preview)
|
|
222
|
+
h['params_preview_keys'] = (preview.is_a?(Hash) ? preview.keys.size : nil)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def format_compact_event(name:, short:, cid:, collection:, duration:, status:, payload:)
|
|
226
|
+
return compact_facet(short, cid, collection, duration, payload) if name == 'search_engine.facet.compile'
|
|
227
|
+
return compact_highlight(short, cid, collection, duration, payload) if name == 'search_engine.highlight.compile'
|
|
228
|
+
return compact_synonyms(short, cid, collection, duration, payload) if name == 'search_engine.synonyms.apply'
|
|
229
|
+
return compact_geo(short, cid, collection, duration, payload) if name == 'search_engine.geo.compile'
|
|
230
|
+
return compact_vector(short, cid, collection, duration, payload) if name == 'search_engine.vector.compile'
|
|
231
|
+
|
|
232
|
+
if name == 'search_engine.hits.limit'
|
|
233
|
+
return compact_hits_limit(short, cid, collection, duration, status, payload)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def compact_facet(short, cid, collection, duration, p)
|
|
240
|
+
parts = []
|
|
241
|
+
parts << "[#{short}]"
|
|
242
|
+
parts << "id=#{cid}"
|
|
243
|
+
parts << "coll=#{collection}" if collection
|
|
244
|
+
parts << "fields=#{p[:fields_count] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
245
|
+
parts << "queries=#{p[:queries_count] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
246
|
+
parts << "max=#{p[:max_facet_values] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
247
|
+
parts << "dur=#{duration}ms"
|
|
248
|
+
parts.join(' ')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def compact_highlight(short, cid, collection, duration, p)
|
|
252
|
+
parts = []
|
|
253
|
+
parts << "[#{short}]"
|
|
254
|
+
parts << "id=#{cid}"
|
|
255
|
+
parts << "coll=#{collection}" if collection
|
|
256
|
+
parts << "fields=#{p[:fields_count] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
257
|
+
parts << "full=#{p[:full_fields_count] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
258
|
+
parts << "affix=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :affix_tokens)}"
|
|
259
|
+
parts << "tag=#{p[:tag_kind] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
260
|
+
parts << "dur=#{duration}ms"
|
|
261
|
+
parts.join(' ')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def compact_synonyms(short, cid, collection, duration, p)
|
|
265
|
+
parts = []
|
|
266
|
+
parts << "[#{short}]"
|
|
267
|
+
parts << "id=#{cid}"
|
|
268
|
+
parts << "coll=#{collection}" if collection
|
|
269
|
+
parts << "syn=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :use_synonyms)}"
|
|
270
|
+
parts << "stop=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :use_stopwords)}"
|
|
271
|
+
parts << "src=#{p[:source] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
272
|
+
parts << "dur=#{duration}ms"
|
|
273
|
+
parts.join(' ')
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def compact_geo(short, cid, collection, duration, p)
|
|
277
|
+
shapes = p[:shapes] || {}
|
|
278
|
+
point = shapes[:point] || 0
|
|
279
|
+
rect = shapes[:rect] || 0
|
|
280
|
+
circle = shapes[:circle] || 0
|
|
281
|
+
parts = []
|
|
282
|
+
parts << "[#{short}]"
|
|
283
|
+
parts << "id=#{cid}"
|
|
284
|
+
parts << "coll=#{collection}" if collection
|
|
285
|
+
parts << "shapes=#{point}/#{rect}/#{circle}"
|
|
286
|
+
parts << "sort=#{p[:sort_mode] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
287
|
+
parts << "radius=#{p[:radius_bucket] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
288
|
+
parts << "dur=#{duration}ms"
|
|
289
|
+
parts.join(' ')
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def compact_vector(short, cid, collection, duration, p)
|
|
293
|
+
parts = []
|
|
294
|
+
parts << "[#{short}]"
|
|
295
|
+
parts << "id=#{cid}"
|
|
296
|
+
parts << "coll=#{collection}" if collection
|
|
297
|
+
parts << "qvec=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :query_vector_present)}"
|
|
298
|
+
parts << "dims=#{p[:dims] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
299
|
+
parts << "hybrid=#{p[:hybrid_weight] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
300
|
+
parts << "ann=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :ann_params_present)}"
|
|
301
|
+
parts << "dur=#{duration}ms"
|
|
302
|
+
parts.join(' ')
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def compact_hits_limit(short, cid, collection, duration, status, p)
|
|
306
|
+
parts = []
|
|
307
|
+
parts << "[#{short}]"
|
|
308
|
+
parts << "id=#{cid}"
|
|
309
|
+
parts << "coll=#{collection}" if collection
|
|
310
|
+
parts << "early=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :early_limit)}"
|
|
311
|
+
parts << "max=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :validate_max)}"
|
|
312
|
+
parts << "strat=#{p[:applied_strategy] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
313
|
+
parts << "trig=#{p[:triggered] || SearchEngine::Logging::FormatHelpers::DASH}"
|
|
314
|
+
parts << "total=#{SearchEngine::Logging::FormatHelpers.display_or_dash(p, :total_hits)}"
|
|
315
|
+
parts << "status=#{status}"
|
|
316
|
+
parts << "dur=#{duration}ms"
|
|
317
|
+
parts.join(' ')
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def build_event_json_extras(name, p)
|
|
321
|
+
case name
|
|
322
|
+
when 'search_engine.facet.compile'
|
|
323
|
+
keys = %i[fields_count queries_count max_facet_values sort_flags conflicts]
|
|
324
|
+
keys.each_with_object({}) { |k, h| h[k.to_s] = p[k] if p.key?(k) }
|
|
325
|
+
when 'search_engine.highlight.compile'
|
|
326
|
+
keys = %i[fields_count full_fields_count affix_tokens snippet_threshold tag_kind]
|
|
327
|
+
keys.each_with_object({}) { |k, h| h[k.to_s] = p[k] if p.key?(k) }
|
|
328
|
+
when 'search_engine.synonyms.apply'
|
|
329
|
+
keys = %i[use_synonyms use_stopwords source]
|
|
330
|
+
keys.each_with_object({}) { |k, h| h[k.to_s] = p[k] if p.key?(k) }
|
|
331
|
+
when 'search_engine.geo.compile'
|
|
332
|
+
h = {}
|
|
333
|
+
h['filters_count'] = p[:filters_count] if p.key?(:filters_count)
|
|
334
|
+
h['shapes'] = p[:shapes] if p.key?(:shapes)
|
|
335
|
+
h['sort_mode'] = p[:sort_mode] if p.key?(:sort_mode)
|
|
336
|
+
h['radius_bucket'] = p[:radius_bucket] if p.key?(:radius_bucket)
|
|
337
|
+
h
|
|
338
|
+
when 'search_engine.vector.compile'
|
|
339
|
+
keys = %i[query_vector_present dims hybrid_weight ann_params_present]
|
|
340
|
+
keys.each_with_object({}) { |k, h| h[k.to_s] = p[k] if p.key?(k) }
|
|
341
|
+
when 'search_engine.hits.limit'
|
|
342
|
+
keys = %i[early_limit validate_max applied_strategy triggered total_hits]
|
|
343
|
+
keys.each_with_object({}) { |k, h| h[k.to_s] = p[k] if p.key?(k) }
|
|
344
|
+
else
|
|
345
|
+
{}
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def safe_duration(event, payload)
|
|
350
|
+
d = (event.respond_to?(:duration) ? event.duration : payload[:duration_ms]).to_f
|
|
351
|
+
d = payload[:duration_ms].to_f if d.zero? && payload[:duration_ms]
|
|
352
|
+
d.round(1)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def pick_status(payload)
|
|
356
|
+
payload.key?(:http_status) ? payload[:http_status] : (payload[:status] || 'ok')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def short_correlation_id(cid)
|
|
360
|
+
str = cid.to_s
|
|
361
|
+
return random_hex_4 if str.empty?
|
|
362
|
+
|
|
363
|
+
str[0, 4]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def random_hex_4
|
|
367
|
+
rng = (Thread.current[:__se_log_rng__] ||= Random.new)
|
|
368
|
+
val = rng.rand(0x10000)
|
|
369
|
+
val.to_s(16).rjust(4, '0')
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def safe_jsonable(obj)
|
|
373
|
+
case obj
|
|
374
|
+
when Hash
|
|
375
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
376
|
+
h[k.to_s] = safe_jsonable(v)
|
|
377
|
+
end
|
|
378
|
+
when Array
|
|
379
|
+
obj.map { |v| safe_jsonable(v) }
|
|
380
|
+
when Numeric, TrueClass, FalseClass, NilClass
|
|
381
|
+
obj
|
|
382
|
+
else
|
|
383
|
+
obj.to_s
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|