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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module SearchEngine
|
|
6
|
+
class Relation
|
|
7
|
+
module Dx
|
|
8
|
+
# Pure helpers for producing redacted previews without I/O.
|
|
9
|
+
module DryRun
|
|
10
|
+
# Redact a compiled params hash, preserving only whitelisted keys and
|
|
11
|
+
# masking sensitive fields. Returns a new Hash.
|
|
12
|
+
# @param params [Hash]
|
|
13
|
+
# @return [Hash]
|
|
14
|
+
def self.redact_params(params)
|
|
15
|
+
redacted = SearchEngine::Observability.redact(params)
|
|
16
|
+
return redacted unless params.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
hits = params[:_hits] || params['_hits']
|
|
19
|
+
return redacted unless hits
|
|
20
|
+
|
|
21
|
+
out = redacted.is_a?(Hash) ? redacted.dup : {}
|
|
22
|
+
out[:_hits] = hits
|
|
23
|
+
out
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Return pretty or compact JSON with stable key ordering when pretty.
|
|
27
|
+
# @param value [Object]
|
|
28
|
+
# @param pretty [Boolean]
|
|
29
|
+
# @return [String]
|
|
30
|
+
def self.to_json(value, pretty: true)
|
|
31
|
+
if pretty && value.is_a?(Hash)
|
|
32
|
+
ordered = value.sort_by { |(k, _v)| k.to_s }.to_h
|
|
33
|
+
JSON.pretty_generate(ordered)
|
|
34
|
+
else
|
|
35
|
+
JSON.generate(value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build a single-line curl string with redacted body.
|
|
40
|
+
# @param url [String]
|
|
41
|
+
# @param params [Hash]
|
|
42
|
+
# @return [String]
|
|
43
|
+
def self.curl(url, params)
|
|
44
|
+
body_json = JSON.generate(redact_params(params))
|
|
45
|
+
%(curl -X POST #{url} -H 'Content-Type: application/json' -H 'X-TYPESENSE-API-KEY: ***' -d '#{body_json}')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return a structured dry-run payload with redacted JSON body and url options.
|
|
49
|
+
# @param url [String]
|
|
50
|
+
# @param params [Hash]
|
|
51
|
+
# @param url_opts [Hash]
|
|
52
|
+
# @return [Hash]
|
|
53
|
+
def self.payload(url:, params:, url_opts: {})
|
|
54
|
+
{ url: url, body: JSON.generate(redact_params(params)), url_opts: url_opts.freeze }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Relation
|
|
5
|
+
module Dx
|
|
6
|
+
# Pure helper for human-friendly rendering of Typesense filter strings.
|
|
7
|
+
# Accepts a String and returns a transformed String without mutating input.
|
|
8
|
+
module FriendlyWhere
|
|
9
|
+
# Render a human-friendly `filter_by` string.
|
|
10
|
+
# @param filter_by [String]
|
|
11
|
+
# @return [String]
|
|
12
|
+
def self.render(filter_by)
|
|
13
|
+
s = filter_by.to_s
|
|
14
|
+
return s if s.empty?
|
|
15
|
+
|
|
16
|
+
s.gsub(' && ', ' AND ')
|
|
17
|
+
.gsub(' || ', ' OR ')
|
|
18
|
+
.gsub(':=[', ' IN [')
|
|
19
|
+
.gsub(':!=[', ' NOT IN [')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'search_engine/relation/dx/friendly_where'
|
|
5
|
+
require 'search_engine/relation/dx/dry_run'
|
|
6
|
+
|
|
7
|
+
module SearchEngine
|
|
8
|
+
# Immutable, chainable query relation bound to a model class.
|
|
9
|
+
# See `lib/search_engine/relation.rb` for full API; DX helpers are mixed in here.
|
|
10
|
+
class Relation
|
|
11
|
+
# DX helpers are mixed into `Relation` to offer redaction‑aware, zero‑I/O explain and preview utilities.
|
|
12
|
+
# These helpers are pure and never mutate relation state.
|
|
13
|
+
module Dx
|
|
14
|
+
# Return the request body JSON after compile, fully redacted.
|
|
15
|
+
# @param pretty [Boolean] pretty-print with stable key ordering when true
|
|
16
|
+
# @return [String]
|
|
17
|
+
# @since M8
|
|
18
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx
|
|
19
|
+
def to_params_json(pretty: true)
|
|
20
|
+
params = SearchEngine::CompiledParams.from(to_typesense_params)
|
|
21
|
+
redacted = SearchEngine::Relation::Dx::DryRun.redact_params(params.to_h)
|
|
22
|
+
SearchEngine::Relation::Dx::DryRun.to_json(redacted, pretty: pretty)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return a single-line curl command with redacted API key and JSON body.
|
|
26
|
+
# @return [String]
|
|
27
|
+
# @since M8
|
|
28
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx
|
|
29
|
+
def to_curl
|
|
30
|
+
url = compiled_url
|
|
31
|
+
params = SearchEngine::CompiledParams.from(to_typesense_params).to_h
|
|
32
|
+
SearchEngine::Relation::Dx::DryRun.curl(url, params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Compile and validate without performing network I/O.
|
|
36
|
+
# Returns a structured hash with URL and post-redaction body.
|
|
37
|
+
# @return [Hash] { url:, body:, url_opts: }
|
|
38
|
+
# @raise [SearchEngine::Errors::*] same validation errors as runtime path
|
|
39
|
+
# @since M8
|
|
40
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx
|
|
41
|
+
def dry_run!
|
|
42
|
+
params = SearchEngine::CompiledParams.from(to_typesense_params).to_h
|
|
43
|
+
SearchEngine::Relation::Dx::DryRun.payload(url: compiled_url, params: params, url_opts: compiled_url_opts)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Enhanced explain output with overview, parts, conflicts, and predicted events.
|
|
47
|
+
# Builds a redaction-aware summary without network I/O.
|
|
48
|
+
# @param to [Symbol, nil]
|
|
49
|
+
# @return [String]
|
|
50
|
+
# @since M8
|
|
51
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/dx#helpers--examples
|
|
52
|
+
def explain(to: nil)
|
|
53
|
+
params = SearchEngine::CompiledParams.from(to_typesense_params)
|
|
54
|
+
lines = []
|
|
55
|
+
|
|
56
|
+
lines << header_line
|
|
57
|
+
append_preset_explain_line(lines, params)
|
|
58
|
+
append_curation_explain_lines(lines)
|
|
59
|
+
append_where_line(lines, params)
|
|
60
|
+
append_order_line(lines, params)
|
|
61
|
+
append_group_line(lines)
|
|
62
|
+
append_facets_line(lines, params)
|
|
63
|
+
append_selection_explain_lines(lines, params)
|
|
64
|
+
add_effective_selection_tokens!(lines)
|
|
65
|
+
add_pagination_line!(lines, params)
|
|
66
|
+
append_hit_limits_line(lines, params)
|
|
67
|
+
append_ranking_line(lines, params)
|
|
68
|
+
lines << overview_line(params)
|
|
69
|
+
append_conflicts_line(lines, params)
|
|
70
|
+
append_events_line(lines, params)
|
|
71
|
+
|
|
72
|
+
out = lines.join("\n")
|
|
73
|
+
puts(out) if to == :stdout
|
|
74
|
+
out
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def redact_body(params)
|
|
80
|
+
hash = params.dup
|
|
81
|
+
preview = SearchEngine::Observability.redact(params)
|
|
82
|
+
hash[:filter_by] = preview[:filter_by] if preview.is_a?(Hash) && preview.key?(:filter_by)
|
|
83
|
+
hash
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def predicted_events_for_plan(params)
|
|
87
|
+
events = []
|
|
88
|
+
events << 'search_engine.compile' if Array(@state[:ast]).any?
|
|
89
|
+
events << 'search_engine.joins.compile' if Array(params[:_join] && params[:_join][:assocs]).any?
|
|
90
|
+
events << 'search_engine.grouping.compile' if params.key?(:group_by)
|
|
91
|
+
if preset_name
|
|
92
|
+
events << 'search_engine.preset.apply'
|
|
93
|
+
events << 'search_engine.preset.conflict' if Array(params[:_preset_conflicts]).any?
|
|
94
|
+
end
|
|
95
|
+
events << 'search_engine.search'
|
|
96
|
+
events
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compiled_url
|
|
100
|
+
collection = collection_name_for_klass
|
|
101
|
+
cfg = SearchEngine.config
|
|
102
|
+
proto = cfg.protocol.to_s.strip.presence
|
|
103
|
+
base = [proto, "#{cfg.host}:#{cfg.port}"].compact.join('://')
|
|
104
|
+
"#{base}/collections/#{collection}/documents/search"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def compiled_url_opts
|
|
108
|
+
url_opts = ClientOptions.url_options_from_config(SearchEngine.config)
|
|
109
|
+
overrides = build_url_opts
|
|
110
|
+
url_opts.merge!(overrides) unless overrides.empty?
|
|
111
|
+
url_opts
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def header_line
|
|
115
|
+
"#{klass_name_for_inspect} Relation"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def append_where_line(lines, params)
|
|
119
|
+
fb = params[:filter_by]
|
|
120
|
+
return unless fb && !fb.to_s.strip.empty?
|
|
121
|
+
|
|
122
|
+
# Render friendly operators first, then redact the final string to preserve
|
|
123
|
+
# IN/NOT IN tokens while masking literals.
|
|
124
|
+
friendly = SearchEngine::Relation::Dx::FriendlyWhere.render(fb.to_s)
|
|
125
|
+
masked = SearchEngine::Observability.redact(friendly)
|
|
126
|
+
where_str = masked.is_a?(String) ? masked : friendly
|
|
127
|
+
lines << " where: #{where_str}" unless where_str.to_s.strip.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def append_order_line(lines, params)
|
|
131
|
+
sb = params[:sort_by]
|
|
132
|
+
lines << " order: #{sb}" if sb && !sb.to_s.strip.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def append_group_line(lines)
|
|
136
|
+
g = @state[:grouping]
|
|
137
|
+
return unless g
|
|
138
|
+
|
|
139
|
+
gparts = ["group_by=#{g[:field]}"]
|
|
140
|
+
gparts << "limit=#{g[:limit]}" if g[:limit]
|
|
141
|
+
gparts << 'missing_values=true' if g[:missing_values]
|
|
142
|
+
lines << " group: #{gparts.join(' ')}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def overview_line(params)
|
|
146
|
+
parts = []
|
|
147
|
+
parts << "collection=#{collection_name_for_klass}"
|
|
148
|
+
if (g = grouping)
|
|
149
|
+
seg = [g[:field]].compact
|
|
150
|
+
seg << "limit=#{g[:limit]}" if g[:limit]
|
|
151
|
+
seg << 'missing_values' if g[:missing_values]
|
|
152
|
+
parts << "grouping=#{seg.join(':')}"
|
|
153
|
+
end
|
|
154
|
+
if params.key?(:page) || params.key?(:per_page)
|
|
155
|
+
p = params[:page]
|
|
156
|
+
per = params[:per_page]
|
|
157
|
+
parts << "page/per=#{p || ''}/#{per || ''}"
|
|
158
|
+
end
|
|
159
|
+
parts << (preset_name ? "preset=#{preset_name}(mode=#{preset_mode})" : 'preset=—')
|
|
160
|
+
parts << "joins=#{Array(joins_list).size}"
|
|
161
|
+
cid = SearchEngine::Instrumentation.current_correlation_id
|
|
162
|
+
parts << "cid=#{cid && !cid.to_s.empty? ? cid : '—'}"
|
|
163
|
+
"Overview: #{parts.join(' ')}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def append_conflicts_line(lines, params)
|
|
167
|
+
conflicts = Array(params[:_preset_conflicts]).map(&:to_s).sort
|
|
168
|
+
lines << "Conflicts & Warnings: dropped keys in :lock mode => #{conflicts.join(', ')}" unless conflicts.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def append_events_line(lines, params)
|
|
172
|
+
events = predicted_events_for_plan(params)
|
|
173
|
+
lines << "Events that would fire: #{events.join(' → ')}" unless events.empty?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def append_facets_line(lines, params)
|
|
177
|
+
fb = params[:facet_by]
|
|
178
|
+
fq = params[:facet_query]
|
|
179
|
+
mv = params[:max_facet_values]
|
|
180
|
+
segs = []
|
|
181
|
+
segs << "facet_by=#{fb}" if fb && !fb.to_s.strip.empty?
|
|
182
|
+
segs << "max=#{mv}" if mv
|
|
183
|
+
segs << "queries=#{fq}" if fq && !fq.to_s.strip.empty?
|
|
184
|
+
lines << " facets: #{segs.join(' ')}" unless segs.empty?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def append_ranking_line(lines, params)
|
|
188
|
+
rk = @state[:ranking]
|
|
189
|
+
has_ranking_param = rk ||
|
|
190
|
+
params.key?(:query_by_weights) || params.key?(:num_typos) ||
|
|
191
|
+
params.key?(:drop_tokens_threshold) || params.key?(:prioritize_exact_match) ||
|
|
192
|
+
params.key?(:infix)
|
|
193
|
+
return unless has_ranking_param
|
|
194
|
+
|
|
195
|
+
parts = []
|
|
196
|
+
begin
|
|
197
|
+
plan = SearchEngine::RankingPlan.new(relation: self, query_by: params[:query_by], ranking: rk || {})
|
|
198
|
+
fields = plan.effective_query_by_fields
|
|
199
|
+
parts << "query_by=[#{fields.join(', ')}]" unless fields.empty?
|
|
200
|
+
rescue StandardError
|
|
201
|
+
# ignore
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
parts << "weights=#{params[:query_by_weights]}" if params[:query_by_weights]
|
|
205
|
+
parts << "num_typos=#{params[:num_typos]}" if params.key?(:num_typos)
|
|
206
|
+
parts << "drop_tokens_threshold=#{params[:drop_tokens_threshold]}" if params.key?(:drop_tokens_threshold)
|
|
207
|
+
parts << "prioritize_exact_match=#{params[:prioritize_exact_match]}" if params.key?(:prioritize_exact_match)
|
|
208
|
+
parts << "prefix=#{params[:infix]}" if params[:infix]
|
|
209
|
+
|
|
210
|
+
lines << " ranking: #{parts.join(' ')}" unless parts.empty?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def append_hit_limits_line(lines, params)
|
|
214
|
+
hits = params[:_hits]
|
|
215
|
+
return unless hits.is_a?(Hash) && (hits[:early_limit] || hits[:max])
|
|
216
|
+
|
|
217
|
+
segs = []
|
|
218
|
+
segs << "early_limit=#{hits[:early_limit]}" if hits[:early_limit]
|
|
219
|
+
if hits[:per_adjusted] == true
|
|
220
|
+
segs << 'per_adjusted=true'
|
|
221
|
+
elsif hits.key?(:per_adjusted)
|
|
222
|
+
segs << 'per_adjusted=false'
|
|
223
|
+
end
|
|
224
|
+
segs << "validator_max=#{hits[:max]}" if hits[:max]
|
|
225
|
+
lines << " hits: #{segs.join(' ')}" unless segs.empty?
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
include Dx
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Relation
|
|
5
|
+
# Materializers delegated to Hydration layer (single network call per Relation instance).
|
|
6
|
+
module Materializers
|
|
7
|
+
# Execute the relation and return the memoized Result.
|
|
8
|
+
# @return [SearchEngine::Result]
|
|
9
|
+
def execute
|
|
10
|
+
SearchEngine::Hydration::Materializers.execute(self)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Return a shallow copy of hydrated hits.
|
|
14
|
+
# @return [Array<Object>]
|
|
15
|
+
def to_a
|
|
16
|
+
SearchEngine::Hydration::Materializers.to_a(self)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Iterate over hydrated hits.
|
|
20
|
+
# @yieldparam obj [Object]
|
|
21
|
+
# @return [Enumerator] when no block is given
|
|
22
|
+
def each(&block)
|
|
23
|
+
SearchEngine::Hydration::Materializers.each(self, &block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Return the first element or the first N elements from the loaded page.
|
|
27
|
+
# @param n [Integer, nil]
|
|
28
|
+
# @return [Object, Array<Object>]
|
|
29
|
+
def first(n = nil)
|
|
30
|
+
SearchEngine::Hydration::Materializers.first(self, n)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Return the last element or the last N elements from the currently fetched page.
|
|
34
|
+
# @param n [Integer, nil]
|
|
35
|
+
# @return [Object, Array<Object>]
|
|
36
|
+
def last(n = nil)
|
|
37
|
+
SearchEngine::Hydration::Materializers.last(self, n)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Take N elements from the head. When N==1, returns a single object.
|
|
41
|
+
# @param n [Integer]
|
|
42
|
+
# @return [Object, Array<Object>]
|
|
43
|
+
def take(n = 1)
|
|
44
|
+
SearchEngine::Hydration::Materializers.take(self, n)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Return raw Typesense response for this relation.
|
|
48
|
+
# Executes the query (memoized) and returns Result#raw.
|
|
49
|
+
# @return [Hash]
|
|
50
|
+
def raw
|
|
51
|
+
SearchEngine::Hydration::Materializers.execute(self).raw
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Find the first matching record using where-like inputs.
|
|
55
|
+
# Accepts the same arguments as `.where` (Hash, String, Array, Symbol),
|
|
56
|
+
# applies them if provided, then limits to a single result and returns it.
|
|
57
|
+
#
|
|
58
|
+
# @param args [Array<Object>] where-compatible arguments
|
|
59
|
+
# @return [Object, nil]
|
|
60
|
+
# @example
|
|
61
|
+
# SearchEngine::Product.find_by(article_code: 12312, store_id: 1031)
|
|
62
|
+
# SearchEngine::Product.where(active: true).find_by('price:>100')
|
|
63
|
+
def find_by(*args)
|
|
64
|
+
relation = args.nil? || args.empty? ? self : where(*args)
|
|
65
|
+
relation.per(1).page(1).first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convenience for plucking :id values.
|
|
69
|
+
# @return [Array<Object>]
|
|
70
|
+
def ids
|
|
71
|
+
SearchEngine::Hydration::Materializers.ids(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Pick one or multiple fields from the first matching record.
|
|
75
|
+
# Returns a single value for one field, or an Array for multiple fields.
|
|
76
|
+
# Returns nil when no records match.
|
|
77
|
+
# @param fields [Array<#to_sym,#to_s>]
|
|
78
|
+
# @return [Object, Array<Object>, nil]
|
|
79
|
+
# @see https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/materializers#pick
|
|
80
|
+
def pick(*fields)
|
|
81
|
+
SearchEngine::Hydration::Materializers.pick(self, *fields)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Fetch and hydrate all matching records across all pages.
|
|
85
|
+
# Performs a count first, then retrieves pages in batches via multi-search.
|
|
86
|
+
# Warning: This can be memory and time intensive.
|
|
87
|
+
# @return [Array<Object>]
|
|
88
|
+
def all!
|
|
89
|
+
SearchEngine::Hydration::Materializers.all!(self)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Pluck one or multiple fields.
|
|
93
|
+
# @param fields [Array<#to_sym,#to_s>]
|
|
94
|
+
# @return [Array<Object>, Array<Array<Object>>]
|
|
95
|
+
def pluck(*fields)
|
|
96
|
+
SearchEngine::Hydration::Materializers.pluck(self, *fields)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Whether any matching documents exist.
|
|
100
|
+
# @return [Boolean]
|
|
101
|
+
def exists?
|
|
102
|
+
SearchEngine::Hydration::Materializers.exists?(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Return total number of matching documents.
|
|
106
|
+
# @return [Integer]
|
|
107
|
+
def count
|
|
108
|
+
SearchEngine::Hydration::Materializers.count(self)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Return total number of pages for this relation based on total hits and per-page size.
|
|
112
|
+
# @return [Integer]
|
|
113
|
+
def pages_count
|
|
114
|
+
SearchEngine::Hydration::Materializers.pages_count(self)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Relation
|
|
5
|
+
# Options and pagination normalization helpers.
|
|
6
|
+
# Centralizes precedence rules and guards for page/per vs limit/offset.
|
|
7
|
+
module Options
|
|
8
|
+
# Compute page/per_page with precedence:
|
|
9
|
+
# - When page/per are provided, prefer them (fill missing per with nil, page defaults to 1 when only per is set)
|
|
10
|
+
# - Otherwise, when limit is set, derive page from offset and per_page from limit
|
|
11
|
+
# - Otherwise, return empty Hash
|
|
12
|
+
# @return [Hash{Symbol=>Integer}]
|
|
13
|
+
def compute_pagination
|
|
14
|
+
page = @state[:page]
|
|
15
|
+
per = @state[:per_page]
|
|
16
|
+
|
|
17
|
+
if page || per
|
|
18
|
+
out = {}
|
|
19
|
+
if per && page
|
|
20
|
+
out[:page] = page
|
|
21
|
+
out[:per_page] = per
|
|
22
|
+
elsif per && !page
|
|
23
|
+
out[:page] = 1
|
|
24
|
+
out[:per_page] = per
|
|
25
|
+
elsif page && !per
|
|
26
|
+
out[:page] = page
|
|
27
|
+
end
|
|
28
|
+
return out
|
|
29
|
+
elsif @state[:limit]
|
|
30
|
+
limit = @state[:limit]
|
|
31
|
+
off = @state[:offset] || 0
|
|
32
|
+
computed_page = (off.to_i / limit.to_i) + 1
|
|
33
|
+
return { page: computed_page, per_page: limit }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Normalize hit limits input into a compact Hash.
|
|
40
|
+
# Accepts keys :early_limit and :max and coerces to positive Integers when possible.
|
|
41
|
+
# @param value [Hash]
|
|
42
|
+
# @return [Hash]
|
|
43
|
+
def normalize_hit_limits_input(value)
|
|
44
|
+
unless value.is_a?(Hash)
|
|
45
|
+
raise SearchEngine::Errors::InvalidOption.new(
|
|
46
|
+
'InvalidOption: hit_limits expects a Hash of options',
|
|
47
|
+
doc: 'https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/hit-limits'
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
out = {}
|
|
52
|
+
if value.key?(:early_limit) || value.key?('early_limit')
|
|
53
|
+
raw = value[:early_limit] || value['early_limit']
|
|
54
|
+
begin
|
|
55
|
+
iv = Integer(raw)
|
|
56
|
+
out[:early_limit] = iv if iv.positive?
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
if value.key?(:max) || value.key?('max')
|
|
62
|
+
raw = value[:max] || value['max']
|
|
63
|
+
begin
|
|
64
|
+
iv = Integer(raw)
|
|
65
|
+
out[:max] = iv if iv.positive?
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
out
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Coerce integers with a minimum bound; nil passes through.
|
|
74
|
+
# @param value [Object]
|
|
75
|
+
# @param name [Symbol]
|
|
76
|
+
# @param min [Integer]
|
|
77
|
+
# @return [Integer, nil]
|
|
78
|
+
# @raise [ArgumentError] when not coercible or below min
|
|
79
|
+
def coerce_integer_min(value, name, min)
|
|
80
|
+
return nil if value.nil?
|
|
81
|
+
|
|
82
|
+
integer =
|
|
83
|
+
case value
|
|
84
|
+
when Integer then value
|
|
85
|
+
else Integer(value)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "#{name} must be >= #{min}" if integer < min
|
|
89
|
+
|
|
90
|
+
integer
|
|
91
|
+
rescue ArgumentError, TypeError
|
|
92
|
+
raise ArgumentError, "#{name} must be an Integer or nil"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Strict boolean coercion with helpful errors.
|
|
96
|
+
# @param value [Object]
|
|
97
|
+
# @param name [Symbol]
|
|
98
|
+
# @return [true,false]
|
|
99
|
+
# @raise [ArgumentError]
|
|
100
|
+
def coerce_boolean_strict(value, name)
|
|
101
|
+
case value
|
|
102
|
+
when true, false
|
|
103
|
+
value
|
|
104
|
+
when String
|
|
105
|
+
s = value.to_s.strip.downcase
|
|
106
|
+
return true if %w[1 true yes on t].include?(s)
|
|
107
|
+
return false if %w[0 false no off f].include?(s)
|
|
108
|
+
|
|
109
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
110
|
+
when Integer
|
|
111
|
+
return true if value == 1
|
|
112
|
+
return false if value.zero?
|
|
113
|
+
|
|
114
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
115
|
+
else
|
|
116
|
+
raise ArgumentError, "#{name} must be a boolean"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Access indifferent key from Hash
|
|
121
|
+
def option_value(hash, key)
|
|
122
|
+
if hash.key?(key)
|
|
123
|
+
hash[key]
|
|
124
|
+
else
|
|
125
|
+
hash[key.to_s]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Stable truncation for inspect helpers
|
|
130
|
+
def truncate_for_inspect(str, max = 80)
|
|
131
|
+
return str unless str.is_a?(String)
|
|
132
|
+
return str if str.length <= max
|
|
133
|
+
|
|
134
|
+
"#{str[0, max]}..."
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|