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.
Files changed (139) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +148 -0
  4. data/app/search_engine/search_engine/app_info.rb +11 -0
  5. data/app/search_engine/search_engine/index_partition_job.rb +170 -0
  6. data/lib/generators/search_engine/install/install_generator.rb +20 -0
  7. data/lib/generators/search_engine/install/templates/initializer.rb.tt +230 -0
  8. data/lib/generators/search_engine/model/model_generator.rb +86 -0
  9. data/lib/generators/search_engine/model/templates/model.rb.tt +12 -0
  10. data/lib/search-engine-for-typesense.rb +12 -0
  11. data/lib/search_engine/active_record_syncable.rb +247 -0
  12. data/lib/search_engine/admin/stopwords.rb +125 -0
  13. data/lib/search_engine/admin/synonyms.rb +125 -0
  14. data/lib/search_engine/admin.rb +12 -0
  15. data/lib/search_engine/ast/and.rb +52 -0
  16. data/lib/search_engine/ast/binary_op.rb +75 -0
  17. data/lib/search_engine/ast/eq.rb +19 -0
  18. data/lib/search_engine/ast/group.rb +18 -0
  19. data/lib/search_engine/ast/gt.rb +12 -0
  20. data/lib/search_engine/ast/gte.rb +12 -0
  21. data/lib/search_engine/ast/in.rb +28 -0
  22. data/lib/search_engine/ast/lt.rb +12 -0
  23. data/lib/search_engine/ast/lte.rb +12 -0
  24. data/lib/search_engine/ast/matches.rb +55 -0
  25. data/lib/search_engine/ast/node.rb +176 -0
  26. data/lib/search_engine/ast/not_eq.rb +13 -0
  27. data/lib/search_engine/ast/not_in.rb +24 -0
  28. data/lib/search_engine/ast/or.rb +52 -0
  29. data/lib/search_engine/ast/prefix.rb +51 -0
  30. data/lib/search_engine/ast/raw.rb +41 -0
  31. data/lib/search_engine/ast/unary_op.rb +43 -0
  32. data/lib/search_engine/ast.rb +101 -0
  33. data/lib/search_engine/base/creation.rb +727 -0
  34. data/lib/search_engine/base/deletion.rb +80 -0
  35. data/lib/search_engine/base/display_coercions.rb +36 -0
  36. data/lib/search_engine/base/hydration.rb +312 -0
  37. data/lib/search_engine/base/index_maintenance/cleanup.rb +202 -0
  38. data/lib/search_engine/base/index_maintenance/lifecycle.rb +251 -0
  39. data/lib/search_engine/base/index_maintenance/schema.rb +117 -0
  40. data/lib/search_engine/base/index_maintenance.rb +459 -0
  41. data/lib/search_engine/base/indexing_dsl.rb +255 -0
  42. data/lib/search_engine/base/joins.rb +479 -0
  43. data/lib/search_engine/base/model_dsl.rb +472 -0
  44. data/lib/search_engine/base/presets.rb +43 -0
  45. data/lib/search_engine/base/pretty_printer.rb +315 -0
  46. data/lib/search_engine/base/relation_delegation.rb +42 -0
  47. data/lib/search_engine/base/scopes.rb +113 -0
  48. data/lib/search_engine/base/updating.rb +92 -0
  49. data/lib/search_engine/base.rb +38 -0
  50. data/lib/search_engine/bulk.rb +284 -0
  51. data/lib/search_engine/cache.rb +33 -0
  52. data/lib/search_engine/cascade.rb +531 -0
  53. data/lib/search_engine/cli/doctor.rb +631 -0
  54. data/lib/search_engine/cli/support.rb +217 -0
  55. data/lib/search_engine/cli.rb +222 -0
  56. data/lib/search_engine/client/http_adapter.rb +63 -0
  57. data/lib/search_engine/client/request_builder.rb +92 -0
  58. data/lib/search_engine/client/services/base.rb +74 -0
  59. data/lib/search_engine/client/services/collections.rb +161 -0
  60. data/lib/search_engine/client/services/documents.rb +214 -0
  61. data/lib/search_engine/client/services/operations.rb +152 -0
  62. data/lib/search_engine/client/services/search.rb +190 -0
  63. data/lib/search_engine/client/services.rb +29 -0
  64. data/lib/search_engine/client.rb +765 -0
  65. data/lib/search_engine/client_options.rb +20 -0
  66. data/lib/search_engine/collection_resolver.rb +191 -0
  67. data/lib/search_engine/collections_graph.rb +330 -0
  68. data/lib/search_engine/compiled_params.rb +143 -0
  69. data/lib/search_engine/compiler.rb +383 -0
  70. data/lib/search_engine/config/observability.rb +27 -0
  71. data/lib/search_engine/config/presets.rb +92 -0
  72. data/lib/search_engine/config/selection.rb +16 -0
  73. data/lib/search_engine/config/typesense.rb +48 -0
  74. data/lib/search_engine/config/validators.rb +97 -0
  75. data/lib/search_engine/config.rb +917 -0
  76. data/lib/search_engine/console_helpers.rb +130 -0
  77. data/lib/search_engine/deletion.rb +103 -0
  78. data/lib/search_engine/dispatcher.rb +125 -0
  79. data/lib/search_engine/dsl/parser.rb +582 -0
  80. data/lib/search_engine/engine.rb +167 -0
  81. data/lib/search_engine/errors.rb +290 -0
  82. data/lib/search_engine/filters/sanitizer.rb +189 -0
  83. data/lib/search_engine/hydration/materializers.rb +808 -0
  84. data/lib/search_engine/hydration/selection_context.rb +96 -0
  85. data/lib/search_engine/indexer/batch_planner.rb +76 -0
  86. data/lib/search_engine/indexer/bulk_import.rb +626 -0
  87. data/lib/search_engine/indexer/import_dispatcher.rb +198 -0
  88. data/lib/search_engine/indexer/retry_policy.rb +103 -0
  89. data/lib/search_engine/indexer.rb +747 -0
  90. data/lib/search_engine/instrumentation.rb +308 -0
  91. data/lib/search_engine/joins/guard.rb +202 -0
  92. data/lib/search_engine/joins/resolver.rb +95 -0
  93. data/lib/search_engine/logging/color.rb +78 -0
  94. data/lib/search_engine/logging/format_helpers.rb +92 -0
  95. data/lib/search_engine/logging/partition_progress.rb +53 -0
  96. data/lib/search_engine/logging_subscriber.rb +388 -0
  97. data/lib/search_engine/mapper.rb +785 -0
  98. data/lib/search_engine/multi.rb +286 -0
  99. data/lib/search_engine/multi_result.rb +186 -0
  100. data/lib/search_engine/notifications/compact_logger.rb +675 -0
  101. data/lib/search_engine/observability.rb +162 -0
  102. data/lib/search_engine/operations.rb +58 -0
  103. data/lib/search_engine/otel.rb +227 -0
  104. data/lib/search_engine/partitioner.rb +128 -0
  105. data/lib/search_engine/ranking_plan.rb +118 -0
  106. data/lib/search_engine/registry.rb +158 -0
  107. data/lib/search_engine/relation/compiler.rb +711 -0
  108. data/lib/search_engine/relation/deletion.rb +37 -0
  109. data/lib/search_engine/relation/dsl/filters.rb +624 -0
  110. data/lib/search_engine/relation/dsl/selection.rb +240 -0
  111. data/lib/search_engine/relation/dsl.rb +903 -0
  112. data/lib/search_engine/relation/dx/dry_run.rb +59 -0
  113. data/lib/search_engine/relation/dx/friendly_where.rb +24 -0
  114. data/lib/search_engine/relation/dx.rb +231 -0
  115. data/lib/search_engine/relation/materializers.rb +118 -0
  116. data/lib/search_engine/relation/options.rb +138 -0
  117. data/lib/search_engine/relation/state.rb +274 -0
  118. data/lib/search_engine/relation/updating.rb +44 -0
  119. data/lib/search_engine/relation.rb +623 -0
  120. data/lib/search_engine/result.rb +664 -0
  121. data/lib/search_engine/schema.rb +1083 -0
  122. data/lib/search_engine/sources/active_record_source.rb +185 -0
  123. data/lib/search_engine/sources/base.rb +62 -0
  124. data/lib/search_engine/sources/lambda_source.rb +55 -0
  125. data/lib/search_engine/sources/sql_source.rb +196 -0
  126. data/lib/search_engine/sources.rb +71 -0
  127. data/lib/search_engine/stale_rules.rb +160 -0
  128. data/lib/search_engine/test/minitest_assertions.rb +57 -0
  129. data/lib/search_engine/test/offline_client.rb +134 -0
  130. data/lib/search_engine/test/rspec_matchers.rb +77 -0
  131. data/lib/search_engine/test/stub_client.rb +201 -0
  132. data/lib/search_engine/test.rb +66 -0
  133. data/lib/search_engine/test_autoload.rb +8 -0
  134. data/lib/search_engine/update.rb +35 -0
  135. data/lib/search_engine/version.rb +7 -0
  136. data/lib/search_engine.rb +332 -0
  137. data/lib/tasks/search_engine.rake +501 -0
  138. data/lib/tasks/search_engine_doctor.rake +16 -0
  139. 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