search-engine-for-typesense 30.1.0 → 30.1.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/app/search_engine/search_engine/index_partition_job.rb +7 -26
  4. data/lib/generators/search_engine/install/install_generator.rb +1 -1
  5. data/lib/generators/search_engine/install/templates/initializer.rb.tt +11 -11
  6. data/lib/generators/search_engine/model/model_generator.rb +2 -2
  7. data/lib/generators/search_engine/model/templates/model.rb.tt +2 -2
  8. data/lib/search_engine/admin/stopwords.rb +1 -1
  9. data/lib/search_engine/admin/synonyms.rb +1 -1
  10. data/lib/search_engine/ast/node.rb +1 -1
  11. data/lib/search_engine/ast.rb +1 -1
  12. data/lib/search_engine/base/creation.rb +4 -4
  13. data/lib/search_engine/cli/doctor.rb +6 -6
  14. data/lib/search_engine/client/request_builder.rb +2 -2
  15. data/lib/search_engine/client.rb +19 -19
  16. data/lib/search_engine/collection_resolver.rb +7 -2
  17. data/lib/search_engine/config/presets.rb +7 -7
  18. data/lib/search_engine/config.rb +5 -5
  19. data/lib/search_engine/console_helpers.rb +4 -4
  20. data/lib/search_engine/dispatcher.rb +2 -2
  21. data/lib/search_engine/dsl/parser.rb +9 -9
  22. data/lib/search_engine/errors.rb +6 -6
  23. data/lib/search_engine/filters/sanitizer.rb +24 -44
  24. data/lib/search_engine/hydration/materializers.rb +13 -7
  25. data/lib/search_engine/indexer/batch_planner.rb +3 -8
  26. data/lib/search_engine/indexer/import_dispatcher.rb +1 -1
  27. data/lib/search_engine/indexer/retry_policy.rb +9 -6
  28. data/lib/search_engine/indexer.rb +3 -176
  29. data/lib/search_engine/instrumentation.rb +1 -1
  30. data/lib/search_engine/joins/guard.rb +4 -4
  31. data/lib/search_engine/joins/resolver.rb +2 -2
  32. data/lib/search_engine/logging_subscriber.rb +4 -4
  33. data/lib/search_engine/mapper.rb +13 -21
  34. data/lib/search_engine/multi.rb +2 -2
  35. data/lib/search_engine/multi_result.rb +1 -1
  36. data/lib/search_engine/notifications/compact_logger.rb +3 -3
  37. data/lib/search_engine/otel.rb +5 -5
  38. data/lib/search_engine/partitioner.rb +5 -5
  39. data/lib/search_engine/ranking_plan.rb +4 -4
  40. data/lib/search_engine/relation/compiler.rb +11 -6
  41. data/lib/search_engine/relation/dsl/filters.rb +9 -9
  42. data/lib/search_engine/relation/dsl/selection.rb +3 -3
  43. data/lib/search_engine/relation/dsl.rb +17 -17
  44. data/lib/search_engine/relation/dx.rb +4 -4
  45. data/lib/search_engine/relation/materializers.rb +1 -1
  46. data/lib/search_engine/relation/options.rb +1 -1
  47. data/lib/search_engine/relation.rb +1 -1
  48. data/lib/search_engine/result.rb +1 -1
  49. data/lib/search_engine/schema.rb +4 -4
  50. data/lib/search_engine/sources/active_record_source.rb +0 -1
  51. data/lib/search_engine/sources/lambda_source.rb +1 -1
  52. data/lib/search_engine/sources/sql_source.rb +1 -1
  53. data/lib/search_engine/sources.rb +3 -3
  54. data/lib/search_engine/test/stub_client.rb +8 -8
  55. data/lib/search_engine/test.rb +2 -2
  56. data/lib/search_engine/version.rb +1 -1
  57. metadata +2 -2
@@ -305,13 +305,13 @@ module SearchEngine
305
305
  # Controls validation rules and list limits.
306
306
  class CurationConfig
307
307
  # @return [Integer] maximum number of pinned IDs allowed (default: 50)
308
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
308
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
309
309
  attr_accessor :max_pins
310
310
  # @return [Integer] maximum number of hidden IDs allowed (default: 200)
311
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
311
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
312
312
  attr_accessor :max_hidden
313
313
  # @return [Regexp] allowed curated ID pattern (used for IDs and override tags)
314
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
314
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
315
315
  attr_accessor :id_regex
316
316
 
317
317
  def initialize
@@ -393,7 +393,7 @@ module SearchEngine
393
393
 
394
394
  # Expose presets configuration.
395
395
  # @return [SearchEngine::Config::PresetsConfig]
396
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/presets
396
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/presets
397
397
  def presets
398
398
  @presets ||= PresetsConfig.new
399
399
  end
@@ -403,7 +403,7 @@ module SearchEngine
403
403
  # Normalizes values on assignment.
404
404
  # @param value [Object]
405
405
  # @return [void]
406
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/presets#config-default-preset
406
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/presets#config-default-preset
407
407
  def presets=(value)
408
408
  cfg = presets
409
409
  if value.is_a?(PresetsConfig)
@@ -38,7 +38,7 @@ module SearchEngine
38
38
  # @example
39
39
  # SE.q('milk').per(5)
40
40
  # SE.q.where(category: 'dairy')
41
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx
41
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/dx
42
42
  def q(query = nil, **opts)
43
43
  model = default_model!
44
44
  rel = model.all
@@ -91,7 +91,7 @@ module SearchEngine
91
91
  raise ArgumentError,
92
92
  'No default model configured. Set SearchEngine.config.default_console_model ' \
93
93
  'or define a single SearchEngine::Base model. See ' \
94
- 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx#generators--console-helpers.'
94
+ 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/dx#generators--console-helpers.'
95
95
  end
96
96
 
97
97
  uniq_klasses = mapping.values.uniq
@@ -100,7 +100,7 @@ module SearchEngine
100
100
  names = uniq_klasses.map { |k| k.respond_to?(:name) && k.name ? k.name : k.to_s }.sort
101
101
  raise ArgumentError,
102
102
  "Ambiguous default model: #{names.join(', ')}. Set SearchEngine.config.default_console_model. " \
103
- 'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx#generators--console-helpers.'
103
+ 'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/dx#generators--console-helpers.'
104
104
  end
105
105
 
106
106
  def resolve_model_class(value)
@@ -121,7 +121,7 @@ module SearchEngine
121
121
  rescue NameError
122
122
  raise ArgumentError,
123
123
  "Unknown model constant #{name.inspect} for default_console_model. Ensure it's loaded. " \
124
- 'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx#generators--console-helpers.'
124
+ 'See https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/dx#generators--console-helpers.'
125
125
  end
126
126
 
127
127
  private_class_method :resolve_model_class
@@ -20,14 +20,14 @@ module SearchEngine
20
20
  unless klass.is_a?(Class)
21
21
  raise SearchEngine::Errors::InvalidParams.new(
22
22
  'klass must be a Class',
23
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer#troubleshooting',
23
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer#troubleshooting',
24
24
  details: { arg: :klass }
25
25
  )
26
26
  end
27
27
  unless klass.ancestors.include?(SearchEngine::Base)
28
28
  raise SearchEngine::Errors::InvalidParams.new(
29
29
  'klass must inherit from SearchEngine::Base',
30
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer#troubleshooting',
30
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer#troubleshooting',
31
31
  details: { klass: klass.to_s }
32
32
  )
33
33
  end
@@ -189,7 +189,7 @@ module SearchEngine
189
189
  unless m
190
190
  raise SearchEngine::Errors::InvalidOperator.new(
191
191
  "invalid template '#{template}'. Supported: =, !=, >, >=, <, <=, IN, NOT IN, MATCHES, PREFIX",
192
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
192
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
193
193
  details: { template: template }
194
194
  )
195
195
  end
@@ -272,7 +272,7 @@ module SearchEngine
272
272
 
273
273
  raise SearchEngine::Errors::InvalidOperator.new(
274
274
  "expected #{needed} args for #{needed} placeholders in template '#{template}', got #{provided}.",
275
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
275
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
276
276
  details: { needed: needed, provided: provided, template: template }
277
277
  )
278
278
  end
@@ -337,7 +337,7 @@ module SearchEngine
337
337
 
338
338
  raise SearchEngine::Errors::InvalidType.new(
339
339
  invalid_type_message(field: field, klass: klass, expectation: 'a non-empty Array', got: values),
340
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
340
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
341
341
  details: { field: field, got_class: values.class.name }
342
342
  )
343
343
  end
@@ -394,7 +394,7 @@ module SearchEngine
394
394
 
395
395
  raise SearchEngine::Errors::InvalidType.new(
396
396
  invalid_type_message(field: nil, klass: nil, expectation: 'boolean', got: value),
397
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
397
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
398
398
  details: { got: value }
399
399
  )
400
400
  end
@@ -426,7 +426,7 @@ module SearchEngine
426
426
  rescue StandardError
427
427
  raise SearchEngine::Errors::InvalidType.new(
428
428
  invalid_type_message(field: field, klass: klass, expectation: 'time', got: value),
429
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
429
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
430
430
  details: { field: field, got: value }
431
431
  )
432
432
  end
@@ -443,7 +443,7 @@ module SearchEngine
443
443
  rescue StandardError
444
444
  raise SearchEngine::Errors::InvalidType.new(
445
445
  invalid_type_message(field: field, klass: klass, expectation: 'integer', got: value),
446
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
446
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
447
447
  details: { field: field, got: value }
448
448
  )
449
449
  end
@@ -452,7 +452,7 @@ module SearchEngine
452
452
  if value.is_a?(Numeric)
453
453
  raise SearchEngine::Errors::InvalidType.new(
454
454
  invalid_type_message(field: field, klass: klass, expectation: 'integer', got: value),
455
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
455
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
456
456
  details: { field: field, got: value }
457
457
  )
458
458
  end
@@ -470,7 +470,7 @@ module SearchEngine
470
470
  rescue StandardError
471
471
  raise SearchEngine::Errors::InvalidType.new(
472
472
  invalid_type_message(field: field, klass: klass, expectation: 'numeric', got: value),
473
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#troubleshooting',
473
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#troubleshooting',
474
474
  details: { field: field, got: value }
475
475
  )
476
476
  end
@@ -573,7 +573,7 @@ module SearchEngine
573
573
 
574
574
  raise SearchEngine::Errors::JoinNotApplied.new(
575
575
  "Call .joins(:#{assoc_name}) before filtering/sorting on #{assoc_name} fields",
576
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/joins#troubleshooting',
576
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/joins#troubleshooting',
577
577
  details: { assoc: assoc_name, used_for: 'filtering' }
578
578
  )
579
579
  end
@@ -15,7 +15,7 @@ module SearchEngine
15
15
  # @!attribute [r] hint
16
16
  # @return [String, nil] short actionable suggestion (no secrets)
17
17
  # @!attribute [r] doc
18
- # @return [String, nil] docs URL with optional anchor (e.g., "https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/query-dsl#operators")
18
+ # @return [String, nil] docs URL with optional anchor (e.g., "https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/query-dsl#operators")
19
19
  # @!attribute [r] details
20
20
  # @return [Object, nil] machine-readable context (JSON-serializable)
21
21
  # @!attribute [r] code
@@ -240,21 +240,21 @@ module SearchEngine
240
240
 
241
241
  # Raised when a curated ID does not match the configured pattern.
242
242
  #
243
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
243
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
244
244
  # @example
245
245
  # raise SearchEngine::Errors::InvalidCuratedId, 'InvalidCuratedId: "foo bar" is not a valid curated ID. Expected pattern: /\A[\w\-:\.]+\z/. Try removing illegal characters.'
246
246
  class InvalidCuratedId < Error; end
247
247
 
248
248
  # Raised when pinned/hidden lists exceed configured limits after normalization.
249
249
  #
250
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
250
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
251
251
  # @example
252
252
  # raise SearchEngine::Errors::CurationLimitExceeded, 'CurationLimitExceeded: pinned list exceeds max_pins=50 (attempted 51). Reduce inputs or raise the limit in SearchEngine.config.curation.'
253
253
  class CurationLimitExceeded < Error; end
254
254
 
255
255
  # Raised when an override tag is blank or invalid per allowed pattern.
256
256
  #
257
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/curation
257
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/curation
258
258
  # @example
259
259
  # raise SearchEngine::Errors::InvalidOverrideTag, 'InvalidOverrideTag: "" is invalid. Use non-blank strings that match the allowed pattern.'
260
260
  class InvalidOverrideTag < Error; end
@@ -269,7 +269,7 @@ module SearchEngine
269
269
  # raise SearchEngine::Errors::InvalidOption.new(
270
270
  # 'InvalidOption: tag must be a simple HTML-like token',
271
271
  # hint: 'Use a simple tag like <em> or <mark>',
272
- # doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/highlighting#options'
272
+ # doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/highlighting#options'
273
273
  # )
274
274
  class InvalidOption < Error; end
275
275
 
@@ -282,7 +282,7 @@ module SearchEngine
282
282
  # raise SearchEngine::Errors::HitLimitExceeded.new(
283
283
  # 'HitLimitExceeded: 12000 results exceed max=10000',
284
284
  # hint: 'Increase `validate_hits!(max:)` or narrow your filters.',
285
- # doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/hit-limits#validation',
285
+ # doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/hit-limits#validation',
286
286
  # details: { total_hits: 12_000, max: 10_000, collection: 'products' }
287
287
  # )
288
288
  class HitLimitExceeded < Error; end
@@ -22,31 +22,13 @@ module SearchEngine
22
22
  # @return [String]
23
23
  def quote(value)
24
24
  case value
25
- when NilClass
26
- 'null'
27
- when TrueClass
28
- 'true'
29
- when FalseClass
30
- 'false'
31
- when Numeric
32
- value.to_s
33
25
  when String
34
26
  %("#{escape_string(value)}")
35
- when Time
36
- %("#{value.iso8601}")
37
- when DateTime
38
- %("#{value.iso8601}")
39
- when Date
40
- %("#{value.iso8601}")
41
27
  when Array
42
28
  elements = value.flatten(1).map { |el| quote(el) }
43
29
  "[#{elements.join(', ')}]"
44
30
  else
45
- if value.respond_to?(:to_time)
46
- %("#{value.to_time.iso8601}")
47
- else
48
- %("#{escape_string(value.to_s)}")
49
- end
31
+ quote_non_string(value)
50
32
  end
51
33
  end
52
34
 
@@ -64,37 +46,14 @@ module SearchEngine
64
46
  return quote(value) if value.is_a?(Array)
65
47
 
66
48
  case value
67
- when NilClass
68
- 'null'
69
- when TrueClass
70
- 'true'
71
- when FalseClass
72
- 'false'
73
- when Numeric
74
- value.to_s
75
49
  when String, Symbol
76
50
  str = value.to_s
77
51
  lc = str.strip.downcase
78
- # Avoid ambiguity with special literals when user passes them as strings
79
52
  return %("#{escape_string(str)}") if %w[true false null].include?(lc)
80
53
 
81
- if safe_bare_string?(str)
82
- str
83
- else
84
- %("#{escape_string(str)}")
85
- end
86
- when Time
87
- %("#{value.iso8601}")
88
- when DateTime
89
- %("#{value.iso8601}")
90
- when Date
91
- %("#{value.iso8601}")
54
+ safe_bare_string?(str) ? str : %("#{escape_string(str)}")
92
55
  else
93
- if value.respond_to?(:to_time)
94
- %("#{value.to_time.iso8601}")
95
- else
96
- %("#{escape_string(value.to_s)}")
97
- end
56
+ quote_non_string(value)
98
57
  end
99
58
  end
100
59
 
@@ -160,6 +119,27 @@ module SearchEngine
160
119
  count
161
120
  end
162
121
 
122
+ # Shared quoting logic for non-String, non-Array values.
123
+ # @param value [Object]
124
+ # @return [String]
125
+ # @api private
126
+ def quote_non_string(value)
127
+ case value
128
+ when NilClass then 'null'
129
+ when TrueClass then 'true'
130
+ when FalseClass then 'false'
131
+ when Numeric then value.to_s
132
+ when Time, DateTime then %("#{value.iso8601}")
133
+ when Date then %("#{value.iso8601}")
134
+ else
135
+ if value.respond_to?(:to_time)
136
+ %("#{value.to_time.iso8601}")
137
+ else
138
+ %("#{escape_string(value.to_s)}")
139
+ end
140
+ end
141
+ end
142
+
163
143
  # Escape a raw string for inclusion inside double quotes.
164
144
  # @param str [String]
165
145
  # @return [String]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module SearchEngine
4
6
  module Hydration
5
7
  # Centralized executors for materialization: to_a/each/count/ids/pluck/pick.
@@ -505,7 +507,7 @@ module SearchEngine
505
507
  if orders.any? { |o| o.start_with?('$') }
506
508
  raise SearchEngine::Errors::InvalidOption.new(
507
509
  'Sorting by joined fields is not supported by client-side join fallback',
508
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/joins#client-side-fallback'
510
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/joins#client-side-fallback'
509
511
  )
510
512
  end
511
513
  include_str = begin
@@ -516,7 +518,7 @@ module SearchEngine
516
518
  if include_str&.split(',')&.any? { |seg| seg.strip.start_with?('$') }
517
519
  raise SearchEngine::Errors::InvalidOption.new(
518
520
  'Selecting joined fields is not supported by client-side join fallback',
519
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/joins#client-side-fallback'
521
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/joins#client-side-fallback'
520
522
  )
521
523
  end
522
524
 
@@ -569,7 +571,7 @@ module SearchEngine
569
571
  # Unsupported node type for fallback (e.g., ranges, not_eq, etc.)
570
572
  raise SearchEngine::Errors::InvalidOption.new(
571
573
  'Only equality and IN predicates on joined fields are supported by client-side join fallback',
572
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/joins#client-side-fallback'
574
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/joins#client-side-fallback'
573
575
  )
574
576
  end
575
577
  end
@@ -627,7 +629,7 @@ module SearchEngine
627
629
  # Fallback does not support OR with joined nodes; reject early
628
630
  raise SearchEngine::Errors::InvalidOption.new(
629
631
  'OR with joined predicates is not supported by client-side join fallback',
630
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/joins#client-side-fallback'
632
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/joins#client-side-fallback'
631
633
  )
632
634
  else
633
635
  if node.respond_to?(:field)
@@ -715,7 +717,10 @@ module SearchEngine
715
717
  end
716
718
 
717
719
  def coerce_pluck_field_names(fields)
718
- Array(fields).flatten.compact.map(&:to_s).map(&:strip).reject(&:empty?)
720
+ Array(fields).flatten.filter_map do |f|
721
+ s = f.to_s.strip
722
+ s unless s.empty?
723
+ end
719
724
  end
720
725
  module_function :coerce_pluck_field_names
721
726
 
@@ -744,7 +749,7 @@ module SearchEngine
744
749
  raise SearchEngine::Errors::InvalidSelection.new(
745
750
  msg,
746
751
  hint: hint,
747
- doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/field-selection#guardrails--errors',
752
+ doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/field-selection#guardrails--errors',
748
753
  details: { requested: names, include_base: include_base, exclude_base: exclude_base }
749
754
  )
750
755
  end
@@ -755,8 +760,9 @@ module SearchEngine
755
760
  if exclude_base.include?(field)
756
761
  "InvalidSelection: field :#{field} not in effective selection. Remove exclude(:#{field})."
757
762
  else
763
+ seen = Set.new(include_base)
758
764
  suggestion_fields = include_base.dup
759
- requested.each { |f| suggestion_fields << f unless suggestion_fields.include?(f) }
765
+ requested.each { |f| suggestion_fields << f if seen.add?(f) }
760
766
  symbols = suggestion_fields.map { |t| ":#{t}" }.join(',')
761
767
  "InvalidSelection: field :#{field} not in effective selection. Use `reselect(#{symbols})`."
762
768
  end
@@ -19,20 +19,15 @@ module SearchEngine
19
19
  # @param buffer [String] a reusable String buffer to encode into
20
20
  # @return [Array(Integer, Integer)] [docs_count, bytes_sent]
21
21
  # @raise [SearchEngine::Errors::InvalidParams] when a document is not a Hash or missing :id
22
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer
22
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer
23
23
  def self.encode_jsonl!(docs, buffer)
24
24
  count = 0
25
25
  buffer.clear
26
26
  size = docs.size
27
+ now_i = defined?(Time.zone) && Time.zone ? Time.zone.now.to_i : Time.now.to_i
27
28
  docs.each_with_index do |raw, idx|
28
29
  doc = ensure_hash_document(raw)
29
30
  ensure_id!(doc)
30
- # Force system timestamp field prior to serialization to Typesense
31
- now_i = if defined?(Time) && defined?(Time.zone) && Time.zone
32
- Time.zone.now.to_i
33
- else
34
- Time.now.to_i
35
- end
36
31
  doc[:doc_updated_at] = now_i if doc.is_a?(Hash)
37
32
  buffer << JSON.generate(doc)
38
33
  buffer << "\n" if idx < (size - 1)
@@ -62,7 +57,7 @@ module SearchEngine
62
57
  raise SearchEngine::Errors::InvalidParams,
63
58
  'Indexer requires batches of Hash-like documents with at least an :id key. ' \
64
59
  'Mapping DSL is not available yet. See ' \
65
- 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer.'
60
+ 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer.'
66
61
  end
67
62
  end
68
63
 
@@ -28,7 +28,7 @@ module SearchEngine
28
28
  # @param dry_run [Boolean] when true, do not perform network call
29
29
  # @return [Hash] stats payload: { index:, docs_count:, success_count:, failure_count:, attempts:, http_status:, duration_ms:, bytes_sent:, errors_sample: [] }
30
30
  # @raise [SearchEngine::Errors::Api] when the underlying client raises an API error (propagated)
31
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer
31
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer
32
32
  if dry_run
33
33
  # Emit instrumentation parity without network
34
34
  http_status = 200
@@ -39,7 +39,7 @@ module SearchEngine
39
39
  # Build a policy from a config-like Hash.
40
40
  # @param cfg [Hash]
41
41
  # @return [RetryPolicy]
42
- # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer
42
+ # @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/v30.1/indexer
43
43
  def self.from_config(cfg)
44
44
  c = cfg || {}
45
45
  new(
@@ -72,7 +72,6 @@ module SearchEngine
72
72
  # @param error [Exception]
73
73
  # @return [Float]
74
74
  def next_delay(attempt, _error)
75
- # Exponential backoff with bounded jitter
76
75
  exp = [@base * (2 ** (attempt - 1)), @max].min
77
76
  jitter = exp * @jitter_fraction
78
77
  delta = random_in_range(-jitter..jitter)
@@ -80,13 +79,17 @@ module SearchEngine
80
79
  sleep_time.positive? ? sleep_time : 0.0
81
80
  end
82
81
 
82
+ # Whether an HTTP status code represents a transient/retryable error.
83
+ # @param code [Integer]
84
+ # @return [Boolean]
85
+ def self.transient_status?(code)
86
+ code == 429 || (code >= 500 && code <= 599)
87
+ end
88
+
83
89
  private
84
90
 
85
91
  def transient_status?(code)
86
- return true if code == 429
87
- return true if code >= 500 && code <= 599
88
-
89
- false
92
+ self.class.transient_status?(code)
90
93
  end
91
94
 
92
95
  def random_in_range(range)