noiseless 0.0.0 → 0.1.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +28 -0
  3. data/README.md +214 -0
  4. data/lib/application_search.rb +15 -0
  5. data/lib/noiseless/adapter.rb +313 -0
  6. data/lib/noiseless/adapters/elasticsearch.rb +70 -0
  7. data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +188 -0
  8. data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +377 -0
  9. data/lib/noiseless/adapters/execution_modules/pgvector_support.rb +219 -0
  10. data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +461 -0
  11. data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +472 -0
  12. data/lib/noiseless/adapters/open_search.rb +208 -0
  13. data/lib/noiseless/adapters/postgresql.rb +171 -0
  14. data/lib/noiseless/adapters/typesense.rb +70 -0
  15. data/lib/noiseless/adapters.rb +14 -0
  16. data/lib/noiseless/ast/aggregation.rb +56 -0
  17. data/lib/noiseless/ast/bool.rb +16 -0
  18. data/lib/noiseless/ast/bulk.rb +18 -0
  19. data/lib/noiseless/ast/collapse.rb +16 -0
  20. data/lib/noiseless/ast/combined_fields.rb +33 -0
  21. data/lib/noiseless/ast/conversation.rb +29 -0
  22. data/lib/noiseless/ast/filter.rb +15 -0
  23. data/lib/noiseless/ast/hybrid.rb +35 -0
  24. data/lib/noiseless/ast/image_query.rb +29 -0
  25. data/lib/noiseless/ast/join.rb +31 -0
  26. data/lib/noiseless/ast/match.rb +15 -0
  27. data/lib/noiseless/ast/multi_match.rb +24 -0
  28. data/lib/noiseless/ast/paginate.rb +15 -0
  29. data/lib/noiseless/ast/prefix.rb +15 -0
  30. data/lib/noiseless/ast/range.rb +18 -0
  31. data/lib/noiseless/ast/root.rb +69 -0
  32. data/lib/noiseless/ast/search_after.rb +14 -0
  33. data/lib/noiseless/ast/sort.rb +15 -0
  34. data/lib/noiseless/ast/vector.rb +27 -0
  35. data/lib/noiseless/ast/wildcard.rb +15 -0
  36. data/lib/noiseless/ast.rb +30 -0
  37. data/lib/noiseless/bulk_importer.rb +195 -0
  38. data/lib/noiseless/callbacks.rb +138 -0
  39. data/lib/noiseless/connection_manager.rb +26 -0
  40. data/lib/noiseless/document_manager.rb +137 -0
  41. data/lib/noiseless/dsl.rb +107 -0
  42. data/lib/noiseless/generators/application_search_generator.rb +24 -0
  43. data/lib/noiseless/instrumentation.rb +174 -0
  44. data/lib/noiseless/introspection/console.rb +228 -0
  45. data/lib/noiseless/introspection/query_visualizer.rb +533 -0
  46. data/lib/noiseless/introspection.rb +221 -0
  47. data/lib/noiseless/mapping.rb +253 -0
  48. data/lib/noiseless/mapping_definition_processor.rb +231 -0
  49. data/lib/noiseless/model.rb +111 -0
  50. data/lib/noiseless/model_registry.rb +77 -0
  51. data/lib/noiseless/multi_search.rb +244 -0
  52. data/lib/noiseless/pagination.rb +375 -0
  53. data/lib/noiseless/query_builder.rb +284 -0
  54. data/lib/noiseless/railtie.rb +35 -0
  55. data/lib/noiseless/response/aggregations.rb +46 -0
  56. data/lib/noiseless/response/empty.rb +20 -0
  57. data/lib/noiseless/response/records.rb +94 -0
  58. data/lib/noiseless/response/results.rb +110 -0
  59. data/lib/noiseless/response/suggestions.rb +55 -0
  60. data/lib/noiseless/response.rb +98 -0
  61. data/lib/noiseless/response_factory.rb +32 -0
  62. data/lib/noiseless/runtime_reset_middleware.rb +15 -0
  63. data/lib/noiseless/search_index_update_job.rb +84 -0
  64. data/lib/noiseless/test_case.rb +230 -0
  65. data/lib/noiseless/test_helper.rb +295 -0
  66. data/lib/noiseless/version.rb +2 -2
  67. data/lib/noiseless.rb +130 -2
  68. data/lib/tasks/benchmark.rake +35 -0
  69. data/lib/tasks/release.rake +22 -0
  70. data/lib/tasks/test.rake +11 -0
  71. metadata +260 -14
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Noiseless
6
+ module Adapters
7
+ module ExecutionModules
8
+ module ElasticsearchExecution
9
+ def close
10
+ @clients&.each_value(&:close)
11
+ end
12
+
13
+ private
14
+
15
+ def execute_search(query_hash, indexes: [], **_opts)
16
+ path = indexes.any? ? "/#{indexes.join(',')}/_search" : "/_search"
17
+ body = JSON.generate(query_hash)
18
+
19
+ response = post_request(path, body)
20
+ JSON.parse(response.read)
21
+ ensure
22
+ response&.close
23
+ end
24
+
25
+ def execute_bulk(actions, **_opts)
26
+ body = "#{actions.map { |action| JSON.generate(action) }.join("\n")}\n"
27
+
28
+ response = post_request("/_bulk", body, content_type: "application/x-ndjson")
29
+ JSON.parse(response.read)
30
+ ensure
31
+ response&.close
32
+ end
33
+
34
+ def execute_create_index(index_name, mappings: nil, settings: nil, **_opts)
35
+ body = {}
36
+ body[:mappings] = mappings if mappings
37
+ body[:settings] = settings if settings
38
+
39
+ response = put_request("/#{index_name}", body.any? ? JSON.generate(body) : nil)
40
+ JSON.parse(response.read)
41
+ ensure
42
+ response&.close
43
+ end
44
+
45
+ def execute_delete_index(index_name, **_opts)
46
+ response = delete_request("/#{index_name}")
47
+ JSON.parse(response.read)
48
+ ensure
49
+ response&.close
50
+ end
51
+
52
+ def execute_refresh_index(index_name)
53
+ response = post_request("/#{index_name}/_refresh", nil)
54
+ JSON.parse(response.read)
55
+ rescue StandardError => e
56
+ {
57
+ "_shards" => {
58
+ "total" => 0,
59
+ "successful" => 0,
60
+ "failed" => 0
61
+ },
62
+ "error" => {
63
+ "type" => e.class.name,
64
+ "reason" => e.message
65
+ }
66
+ }
67
+ ensure
68
+ response&.close
69
+ end
70
+
71
+ def execute_index_exists?(index_name)
72
+ response = head_request("/#{index_name}")
73
+ response.success?
74
+ rescue StandardError
75
+ false
76
+ ensure
77
+ response&.close
78
+ end
79
+
80
+ def execute_index_document(index, id, document, **_opts)
81
+ path = id ? "/#{index}/_doc/#{id}" : "/#{index}/_doc"
82
+ body = JSON.generate(document)
83
+
84
+ response = id ? put_request(path, body) : post_request(path, body)
85
+ JSON.parse(response.read)
86
+ ensure
87
+ response&.close
88
+ end
89
+
90
+ def execute_update_document(index, id, changes, **_opts)
91
+ body = JSON.generate(doc: changes)
92
+
93
+ response = post_request("/#{index}/_update/#{id}", body)
94
+ JSON.parse(response.read)
95
+ ensure
96
+ response&.close
97
+ end
98
+
99
+ def execute_delete_document(index, id, **_opts)
100
+ response = delete_request("/#{index}/_doc/#{id}")
101
+ JSON.parse(response.read)
102
+ ensure
103
+ response&.close
104
+ end
105
+
106
+ def execute_document_exists?(index, id)
107
+ response = head_request("/#{index}/_doc/#{id}")
108
+ response.success?
109
+ rescue StandardError
110
+ false
111
+ ensure
112
+ response&.close
113
+ end
114
+
115
+ def execute_cluster_health(**_opts)
116
+ response = get_request("/_cluster/health")
117
+ JSON.parse(response.read)
118
+ rescue StandardError => e
119
+ {
120
+ "cluster_name" => "unknown",
121
+ "status" => "red",
122
+ "timed_out" => false,
123
+ "number_of_nodes" => 0,
124
+ "number_of_data_nodes" => 0,
125
+ "active_primary_shards" => 0,
126
+ "active_shards" => 0,
127
+ "error" => {
128
+ "type" => e.class.name,
129
+ "reason" => e.message
130
+ }
131
+ }
132
+ ensure
133
+ response&.close
134
+ end
135
+
136
+ # HTTP helpers using Async::HTTP with connection pooling
137
+ def get_request(path)
138
+ with_client do |client|
139
+ client.get(path, default_headers)
140
+ end
141
+ end
142
+
143
+ def post_request(path, body, content_type: "application/json")
144
+ headers = body ? default_headers + [["content-type", content_type]] : default_headers
145
+
146
+ with_client do |client|
147
+ client.post(path, headers, body)
148
+ end
149
+ end
150
+
151
+ def put_request(path, body, content_type: "application/json")
152
+ headers = body ? default_headers + [["content-type", content_type]] : default_headers
153
+
154
+ with_client do |client|
155
+ client.put(path, headers, body)
156
+ end
157
+ end
158
+
159
+ def delete_request(path)
160
+ with_client do |client|
161
+ client.delete(path, default_headers)
162
+ end
163
+ end
164
+
165
+ def head_request(path)
166
+ with_client do |client|
167
+ client.head(path, default_headers)
168
+ end
169
+ end
170
+
171
+ def with_client
172
+ # Select a random host for load balancing
173
+ host = @hosts.sample
174
+ client = @clients[host]
175
+
176
+ yield(client)
177
+ end
178
+
179
+ def default_headers
180
+ [
181
+ ["accept", "application/json"],
182
+ ["user-agent", "Noiseless/#{Noiseless::VERSION} (Ruby/#{RUBY_VERSION})"]
183
+ ]
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Noiseless
6
+ module Adapters
7
+ module ExecutionModules
8
+ module OpensearchExecution
9
+ def close
10
+ @clients&.each_value(&:close)
11
+ end
12
+
13
+ private
14
+
15
+ def execute_search(query_hash, indexes: [], **_opts)
16
+ index_path = indexes.any? ? indexes.join(",") : "_all"
17
+ path = "/#{index_path}/_search"
18
+ body = JSON.generate(query_hash)
19
+
20
+ response = post_request(path, body)
21
+ JSON.parse(response.read)
22
+ rescue StandardError => e
23
+ # Return empty response on error to maintain compatibility
24
+ {
25
+ took: 0,
26
+ timed_out: false,
27
+ _shards: { total: 0, successful: 0, skipped: 0, failed: 0 },
28
+ hits: {
29
+ total: { value: 0, relation: "eq" },
30
+ max_score: nil,
31
+ hits: []
32
+ },
33
+ error: {
34
+ type: e.class.name,
35
+ reason: e.message
36
+ }
37
+ }
38
+ ensure
39
+ response&.close
40
+ end
41
+
42
+ def execute_bulk(actions, **_opts)
43
+ # Build bulk request body
44
+ bulk_body = actions.map do |action|
45
+ if action[:index]
46
+ action_line = { index: { _index: action[:index][:_index], _id: action[:index][:_id] } }
47
+ data_line = action[:index][:data]
48
+ "#{JSON.generate(action_line)}\n#{JSON.generate(data_line)}\n"
49
+ else
50
+ "#{JSON.generate(action)}\n"
51
+ end
52
+ end.join
53
+
54
+ response = post_request("/_bulk", bulk_body, content_type: "application/x-ndjson")
55
+ JSON.parse(response.read)
56
+ rescue StandardError => e
57
+ { items: [], errors: true, error: { type: e.class.name, reason: e.message } }
58
+ ensure
59
+ response&.close
60
+ end
61
+
62
+ def execute_create_index(index_name, mappings: nil, settings: nil, **opts)
63
+ body = opts.dup
64
+ body[:mappings] = mappings if mappings
65
+ body[:settings] = settings if settings
66
+
67
+ response = put_request("/#{index_name}", body.any? ? JSON.generate(body) : nil)
68
+ JSON.parse(response.read)
69
+ rescue StandardError => e
70
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
71
+ ensure
72
+ response&.close
73
+ end
74
+
75
+ def execute_delete_index(index_name, **_opts)
76
+ response = delete_request("/#{index_name}")
77
+ JSON.parse(response.read)
78
+ rescue StandardError => e
79
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
80
+ ensure
81
+ response&.close
82
+ end
83
+
84
+ def execute_refresh_index(index_name)
85
+ response = post_request("/#{index_name}/_refresh", nil)
86
+ JSON.parse(response.read)
87
+ rescue StandardError => e
88
+ {
89
+ "_shards" => {
90
+ "total" => 0,
91
+ "successful" => 0,
92
+ "failed" => 0
93
+ },
94
+ "error" => {
95
+ "type" => e.class.name,
96
+ "reason" => e.message
97
+ }
98
+ }
99
+ ensure
100
+ response&.close
101
+ end
102
+
103
+ def execute_index_exists?(index_name)
104
+ response = head_request("/#{index_name}")
105
+ response.success?
106
+ rescue StandardError
107
+ false
108
+ ensure
109
+ response&.close
110
+ end
111
+
112
+ def execute_index_document(index, id, document, **_opts)
113
+ path = "/#{index}/_doc/#{id}"
114
+ body = JSON.generate(document)
115
+
116
+ response = put_request(path, body)
117
+ JSON.parse(response.read)
118
+ rescue StandardError => e
119
+ { _index: index, _id: id, result: "error", error: { type: e.class.name, reason: e.message } }
120
+ ensure
121
+ response&.close
122
+ end
123
+
124
+ def execute_update_document(index, id, changes, **_opts)
125
+ body = JSON.generate(doc: changes)
126
+
127
+ response = post_request("/#{index}/_update/#{id}", body)
128
+ JSON.parse(response.read)
129
+ rescue StandardError => e
130
+ { _index: index, _id: id, result: "error", error: { type: e.class.name, reason: e.message } }
131
+ ensure
132
+ response&.close
133
+ end
134
+
135
+ def execute_delete_document(index, id, **_opts)
136
+ response = delete_request("/#{index}/_doc/#{id}")
137
+ JSON.parse(response.read)
138
+ rescue StandardError => e
139
+ { _index: index, _id: id, result: "error", error: { type: e.class.name, reason: e.message } }
140
+ ensure
141
+ response&.close
142
+ end
143
+
144
+ def execute_document_exists?(index, id)
145
+ response = head_request("/#{index}/_doc/#{id}")
146
+ response.success?
147
+ rescue StandardError
148
+ false
149
+ ensure
150
+ response&.close
151
+ end
152
+
153
+ def execute_cluster_health(**_opts)
154
+ response = get_request("/_cluster/health")
155
+ JSON.parse(response.read)
156
+ rescue StandardError => e
157
+ {
158
+ cluster_name: "unknown",
159
+ status: "red",
160
+ timed_out: false,
161
+ number_of_nodes: 0,
162
+ number_of_data_nodes: 0,
163
+ active_primary_shards: 0,
164
+ active_shards: 0,
165
+ relocating_shards: 0,
166
+ initializing_shards: 0,
167
+ unassigned_shards: 0,
168
+ error: { type: e.class.name, reason: e.message }
169
+ }
170
+ ensure
171
+ response&.close
172
+ end
173
+
174
+ # OpenSearch-specific features
175
+ def execute_point_in_time_search(query_hash, pit_id:, **_opts)
176
+ # Point-in-time search for consistent pagination
177
+ enhanced_query = query_hash.merge(pit: { id: pit_id })
178
+ body = JSON.generate(enhanced_query)
179
+
180
+ response = post_request("/_search", body)
181
+ JSON.parse(response.read)
182
+ rescue StandardError => e
183
+ {
184
+ pit_id: pit_id,
185
+ error: { type: e.class.name, reason: e.message },
186
+ hits: { total: { value: 0 }, hits: [] }
187
+ }
188
+ ensure
189
+ response&.close
190
+ end
191
+
192
+ def execute_search_template(template_id:, params: {}, **_opts)
193
+ # OpenSearch search templates
194
+ template_query = {
195
+ id: template_id,
196
+ params: params
197
+ }
198
+ body = JSON.generate(template_query)
199
+
200
+ response = post_request("/_search/template", body)
201
+ JSON.parse(response.read)
202
+ rescue StandardError => e
203
+ {
204
+ error: { type: e.class.name, reason: e.message },
205
+ hits: { total: { value: 0 }, hits: [] }
206
+ }
207
+ ensure
208
+ response&.close
209
+ end
210
+
211
+ # ============================================
212
+ # Search Pipeline API (OpenSearch 3.x)
213
+ # ============================================
214
+
215
+ def execute_create_pipeline(name, request_processors:, response_processors:, description: nil)
216
+ body = {
217
+ description: description,
218
+ request_processors: request_processors,
219
+ response_processors: response_processors
220
+ }.compact
221
+
222
+ response = put_request("/_search/pipeline/#{name}", JSON.generate(body))
223
+ JSON.parse(response.read)
224
+ rescue StandardError => e
225
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
226
+ ensure
227
+ response&.close
228
+ end
229
+
230
+ def execute_get_pipeline(name)
231
+ response = get_request("/_search/pipeline/#{name}")
232
+ JSON.parse(response.read)
233
+ rescue StandardError => e
234
+ { error: { type: e.class.name, reason: e.message } }
235
+ ensure
236
+ response&.close
237
+ end
238
+
239
+ def execute_list_pipelines
240
+ response = get_request("/_search/pipeline")
241
+ JSON.parse(response.read)
242
+ rescue StandardError => e
243
+ { error: { type: e.class.name, reason: e.message } }
244
+ ensure
245
+ response&.close
246
+ end
247
+
248
+ def execute_delete_pipeline(name)
249
+ response = delete_request("/_search/pipeline/#{name}")
250
+ JSON.parse(response.read)
251
+ rescue StandardError => e
252
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
253
+ ensure
254
+ response&.close
255
+ end
256
+
257
+ def execute_pipeline_exists?(name)
258
+ response = head_request("/_search/pipeline/#{name}")
259
+ response.success?
260
+ rescue StandardError
261
+ false
262
+ ensure
263
+ response&.close
264
+ end
265
+
266
+ # ============================================
267
+ # Query Rules API (OpenSearch 3.x)
268
+ # ============================================
269
+
270
+ def execute_create_rule(feature_type, rule_id, attributes:, feature_value:)
271
+ body = {
272
+ match_criteria: {
273
+ query: attributes
274
+ },
275
+ feature_value: feature_value
276
+ }
277
+
278
+ response = put_request("/_rules/#{feature_type}/#{rule_id}", JSON.generate(body))
279
+ JSON.parse(response.read)
280
+ rescue StandardError => e
281
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
282
+ ensure
283
+ response&.close
284
+ end
285
+
286
+ def execute_get_rule(feature_type, rule_id)
287
+ response = get_request("/_rules/#{feature_type}/#{rule_id}")
288
+ JSON.parse(response.read)
289
+ rescue StandardError => e
290
+ { error: { type: e.class.name, reason: e.message } }
291
+ ensure
292
+ response&.close
293
+ end
294
+
295
+ def execute_list_rules(feature_type, search_after: nil)
296
+ path = "/_rules/#{feature_type}"
297
+ path += "?search_after=#{search_after}" if search_after
298
+
299
+ response = get_request(path)
300
+ JSON.parse(response.read)
301
+ rescue StandardError => e
302
+ { rules: [], error: { type: e.class.name, reason: e.message } }
303
+ ensure
304
+ response&.close
305
+ end
306
+
307
+ def execute_delete_rule(feature_type, rule_id)
308
+ response = delete_request("/_rules/#{feature_type}/#{rule_id}")
309
+ JSON.parse(response.read)
310
+ rescue StandardError => e
311
+ { acknowledged: false, error: { type: e.class.name, reason: e.message } }
312
+ ensure
313
+ response&.close
314
+ end
315
+
316
+ def execute_rule_exists?(feature_type, rule_id)
317
+ response = head_request("/_rules/#{feature_type}/#{rule_id}")
318
+ response.success?
319
+ rescue StandardError
320
+ false
321
+ ensure
322
+ response&.close
323
+ end
324
+
325
+ # HTTP helpers using Async::HTTP with connection pooling
326
+ def get_request(path)
327
+ with_client do |client|
328
+ client.get(path, default_headers)
329
+ end
330
+ end
331
+
332
+ def post_request(path, body, content_type: "application/json")
333
+ headers = body ? default_headers + [["content-type", content_type]] : default_headers
334
+
335
+ with_client do |client|
336
+ client.post(path, headers, body)
337
+ end
338
+ end
339
+
340
+ def put_request(path, body, content_type: "application/json")
341
+ headers = body ? default_headers + [["content-type", content_type]] : default_headers
342
+
343
+ with_client do |client|
344
+ client.put(path, headers, body)
345
+ end
346
+ end
347
+
348
+ def delete_request(path)
349
+ with_client do |client|
350
+ client.delete(path, default_headers)
351
+ end
352
+ end
353
+
354
+ def head_request(path)
355
+ with_client do |client|
356
+ client.head(path, default_headers)
357
+ end
358
+ end
359
+
360
+ def with_client
361
+ # Select a random host for load balancing
362
+ host = @hosts.sample
363
+ client = @clients[host]
364
+
365
+ yield(client)
366
+ end
367
+
368
+ def default_headers
369
+ [
370
+ ["accept", "application/json"],
371
+ ["user-agent", "Noiseless/#{Noiseless::VERSION} (Ruby/#{RUBY_VERSION})"]
372
+ ]
373
+ end
374
+ end
375
+ end
376
+ end
377
+ end