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,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Client
|
|
5
|
+
module Services
|
|
6
|
+
# Collection-related operations (schema lifecycle, alias management, listings).
|
|
7
|
+
class Collections < Base
|
|
8
|
+
# @param logical_name [String]
|
|
9
|
+
# @return [String, nil]
|
|
10
|
+
def resolve_alias(logical_name, timeout_ms: nil)
|
|
11
|
+
name = logical_name.to_s
|
|
12
|
+
start = current_monotonic_ms
|
|
13
|
+
path = [Client::RequestBuilder::ALIASES_PREFIX, name].join
|
|
14
|
+
|
|
15
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
16
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
17
|
+
else
|
|
18
|
+
typesense
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
22
|
+
ts.aliases[name].retrieve
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
(result && (result['collection_name'] || result[:collection_name])).to_s
|
|
26
|
+
rescue Errors::Api => error
|
|
27
|
+
return nil if error.status.to_i == 404
|
|
28
|
+
|
|
29
|
+
raise
|
|
30
|
+
ensure
|
|
31
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param collection_name [String]
|
|
35
|
+
# @return [Hash, nil]
|
|
36
|
+
def retrieve_schema(collection_name, timeout_ms: nil)
|
|
37
|
+
name = collection_name.to_s
|
|
38
|
+
start = current_monotonic_ms
|
|
39
|
+
path = [Client::RequestBuilder::COLLECTIONS_PREFIX, name].join
|
|
40
|
+
|
|
41
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
42
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
43
|
+
else
|
|
44
|
+
typesense
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
48
|
+
ts.collections[name].retrieve
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
symbolize_keys_deep(result)
|
|
52
|
+
rescue Errors::Api => error
|
|
53
|
+
return nil if error.status.to_i == 404
|
|
54
|
+
|
|
55
|
+
raise
|
|
56
|
+
ensure
|
|
57
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param alias_name [String]
|
|
61
|
+
# @param physical_name [String]
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
def upsert_alias(alias_name, physical_name)
|
|
64
|
+
a = alias_name.to_s
|
|
65
|
+
p = physical_name.to_s
|
|
66
|
+
start = current_monotonic_ms
|
|
67
|
+
path = [Client::RequestBuilder::ALIASES_PREFIX, a].join
|
|
68
|
+
|
|
69
|
+
result = with_exception_mapping(:put, path, {}, start) do
|
|
70
|
+
typesense.aliases.upsert(a, collection_name: p)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
symbolize_keys_deep(result)
|
|
74
|
+
ensure
|
|
75
|
+
instrument(:put, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param schema [Hash]
|
|
79
|
+
# @return [Hash]
|
|
80
|
+
def create(schema)
|
|
81
|
+
start = current_monotonic_ms
|
|
82
|
+
path = Client::RequestBuilder::COLLECTIONS_ROOT
|
|
83
|
+
body = schema.dup
|
|
84
|
+
|
|
85
|
+
result = with_exception_mapping(:post, path, {}, start) do
|
|
86
|
+
typesense.collections.create(body)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
symbolize_keys_deep(result)
|
|
90
|
+
ensure
|
|
91
|
+
instrument(:post, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param name [String]
|
|
95
|
+
# @param schema [Hash]
|
|
96
|
+
# @return [Hash]
|
|
97
|
+
def update(name, schema)
|
|
98
|
+
n = name.to_s
|
|
99
|
+
start = current_monotonic_ms
|
|
100
|
+
path = Client::RequestBuilder::COLLECTIONS_PREFIX + n
|
|
101
|
+
body = schema.dup
|
|
102
|
+
|
|
103
|
+
result = with_exception_mapping(:patch, path, {}, start) do
|
|
104
|
+
typesense.collections[n].update(body)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
symbolize_keys_deep(result)
|
|
108
|
+
ensure
|
|
109
|
+
instrument(:patch, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @param name [String]
|
|
113
|
+
# @param timeout_ms [Integer, nil]
|
|
114
|
+
# @return [Hash]
|
|
115
|
+
def delete(name, timeout_ms: nil)
|
|
116
|
+
n = name.to_s
|
|
117
|
+
start = current_monotonic_ms
|
|
118
|
+
path = Client::RequestBuilder::COLLECTIONS_PREFIX + n
|
|
119
|
+
|
|
120
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
121
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
122
|
+
else
|
|
123
|
+
typesense
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
result = with_exception_mapping(:delete, path, {}, start) do
|
|
127
|
+
ts.collections[n].delete
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
symbolize_keys_deep(result)
|
|
131
|
+
rescue Errors::Api => error
|
|
132
|
+
return { status: 404 } if error.status.to_i == 404
|
|
133
|
+
|
|
134
|
+
raise
|
|
135
|
+
ensure
|
|
136
|
+
instrument(:delete, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Array<Hash>]
|
|
140
|
+
def list(timeout_ms: nil)
|
|
141
|
+
start = current_monotonic_ms
|
|
142
|
+
path = Client::RequestBuilder::COLLECTIONS_ROOT
|
|
143
|
+
|
|
144
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
145
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
146
|
+
else
|
|
147
|
+
typesense
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
151
|
+
ts.collections.retrieve
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
symbolize_keys_deep(result)
|
|
155
|
+
ensure
|
|
156
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Client
|
|
5
|
+
module Services
|
|
6
|
+
# Document-level operations (import, CRUD, bulk updates).
|
|
7
|
+
class Documents < Base
|
|
8
|
+
# @param collection [String]
|
|
9
|
+
# @param jsonl [String]
|
|
10
|
+
# @param action [Symbol, String]
|
|
11
|
+
# @return [Object]
|
|
12
|
+
def import(collection:, jsonl:, action: :upsert)
|
|
13
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
14
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
15
|
+
end
|
|
16
|
+
raise Errors::InvalidParams, 'jsonl must be a String' unless jsonl.is_a?(String)
|
|
17
|
+
|
|
18
|
+
ts = typesense_for_import
|
|
19
|
+
start = current_monotonic_ms
|
|
20
|
+
path = documents_path(collection)
|
|
21
|
+
|
|
22
|
+
result = with_exception_mapping(:post, path, {}, start) do
|
|
23
|
+
ts.collections[collection].documents.import(jsonl, action: action.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
instrument(:post, path, current_monotonic_ms - start, {})
|
|
27
|
+
result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param collection [String]
|
|
31
|
+
# @param filter_by [String]
|
|
32
|
+
# @param timeout_ms [Integer, nil]
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def delete_by_filter(collection:, filter_by:, timeout_ms: nil)
|
|
35
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
36
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
37
|
+
end
|
|
38
|
+
unless filter_by.is_a?(String) && !filter_by.strip.empty?
|
|
39
|
+
raise Errors::InvalidParams, 'filter_by must be a non-empty String'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
43
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
44
|
+
else
|
|
45
|
+
typesense
|
|
46
|
+
end
|
|
47
|
+
start = current_monotonic_ms
|
|
48
|
+
path = documents_path(collection)
|
|
49
|
+
|
|
50
|
+
result = with_exception_mapping(:delete, path, {}, start) do
|
|
51
|
+
ts.collections[collection].documents.delete(filter_by: filter_by)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
instrument(:delete, path, current_monotonic_ms - start, {})
|
|
55
|
+
symbolize_keys_deep(result)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param collection [String]
|
|
59
|
+
# @param id [String, #to_s]
|
|
60
|
+
# @param timeout_ms [Integer, nil]
|
|
61
|
+
# @return [Hash, nil]
|
|
62
|
+
def delete(collection:, id:, timeout_ms: nil)
|
|
63
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
64
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
s = id.to_s
|
|
68
|
+
raise Errors::InvalidParams, 'id must be a non-empty String' if s.strip.empty?
|
|
69
|
+
|
|
70
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
71
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
72
|
+
else
|
|
73
|
+
typesense
|
|
74
|
+
end
|
|
75
|
+
start = current_monotonic_ms
|
|
76
|
+
path = document_member_path(collection, s)
|
|
77
|
+
|
|
78
|
+
result = with_exception_mapping(:delete, path, {}, start) do
|
|
79
|
+
ts.collections[collection].documents[s].delete
|
|
80
|
+
end
|
|
81
|
+
symbolize_keys_deep(result)
|
|
82
|
+
rescue Errors::Api => error
|
|
83
|
+
return nil if error.status.to_i == 404
|
|
84
|
+
|
|
85
|
+
raise
|
|
86
|
+
ensure
|
|
87
|
+
instrument(:delete, path, current_monotonic_ms - start, {}) if defined?(start)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param collection [String]
|
|
91
|
+
# @param id [String, #to_s]
|
|
92
|
+
# @param fields [Hash]
|
|
93
|
+
# @param timeout_ms [Integer, nil]
|
|
94
|
+
# @return [Hash]
|
|
95
|
+
def update(collection:, id:, fields:, timeout_ms: nil)
|
|
96
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
97
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
s = id.to_s
|
|
101
|
+
raise Errors::InvalidParams, 'id must be a non-empty String' if s.strip.empty?
|
|
102
|
+
raise Errors::InvalidParams, 'fields must be a Hash' unless fields.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
105
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
106
|
+
else
|
|
107
|
+
typesense
|
|
108
|
+
end
|
|
109
|
+
start = current_monotonic_ms
|
|
110
|
+
path = document_member_path(collection, s)
|
|
111
|
+
|
|
112
|
+
result = with_exception_mapping(:patch, path, {}, start) do
|
|
113
|
+
ts.collections[collection].documents[s].update(fields)
|
|
114
|
+
end
|
|
115
|
+
instrument(:patch, path, current_monotonic_ms - start, {})
|
|
116
|
+
symbolize_keys_deep(result)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @param collection [String]
|
|
120
|
+
# @param filter_by [String]
|
|
121
|
+
# @param fields [Hash]
|
|
122
|
+
# @param timeout_ms [Integer, nil]
|
|
123
|
+
# @return [Hash]
|
|
124
|
+
def update_by_filter(collection:, filter_by:, fields:, timeout_ms: nil)
|
|
125
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
126
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
127
|
+
end
|
|
128
|
+
unless filter_by.is_a?(String) && !filter_by.strip.empty?
|
|
129
|
+
raise Errors::InvalidParams, 'filter_by must be a non-empty String'
|
|
130
|
+
end
|
|
131
|
+
raise Errors::InvalidParams, 'fields must be a Hash' unless fields.is_a?(Hash)
|
|
132
|
+
|
|
133
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
134
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
135
|
+
else
|
|
136
|
+
typesense
|
|
137
|
+
end
|
|
138
|
+
start = current_monotonic_ms
|
|
139
|
+
path = documents_path(collection)
|
|
140
|
+
|
|
141
|
+
result = with_exception_mapping(:patch, path, {}, start) do
|
|
142
|
+
ts.collections[collection].documents.update(fields, filter_by: filter_by)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
instrument(:patch, path, current_monotonic_ms - start, {})
|
|
146
|
+
symbolize_keys_deep(result)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @param collection [String]
|
|
150
|
+
# @param document [Hash]
|
|
151
|
+
# @return [Hash]
|
|
152
|
+
def create(collection:, document:)
|
|
153
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
154
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
155
|
+
end
|
|
156
|
+
raise Errors::InvalidParams, 'document must be a Hash' unless document.is_a?(Hash)
|
|
157
|
+
|
|
158
|
+
start = current_monotonic_ms
|
|
159
|
+
path = documents_path(collection)
|
|
160
|
+
|
|
161
|
+
result = with_exception_mapping(:post, path, {}, start) do
|
|
162
|
+
typesense.collections[collection].documents.create(document)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
instrument(:post, path, current_monotonic_ms - start, {})
|
|
166
|
+
symbolize_keys_deep(result)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Retrieve a single document by id.
|
|
170
|
+
# @param collection [String]
|
|
171
|
+
# @param id [String, #to_s]
|
|
172
|
+
# @param timeout_ms [Integer, nil]
|
|
173
|
+
# @return [Hash, nil] document hash or nil when 404
|
|
174
|
+
def retrieve(collection:, id:, timeout_ms: nil)
|
|
175
|
+
unless collection.is_a?(String) && !collection.strip.empty?
|
|
176
|
+
raise Errors::InvalidParams, 'collection must be a non-empty String'
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
s = id.to_s
|
|
180
|
+
raise Errors::InvalidParams, 'id must be a non-empty String' if s.strip.empty?
|
|
181
|
+
|
|
182
|
+
ts = if timeout_ms&.to_i&.positive?
|
|
183
|
+
build_typesense_client_with_read_timeout(timeout_ms.to_i / 1000.0)
|
|
184
|
+
else
|
|
185
|
+
typesense
|
|
186
|
+
end
|
|
187
|
+
start = current_monotonic_ms
|
|
188
|
+
path = document_member_path(collection, s)
|
|
189
|
+
|
|
190
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
191
|
+
ts.collections[collection].documents[s].retrieve
|
|
192
|
+
end
|
|
193
|
+
symbolize_keys_deep(result)
|
|
194
|
+
rescue Errors::Api => error
|
|
195
|
+
return nil if error.status.to_i == 404
|
|
196
|
+
|
|
197
|
+
raise
|
|
198
|
+
ensure
|
|
199
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {}) if defined?(start)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def documents_path(collection)
|
|
205
|
+
Client::RequestBuilder::COLLECTIONS_PREFIX + collection.to_s + Client::RequestBuilder::DOCUMENTS_SUFFIX
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def document_member_path(collection, id)
|
|
209
|
+
documents_path(collection) + "/#{id}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SearchEngine
|
|
4
|
+
class Client
|
|
5
|
+
module Services
|
|
6
|
+
# Operational endpoints (health checks, cache management, API keys).
|
|
7
|
+
class Operations < Base
|
|
8
|
+
def health
|
|
9
|
+
start = current_monotonic_ms
|
|
10
|
+
path = Client::RequestBuilder::HEALTH_PATH
|
|
11
|
+
|
|
12
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
13
|
+
typesense.health.retrieve
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
symbolize_keys_deep(result)
|
|
17
|
+
ensure
|
|
18
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list_api_keys
|
|
22
|
+
start = current_monotonic_ms
|
|
23
|
+
path = '/keys'
|
|
24
|
+
|
|
25
|
+
result = with_exception_mapping(:get, path, {}, start) do
|
|
26
|
+
res = begin
|
|
27
|
+
typesense.keys.retrieve
|
|
28
|
+
rescue NoMethodError
|
|
29
|
+
typesense.keys.list
|
|
30
|
+
end
|
|
31
|
+
if res.is_a?(Hash)
|
|
32
|
+
Array(res[:keys] || res['keys'])
|
|
33
|
+
else
|
|
34
|
+
Array(res)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
symbolize_keys_deep(result)
|
|
39
|
+
ensure
|
|
40
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clear_cache
|
|
44
|
+
start = current_monotonic_ms
|
|
45
|
+
path = '/operations/cache/clear'
|
|
46
|
+
|
|
47
|
+
result = with_exception_mapping(:post, path, {}, start) do
|
|
48
|
+
typesense.operations.perform('cache/clear')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
instrument(:post, path, current_monotonic_ms - start, {})
|
|
52
|
+
symbolize_keys_deep(result)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Return raw cluster metrics from Typesense.
|
|
56
|
+
#
|
|
57
|
+
# Exposes the `/metrics.json` endpoint without symbolizing or coercing keys.
|
|
58
|
+
# When the upstream client does not expose a dedicated endpoint, falls back
|
|
59
|
+
# to a direct HTTP GET using the configured host/port/protocol and API key.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] raw JSON object returned by Typesense `/metrics.json`
|
|
62
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#metrics`
|
|
63
|
+
def metrics
|
|
64
|
+
start = current_monotonic_ms
|
|
65
|
+
path = '/metrics.json'
|
|
66
|
+
|
|
67
|
+
with_exception_mapping(:get, path, {}, start) do
|
|
68
|
+
ts = typesense
|
|
69
|
+
if ts.respond_to?(:metrics) && ts.metrics.respond_to?(:retrieve)
|
|
70
|
+
ts.metrics.retrieve
|
|
71
|
+
elsif ts.respond_to?(:operations) && ts.operations.respond_to?(:perform)
|
|
72
|
+
ts.operations.perform('metrics.json')
|
|
73
|
+
else
|
|
74
|
+
http_get_json(path)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
ensure
|
|
78
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Return raw server statistics from Typesense.
|
|
82
|
+
#
|
|
83
|
+
# Exposes the `/stats.json` endpoint without symbolizing or coercing keys.
|
|
84
|
+
# When the upstream client does not expose a dedicated endpoint, falls back
|
|
85
|
+
# to a direct HTTP GET using the configured host/port/protocol and API key.
|
|
86
|
+
#
|
|
87
|
+
# @return [Hash] raw JSON object returned by Typesense `/stats.json`
|
|
88
|
+
# @see `https://typesense.org/docs/latest/api/cluster-operations.html#stats`
|
|
89
|
+
def stats
|
|
90
|
+
start = current_monotonic_ms
|
|
91
|
+
path = '/stats.json'
|
|
92
|
+
|
|
93
|
+
with_exception_mapping(:get, path, {}, start) do
|
|
94
|
+
ts = typesense
|
|
95
|
+
if ts.respond_to?(:stats) && ts.stats.respond_to?(:retrieve)
|
|
96
|
+
ts.stats.retrieve
|
|
97
|
+
elsif ts.respond_to?(:operations) && ts.operations.respond_to?(:perform)
|
|
98
|
+
ts.operations.perform('stats.json')
|
|
99
|
+
else
|
|
100
|
+
http_get_json(path)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
ensure
|
|
104
|
+
instrument(:get, path, (start ? (current_monotonic_ms - start) : 0.0), {})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def http_get_json(path)
|
|
110
|
+
require 'net/http'
|
|
111
|
+
require 'uri'
|
|
112
|
+
require 'json'
|
|
113
|
+
|
|
114
|
+
proto = begin
|
|
115
|
+
config.protocol.to_s.strip
|
|
116
|
+
rescue StandardError
|
|
117
|
+
'http'
|
|
118
|
+
end
|
|
119
|
+
proto = proto.nil? || proto.empty? ? 'http' : proto
|
|
120
|
+
|
|
121
|
+
host = config.host
|
|
122
|
+
port = config.port
|
|
123
|
+
uri = URI.parse("#{proto}://#{host}:#{port}#{path}")
|
|
124
|
+
|
|
125
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
126
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
127
|
+
|
|
128
|
+
timeout_s = begin
|
|
129
|
+
t = config.timeout_ms.to_i
|
|
130
|
+
t.positive? ? (t / 1000.0) : 2.0
|
|
131
|
+
rescue StandardError
|
|
132
|
+
2.0
|
|
133
|
+
end
|
|
134
|
+
http.open_timeout = timeout_s
|
|
135
|
+
http.read_timeout = timeout_s
|
|
136
|
+
|
|
137
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
138
|
+
req['X-TYPESENSE-API-KEY'] = config.api_key
|
|
139
|
+
|
|
140
|
+
res = http.request(req)
|
|
141
|
+
code = res.code.to_i
|
|
142
|
+
body = res.body.to_s
|
|
143
|
+
unless code >= 200 && code < 300
|
|
144
|
+
raise SearchEngine::Errors::Api.new("typesense api error: #{code}", status: code, body: body)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
JSON.parse(body)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|