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,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEngine
4
+ class Relation
5
+ # Canonical immutable state helpers and defaults for Relation.
6
+ # Owns deep-freeze and duplication utilities, plus initial state normalization.
7
+ module State
8
+ # Internal normalized state keys (authoritative defaults)
9
+ DEFAULT_STATE = {
10
+ filters: [].freeze,
11
+ ast: [].freeze,
12
+ orders: [].freeze,
13
+ select: [].freeze,
14
+ select_nested: {}.freeze,
15
+ select_nested_order: [].freeze,
16
+ exclude: [].freeze,
17
+ exclude_nested: {}.freeze,
18
+ exclude_nested_order: [].freeze,
19
+ joins: [].freeze,
20
+ limit: nil,
21
+ offset: nil,
22
+ page: nil,
23
+ per_page: nil,
24
+ grouping: nil,
25
+ options: {}.freeze,
26
+ preset_name: nil,
27
+ preset_mode: nil,
28
+ facet_fields: [].freeze,
29
+ facet_max_values: [].freeze,
30
+ facet_queries: [].freeze,
31
+ highlight: {}.freeze,
32
+ highlight_fields: [].freeze,
33
+ highlight_full_fields: [].freeze,
34
+ highlight_start_tag: nil,
35
+ highlight_end_tag: nil,
36
+ highlight_affix_num_tokens: nil,
37
+ highlight_snippet_threshold: nil,
38
+ use_synonyms: nil,
39
+ use_stopwords: nil,
40
+ ranking: nil,
41
+ hit_limits: {}.freeze
42
+ }.freeze
43
+
44
+ # Normalize the provided initial state Hash using the Relation's normalizers.
45
+ # @param state [Hash]
46
+ # @return [Hash]
47
+ def normalize_initial_state(state)
48
+ return {} if state.nil? || state.empty?
49
+ raise ArgumentError, 'state must be a Hash' unless state.is_a?(Hash)
50
+
51
+ normalized = {}
52
+ state.each { |key, value| apply_initial_state_key!(normalized, key, value) }
53
+ normalized
54
+ end
55
+
56
+ # Apply a single initial state key with normalization.
57
+ # Delegates to the same normalizers used by DSL chainers.
58
+ def apply_initial_state_key!(normalized, key, value)
59
+ handlers = {
60
+ filters: :handle_state_filters!,
61
+ filters_ast: :handle_state_filters_ast!,
62
+ ast: :handle_state_ast!,
63
+ orders: :handle_state_orders!,
64
+ select: :handle_state_select!,
65
+ select_nested: :handle_state_select_nested!,
66
+ select_nested_order: :handle_state_select_nested_order!,
67
+ exclude: :handle_state_exclude!,
68
+ exclude_nested: :handle_state_exclude_nested!,
69
+ exclude_nested_order: :handle_state_exclude_nested_order!,
70
+ joins: :handle_state_joins!,
71
+ limit: :handle_state_limit!,
72
+ offset: :handle_state_offset!,
73
+ page: :handle_state_page!,
74
+ per_page: :handle_state_per_page!,
75
+ options: :handle_state_options!,
76
+ grouping: :handle_state_grouping!,
77
+ preset_name: :handle_state_preset_name!,
78
+ preset_mode: :handle_state_preset_mode!,
79
+ curation: :handle_state_curation!,
80
+ facet_fields: :handle_state_facet_fields!,
81
+ facet_max_values: :handle_state_facet_max_values!,
82
+ facet_queries: :handle_state_facet_queries!,
83
+ highlight: :handle_state_highlight!,
84
+ ranking: :handle_state_ranking!,
85
+ hit_limits: :handle_state_hit_limits!
86
+ }
87
+ h = handlers[key.to_sym]
88
+ return unless h
89
+
90
+ send(h, normalized, value)
91
+ end
92
+
93
+ private
94
+
95
+ def handle_state_filters!(normalized, value)
96
+ normalized[:filters] = normalize_where(Array(value))
97
+ end
98
+
99
+ def handle_state_filters_ast!(normalized, value)
100
+ nodes = Array(value).flatten.compact
101
+ normalized[:ast] ||= []
102
+ normalized[:ast] += if nodes.all? { |n| n.is_a?(SearchEngine::AST::Node) }
103
+ nodes
104
+ else
105
+ SearchEngine::DSL::Parser.parse_list(nodes, klass: @klass)
106
+ end
107
+ end
108
+
109
+ def handle_state_ast!(normalized, value)
110
+ nodes = Array(value).flatten.compact
111
+ normalized[:ast] = if nodes.all? { |n| n.is_a?(SearchEngine::AST::Node) }
112
+ nodes
113
+ else
114
+ SearchEngine::DSL::Parser.parse_list(nodes, klass: @klass)
115
+ end
116
+ end
117
+
118
+ # One-time migration: when AST is empty and legacy string filters exist, map legacy to AST::Raw.
119
+ # Idempotent and safe for repeated calls.
120
+ # @param state [Hash]
121
+ # @return [void]
122
+ def migrate_legacy_filters_to_ast!(state)
123
+ return unless state.is_a?(Hash)
124
+
125
+ ast_nodes = Array(state[:ast]).flatten.compact
126
+ legacy = Array(state[:filters]).flatten.compact
127
+ return if !ast_nodes.empty? || legacy.empty?
128
+
129
+ raw_nodes = legacy.map { |fragment| SearchEngine::AST.raw(String(fragment)) }
130
+ state[:ast] = raw_nodes
131
+ nil
132
+ end
133
+
134
+ def handle_state_options!(normalized, value)
135
+ normalized[:options] = (value || {}).dup
136
+ end
137
+
138
+ def handle_state_grouping!(normalized, value)
139
+ normalized[:grouping] = normalize_grouping(value)
140
+ end
141
+
142
+ def handle_state_preset_name!(normalized, value)
143
+ normalized[:preset_name] = value&.to_s&.strip
144
+ end
145
+
146
+ def handle_state_preset_mode!(normalized, value)
147
+ normalized[:preset_mode] = value&.to_sym
148
+ end
149
+
150
+ def handle_state_curation!(normalized, value)
151
+ normalized[:curation] = normalize_curation_input(value)
152
+ end
153
+
154
+ def handle_state_facet_fields!(normalized, value)
155
+ normalized[:facet_fields] = Array(value).flatten.compact
156
+ end
157
+
158
+ def handle_state_facet_max_values!(normalized, value)
159
+ normalized[:facet_max_values] = Array(value).flatten.compact
160
+ end
161
+
162
+ def handle_state_facet_queries!(normalized, value)
163
+ normalized[:facet_queries] = Array(value).flatten.compact
164
+ end
165
+
166
+ def handle_state_highlight!(normalized, value)
167
+ normalized[:highlight] = normalize_highlight_input(value)
168
+ end
169
+
170
+ def handle_state_ranking!(normalized, value)
171
+ normalized[:ranking] = normalize_ranking_input(value || {})
172
+ end
173
+
174
+ def handle_state_hit_limits!(normalized, value)
175
+ normalized[:hit_limits] = normalize_hit_limits_input(value || {})
176
+ end
177
+
178
+ # Newly added handlers for remaining state keys
179
+ def handle_state_orders!(normalized, value)
180
+ additions = normalize_order(value)
181
+ normalized[:orders] = dedupe_orders_last_wins(additions)
182
+ end
183
+
184
+ def handle_state_select!(normalized, value)
185
+ normalized[:select] = normalize_select(value)
186
+ end
187
+
188
+ def handle_state_select_nested!(normalized, value)
189
+ nested_in = value || {}
190
+ nested = {}
191
+ nested_in.each do |k, v|
192
+ key = k.to_sym
193
+ fields = Array(v).flatten.compact
194
+ nested[key] = fields
195
+ end
196
+ normalized[:select_nested] = nested
197
+ end
198
+
199
+ def handle_state_select_nested_order!(normalized, value)
200
+ normalized[:select_nested_order] = Array(value).flatten.compact.map(&:to_sym)
201
+ end
202
+
203
+ def handle_state_exclude!(normalized, value)
204
+ normalized[:exclude] = normalize_select(value)
205
+ end
206
+
207
+ def handle_state_exclude_nested!(normalized, value)
208
+ nested_in = value || {}
209
+ nested = {}
210
+ nested_in.each do |k, v|
211
+ key = k.to_sym
212
+ fields = Array(v).flatten.compact
213
+ nested[key] = fields
214
+ end
215
+ normalized[:exclude_nested] = nested
216
+ end
217
+
218
+ def handle_state_exclude_nested_order!(normalized, value)
219
+ normalized[:exclude_nested_order] = Array(value).flatten.compact.map(&:to_sym)
220
+ end
221
+
222
+ def handle_state_joins!(normalized, value)
223
+ normalized[:joins] = normalize_joins(value)
224
+ end
225
+
226
+ def handle_state_limit!(normalized, value)
227
+ normalized[:limit] = coerce_integer_min(value, :limit, 1)
228
+ end
229
+
230
+ def handle_state_offset!(normalized, value)
231
+ normalized[:offset] = coerce_integer_min(value, :offset, 0)
232
+ end
233
+
234
+ def handle_state_page!(normalized, value)
235
+ normalized[:page] = coerce_integer_min(value, :page, 1)
236
+ end
237
+
238
+ def handle_state_per_page!(normalized, value)
239
+ normalized[:per_page] = coerce_integer_min(value, :per, 1)
240
+ end
241
+
242
+ # Deep duplicate Hash/Array trees; leaves are returned as-is.
243
+ # @param obj [Object]
244
+ # @return [Object]
245
+ def deep_dup(obj)
246
+ case obj
247
+ when Hash
248
+ obj.transform_values(&method(:deep_dup))
249
+ when Array
250
+ obj.map(&method(:deep_dup))
251
+ else
252
+ obj
253
+ end
254
+ end
255
+
256
+ # Deep-freeze Hash/Array/String trees in-place; returns the frozen object.
257
+ # @param obj [Object]
258
+ # @return [Object]
259
+ def deep_freeze_inplace(obj)
260
+ case obj
261
+ when Hash
262
+ obj.each_value { |v| deep_freeze_inplace(v) }
263
+ obj.freeze
264
+ when Array
265
+ obj.each { |el| deep_freeze_inplace(el) }
266
+ obj.freeze
267
+ else
268
+ obj.freeze if obj.is_a?(String)
269
+ end
270
+ obj
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEngine
4
+ class Relation
5
+ # Updating helpers bound to a relation instance.
6
+ #
7
+ # Provides `update_all` which updates documents that match the current
8
+ # relation predicates. When no predicates are present, it updates all
9
+ # documents from the collection by using a safe match-all filter.
10
+ module Updating
11
+ # Update all documents matching the current relation filters.
12
+ #
13
+ # When the relation has no filters, updates all documents from the
14
+ # collection using a safe match-all filter (`id:!=null`).
15
+ #
16
+ # @param attributes [Hash, nil] fields to update (or pass as kwargs)
17
+ # @param into [String, nil] override physical collection name
18
+ # @param partition [Object, nil] partition token for resolvers
19
+ # @param timeout_ms [Integer, nil] optional read timeout override in ms
20
+ # @return [Integer] number of updated documents
21
+ def update_all(attributes = nil, into: nil, partition: nil, timeout_ms: nil, **kwattrs)
22
+ attrs = if attributes.is_a?(Hash) && !attributes.empty?
23
+ attributes
24
+ elsif kwattrs && !kwattrs.empty?
25
+ kwattrs
26
+ end
27
+ raise ArgumentError, 'attributes must be a non-empty Hash' if attrs.nil? || attrs.empty?
28
+
29
+ ast_nodes = Array(@state[:ast]).flatten.compact
30
+ filter = compiled_filter_by(ast_nodes)
31
+ filter = 'id:!=null' if filter.to_s.strip.empty?
32
+
33
+ SearchEngine::Update.update_by(
34
+ klass: @klass,
35
+ attributes: attrs,
36
+ filter: filter,
37
+ into: into,
38
+ partition: partition,
39
+ timeout_ms: timeout_ms
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end