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,472 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module SearchEngine
|
|
6
|
+
class Base
|
|
7
|
+
# Model-level DSL for declaring collections, attributes, and inheritance.
|
|
8
|
+
module ModelDsl
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
# Get or set the Typesense collection name for this model.
|
|
13
|
+
#
|
|
14
|
+
# When setting, the name is normalized to String and the mapping is
|
|
15
|
+
# registered in the global collection registry.
|
|
16
|
+
#
|
|
17
|
+
# @param name [#to_s, nil]
|
|
18
|
+
# @return [String, Class] returns the current collection name when reading;
|
|
19
|
+
# returns self when setting (for macro chaining)
|
|
20
|
+
def collection(name = nil)
|
|
21
|
+
return @collection if name.nil?
|
|
22
|
+
|
|
23
|
+
normalized = name.to_s
|
|
24
|
+
raise ArgumentError, 'collection name must be non-empty' if normalized.strip.empty?
|
|
25
|
+
|
|
26
|
+
@collection = normalized
|
|
27
|
+
SearchEngine.register_collection!(@collection, self)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class_methods do
|
|
33
|
+
# Delete documents by filter for this collection's physical index.
|
|
34
|
+
# Accepts either a Typesense filter string (via first arg or :filter_by)
|
|
35
|
+
# or a hash of field=>value which will be converted to a filter string.
|
|
36
|
+
# Supports optional partition to cooperate with default_into_resolver.
|
|
37
|
+
#
|
|
38
|
+
# @param filter_or_str [String, nil]
|
|
39
|
+
# @param filter_by [String, nil]
|
|
40
|
+
# @param into [String, nil]
|
|
41
|
+
# @param partition [Object, nil]
|
|
42
|
+
# @param timeout_ms [Integer, nil]
|
|
43
|
+
# @param hash [Hash]
|
|
44
|
+
# @return [Integer] number of deleted documents
|
|
45
|
+
def delete_by(filter_or_str = nil, into: nil, partition: nil, timeout_ms: nil, filter_by: nil, **hash)
|
|
46
|
+
SearchEngine::Deletion.delete_by(
|
|
47
|
+
klass: self,
|
|
48
|
+
filter: filter_or_str || filter_by,
|
|
49
|
+
hash: (hash.empty? ? nil : hash),
|
|
50
|
+
into: into,
|
|
51
|
+
partition: partition,
|
|
52
|
+
timeout_ms: timeout_ms
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class_methods do
|
|
58
|
+
# Set or get the per-collection default query_by fields.
|
|
59
|
+
#
|
|
60
|
+
# Accepts a String (comma-separated), a Symbol, or an Array of Strings/Symbols.
|
|
61
|
+
# Values are normalized into a canonical comma-separated String with single
|
|
62
|
+
# spaces after commas (e.g., "name, brand, description"). When called
|
|
63
|
+
# without arguments, returns the canonical String or nil if unset.
|
|
64
|
+
#
|
|
65
|
+
# @param values [Array<String,Symbol,Array>] zero or more field tokens; Arrays are flattened
|
|
66
|
+
# @return [String, Class] returns the canonical String on read; returns self on write
|
|
67
|
+
def query_by(*values)
|
|
68
|
+
return @__model_default_query_by__ if values.nil? || values.empty?
|
|
69
|
+
|
|
70
|
+
flat = values.flatten(1).compact
|
|
71
|
+
|
|
72
|
+
list = if flat.size == 1 && flat.first.is_a?(String)
|
|
73
|
+
flat.first.split(',').map { |s| s.to_s.strip }.reject(&:empty?)
|
|
74
|
+
else
|
|
75
|
+
flat.map do |v|
|
|
76
|
+
case v
|
|
77
|
+
when String, Symbol then v.to_s.strip
|
|
78
|
+
else
|
|
79
|
+
raise ArgumentError, 'query_by accepts Symbols, Strings, or Arrays thereof'
|
|
80
|
+
end
|
|
81
|
+
end.reject(&:empty?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
canonical = list.join(', ')
|
|
85
|
+
@__model_default_query_by__ = canonical.empty? ? nil : canonical
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class_methods do
|
|
91
|
+
# Declare an attribute with an optional type (symbol preferred).
|
|
92
|
+
#
|
|
93
|
+
# @param name [#to_sym]
|
|
94
|
+
# @param type [Object] type descriptor (e.g., :string, :integer)
|
|
95
|
+
# @param index [Boolean, nil] when false, omit from compiled Typesense schema (still hydrated/displayed)
|
|
96
|
+
# @param locale [String, nil]
|
|
97
|
+
# @param optional [Boolean, nil]
|
|
98
|
+
# @param sort [Boolean, nil]
|
|
99
|
+
# @param infix [Boolean, nil]
|
|
100
|
+
# @param empty_filtering [Boolean, nil]
|
|
101
|
+
# @param nested [Hash, nil]
|
|
102
|
+
# @return [void]
|
|
103
|
+
def attribute(name, type = :string, index: nil, locale: nil, optional: nil, sort: nil, infix: nil,
|
|
104
|
+
empty_filtering: nil, facet: nil, nested: nil)
|
|
105
|
+
n = name.to_sym
|
|
106
|
+
__se_validate_attribute_name!(n)
|
|
107
|
+
__se_assign_attribute!(n, type)
|
|
108
|
+
__se_update_attribute_options!(n, type, locale: locale, optional: optional, sort: sort, infix: infix,
|
|
109
|
+
empty_filtering: empty_filtering, facet: facet, index: index
|
|
110
|
+
)
|
|
111
|
+
__se_define_reader_if_needed!(n)
|
|
112
|
+
__se_expand_nested_fields!(n, type, nested)
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class_methods do
|
|
118
|
+
# Validate reserved names and raise when invalid.
|
|
119
|
+
def __se_validate_attribute_name!(name_sym)
|
|
120
|
+
return unless name_sym == :id
|
|
121
|
+
|
|
122
|
+
raise SearchEngine::Errors::InvalidField,
|
|
123
|
+
'The :id field is reserved; use `identify_by` to set the Typesense document id.'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class_methods do
|
|
128
|
+
# Assign base attribute type.
|
|
129
|
+
def __se_assign_attribute!(name_sym, type)
|
|
130
|
+
(@attributes ||= {})[name_sym] = type
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class_methods do
|
|
135
|
+
# Update per-attribute options from keyword arguments.
|
|
136
|
+
def __se_update_attribute_options!(
|
|
137
|
+
name_sym,
|
|
138
|
+
type,
|
|
139
|
+
locale:,
|
|
140
|
+
optional:,
|
|
141
|
+
sort:,
|
|
142
|
+
infix:,
|
|
143
|
+
empty_filtering:,
|
|
144
|
+
facet:,
|
|
145
|
+
index:
|
|
146
|
+
)
|
|
147
|
+
has_opts = [locale, optional, sort, infix, empty_filtering, facet, index].any? { |v| !v.nil? }
|
|
148
|
+
if has_opts
|
|
149
|
+
@attribute_options ||= {}
|
|
150
|
+
new_opts = __se_build_attribute_options_for(
|
|
151
|
+
name_sym, type,
|
|
152
|
+
locale: locale, optional: optional, sort: sort, infix: infix,
|
|
153
|
+
empty_filtering: empty_filtering, facet: facet, index: index
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if new_opts.empty?
|
|
157
|
+
@attribute_options = @attribute_options.dup
|
|
158
|
+
@attribute_options.delete(name_sym)
|
|
159
|
+
else
|
|
160
|
+
@attribute_options[name_sym] = new_opts
|
|
161
|
+
end
|
|
162
|
+
elsif instance_variable_defined?(:@attribute_options) && (@attribute_options || {}).key?(name_sym)
|
|
163
|
+
# When re-declared without options, keep prior options as-is (idempotent)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
class_methods do
|
|
169
|
+
# Define an instance reader for the attribute when safe to do so.
|
|
170
|
+
# For boolean attributes, also defines a question-mark alias (e.g., available?).
|
|
171
|
+
def __se_define_reader_if_needed!(name_sym)
|
|
172
|
+
reader_defined = valid_attribute_reader_name?(name_sym) && !method_defined?(name_sym)
|
|
173
|
+
attr_reader name_sym if reader_defined
|
|
174
|
+
|
|
175
|
+
# Always check for boolean alias, even if reader was already defined
|
|
176
|
+
# (handles case where attribute type changes to boolean)
|
|
177
|
+
__se_define_boolean_alias_if_needed!(name_sym) if valid_attribute_reader_name?(name_sym)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Define a question-mark alias for boolean attributes.
|
|
181
|
+
# @param name_sym [Symbol] attribute name
|
|
182
|
+
def __se_define_boolean_alias_if_needed!(name_sym)
|
|
183
|
+
type = (@attributes || {})[name_sym]
|
|
184
|
+
return unless type == :boolean
|
|
185
|
+
|
|
186
|
+
alias_name = "#{name_sym}?".to_sym
|
|
187
|
+
return if method_defined?(alias_name)
|
|
188
|
+
|
|
189
|
+
alias_method alias_name, name_sym
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private :__se_define_boolean_alias_if_needed!
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class_methods do
|
|
196
|
+
# Expand nested subfields for object/object[] attributes when nested: is provided.
|
|
197
|
+
def __se_expand_nested_fields!(name_sym, type, nested)
|
|
198
|
+
return if nested.nil? || (nested.respond_to?(:empty?) && nested.empty?)
|
|
199
|
+
|
|
200
|
+
unless nested.is_a?(Hash)
|
|
201
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
202
|
+
'`nested` must be a Hash of field_name => type'
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
is_object = type.to_s.downcase == 'object'
|
|
206
|
+
is_object_array = type.is_a?(Array) && type.size == 1 && type.first.to_s.downcase == 'object'
|
|
207
|
+
unless is_object || is_object_array
|
|
208
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
209
|
+
"`nested:` is only valid for :object or [:object] attributes (got #{type.inspect})"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
nested.each do |child_name, child_type|
|
|
213
|
+
effective = __se_compute_nested_type_descriptor(child_type, array: is_object_array)
|
|
214
|
+
attribute("#{name_sym}.#{child_name}".to_sym, effective)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
class_methods do
|
|
220
|
+
# Declare nested fields under a base object/object[] attribute.
|
|
221
|
+
#
|
|
222
|
+
# Usage:
|
|
223
|
+
# attribute :retail_prices, [:object]
|
|
224
|
+
# nested :retail_prices,
|
|
225
|
+
# current_price: :float,
|
|
226
|
+
# price_type: :string
|
|
227
|
+
#
|
|
228
|
+
# When the base is :object, nested field types are scalar (e.g., :float -> "float").
|
|
229
|
+
# When the base is [:object], nested field types are array (e.g., :float -> "float[]").
|
|
230
|
+
#
|
|
231
|
+
# @param base [Symbol, String] base field name that must be declared as :object or [:object]
|
|
232
|
+
# @param fields [Hash{Symbol=>Object}] map of nested field name => type descriptor
|
|
233
|
+
# @return [void]
|
|
234
|
+
# @raise [SearchEngine::Errors::InvalidOption] when base is not declared as object/object[]
|
|
235
|
+
def nested(base, **fields)
|
|
236
|
+
base_sym = base.to_sym
|
|
237
|
+
attrs = @attributes || {}
|
|
238
|
+
base_type = attrs[base_sym]
|
|
239
|
+
|
|
240
|
+
is_object = base_type.to_s.downcase == 'object'
|
|
241
|
+
is_object_array = base_type.is_a?(Array) && base_type.size == 1 && base_type.first.to_s.downcase == 'object'
|
|
242
|
+
|
|
243
|
+
unless is_object || is_object_array
|
|
244
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
245
|
+
"`nested` requires base attribute #{base_sym.inspect} to be declared as :object or [:object] " \
|
|
246
|
+
"(got #{base_type.inspect})"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
fields.each do |name, type_descriptor|
|
|
250
|
+
effective_type = __se_compute_nested_type_descriptor(type_descriptor, array: is_object_array)
|
|
251
|
+
# Dotted attribute name is intentional and supported by the schema compiler.
|
|
252
|
+
attribute("#{base_sym}.#{name}".to_sym, effective_type)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Normalize a nested type descriptor to scalar or array form depending on the parent multiplicity.
|
|
259
|
+
# Accepts Symbols (e.g., :float), Arrays (e.g., [:float]), or Strings (e.g., "float", "float[]").
|
|
260
|
+
def __se_compute_nested_type_descriptor(type_descriptor, array:)
|
|
261
|
+
# Already an array type in DSL form ([:float])
|
|
262
|
+
if type_descriptor.is_a?(Array) && type_descriptor.size == 1
|
|
263
|
+
return type_descriptor if array
|
|
264
|
+
|
|
265
|
+
return type_descriptor.first
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# String forms like "float[]" or canonical names
|
|
269
|
+
if type_descriptor.is_a?(String)
|
|
270
|
+
s = type_descriptor.strip
|
|
271
|
+
if s.end_with?('[]')
|
|
272
|
+
inner = s[0..-3]
|
|
273
|
+
return array ? [inner.to_sym] : inner.to_sym
|
|
274
|
+
end
|
|
275
|
+
return array ? [s.to_sym] : s.to_sym
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Symbol or other single token
|
|
279
|
+
array ? [type_descriptor] : type_descriptor
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
class_methods do
|
|
284
|
+
# Validate whether an attribute name is a valid Ruby reader method name
|
|
285
|
+
# (skip dotted names and other invalid identifiers).
|
|
286
|
+
def valid_attribute_reader_name?(name)
|
|
287
|
+
s = name.to_s
|
|
288
|
+
return false if s.empty?
|
|
289
|
+
return false unless s.match?(/\A[a-zA-Z_]\w*\z/)
|
|
290
|
+
|
|
291
|
+
true
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def __se_build_attribute_options_for(
|
|
295
|
+
n,
|
|
296
|
+
type,
|
|
297
|
+
locale:,
|
|
298
|
+
optional: nil,
|
|
299
|
+
sort: nil,
|
|
300
|
+
infix: nil,
|
|
301
|
+
empty_filtering: nil,
|
|
302
|
+
facet: nil,
|
|
303
|
+
index: nil
|
|
304
|
+
)
|
|
305
|
+
new_opts = (@attribute_options[n] || {}).dup
|
|
306
|
+
|
|
307
|
+
# locale
|
|
308
|
+
if locale.nil?
|
|
309
|
+
new_opts.delete(:locale)
|
|
310
|
+
else
|
|
311
|
+
is_string = type.to_s.downcase == 'string'
|
|
312
|
+
is_string_array = type.is_a?(Array) && type.size == 1 && type.first.to_s.downcase == 'string'
|
|
313
|
+
unless is_string || is_string_array
|
|
314
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
315
|
+
"`locale` is only applicable to :string and [:string] (got #{type.inspect})"
|
|
316
|
+
end
|
|
317
|
+
new_opts[:locale] = locale.to_s
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
new_opts = __se_apply_optional_sort_empty_filtering(
|
|
321
|
+
new_opts,
|
|
322
|
+
type,
|
|
323
|
+
optional: optional,
|
|
324
|
+
sort: sort,
|
|
325
|
+
infix: infix,
|
|
326
|
+
empty_filtering: empty_filtering
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# facet
|
|
330
|
+
if facet.nil?
|
|
331
|
+
new_opts.delete(:facet)
|
|
332
|
+
else
|
|
333
|
+
__se_ensure_boolean!(:facet, facet)
|
|
334
|
+
new_opts[:facet] = facet ? true : false
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# index flag (default is true; only store when provided)
|
|
338
|
+
unless index.nil?
|
|
339
|
+
__se_ensure_boolean!(:index, index)
|
|
340
|
+
new_opts[:index] = index ? true : false
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
new_opts
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
private :__se_build_attribute_options_for
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
class_methods do
|
|
350
|
+
# optional, sort, infix, empty_filtering extracted to a separate block to
|
|
351
|
+
# satisfy Metrics/BlockLength without changing semantics.
|
|
352
|
+
def __se_ensure_boolean!(name, value)
|
|
353
|
+
return if [true, false].include?(value)
|
|
354
|
+
|
|
355
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
356
|
+
"`#{name}` should be of boolean data type (currently is #{value.class})"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def __se_apply_optional_sort_empty_filtering(new_opts, type, optional:, sort:, infix:, empty_filtering:)
|
|
360
|
+
# optional
|
|
361
|
+
if optional.nil?
|
|
362
|
+
new_opts.delete(:optional)
|
|
363
|
+
else
|
|
364
|
+
__se_ensure_boolean!(:optional, optional)
|
|
365
|
+
new_opts[:optional] = optional ? true : false
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# sort
|
|
369
|
+
if sort.nil?
|
|
370
|
+
new_opts.delete(:sort)
|
|
371
|
+
else
|
|
372
|
+
__se_ensure_boolean!(:sort, sort)
|
|
373
|
+
new_opts[:sort] = sort ? true : false
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# infix
|
|
377
|
+
if infix.nil?
|
|
378
|
+
new_opts.delete(:infix)
|
|
379
|
+
else
|
|
380
|
+
__se_ensure_boolean!(:infix, infix)
|
|
381
|
+
new_opts[:infix] = infix ? true : false
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# empty_filtering
|
|
385
|
+
if empty_filtering.nil?
|
|
386
|
+
new_opts.delete(:empty_filtering)
|
|
387
|
+
else
|
|
388
|
+
is_array_type = type.is_a?(Array) && type.size == 1
|
|
389
|
+
unless is_array_type
|
|
390
|
+
raise SearchEngine::Errors::InvalidOption,
|
|
391
|
+
"`empty_filtering` is only applicable to array types (e.g., [:string]); got #{type.inspect}"
|
|
392
|
+
end
|
|
393
|
+
new_opts[:empty_filtering] = empty_filtering ? true : false
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
new_opts
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
private :__se_apply_optional_sort_empty_filtering
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
class_methods do
|
|
403
|
+
# Read-only view of declared attributes for this class.
|
|
404
|
+
def attributes
|
|
405
|
+
(@attributes || {}).dup.freeze
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
class_methods do
|
|
410
|
+
# Read-only view of declared per-attribute options (e.g., locale).
|
|
411
|
+
def attribute_options
|
|
412
|
+
(@attribute_options || {}).dup.freeze
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
class_methods do
|
|
417
|
+
# Configure schema retention policy for this collection.
|
|
418
|
+
# @param keep_last [Integer] how many previous physicals to keep after swap
|
|
419
|
+
# @return [void]
|
|
420
|
+
def schema_retention(keep_last: nil)
|
|
421
|
+
return (@schema_retention || {}).dup.freeze if keep_last.nil?
|
|
422
|
+
|
|
423
|
+
value = Integer(keep_last)
|
|
424
|
+
raise ArgumentError, 'keep_last must be >= 0' if value.negative?
|
|
425
|
+
|
|
426
|
+
@schema_retention ||= {}
|
|
427
|
+
@schema_retention[:keep_last] = value
|
|
428
|
+
nil
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
class_methods do
|
|
433
|
+
# Hook to ensure subclasses inherit attributes and schema retention from their parent.
|
|
434
|
+
def inherited(subclass)
|
|
435
|
+
super
|
|
436
|
+
parent_attrs = @attributes || {}
|
|
437
|
+
subclass.instance_variable_set(:@attributes, parent_attrs.dup)
|
|
438
|
+
|
|
439
|
+
parent_attr_opts = @attribute_options || {}
|
|
440
|
+
subclass.instance_variable_set(:@attribute_options, parent_attr_opts.dup)
|
|
441
|
+
|
|
442
|
+
parent_retention = @schema_retention || {}
|
|
443
|
+
subclass.instance_variable_set(:@schema_retention, parent_retention.dup)
|
|
444
|
+
|
|
445
|
+
parent_joins = @joins_config || {}
|
|
446
|
+
subclass.instance_variable_set(:@joins_config, parent_joins.dup.freeze)
|
|
447
|
+
|
|
448
|
+
if instance_variable_defined?(:@__declared_default_preset__)
|
|
449
|
+
token = instance_variable_get(:@__declared_default_preset__)
|
|
450
|
+
subclass.instance_variable_set(:@__declared_default_preset__, token)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
if instance_variable_defined?(:@__model_default_query_by__)
|
|
454
|
+
qb = instance_variable_get(:@__model_default_query_by__)
|
|
455
|
+
subclass.instance_variable_set(:@__model_default_query_by__, qb)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
return unless instance_variable_defined?(:@identify_by_proc)
|
|
459
|
+
|
|
460
|
+
subclass.instance_variable_set(:@identify_by_proc, @identify_by_proc)
|
|
461
|
+
# Propagate identify_by metadata for type hints
|
|
462
|
+
if instance_variable_defined?(:@__identify_by_kind__)
|
|
463
|
+
subclass.instance_variable_set(:@__identify_by_kind__, @__identify_by_kind__)
|
|
464
|
+
end
|
|
465
|
+
return unless instance_variable_defined?(:@__identify_by_symbol__)
|
|
466
|
+
|
|
467
|
+
subclass.instance_variable_set(:@__identify_by_symbol__, @__identify_by_symbol__)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module SearchEngine
|
|
6
|
+
class Base
|
|
7
|
+
# Default preset declaration and resolution.
|
|
8
|
+
module Presets
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
# Declare a default preset token for this collection.
|
|
13
|
+
# @param name [#to_sym]
|
|
14
|
+
# @return [void]
|
|
15
|
+
def default_preset(name)
|
|
16
|
+
raise ArgumentError, 'default_preset requires a name' if name.nil?
|
|
17
|
+
|
|
18
|
+
token = name.to_sym
|
|
19
|
+
raise ArgumentError, 'default_preset name must be non-empty' if token.to_s.strip.empty?
|
|
20
|
+
|
|
21
|
+
instance_variable_set(:@__declared_default_preset__, token)
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Compute the effective default preset name for this collection.
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def default_preset_name
|
|
28
|
+
token = if instance_variable_defined?(:@__declared_default_preset__)
|
|
29
|
+
instance_variable_get(:@__declared_default_preset__)
|
|
30
|
+
end
|
|
31
|
+
return nil if token.nil?
|
|
32
|
+
|
|
33
|
+
presets_cfg = SearchEngine.config.presets
|
|
34
|
+
if presets_cfg.enabled && presets_cfg.namespace
|
|
35
|
+
+"#{presets_cfg.namespace}_#{token}"
|
|
36
|
+
else
|
|
37
|
+
token.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|