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,765 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'search_engine/client_options'
|
|
4
|
+
require 'search_engine/errors'
|
|
5
|
+
require 'search_engine/observability'
|
|
6
|
+
require 'search_engine/client/request_builder'
|
|
7
|
+
require 'search_engine/client/services'
|
|
8
|
+
|
|
9
|
+
module SearchEngine
|
|
10
|
+
# Thin wrapper on top of the official `typesense` gem.
|
|
11
|
+
#
|
|
12
|
+
# Provides single-search and federated multi-search while enforcing that cache
|
|
13
|
+
# knobs live in URL/common-params and not in per-search request bodies.
|
|
14
|
+
class Client
|
|
15
|
+
# @param config [SearchEngine::Config]
|
|
16
|
+
# @param typesense_client [Object, nil] optional injected Typesense::Client (for tests)
|
|
17
|
+
def initialize(config: SearchEngine.config, typesense_client: nil)
|
|
18
|
+
@config = config
|
|
19
|
+
@typesense = typesense_client
|
|
20
|
+
@services = Services.build(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Execute a single search against a collection.
|
|
24
|
+
#
|
|
25
|
+
# @param collection [String] collection name
|
|
26
|
+
# @param params [Hash] Typesense search parameters (q, query_by, etc.)
|
|
27
|
+
# @param url_opts [Hash] URL/common knobs (use_cache, cache_ttl)
|
|
28
|
+
# @return [SearchEngine::Result] Wrapped response with hydrated hits
|
|
29
|
+
# @raise [SearchEngine::Errors::InvalidParams, SearchEngine::Errors::*]
|
|
30
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/client`
|
|
31
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#search-document`
|
|
32
|
+
def search(collection:, params:, url_opts: {})
|
|
33
|
+
services.fetch(:search).call(collection: collection, params: params, url_opts: url_opts)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Resolve a logical collection name that might be an alias to the physical collection name.
|
|
37
|
+
#
|
|
38
|
+
# @param logical_name [String]
|
|
39
|
+
# @param timeout_ms [Integer, nil] optional read-timeout override in ms
|
|
40
|
+
# @return [String, nil] physical collection name when alias exists; nil when alias not found
|
|
41
|
+
# @raise [SearchEngine::Errors::*] on network or API errors other than 404
|
|
42
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema`
|
|
43
|
+
# @see `https://typesense.org/docs/latest/api/aliases.html`
|
|
44
|
+
def resolve_alias(logical_name, timeout_ms: nil)
|
|
45
|
+
services.fetch(:collections).resolve_alias(logical_name, timeout_ms: timeout_ms)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieve the live schema for a physical collection name.
|
|
49
|
+
#
|
|
50
|
+
# @param collection_name [String]
|
|
51
|
+
# @param timeout_ms [Integer, nil] optional read-timeout override in ms
|
|
52
|
+
# @return [Hash, nil] schema hash when found; nil when collection not found (404)
|
|
53
|
+
# @raise [SearchEngine::Errors::*] on other network or API errors
|
|
54
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema`
|
|
55
|
+
# @see `https://typesense.org/docs/latest/api/collections.html`
|
|
56
|
+
def retrieve_collection_schema(collection_name, timeout_ms: nil)
|
|
57
|
+
services.fetch(:collections).retrieve_schema(collection_name, timeout_ms: timeout_ms)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Upsert an alias to point to the provided physical collection (atomic server-side swap).
|
|
61
|
+
# @param alias_name [String]
|
|
62
|
+
# @param physical_name [String]
|
|
63
|
+
# @return [Hash]
|
|
64
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema#lifecycle`
|
|
65
|
+
# @see `https://typesense.org/docs/latest/api/aliases.html#upsert-an-alias`
|
|
66
|
+
def upsert_alias(alias_name, physical_name)
|
|
67
|
+
services.fetch(:collections).upsert_alias(alias_name, physical_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Create a new physical collection with the given schema.
|
|
71
|
+
# @param schema [Hash] Typesense schema body
|
|
72
|
+
# @return [Hash] created collection schema
|
|
73
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema#lifecycle`
|
|
74
|
+
# @see `https://typesense.org/docs/latest/api/collections.html#create-a-collection`
|
|
75
|
+
def create_collection(schema)
|
|
76
|
+
services.fetch(:collections).create(schema)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Update a physical collection's schema in-place.
|
|
80
|
+
# @param name [String]
|
|
81
|
+
# @param schema [Hash] Typesense schema body with fields to add/drop
|
|
82
|
+
# @return [Hash] updated collection schema
|
|
83
|
+
# @see `https://typesense.org/docs/latest/api/collections.html#update-collection`
|
|
84
|
+
def update_collection(name, schema)
|
|
85
|
+
services.fetch(:collections).update(name, schema)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Delete a physical collection by name.
|
|
89
|
+
# @param name [String]
|
|
90
|
+
# @param timeout_ms [Integer, nil] optional read-timeout override in ms; when nil, a safer
|
|
91
|
+
# default suitable for destructive operations is used (prefers indexer timeout, with
|
|
92
|
+
# a minimum of 30s).
|
|
93
|
+
# @return [Hash] Typesense delete response
|
|
94
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema#lifecycle`
|
|
95
|
+
# @see `https://typesense.org/docs/latest/api/collections.html#delete-a-collection`
|
|
96
|
+
def delete_collection(name, timeout_ms: nil)
|
|
97
|
+
effective_timeout_ms = begin
|
|
98
|
+
if timeout_ms&.to_i&.positive?
|
|
99
|
+
timeout_ms.to_i
|
|
100
|
+
else
|
|
101
|
+
# Prefer a longer timeout for potentially long-running delete operations
|
|
102
|
+
idx = begin
|
|
103
|
+
config.indexer&.timeout_ms
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
base = idx.to_i.positive? ? idx.to_i : config.timeout_ms.to_i
|
|
108
|
+
base < 30_000 ? 30_000 : base
|
|
109
|
+
end
|
|
110
|
+
rescue StandardError
|
|
111
|
+
30_000
|
|
112
|
+
end
|
|
113
|
+
services.fetch(:collections).delete(name, timeout_ms: effective_timeout_ms)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# List all collections.
|
|
117
|
+
# @param timeout_ms [Integer, nil] optional read-timeout override in ms
|
|
118
|
+
# @return [Array<Hash>] list of collection metadata
|
|
119
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/schema`
|
|
120
|
+
# @see `https://typesense.org/docs/latest/api/collections.html#list-all-collections`
|
|
121
|
+
def list_collections(timeout_ms: nil)
|
|
122
|
+
services.fetch(:collections).list(timeout_ms: timeout_ms)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Perform a server health check.
|
|
126
|
+
# @return [Hash] Typesense health response (symbolized where applicable)
|
|
127
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/troubleshooting`
|
|
128
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#health`
|
|
129
|
+
def health
|
|
130
|
+
services.fetch(:operations).health
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Retrieve server metrics (raw output).
|
|
134
|
+
#
|
|
135
|
+
# Returns the unmodified JSON object from the Typesense `/metrics.json`
|
|
136
|
+
# endpoint. Keys are not symbolized to preserve the raw shape.
|
|
137
|
+
#
|
|
138
|
+
# @return [Hash] Raw payload from `/metrics.json`
|
|
139
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#metrics`
|
|
140
|
+
def metrics
|
|
141
|
+
services.fetch(:operations).metrics
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Retrieve server stats (raw output).
|
|
145
|
+
#
|
|
146
|
+
# Returns the unmodified JSON object from the Typesense `/stats.json`
|
|
147
|
+
# endpoint. Keys are not symbolized to preserve the raw shape.
|
|
148
|
+
#
|
|
149
|
+
# @return [Hash] Raw payload from `/stats.json`
|
|
150
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#stats`
|
|
151
|
+
def stats
|
|
152
|
+
services.fetch(:operations).stats
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# --- Admin: API Keys ------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
# List API keys configured on the Typesense server.
|
|
158
|
+
#
|
|
159
|
+
# @return [Array<Hash>] list of keys with symbolized fields when possible
|
|
160
|
+
# @see `https://typesense.org/docs/latest/api/api-keys.html#list-keys`
|
|
161
|
+
def list_api_keys
|
|
162
|
+
services.fetch(:operations).list_api_keys
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# --- Admin: Synonyms ----------------------------------------------------
|
|
166
|
+
# NOTE: We rely on the official client's endpoints; names are mapped here.
|
|
167
|
+
|
|
168
|
+
# @param collection [String]
|
|
169
|
+
# @param id [String]
|
|
170
|
+
# @param terms [Array<String>]
|
|
171
|
+
# @return [Hash]
|
|
172
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
173
|
+
# @see `https://typesense.org/docs/latest/api/synonyms.html#upsert-a-synonym`
|
|
174
|
+
def synonyms_upsert(collection:, id:, terms:)
|
|
175
|
+
admin_resource_request(
|
|
176
|
+
resource_type: :synonyms,
|
|
177
|
+
method: :put,
|
|
178
|
+
collection: collection,
|
|
179
|
+
id: id,
|
|
180
|
+
body_data: Array(terms)
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# @return [Array<Hash>]
|
|
185
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
186
|
+
# @see `https://typesense.org/docs/latest/api/synonyms.html#list-all-synonyms-of-a-collection`
|
|
187
|
+
def synonyms_list(collection:)
|
|
188
|
+
admin_resource_request(
|
|
189
|
+
resource_type: :synonyms,
|
|
190
|
+
method: :get,
|
|
191
|
+
collection: collection
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [Hash, nil]
|
|
196
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
197
|
+
# @see `https://typesense.org/docs/latest/api/synonyms.html#retrieve-a-synonym`
|
|
198
|
+
def synonyms_get(collection:, id:)
|
|
199
|
+
admin_resource_request(
|
|
200
|
+
resource_type: :synonyms,
|
|
201
|
+
method: :get,
|
|
202
|
+
collection: collection,
|
|
203
|
+
id: id,
|
|
204
|
+
return_nil_on_404: true
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @return [Hash]
|
|
209
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
210
|
+
# @see `https://typesense.org/docs/latest/api/synonyms.html#delete-a-synonym`
|
|
211
|
+
def synonyms_delete(collection:, id:)
|
|
212
|
+
admin_resource_request(
|
|
213
|
+
resource_type: :synonyms,
|
|
214
|
+
method: :delete,
|
|
215
|
+
collection: collection,
|
|
216
|
+
id: id
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# --- Admin: Stopwords ---------------------------------------------------
|
|
221
|
+
|
|
222
|
+
# @param collection [String]
|
|
223
|
+
# @param id [String]
|
|
224
|
+
# @param terms [Array<String>]
|
|
225
|
+
# @return [Hash]
|
|
226
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
227
|
+
# @see `https://typesense.org/docs/latest/api/stopwords.html#upsert-a-stopwords`
|
|
228
|
+
def stopwords_upsert(collection:, id:, terms:)
|
|
229
|
+
admin_resource_request(
|
|
230
|
+
resource_type: :stopwords,
|
|
231
|
+
method: :put,
|
|
232
|
+
collection: collection,
|
|
233
|
+
id: id,
|
|
234
|
+
body_data: Array(terms)
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# @return [Array<Hash>]
|
|
239
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
240
|
+
# @see `https://typesense.org/docs/latest/api/stopwords.html#list-all-stopwords-of-a-collection`
|
|
241
|
+
def stopwords_list(collection:)
|
|
242
|
+
admin_resource_request(
|
|
243
|
+
resource_type: :stopwords,
|
|
244
|
+
method: :get,
|
|
245
|
+
collection: collection
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @return [Hash, nil]
|
|
250
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
251
|
+
# @see `https://typesense.org/docs/latest/api/stopwords.html#retrieve-a-stopword`
|
|
252
|
+
def stopwords_get(collection:, id:)
|
|
253
|
+
admin_resource_request(
|
|
254
|
+
resource_type: :stopwords,
|
|
255
|
+
method: :get,
|
|
256
|
+
collection: collection,
|
|
257
|
+
id: id,
|
|
258
|
+
return_nil_on_404: true
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @return [Hash]
|
|
263
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/synonyms-stopwords`
|
|
264
|
+
# @see `https://typesense.org/docs/latest/api/stopwords.html#delete-a-stopword`
|
|
265
|
+
def stopwords_delete(collection:, id:)
|
|
266
|
+
admin_resource_request(
|
|
267
|
+
resource_type: :stopwords,
|
|
268
|
+
method: :delete,
|
|
269
|
+
collection: collection,
|
|
270
|
+
id: id
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# -----------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
# Bulk import JSONL documents into a collection using Typesense import API.
|
|
277
|
+
#
|
|
278
|
+
# @param collection [String] physical collection name
|
|
279
|
+
# @param jsonl [String] newline-delimited JSON payload
|
|
280
|
+
# @param action [Symbol, String] one of :upsert, :create, :update (default: :upsert)
|
|
281
|
+
# @return [Object] upstream return (String of JSONL statuses or Array of Hashes depending on gem version)
|
|
282
|
+
# @raise [SearchEngine::Errors::*]
|
|
283
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer`
|
|
284
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#import-documents`
|
|
285
|
+
def import_documents(collection:, jsonl:, action: :upsert)
|
|
286
|
+
services.fetch(:documents).import(collection: collection, jsonl: jsonl, action: action)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Delete documents by filter from a collection.
|
|
290
|
+
# @param collection [String] physical collection name
|
|
291
|
+
# @param filter_by [String] Typesense filter string
|
|
292
|
+
# @param timeout_ms [Integer, nil] optional read timeout override in ms
|
|
293
|
+
# @return [Hash] response from Typesense client (symbolized)
|
|
294
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/indexer#stale-deletes`
|
|
295
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#delete-documents-by-query`
|
|
296
|
+
def delete_documents_by_filter(collection:, filter_by:, timeout_ms: nil)
|
|
297
|
+
services.fetch(:documents).delete_by_filter(collection: collection, filter_by: filter_by, timeout_ms: timeout_ms)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Delete a single document by id from a collection.
|
|
301
|
+
#
|
|
302
|
+
# @param collection [String] physical collection name
|
|
303
|
+
# @param id [String, #to_s] document id
|
|
304
|
+
# @param timeout_ms [Integer, nil] optional read timeout override in ms
|
|
305
|
+
# @return [Hash, nil] response from Typesense client (symbolized) or nil when 404
|
|
306
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#delete-a-document`
|
|
307
|
+
def delete_document(collection:, id:, timeout_ms: nil)
|
|
308
|
+
services.fetch(:documents).delete(collection: collection, id: id, timeout_ms: timeout_ms)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Retrieve a single document by id from a collection.
|
|
312
|
+
# @param collection [String]
|
|
313
|
+
# @param id [String, #to_s]
|
|
314
|
+
# @param timeout_ms [Integer, nil]
|
|
315
|
+
# @return [Hash, nil] document hash or nil when 404
|
|
316
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#retrieve-a-document`
|
|
317
|
+
def retrieve_document(collection:, id:, timeout_ms: nil)
|
|
318
|
+
services.fetch(:documents).retrieve(collection: collection, id: id, timeout_ms: timeout_ms)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Partially update a single document by id.
|
|
322
|
+
#
|
|
323
|
+
# @param collection [String] physical collection name
|
|
324
|
+
# @param id [String, #to_s] document id
|
|
325
|
+
# @param fields [Hash] partial fields to update
|
|
326
|
+
# @param timeout_ms [Integer, nil] optional read timeout override in ms
|
|
327
|
+
# @return [Hash] response from Typesense client (symbolized)
|
|
328
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#update-a-document`
|
|
329
|
+
def update_document(collection:, id:, fields:, timeout_ms: nil)
|
|
330
|
+
services.fetch(:documents).update(collection: collection, id: id, fields: fields, timeout_ms: timeout_ms)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Partially update documents that match a filter.
|
|
334
|
+
#
|
|
335
|
+
# @param collection [String] physical collection name
|
|
336
|
+
# @param filter_by [String] Typesense filter string
|
|
337
|
+
# @param fields [Hash] partial fields to update
|
|
338
|
+
# @param timeout_ms [Integer, nil] optional read timeout override in ms
|
|
339
|
+
# @return [Hash] response from Typesense client (symbolized)
|
|
340
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#update-documents-by-query`
|
|
341
|
+
def update_documents_by_filter(collection:, filter_by:, fields:, timeout_ms: nil)
|
|
342
|
+
services.fetch(:documents).update_by_filter(collection: collection, filter_by: filter_by, fields: fields,
|
|
343
|
+
timeout_ms: timeout_ms
|
|
344
|
+
)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Create a single document in a collection.
|
|
348
|
+
#
|
|
349
|
+
# @param collection [String] physical collection name
|
|
350
|
+
# @param document [Hash] Typesense document body
|
|
351
|
+
# @return [Hash] created document as returned by Typesense (symbolized)
|
|
352
|
+
# @raise [SearchEngine::Errors::InvalidParams, SearchEngine::Errors::*]
|
|
353
|
+
# @see `https://typesense.org/docs/latest/api/documents.html#create-a-document`
|
|
354
|
+
def create_document(collection:, document:)
|
|
355
|
+
services.fetch(:documents).create(collection: collection, document: document)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Execute a multi-search across multiple collections.
|
|
359
|
+
#
|
|
360
|
+
# @param searches [Array<Hash>] per-entry request bodies produced by Multi#to_payloads
|
|
361
|
+
# @param url_opts [Hash] URL/common knobs (use_cache, cache_ttl)
|
|
362
|
+
# @return [Hash] Raw Typesense multi-search response with key 'results'
|
|
363
|
+
# @raise [SearchEngine::Errors::InvalidParams, SearchEngine::Errors::*]
|
|
364
|
+
# @see `https://nikita-shkoda.mintlify.app/projects/search-engine-for-typesense/multi-search-guide`
|
|
365
|
+
# @see `https://typesense.org/docs/latest/api/#multi-search`
|
|
366
|
+
def multi_search(searches:, url_opts: {})
|
|
367
|
+
services.fetch(:search).multi(searches: searches, url_opts: url_opts)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Clear the Typesense server-side search cache.
|
|
371
|
+
#
|
|
372
|
+
# @return [Hash] response payload from Typesense (symbolized keys)
|
|
373
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#clear-cache`
|
|
374
|
+
def clear_cache
|
|
375
|
+
services.fetch(:operations).clear_cache
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
attr_reader :config, :services
|
|
381
|
+
|
|
382
|
+
# Internal helper for synonyms and stopwords CRUD operations.
|
|
383
|
+
#
|
|
384
|
+
# @param resource_type [Symbol] :synonyms or :stopwords
|
|
385
|
+
# @param method [Symbol] HTTP method :get, :put, or :delete
|
|
386
|
+
# @param collection [String] collection name
|
|
387
|
+
# @param id [String, nil] resource id (required for get/put/delete on specific resource)
|
|
388
|
+
# @param body_data [Hash, nil] request body data (for put operations)
|
|
389
|
+
# @param return_nil_on_404 [Boolean] return nil instead of raising on 404 (for get operations)
|
|
390
|
+
# @return [Hash, Array<Hash>, nil] response data (symbolized keys) or nil if 404 and return_nil_on_404 is true
|
|
391
|
+
# @raise [SearchEngine::Errors::Api] on API errors (unless 404 and return_nil_on_404 is true)
|
|
392
|
+
def admin_resource_request(resource_type:, method:, collection:, id: nil, body_data: nil, return_nil_on_404: false)
|
|
393
|
+
c = collection.to_s
|
|
394
|
+
s = id.to_s if id
|
|
395
|
+
ts = typesense
|
|
396
|
+
start = current_monotonic_ms
|
|
397
|
+
|
|
398
|
+
# Build path based on resource type and operation
|
|
399
|
+
path = if s
|
|
400
|
+
# For operations with id: /collections/{c}/synonyms/{id} or /collections/{c}/stopwords/{id}
|
|
401
|
+
path_prefix = case resource_type
|
|
402
|
+
when :synonyms
|
|
403
|
+
Client::RequestBuilder::COLLECTIONS_PREFIX + c + Client::RequestBuilder::SYNONYMS_PREFIX
|
|
404
|
+
when :stopwords
|
|
405
|
+
Client::RequestBuilder::COLLECTIONS_PREFIX + c + Client::RequestBuilder::STOPWORDS_PREFIX
|
|
406
|
+
else
|
|
407
|
+
raise ArgumentError, "Unknown resource_type: #{resource_type.inspect}"
|
|
408
|
+
end
|
|
409
|
+
path_prefix + s
|
|
410
|
+
else
|
|
411
|
+
# For list (get without id): /collections/{c}/synonyms or /collections/{c}/stopwords
|
|
412
|
+
path_suffix = case resource_type
|
|
413
|
+
when :synonyms
|
|
414
|
+
Client::RequestBuilder::SYNONYMS_SUFFIX
|
|
415
|
+
when :stopwords
|
|
416
|
+
Client::RequestBuilder::STOPWORDS_SUFFIX
|
|
417
|
+
else
|
|
418
|
+
raise ArgumentError, "Unknown resource_type: #{resource_type.inspect}"
|
|
419
|
+
end
|
|
420
|
+
Client::RequestBuilder::COLLECTIONS_PREFIX + c + path_suffix
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Build request body for put operations
|
|
424
|
+
request_body = if method == :put && body_data
|
|
425
|
+
resource_key = resource_type == :synonyms ? :synonyms : :stopwords
|
|
426
|
+
{ resource_key => body_data }
|
|
427
|
+
else
|
|
428
|
+
{}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
result = with_exception_mapping(method, path, {}, start) do
|
|
432
|
+
execute_admin_resource_request(ts, c, s, resource_type, method, request_body)
|
|
433
|
+
end
|
|
434
|
+
symbolize_keys_deep(result)
|
|
435
|
+
rescue Errors::Api => error
|
|
436
|
+
return nil if return_nil_on_404 && error.status.to_i == 404
|
|
437
|
+
|
|
438
|
+
raise
|
|
439
|
+
ensure
|
|
440
|
+
instrument(method, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Execute the actual Typesense API call for admin resources.
|
|
444
|
+
def execute_admin_resource_request(ts, collection, id, resource_type, method, request_body)
|
|
445
|
+
case method
|
|
446
|
+
when :get
|
|
447
|
+
if id
|
|
448
|
+
ts.collections[collection].public_send(resource_type)[id].retrieve
|
|
449
|
+
else
|
|
450
|
+
ts.collections[collection].public_send(resource_type).retrieve
|
|
451
|
+
end
|
|
452
|
+
when :put
|
|
453
|
+
ts.collections[collection].public_send(resource_type)[id].upsert(request_body)
|
|
454
|
+
when :delete
|
|
455
|
+
ts.collections[collection].public_send(resource_type)[id].delete
|
|
456
|
+
else
|
|
457
|
+
raise ArgumentError, "Unsupported method: #{method.inspect}"
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def typesense
|
|
462
|
+
@typesense ||= build_typesense_client
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def typesense_for_import
|
|
466
|
+
import_timeout = begin
|
|
467
|
+
config.indexer&.timeout_ms
|
|
468
|
+
rescue StandardError
|
|
469
|
+
nil
|
|
470
|
+
end
|
|
471
|
+
if import_timeout&.to_i&.positive? && import_timeout.to_i != config.timeout_ms.to_i
|
|
472
|
+
build_typesense_client_with_read_timeout(import_timeout.to_i / 1000.0)
|
|
473
|
+
else
|
|
474
|
+
typesense
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def build_typesense_client_with_read_timeout(read_timeout_seconds)
|
|
479
|
+
require 'typesense'
|
|
480
|
+
|
|
481
|
+
Typesense::Client.new(
|
|
482
|
+
nodes: build_nodes,
|
|
483
|
+
api_key: config.api_key,
|
|
484
|
+
# typesense-ruby v4.1.0 uses a single connection timeout for both open+read
|
|
485
|
+
connection_timeout_seconds: read_timeout_seconds,
|
|
486
|
+
num_retries: safe_retry_attempts,
|
|
487
|
+
retry_interval_seconds: safe_retry_backoff,
|
|
488
|
+
logger: safe_logger,
|
|
489
|
+
log_level: safe_typesense_log_level
|
|
490
|
+
)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def build_typesense_client
|
|
494
|
+
require 'typesense'
|
|
495
|
+
|
|
496
|
+
Typesense::Client.new(
|
|
497
|
+
nodes: build_nodes,
|
|
498
|
+
api_key: config.api_key,
|
|
499
|
+
# Single timeout governs both open/read in typesense-ruby
|
|
500
|
+
connection_timeout_seconds: (config.timeout_ms.to_i / 1000.0),
|
|
501
|
+
num_retries: safe_retry_attempts,
|
|
502
|
+
retry_interval_seconds: safe_retry_backoff,
|
|
503
|
+
logger: safe_logger,
|
|
504
|
+
log_level: safe_typesense_log_level
|
|
505
|
+
)
|
|
506
|
+
rescue StandardError => error
|
|
507
|
+
raise error
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def build_nodes
|
|
511
|
+
proto =
|
|
512
|
+
begin
|
|
513
|
+
config.protocol.to_s.strip.presence || 'http'
|
|
514
|
+
rescue StandardError
|
|
515
|
+
nil
|
|
516
|
+
end
|
|
517
|
+
[
|
|
518
|
+
{
|
|
519
|
+
host: config.host,
|
|
520
|
+
port: config.port,
|
|
521
|
+
protocol: proto
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def safe_logger
|
|
527
|
+
config.logger
|
|
528
|
+
rescue StandardError
|
|
529
|
+
nil
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def safe_retry_attempts
|
|
533
|
+
r = begin
|
|
534
|
+
config.retries
|
|
535
|
+
rescue StandardError
|
|
536
|
+
nil
|
|
537
|
+
end
|
|
538
|
+
return 0 unless r.is_a?(Hash)
|
|
539
|
+
|
|
540
|
+
v = r[:attempts]
|
|
541
|
+
v = v.to_i if v.respond_to?(:to_i)
|
|
542
|
+
v.is_a?(Integer) && v >= 0 ? v : 0
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def safe_retry_backoff
|
|
546
|
+
r = begin
|
|
547
|
+
config.retries
|
|
548
|
+
rescue StandardError
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
return 0.0 unless r.is_a?(Hash)
|
|
552
|
+
|
|
553
|
+
v = r[:backoff]
|
|
554
|
+
v = v.to_f if v.respond_to?(:to_f)
|
|
555
|
+
v.is_a?(Float) && v >= 0.0 ? v : 0.0
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def safe_typesense_log_level
|
|
559
|
+
lvl_sym = begin
|
|
560
|
+
SearchEngine.config.logging.level if SearchEngine.config.respond_to?(:logging) && SearchEngine.config.logging
|
|
561
|
+
rescue StandardError
|
|
562
|
+
nil
|
|
563
|
+
end
|
|
564
|
+
require 'logger'
|
|
565
|
+
mapping = {
|
|
566
|
+
debug: ::Logger::DEBUG,
|
|
567
|
+
info: ::Logger::INFO,
|
|
568
|
+
warn: ::Logger::WARN,
|
|
569
|
+
error: ::Logger::ERROR,
|
|
570
|
+
fatal: ::Logger::FATAL
|
|
571
|
+
}
|
|
572
|
+
resolved = mapping[lvl_sym.to_s.downcase.to_sym] || ::Logger::WARN
|
|
573
|
+
# Clamp noisy Typesense debug unless explicitly enabled via env.
|
|
574
|
+
if resolved == ::Logger::DEBUG && !debug_http_enabled_env?
|
|
575
|
+
::Logger::INFO
|
|
576
|
+
else
|
|
577
|
+
resolved
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def debug_http_enabled_env?
|
|
582
|
+
val = ENV['SEARCH_ENGINE_DEBUG_HTTP']
|
|
583
|
+
return false if val.nil?
|
|
584
|
+
|
|
585
|
+
s = val.to_s.strip.downcase
|
|
586
|
+
return false if s.empty?
|
|
587
|
+
|
|
588
|
+
%w[1 true yes on].include?(s)
|
|
589
|
+
rescue StandardError
|
|
590
|
+
false
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def derive_cache_opts(url_opts)
|
|
594
|
+
merged = ClientOptions.url_options_from_config(config)
|
|
595
|
+
merged[:use_cache] = url_opts[:use_cache] if url_opts.key?(:use_cache) && !url_opts[:use_cache].nil?
|
|
596
|
+
merged[:cache_ttl] = Integer(url_opts[:cache_ttl]) if url_opts.key?(:cache_ttl)
|
|
597
|
+
merged
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def validate_single!(collection, params)
|
|
601
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
602
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
raise Errors::InvalidParams, 'params must be a Hash' unless params.is_a?(Hash)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def validate_multi!(searches)
|
|
609
|
+
unless searches.is_a?(Array) && searches.all? { |s| s.is_a?(Hash) }
|
|
610
|
+
raise Errors::InvalidParams, 'searches must be an Array of Hashes'
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
searches.each_with_index do |s, idx|
|
|
614
|
+
unless s.key?(:collection) && s[:collection].is_a?(String) && !s[:collection].strip.empty?
|
|
615
|
+
raise Errors::InvalidParams, "searches[#{idx}][:collection] must be a non-empty String"
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def with_exception_mapping(method, path, cache_params, start_ms)
|
|
621
|
+
yield
|
|
622
|
+
rescue StandardError => error
|
|
623
|
+
map_and_raise(error, method, path, cache_params, start_ms)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Map network and API exceptions into stable SearchEngine errors, with
|
|
627
|
+
# redaction and logging.
|
|
628
|
+
def map_and_raise(error, method, path, cache_params, start_ms)
|
|
629
|
+
duration_ms = current_monotonic_ms - start_ms
|
|
630
|
+
|
|
631
|
+
return handle_api_error(error, method, path, cache_params, duration_ms) if api_error?(error)
|
|
632
|
+
return handle_timeout_error(error, method, path, cache_params, duration_ms) if timeout_error?(error)
|
|
633
|
+
return handle_connection_error(error, method, path, cache_params, duration_ms) if connection_error?(error)
|
|
634
|
+
|
|
635
|
+
# Unmapped error: instrument and re-raise as-is
|
|
636
|
+
instrument(method, path, duration_ms, cache_params, error_class: error.class.name)
|
|
637
|
+
raise error
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Check if error is a Typesense API error.
|
|
641
|
+
def api_error?(error)
|
|
642
|
+
error.respond_to?(:http_code) || error.class.name.start_with?('Typesense::Error')
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Handle Typesense API errors by converting to SearchEngine::Errors::Api.
|
|
646
|
+
def handle_api_error(error, method, path, cache_params, duration_ms)
|
|
647
|
+
status = if error.respond_to?(:http_code)
|
|
648
|
+
error.http_code
|
|
649
|
+
else
|
|
650
|
+
infer_typesense_status(error)
|
|
651
|
+
end
|
|
652
|
+
body = parse_error_body(error)
|
|
653
|
+
err = Errors::Api.new(
|
|
654
|
+
"typesense api error: #{status}",
|
|
655
|
+
status: status || 500,
|
|
656
|
+
body: body,
|
|
657
|
+
doc: Client::RequestBuilder::DOC_CLIENT_ERRORS,
|
|
658
|
+
details: { http_status: status, body: body.is_a?(String) ? body[0, 120] : body }
|
|
659
|
+
)
|
|
660
|
+
instrument(method, path, duration_ms, cache_params, error_class: err.class.name)
|
|
661
|
+
raise err
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# Handle timeout errors by converting to SearchEngine::Errors::Timeout.
|
|
665
|
+
def handle_timeout_error(error, method, path, cache_params, duration_ms)
|
|
666
|
+
instrument(method, path, duration_ms, cache_params, error_class: Errors::Timeout.name)
|
|
667
|
+
raise Errors::Timeout.new(
|
|
668
|
+
error.message,
|
|
669
|
+
doc: Client::RequestBuilder::DOC_CLIENT_ERRORS,
|
|
670
|
+
details: { op: method, path: path }
|
|
671
|
+
)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Handle connection errors by converting to SearchEngine::Errors::Connection.
|
|
675
|
+
def handle_connection_error(error, method, path, cache_params, duration_ms)
|
|
676
|
+
instrument(method, path, duration_ms, cache_params, error_class: Errors::Connection.name)
|
|
677
|
+
raise Errors::Connection.new(
|
|
678
|
+
error.message,
|
|
679
|
+
doc: Client::RequestBuilder::DOC_CLIENT_ERRORS,
|
|
680
|
+
details: { op: method, path: path }
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def timeout_error?(error)
|
|
685
|
+
error.is_a?(::Timeout::Error) || error.class.name.include?('Timeout')
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def connection_error?(error)
|
|
689
|
+
return true if error.is_a?(SocketError) || error.is_a?(Errno::ECONNREFUSED) || error.is_a?(Errno::ETIMEDOUT)
|
|
690
|
+
return true if error.class.name.include?('Connection')
|
|
691
|
+
|
|
692
|
+
defined?(OpenSSL::SSL::SSLError) && error.is_a?(OpenSSL::SSL::SSLError)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Infer HTTP status code from typesense-ruby error class names when http_code is unavailable.
|
|
696
|
+
def infer_typesense_status(error)
|
|
697
|
+
klass = error.class.name
|
|
698
|
+
return 404 if klass.include?('ObjectNotFound')
|
|
699
|
+
return 401 if klass.include?('RequestUnauthorized')
|
|
700
|
+
return 403 if klass.include?('RequestForbidden')
|
|
701
|
+
return 400 if klass.include?('RequestMalformed')
|
|
702
|
+
return 409 if klass.include?('ObjectAlreadyExists')
|
|
703
|
+
return 500 if klass.include?('ServerError')
|
|
704
|
+
|
|
705
|
+
500
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def instrument(method, path, duration_ms, cache_params, error_class: nil)
|
|
709
|
+
return unless defined?(ActiveSupport::Notifications)
|
|
710
|
+
|
|
711
|
+
ActiveSupport::Notifications.instrument(
|
|
712
|
+
'search_engine.request',
|
|
713
|
+
method: method,
|
|
714
|
+
path: path,
|
|
715
|
+
duration_ms: duration_ms,
|
|
716
|
+
url_opts: Observability.filtered_url_opts(cache_params),
|
|
717
|
+
error_class: error_class
|
|
718
|
+
)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def log_success(method, path, start_ms, cache_params)
|
|
722
|
+
return unless safe_logger
|
|
723
|
+
|
|
724
|
+
elapsed = current_monotonic_ms - start_ms
|
|
725
|
+
msg = +'search_engine '
|
|
726
|
+
msg << method.to_s.upcase
|
|
727
|
+
msg << ' '
|
|
728
|
+
msg << path
|
|
729
|
+
msg << ' completed in '
|
|
730
|
+
msg << elapsed.round(1).to_s
|
|
731
|
+
msg << 'ms'
|
|
732
|
+
msg << ' cache='
|
|
733
|
+
msg << (cache_params[:use_cache] ? 'true' : 'false')
|
|
734
|
+
msg << ' ttl='
|
|
735
|
+
msg << cache_params[:cache_ttl].to_s
|
|
736
|
+
safe_logger.info(msg)
|
|
737
|
+
rescue StandardError
|
|
738
|
+
nil
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def parse_error_body(error)
|
|
742
|
+
return error.body if error.respond_to?(:body) && error.body
|
|
743
|
+
return error.message if error.respond_to?(:message) && error.message
|
|
744
|
+
|
|
745
|
+
nil
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def current_monotonic_ms
|
|
749
|
+
SearchEngine::Instrumentation.monotonic_ms
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def symbolize_keys_deep(obj)
|
|
753
|
+
case obj
|
|
754
|
+
when Hash
|
|
755
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
756
|
+
h[k.to_sym] = symbolize_keys_deep(v)
|
|
757
|
+
end
|
|
758
|
+
when Array
|
|
759
|
+
obj.map { |e| symbolize_keys_deep(e) }
|
|
760
|
+
else
|
|
761
|
+
obj
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
end
|