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.
Files changed (139) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +148 -0
  4. data/app/search_engine/search_engine/app_info.rb +11 -0
  5. data/app/search_engine/search_engine/index_partition_job.rb +170 -0
  6. data/lib/generators/search_engine/install/install_generator.rb +20 -0
  7. data/lib/generators/search_engine/install/templates/initializer.rb.tt +230 -0
  8. data/lib/generators/search_engine/model/model_generator.rb +86 -0
  9. data/lib/generators/search_engine/model/templates/model.rb.tt +12 -0
  10. data/lib/search-engine-for-typesense.rb +12 -0
  11. data/lib/search_engine/active_record_syncable.rb +247 -0
  12. data/lib/search_engine/admin/stopwords.rb +125 -0
  13. data/lib/search_engine/admin/synonyms.rb +125 -0
  14. data/lib/search_engine/admin.rb +12 -0
  15. data/lib/search_engine/ast/and.rb +52 -0
  16. data/lib/search_engine/ast/binary_op.rb +75 -0
  17. data/lib/search_engine/ast/eq.rb +19 -0
  18. data/lib/search_engine/ast/group.rb +18 -0
  19. data/lib/search_engine/ast/gt.rb +12 -0
  20. data/lib/search_engine/ast/gte.rb +12 -0
  21. data/lib/search_engine/ast/in.rb +28 -0
  22. data/lib/search_engine/ast/lt.rb +12 -0
  23. data/lib/search_engine/ast/lte.rb +12 -0
  24. data/lib/search_engine/ast/matches.rb +55 -0
  25. data/lib/search_engine/ast/node.rb +176 -0
  26. data/lib/search_engine/ast/not_eq.rb +13 -0
  27. data/lib/search_engine/ast/not_in.rb +24 -0
  28. data/lib/search_engine/ast/or.rb +52 -0
  29. data/lib/search_engine/ast/prefix.rb +51 -0
  30. data/lib/search_engine/ast/raw.rb +41 -0
  31. data/lib/search_engine/ast/unary_op.rb +43 -0
  32. data/lib/search_engine/ast.rb +101 -0
  33. data/lib/search_engine/base/creation.rb +727 -0
  34. data/lib/search_engine/base/deletion.rb +80 -0
  35. data/lib/search_engine/base/display_coercions.rb +36 -0
  36. data/lib/search_engine/base/hydration.rb +312 -0
  37. data/lib/search_engine/base/index_maintenance/cleanup.rb +202 -0
  38. data/lib/search_engine/base/index_maintenance/lifecycle.rb +251 -0
  39. data/lib/search_engine/base/index_maintenance/schema.rb +117 -0
  40. data/lib/search_engine/base/index_maintenance.rb +459 -0
  41. data/lib/search_engine/base/indexing_dsl.rb +255 -0
  42. data/lib/search_engine/base/joins.rb +479 -0
  43. data/lib/search_engine/base/model_dsl.rb +472 -0
  44. data/lib/search_engine/base/presets.rb +43 -0
  45. data/lib/search_engine/base/pretty_printer.rb +315 -0
  46. data/lib/search_engine/base/relation_delegation.rb +42 -0
  47. data/lib/search_engine/base/scopes.rb +113 -0
  48. data/lib/search_engine/base/updating.rb +92 -0
  49. data/lib/search_engine/base.rb +38 -0
  50. data/lib/search_engine/bulk.rb +284 -0
  51. data/lib/search_engine/cache.rb +33 -0
  52. data/lib/search_engine/cascade.rb +531 -0
  53. data/lib/search_engine/cli/doctor.rb +631 -0
  54. data/lib/search_engine/cli/support.rb +217 -0
  55. data/lib/search_engine/cli.rb +222 -0
  56. data/lib/search_engine/client/http_adapter.rb +63 -0
  57. data/lib/search_engine/client/request_builder.rb +92 -0
  58. data/lib/search_engine/client/services/base.rb +74 -0
  59. data/lib/search_engine/client/services/collections.rb +161 -0
  60. data/lib/search_engine/client/services/documents.rb +214 -0
  61. data/lib/search_engine/client/services/operations.rb +152 -0
  62. data/lib/search_engine/client/services/search.rb +190 -0
  63. data/lib/search_engine/client/services.rb +29 -0
  64. data/lib/search_engine/client.rb +765 -0
  65. data/lib/search_engine/client_options.rb +20 -0
  66. data/lib/search_engine/collection_resolver.rb +191 -0
  67. data/lib/search_engine/collections_graph.rb +330 -0
  68. data/lib/search_engine/compiled_params.rb +143 -0
  69. data/lib/search_engine/compiler.rb +383 -0
  70. data/lib/search_engine/config/observability.rb +27 -0
  71. data/lib/search_engine/config/presets.rb +92 -0
  72. data/lib/search_engine/config/selection.rb +16 -0
  73. data/lib/search_engine/config/typesense.rb +48 -0
  74. data/lib/search_engine/config/validators.rb +97 -0
  75. data/lib/search_engine/config.rb +917 -0
  76. data/lib/search_engine/console_helpers.rb +130 -0
  77. data/lib/search_engine/deletion.rb +103 -0
  78. data/lib/search_engine/dispatcher.rb +125 -0
  79. data/lib/search_engine/dsl/parser.rb +582 -0
  80. data/lib/search_engine/engine.rb +167 -0
  81. data/lib/search_engine/errors.rb +290 -0
  82. data/lib/search_engine/filters/sanitizer.rb +189 -0
  83. data/lib/search_engine/hydration/materializers.rb +808 -0
  84. data/lib/search_engine/hydration/selection_context.rb +96 -0
  85. data/lib/search_engine/indexer/batch_planner.rb +76 -0
  86. data/lib/search_engine/indexer/bulk_import.rb +626 -0
  87. data/lib/search_engine/indexer/import_dispatcher.rb +198 -0
  88. data/lib/search_engine/indexer/retry_policy.rb +103 -0
  89. data/lib/search_engine/indexer.rb +747 -0
  90. data/lib/search_engine/instrumentation.rb +308 -0
  91. data/lib/search_engine/joins/guard.rb +202 -0
  92. data/lib/search_engine/joins/resolver.rb +95 -0
  93. data/lib/search_engine/logging/color.rb +78 -0
  94. data/lib/search_engine/logging/format_helpers.rb +92 -0
  95. data/lib/search_engine/logging/partition_progress.rb +53 -0
  96. data/lib/search_engine/logging_subscriber.rb +388 -0
  97. data/lib/search_engine/mapper.rb +785 -0
  98. data/lib/search_engine/multi.rb +286 -0
  99. data/lib/search_engine/multi_result.rb +186 -0
  100. data/lib/search_engine/notifications/compact_logger.rb +675 -0
  101. data/lib/search_engine/observability.rb +162 -0
  102. data/lib/search_engine/operations.rb +58 -0
  103. data/lib/search_engine/otel.rb +227 -0
  104. data/lib/search_engine/partitioner.rb +128 -0
  105. data/lib/search_engine/ranking_plan.rb +118 -0
  106. data/lib/search_engine/registry.rb +158 -0
  107. data/lib/search_engine/relation/compiler.rb +711 -0
  108. data/lib/search_engine/relation/deletion.rb +37 -0
  109. data/lib/search_engine/relation/dsl/filters.rb +624 -0
  110. data/lib/search_engine/relation/dsl/selection.rb +240 -0
  111. data/lib/search_engine/relation/dsl.rb +903 -0
  112. data/lib/search_engine/relation/dx/dry_run.rb +59 -0
  113. data/lib/search_engine/relation/dx/friendly_where.rb +24 -0
  114. data/lib/search_engine/relation/dx.rb +231 -0
  115. data/lib/search_engine/relation/materializers.rb +118 -0
  116. data/lib/search_engine/relation/options.rb +138 -0
  117. data/lib/search_engine/relation/state.rb +274 -0
  118. data/lib/search_engine/relation/updating.rb +44 -0
  119. data/lib/search_engine/relation.rb +623 -0
  120. data/lib/search_engine/result.rb +664 -0
  121. data/lib/search_engine/schema.rb +1083 -0
  122. data/lib/search_engine/sources/active_record_source.rb +185 -0
  123. data/lib/search_engine/sources/base.rb +62 -0
  124. data/lib/search_engine/sources/lambda_source.rb +55 -0
  125. data/lib/search_engine/sources/sql_source.rb +196 -0
  126. data/lib/search_engine/sources.rb +71 -0
  127. data/lib/search_engine/stale_rules.rb +160 -0
  128. data/lib/search_engine/test/minitest_assertions.rb +57 -0
  129. data/lib/search_engine/test/offline_client.rb +134 -0
  130. data/lib/search_engine/test/rspec_matchers.rb +77 -0
  131. data/lib/search_engine/test/stub_client.rb +201 -0
  132. data/lib/search_engine/test.rb +66 -0
  133. data/lib/search_engine/test_autoload.rb +8 -0
  134. data/lib/search_engine/update.rb +35 -0
  135. data/lib/search_engine/version.rb +7 -0
  136. data/lib/search_engine.rb +332 -0
  137. data/lib/tasks/search_engine.rake +501 -0
  138. data/lib/tasks/search_engine_doctor.rake +16 -0
  139. 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