noiseless 0.1.0 → 0.3.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 +4 -4
- data/lib/noiseless/adapter.rb +75 -1
- data/lib/noiseless/adapters/cluster_api.rb +18 -0
- data/lib/noiseless/adapters/elasticsearch.rb +5 -45
- data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +5 -125
- data/lib/noiseless/adapters/execution_modules/es_compatible_execution.rb +91 -0
- data/lib/noiseless/adapters/execution_modules/http_transport.rb +83 -0
- data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +7 -175
- data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +3 -50
- data/lib/noiseless/adapters/indices_api.rb +26 -0
- data/lib/noiseless/adapters/open_search.rb +8 -48
- data/lib/noiseless/adapters/typesense.rb +8 -42
- data/lib/noiseless/ast/field_value_node.rb +16 -0
- data/lib/noiseless/ast/filter.rb +1 -8
- data/lib/noiseless/ast/match.rb +1 -8
- data/lib/noiseless/ast/prefix.rb +1 -8
- data/lib/noiseless/ast/wildcard.rb +1 -8
- data/lib/noiseless/version.rb +1 -1
- data/lib/noiseless.rb +17 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f66be9c1d81e3b887f0c0d4485381c4535f087f8365e2581c4c41d89cdba0934
|
|
4
|
+
data.tar.gz: 1565bffbcfb554d2b6afcc78ebb1ded79c5c58a8f157b1e25ccd9a449cd2abe2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a7e23f4eb136bc6c0dc457dcb6f267e000269aceddd6450264ac489429dda2e20c7b382b32c2ab2a91271faeccee419a5c72530e3781087a9f630e8bd16a15d
|
|
7
|
+
data.tar.gz: 605cf0980142cbe65692b9ef8d2a543969578620fa17b35e069a0344ac1a5ca5947781c6d9cf617203408a73e11cd08626c000d046e4a7a9839508919de8d463
|
data/lib/noiseless/adapter.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "async"
|
|
4
|
+
require "json"
|
|
4
5
|
require_relative "introspection"
|
|
5
6
|
|
|
6
7
|
module Noiseless
|
|
@@ -8,6 +9,17 @@ module Noiseless
|
|
|
8
9
|
include Instrumentation
|
|
9
10
|
include Introspection
|
|
10
11
|
|
|
12
|
+
GEO_FILTER_KEYS = %i[geo_distance geo_bounding_box geo_polygon geo_shape].freeze
|
|
13
|
+
RANGE_OPERATORS = %i[gte lte gt lt].freeze
|
|
14
|
+
|
|
15
|
+
# Aggregation types that require a field (or script) to execute.
|
|
16
|
+
FIELD_BASED_AGG_TYPES = %i[
|
|
17
|
+
terms rare_terms significant_terms missing
|
|
18
|
+
avg sum min max cardinality value_count stats extended_stats
|
|
19
|
+
percentiles percentile_ranks histogram date_histogram
|
|
20
|
+
geohash_grid geotile_grid geo_distance
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
11
23
|
def initialize(hosts: [], **connection_params)
|
|
12
24
|
@hosts = hosts
|
|
13
25
|
@connection_params = connection_params.dup
|
|
@@ -60,6 +72,14 @@ module Noiseless
|
|
|
60
72
|
end
|
|
61
73
|
end
|
|
62
74
|
|
|
75
|
+
def refresh_index(index_name)
|
|
76
|
+
Async do
|
|
77
|
+
instrument(:refresh_index, index: index_name) do
|
|
78
|
+
execute_refresh_index(index_name)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
63
83
|
def index_exists?(index_name)
|
|
64
84
|
Async do
|
|
65
85
|
execute_index_exists?(index_name)
|
|
@@ -188,11 +208,32 @@ module Noiseless
|
|
|
188
208
|
{
|
|
189
209
|
bool: {
|
|
190
210
|
must: must_queries,
|
|
191
|
-
filter: bool_node.filter.map { |f|
|
|
211
|
+
filter: bool_node.filter.map { |f| build_filter_clause(f) }
|
|
192
212
|
}.reject { |_, v| v.empty? }
|
|
193
213
|
}
|
|
194
214
|
end
|
|
195
215
|
|
|
216
|
+
def build_filter_clause(node)
|
|
217
|
+
value = node.value
|
|
218
|
+
case value
|
|
219
|
+
when Hash
|
|
220
|
+
keys = value.keys.map(&:to_sym)
|
|
221
|
+
if keys.intersect?(GEO_FILTER_KEYS)
|
|
222
|
+
# Geo filters are already complete query clauses, e.g.
|
|
223
|
+
# { geo_distance: { distance: "100km", location: { lat:, lon: } } }
|
|
224
|
+
value
|
|
225
|
+
elsif (keys - RANGE_OPERATORS).empty?
|
|
226
|
+
{ range: { node.field => value } }
|
|
227
|
+
else
|
|
228
|
+
{ term: { node.field => value } }
|
|
229
|
+
end
|
|
230
|
+
when Array
|
|
231
|
+
{ terms: { node.field => value } }
|
|
232
|
+
else
|
|
233
|
+
{ term: { node.field => value } }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
196
237
|
def build_sort_hash(sort_nodes)
|
|
197
238
|
return [] if sort_nodes.empty?
|
|
198
239
|
|
|
@@ -208,6 +249,31 @@ module Noiseless
|
|
|
208
249
|
}
|
|
209
250
|
end
|
|
210
251
|
|
|
252
|
+
# Parses a backend HTTP response, raising when the backend reported an error.
|
|
253
|
+
# Success responses return the parsed JSON payload; error responses raise
|
|
254
|
+
# `error_class` with the backend's error type/reason so failures are never
|
|
255
|
+
# silently converted into empty results.
|
|
256
|
+
def parse_json_response!(response, error_class: Noiseless::RequestError, context: nil)
|
|
257
|
+
body = response.read
|
|
258
|
+
return JSON.parse(body) if response.success?
|
|
259
|
+
|
|
260
|
+
payload = begin
|
|
261
|
+
JSON.parse(body)
|
|
262
|
+
rescue JSON::ParserError, TypeError
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
error = payload.is_a?(Hash) ? payload["error"] : nil
|
|
266
|
+
reason = if error.is_a?(Hash)
|
|
267
|
+
[error["type"], error["reason"]].compact.join(": ")
|
|
268
|
+
elsif error
|
|
269
|
+
error.to_s
|
|
270
|
+
else
|
|
271
|
+
"HTTP #{response.status}"
|
|
272
|
+
end
|
|
273
|
+
message = context ? "#{context}: #{reason}" : reason
|
|
274
|
+
raise error_class.new(message, status: response.status, error_type: error.is_a?(Hash) ? error["type"] : nil)
|
|
275
|
+
end
|
|
276
|
+
|
|
211
277
|
# Override in subclasses
|
|
212
278
|
def execute_search(_query_hash, **_opts)
|
|
213
279
|
{
|
|
@@ -233,6 +299,10 @@ module Noiseless
|
|
|
233
299
|
{ acknowledged: true }
|
|
234
300
|
end
|
|
235
301
|
|
|
302
|
+
def execute_refresh_index(_index_name)
|
|
303
|
+
{ "acknowledged" => true }
|
|
304
|
+
end
|
|
305
|
+
|
|
236
306
|
def execute_index_exists?(_index_name)
|
|
237
307
|
true
|
|
238
308
|
end
|
|
@@ -302,6 +372,10 @@ module Noiseless
|
|
|
302
372
|
agg_body[:field] = agg.field if agg.field
|
|
303
373
|
agg_body.merge!(agg.options)
|
|
304
374
|
|
|
375
|
+
# Convention: an unqualified field-based aggregation targets the field
|
|
376
|
+
# named after the aggregation itself.
|
|
377
|
+
agg_body[:field] = agg.name if !agg_body.key?(:field) && !agg_body.key?(:script) && FIELD_BASED_AGG_TYPES.include?(agg.type.to_sym)
|
|
378
|
+
|
|
305
379
|
result[agg.type] = agg_body
|
|
306
380
|
|
|
307
381
|
# Add sub-aggregations if any
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Adapters
|
|
5
|
+
# Cluster health API - needed for Rails healthcheck
|
|
6
|
+
class ClusterAPI
|
|
7
|
+
def initialize(adapter)
|
|
8
|
+
@adapter = adapter
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def health(**)
|
|
12
|
+
Sync do
|
|
13
|
+
@adapter.send(:execute_cluster_health, **)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -7,22 +7,8 @@ module Noiseless
|
|
|
7
7
|
class Elasticsearch < Adapter
|
|
8
8
|
include ExecutionModules::ElasticsearchExecution
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
hosts_array = Array(hosts)
|
|
13
|
-
default_port = ENV["ELASTICSEARCH_PORT"] || 9200
|
|
14
|
-
@hosts = hosts_array.empty? ? ["http://localhost:#{default_port}"] : hosts_array
|
|
15
|
-
@connection_params = connection_params
|
|
16
|
-
|
|
17
|
-
# Initialize HTTP clients for each host
|
|
18
|
-
@clients = {}
|
|
19
|
-
@hosts.each do |host|
|
|
20
|
-
endpoint = Async::HTTP::Endpoint.parse(host)
|
|
21
|
-
@clients[host] = Async::HTTP::Client.new(endpoint)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
super(hosts: @hosts, **connection_params)
|
|
25
|
-
end
|
|
10
|
+
ClusterAPI = Adapters::ClusterAPI
|
|
11
|
+
IndicesAPI = Adapters::IndicesAPI
|
|
26
12
|
|
|
27
13
|
# Cluster health API - needed for Rails healthcheck
|
|
28
14
|
def cluster
|
|
@@ -34,36 +20,10 @@ module Noiseless
|
|
|
34
20
|
@indices ||= IndicesAPI.new(self)
|
|
35
21
|
end
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
def initialize(adapter)
|
|
39
|
-
@adapter = adapter
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def health(**)
|
|
43
|
-
Sync do
|
|
44
|
-
@adapter.send(:execute_cluster_health, **)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class IndicesAPI
|
|
50
|
-
def initialize(adapter)
|
|
51
|
-
@adapter = adapter
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def get(index:)
|
|
55
|
-
@adapter.execute_index_exists?(index) ? { index => {} } : raise("Index not found")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def stats(index:)
|
|
59
|
-
# Return basic stats structure
|
|
60
|
-
{ "indices" => { index => {} } }
|
|
61
|
-
end
|
|
23
|
+
private
|
|
62
24
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@adapter.send(:execute_refresh_index, index)
|
|
66
|
-
end
|
|
25
|
+
def default_port
|
|
26
|
+
ENV["ELASTICSEARCH_PORT"] || 9200
|
|
67
27
|
end
|
|
68
28
|
end
|
|
69
29
|
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "es_compatible_execution"
|
|
4
5
|
|
|
5
6
|
module Noiseless
|
|
6
7
|
module Adapters
|
|
7
8
|
module ExecutionModules
|
|
8
9
|
module ElasticsearchExecution
|
|
9
|
-
|
|
10
|
-
@clients&.each_value(&:close)
|
|
11
|
-
end
|
|
10
|
+
include EsCompatibleExecution
|
|
12
11
|
|
|
13
12
|
private
|
|
14
13
|
|
|
@@ -17,16 +16,7 @@ module Noiseless
|
|
|
17
16
|
body = JSON.generate(query_hash)
|
|
18
17
|
|
|
19
18
|
response = post_request(path, body)
|
|
20
|
-
|
|
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)
|
|
19
|
+
parse_json_response!(response, error_class: Noiseless::SearchError, context: "search")
|
|
30
20
|
ensure
|
|
31
21
|
response&.close
|
|
32
22
|
end
|
|
@@ -37,42 +27,7 @@ module Noiseless
|
|
|
37
27
|
body[:settings] = settings if settings
|
|
38
28
|
|
|
39
29
|
response = put_request("/#{index_name}", body.any? ? JSON.generate(body) : nil)
|
|
40
|
-
|
|
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
|
|
30
|
+
parse_json_response!(response, context: "create index #{index_name}")
|
|
76
31
|
ensure
|
|
77
32
|
response&.close
|
|
78
33
|
end
|
|
@@ -82,32 +37,7 @@ module Noiseless
|
|
|
82
37
|
body = JSON.generate(document)
|
|
83
38
|
|
|
84
39
|
response = id ? put_request(path, body) : post_request(path, body)
|
|
85
|
-
|
|
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
|
|
40
|
+
parse_json_response!(response, context: "index document #{index}/#{id}")
|
|
111
41
|
ensure
|
|
112
42
|
response&.close
|
|
113
43
|
end
|
|
@@ -132,56 +62,6 @@ module Noiseless
|
|
|
132
62
|
ensure
|
|
133
63
|
response&.close
|
|
134
64
|
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
65
|
end
|
|
186
66
|
end
|
|
187
67
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "http_transport"
|
|
5
|
+
|
|
6
|
+
module Noiseless
|
|
7
|
+
module Adapters
|
|
8
|
+
module ExecutionModules
|
|
9
|
+
# Document and index operations shared by the wire-compatible
|
|
10
|
+
# Elasticsearch and OpenSearch HTTP APIs.
|
|
11
|
+
module EsCompatibleExecution
|
|
12
|
+
include HttpTransport
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def execute_bulk(actions, **_opts)
|
|
17
|
+
body = actions.map do |action|
|
|
18
|
+
if action[:index]
|
|
19
|
+
action_line = { index: { _index: action[:index][:_index], _id: action[:index][:_id] } }
|
|
20
|
+
data_line = action[:index][:data]
|
|
21
|
+
"#{JSON.generate(action_line)}\n#{JSON.generate(data_line)}\n"
|
|
22
|
+
else
|
|
23
|
+
"#{JSON.generate(action)}\n"
|
|
24
|
+
end
|
|
25
|
+
end.join
|
|
26
|
+
|
|
27
|
+
response = post_request("/_bulk", body, content_type: "application/x-ndjson")
|
|
28
|
+
parse_json_response!(response, context: "bulk")
|
|
29
|
+
ensure
|
|
30
|
+
response&.close
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def execute_delete_index(index_name, **_opts)
|
|
34
|
+
response = delete_request("/#{index_name}")
|
|
35
|
+
# Deleting an absent index is idempotent, matching official ES/OS
|
|
36
|
+
# clients' ignore-404 behaviour.
|
|
37
|
+
return { "acknowledged" => true, "result" => "not_found" } if response.status == 404
|
|
38
|
+
|
|
39
|
+
parse_json_response!(response, context: "delete index #{index_name}")
|
|
40
|
+
ensure
|
|
41
|
+
response&.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def execute_refresh_index(index_name)
|
|
45
|
+
response = post_request("/#{index_name}/_refresh", nil)
|
|
46
|
+
parse_json_response!(response, context: "refresh index #{index_name}")
|
|
47
|
+
ensure
|
|
48
|
+
response&.close
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def execute_index_exists?(index_name)
|
|
52
|
+
response = head_request("/#{index_name}")
|
|
53
|
+
response.success?
|
|
54
|
+
rescue StandardError
|
|
55
|
+
false
|
|
56
|
+
ensure
|
|
57
|
+
response&.close
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def execute_update_document(index, id, changes, **_opts)
|
|
61
|
+
body = JSON.generate(doc: changes)
|
|
62
|
+
|
|
63
|
+
response = post_request("/#{index}/_update/#{id}", body)
|
|
64
|
+
parse_json_response!(response, context: "update document #{index}/#{id}")
|
|
65
|
+
ensure
|
|
66
|
+
response&.close
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def execute_delete_document(index, id, **_opts)
|
|
70
|
+
response = delete_request("/#{index}/_doc/#{id}")
|
|
71
|
+
# 404 covers both a missing document and a missing index; either way
|
|
72
|
+
# the delete is idempotent.
|
|
73
|
+
return { "_index" => index, "_id" => id, "result" => "not_found" } if response.status == 404
|
|
74
|
+
|
|
75
|
+
parse_json_response!(response, context: "delete document #{index}/#{id}")
|
|
76
|
+
ensure
|
|
77
|
+
response&.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def execute_document_exists?(index, id)
|
|
81
|
+
response = head_request("/#{index}/_doc/#{id}")
|
|
82
|
+
response.success?
|
|
83
|
+
rescue StandardError
|
|
84
|
+
false
|
|
85
|
+
ensure
|
|
86
|
+
response&.close
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Adapters
|
|
5
|
+
module ExecutionModules
|
|
6
|
+
# Shared Async::HTTP connection handling for HTTP-based adapters.
|
|
7
|
+
# Host classes must provide a private +default_port+ method.
|
|
8
|
+
module HttpTransport
|
|
9
|
+
def initialize(hosts: [], **connection_params)
|
|
10
|
+
# Ensure we always have at least one host
|
|
11
|
+
hosts_array = Array(hosts)
|
|
12
|
+
@hosts = hosts_array.empty? ? ["http://localhost:#{default_port}"] : hosts_array
|
|
13
|
+
@connection_params = connection_params
|
|
14
|
+
|
|
15
|
+
# Initialize HTTP clients for each host
|
|
16
|
+
@clients = {}
|
|
17
|
+
@hosts.each do |host|
|
|
18
|
+
endpoint = Async::HTTP::Endpoint.parse(host)
|
|
19
|
+
@clients[host] = Async::HTTP::Client.new(endpoint)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
super(hosts: @hosts, **connection_params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close
|
|
26
|
+
@clients&.each_value(&:close)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# HTTP helpers using Async::HTTP with connection pooling
|
|
32
|
+
def get_request(path)
|
|
33
|
+
with_client do |client|
|
|
34
|
+
client.get(path, default_headers)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def post_request(path, body, content_type: "application/json")
|
|
39
|
+
headers = body ? default_headers + [["content-type", content_type]] : default_headers
|
|
40
|
+
|
|
41
|
+
with_client do |client|
|
|
42
|
+
client.post(path, headers, body)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def put_request(path, body, content_type: "application/json")
|
|
47
|
+
headers = body ? default_headers + [["content-type", content_type]] : default_headers
|
|
48
|
+
|
|
49
|
+
with_client do |client|
|
|
50
|
+
client.put(path, headers, body)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def delete_request(path)
|
|
55
|
+
with_client do |client|
|
|
56
|
+
client.delete(path, default_headers)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def head_request(path)
|
|
61
|
+
with_client do |client|
|
|
62
|
+
client.head(path, default_headers)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def with_client
|
|
67
|
+
# Select a random host for load balancing
|
|
68
|
+
host = @hosts.sample
|
|
69
|
+
client = @clients[host]
|
|
70
|
+
|
|
71
|
+
yield(client)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_headers
|
|
75
|
+
[
|
|
76
|
+
["accept", "application/json"],
|
|
77
|
+
["user-agent", "Noiseless/#{Noiseless::VERSION} (Ruby/#{RUBY_VERSION})"]
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "es_compatible_execution"
|
|
4
5
|
|
|
5
6
|
module Noiseless
|
|
6
7
|
module Adapters
|
|
7
8
|
module ExecutionModules
|
|
8
9
|
module OpensearchExecution
|
|
9
|
-
|
|
10
|
-
@clients&.each_value(&:close)
|
|
11
|
-
end
|
|
10
|
+
include EsCompatibleExecution
|
|
12
11
|
|
|
13
12
|
private
|
|
14
13
|
|
|
@@ -18,43 +17,7 @@ module Noiseless
|
|
|
18
17
|
body = JSON.generate(query_hash)
|
|
19
18
|
|
|
20
19
|
response = post_request(path, body)
|
|
21
|
-
|
|
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 } }
|
|
20
|
+
parse_json_response!(response, error_class: Noiseless::SearchError, context: "search #{index_path}")
|
|
58
21
|
ensure
|
|
59
22
|
response&.close
|
|
60
23
|
end
|
|
@@ -65,46 +28,7 @@ module Noiseless
|
|
|
65
28
|
body[:settings] = settings if settings
|
|
66
29
|
|
|
67
30
|
response = put_request("/#{index_name}", body.any? ? JSON.generate(body) : nil)
|
|
68
|
-
|
|
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
|
|
31
|
+
parse_json_response!(response, context: "create index #{index_name}")
|
|
108
32
|
ensure
|
|
109
33
|
response&.close
|
|
110
34
|
end
|
|
@@ -114,38 +38,7 @@ module Noiseless
|
|
|
114
38
|
body = JSON.generate(document)
|
|
115
39
|
|
|
116
40
|
response = put_request(path, body)
|
|
117
|
-
|
|
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
|
|
41
|
+
parse_json_response!(response, context: "index document #{index}/#{id}")
|
|
149
42
|
ensure
|
|
150
43
|
response&.close
|
|
151
44
|
end
|
|
@@ -178,13 +71,7 @@ module Noiseless
|
|
|
178
71
|
body = JSON.generate(enhanced_query)
|
|
179
72
|
|
|
180
73
|
response = post_request("/_search", body)
|
|
181
|
-
|
|
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
|
-
}
|
|
74
|
+
parse_json_response!(response, error_class: Noiseless::SearchError, context: "point-in-time search")
|
|
188
75
|
ensure
|
|
189
76
|
response&.close
|
|
190
77
|
end
|
|
@@ -198,12 +85,7 @@ module Noiseless
|
|
|
198
85
|
body = JSON.generate(template_query)
|
|
199
86
|
|
|
200
87
|
response = post_request("/_search/template", body)
|
|
201
|
-
|
|
202
|
-
rescue StandardError => e
|
|
203
|
-
{
|
|
204
|
-
error: { type: e.class.name, reason: e.message },
|
|
205
|
-
hits: { total: { value: 0 }, hits: [] }
|
|
206
|
-
}
|
|
88
|
+
parse_json_response!(response, error_class: Noiseless::SearchError, context: "search template #{template_id}")
|
|
207
89
|
ensure
|
|
208
90
|
response&.close
|
|
209
91
|
end
|
|
@@ -321,56 +203,6 @@ module Noiseless
|
|
|
321
203
|
ensure
|
|
322
204
|
response&.close
|
|
323
205
|
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
206
|
end
|
|
375
207
|
end
|
|
376
208
|
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "http_transport"
|
|
4
5
|
|
|
5
6
|
module Noiseless
|
|
6
7
|
module Adapters
|
|
7
8
|
module ExecutionModules
|
|
8
9
|
module TypesenseExecution
|
|
9
|
-
|
|
10
|
-
@clients&.each_value(&:close)
|
|
11
|
-
end
|
|
10
|
+
include HttpTransport
|
|
12
11
|
|
|
13
12
|
private
|
|
14
13
|
|
|
@@ -391,54 +390,8 @@ module Noiseless
|
|
|
391
390
|
response&.close
|
|
392
391
|
end
|
|
393
392
|
|
|
394
|
-
# HTTP helpers using Async::HTTP with connection pooling
|
|
395
|
-
def get_request(path)
|
|
396
|
-
with_client do |client|
|
|
397
|
-
client.get(path, default_headers)
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def post_request(path, body, content_type: "application/json")
|
|
402
|
-
headers = body ? default_headers + [["content-type", content_type]] : default_headers
|
|
403
|
-
|
|
404
|
-
with_client do |client|
|
|
405
|
-
client.post(path, headers, body)
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def put_request(path, body, content_type: "application/json")
|
|
410
|
-
headers = body ? default_headers + [["content-type", content_type]] : default_headers
|
|
411
|
-
|
|
412
|
-
with_client do |client|
|
|
413
|
-
client.put(path, headers, body)
|
|
414
|
-
end
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
def delete_request(path)
|
|
418
|
-
with_client do |client|
|
|
419
|
-
client.delete(path, default_headers)
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def head_request(path)
|
|
424
|
-
with_client do |client|
|
|
425
|
-
client.head(path, default_headers)
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def with_client
|
|
430
|
-
# Select a random host for load balancing
|
|
431
|
-
host = @hosts.sample
|
|
432
|
-
client = @clients[host]
|
|
433
|
-
|
|
434
|
-
yield(client)
|
|
435
|
-
end
|
|
436
|
-
|
|
437
393
|
def default_headers
|
|
438
|
-
headers =
|
|
439
|
-
["accept", "application/json"],
|
|
440
|
-
["user-agent", "Noiseless/#{Noiseless::VERSION} (Ruby/#{RUBY_VERSION})"]
|
|
441
|
-
]
|
|
394
|
+
headers = super
|
|
442
395
|
|
|
443
396
|
# Add Typesense API key if configured
|
|
444
397
|
if @connection_params && @connection_params[:api_key]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Adapters
|
|
5
|
+
# Indices API - needed for index management operations
|
|
6
|
+
class IndicesAPI
|
|
7
|
+
def initialize(adapter)
|
|
8
|
+
@adapter = adapter
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get(index:)
|
|
12
|
+
@adapter.execute_index_exists?(index) ? { index => {} } : raise("Index not found")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stats(index:)
|
|
16
|
+
# Return basic stats structure
|
|
17
|
+
{ "indices" => { index => {} } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def refresh(index:)
|
|
21
|
+
# Refresh the index to make documents immediately searchable
|
|
22
|
+
@adapter.refresh_index(index).wait
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -7,22 +7,8 @@ module Noiseless
|
|
|
7
7
|
class OpenSearch < Adapter
|
|
8
8
|
include ExecutionModules::OpensearchExecution
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
hosts_array = Array(hosts)
|
|
13
|
-
default_port = ENV["OPENSEARCH_PORT"] || 9200
|
|
14
|
-
@hosts = hosts_array.empty? ? ["http://localhost:#{default_port}"] : hosts_array
|
|
15
|
-
@connection_params = connection_params
|
|
16
|
-
|
|
17
|
-
# Initialize HTTP clients for each host
|
|
18
|
-
@clients = {}
|
|
19
|
-
@hosts.each do |host|
|
|
20
|
-
endpoint = Async::HTTP::Endpoint.parse(host)
|
|
21
|
-
@clients[host] = Async::HTTP::Client.new(endpoint)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
super(hosts: @hosts, **connection_params)
|
|
25
|
-
end
|
|
10
|
+
ClusterAPI = Adapters::ClusterAPI
|
|
11
|
+
IndicesAPI = Adapters::IndicesAPI
|
|
26
12
|
|
|
27
13
|
# OpenSearch-specific features
|
|
28
14
|
def point_in_time_search(ast_node, pit_id:, **)
|
|
@@ -65,38 +51,6 @@ module Noiseless
|
|
|
65
51
|
end
|
|
66
52
|
end
|
|
67
53
|
|
|
68
|
-
class ClusterAPI
|
|
69
|
-
def initialize(adapter)
|
|
70
|
-
@adapter = adapter
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def health(**)
|
|
74
|
-
Sync do
|
|
75
|
-
@adapter.send(:execute_cluster_health, **)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
class IndicesAPI
|
|
81
|
-
def initialize(adapter)
|
|
82
|
-
@adapter = adapter
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def get(index:)
|
|
86
|
-
@adapter.execute_index_exists?(index) ? { index => {} } : raise("Index not found")
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def stats(index:)
|
|
90
|
-
# Return basic stats structure
|
|
91
|
-
{ "indices" => { index => {} } }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def refresh(index:)
|
|
95
|
-
# Refresh the index to make documents immediately searchable
|
|
96
|
-
@adapter.send(:execute_refresh_index, index)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
54
|
# Search Pipelines API for OpenSearch 3.x
|
|
101
55
|
# Pipelines can include request and response processors for neural search, reranking, etc.
|
|
102
56
|
class PipelinesAPI
|
|
@@ -203,6 +157,12 @@ module Noiseless
|
|
|
203
157
|
end
|
|
204
158
|
end
|
|
205
159
|
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def default_port
|
|
164
|
+
ENV["OPENSEARCH_PORT"] || 9200
|
|
165
|
+
end
|
|
206
166
|
end
|
|
207
167
|
end
|
|
208
168
|
end
|
|
@@ -7,22 +7,7 @@ module Noiseless
|
|
|
7
7
|
class Typesense < Adapter
|
|
8
8
|
include ExecutionModules::TypesenseExecution
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
# Ensure we always have at least one host
|
|
12
|
-
hosts_array = Array(hosts)
|
|
13
|
-
default_port = ENV["TYPESENSE_PORT"] || 8108
|
|
14
|
-
@hosts = hosts_array.empty? ? ["http://localhost:#{default_port}"] : hosts_array
|
|
15
|
-
@connection_params = connection_params
|
|
16
|
-
|
|
17
|
-
# Initialize HTTP clients for each host
|
|
18
|
-
@clients = {}
|
|
19
|
-
@hosts.each do |host|
|
|
20
|
-
endpoint = Async::HTTP::Endpoint.parse(host)
|
|
21
|
-
@clients[host] = Async::HTTP::Client.new(endpoint)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
super(hosts: @hosts, **connection_params)
|
|
25
|
-
end
|
|
10
|
+
ClusterAPI = Adapters::ClusterAPI
|
|
26
11
|
|
|
27
12
|
# Cluster health API - needed for Rails healthcheck
|
|
28
13
|
def cluster
|
|
@@ -34,37 +19,18 @@ module Noiseless
|
|
|
34
19
|
@indices ||= IndicesAPI.new(self)
|
|
35
20
|
end
|
|
36
21
|
|
|
37
|
-
class
|
|
38
|
-
def initialize(adapter)
|
|
39
|
-
@adapter = adapter
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def health(**)
|
|
43
|
-
Sync do
|
|
44
|
-
@adapter.send(:execute_cluster_health, **)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
class IndicesAPI
|
|
50
|
-
def initialize(adapter)
|
|
51
|
-
@adapter = adapter
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def get(index:)
|
|
55
|
-
@adapter.execute_index_exists?(index) ? { index => {} } : raise("Index not found")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def stats(index:)
|
|
59
|
-
# Return basic stats structure
|
|
60
|
-
{ "indices" => { index => {} } }
|
|
61
|
-
end
|
|
62
|
-
|
|
22
|
+
class IndicesAPI < Adapters::IndicesAPI
|
|
63
23
|
def refresh(index: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
64
24
|
# Typesense doesn't require explicit refresh - documents are immediately available
|
|
65
25
|
{ "_shards" => { "total" => 1, "successful" => 1, "failed" => 0 } }
|
|
66
26
|
end
|
|
67
27
|
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def default_port
|
|
32
|
+
ENV["TYPESENSE_PORT"] || 8108
|
|
33
|
+
end
|
|
68
34
|
end
|
|
69
35
|
end
|
|
70
36
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module AST
|
|
5
|
+
# Base for leaf nodes that pair a single field with a value.
|
|
6
|
+
class FieldValueNode < Node
|
|
7
|
+
attr_reader :field, :value
|
|
8
|
+
|
|
9
|
+
def initialize(field, value)
|
|
10
|
+
super()
|
|
11
|
+
@field = field
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/noiseless/ast/filter.rb
CHANGED
data/lib/noiseless/ast/match.rb
CHANGED
data/lib/noiseless/ast/prefix.rb
CHANGED
data/lib/noiseless/version.rb
CHANGED
data/lib/noiseless.rb
CHANGED
|
@@ -17,6 +17,21 @@ require_relative "noiseless/version"
|
|
|
17
17
|
module Noiseless
|
|
18
18
|
class Error < StandardError; end
|
|
19
19
|
|
|
20
|
+
# HTTP-level failure from the search backend (non-2xx response).
|
|
21
|
+
class RequestError < Error
|
|
22
|
+
attr_reader :status, :error_type
|
|
23
|
+
|
|
24
|
+
def initialize(message, status: nil, error_type: nil)
|
|
25
|
+
super(message)
|
|
26
|
+
@status = status
|
|
27
|
+
@error_type = error_type
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Raised when a search query is rejected by the backend
|
|
32
|
+
# (malformed query, missing index, shard failures).
|
|
33
|
+
class SearchError < RequestError; end
|
|
34
|
+
|
|
20
35
|
class Configuration
|
|
21
36
|
attr_accessor :connections_config, :default_connection, :default_adapter, :config_path
|
|
22
37
|
|
|
@@ -85,7 +100,8 @@ module Noiseless
|
|
|
85
100
|
|
|
86
101
|
# Setup Zeitwerk autoloader
|
|
87
102
|
loader = Zeitwerk::Loader.for_gem
|
|
88
|
-
loader.inflector.inflect("ast" => "AST", "dsl" => "DSL", "open_search" => "OpenSearch"
|
|
103
|
+
loader.inflector.inflect("ast" => "AST", "dsl" => "DSL", "open_search" => "OpenSearch",
|
|
104
|
+
"cluster_api" => "ClusterAPI", "indices_api" => "IndicesAPI")
|
|
89
105
|
loader.ignore("#{__dir__}/application_search.rb")
|
|
90
106
|
loader.ignore("#{__dir__}/noiseless/test_helper.rb")
|
|
91
107
|
loader.ignore("#{__dir__}/noiseless/test_case.rb")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: noiseless
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -206,12 +206,16 @@ files:
|
|
|
206
206
|
- lib/noiseless.rb
|
|
207
207
|
- lib/noiseless/adapter.rb
|
|
208
208
|
- lib/noiseless/adapters.rb
|
|
209
|
+
- lib/noiseless/adapters/cluster_api.rb
|
|
209
210
|
- lib/noiseless/adapters/elasticsearch.rb
|
|
210
211
|
- lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb
|
|
212
|
+
- lib/noiseless/adapters/execution_modules/es_compatible_execution.rb
|
|
213
|
+
- lib/noiseless/adapters/execution_modules/http_transport.rb
|
|
211
214
|
- lib/noiseless/adapters/execution_modules/opensearch_execution.rb
|
|
212
215
|
- lib/noiseless/adapters/execution_modules/pgvector_support.rb
|
|
213
216
|
- lib/noiseless/adapters/execution_modules/postgresql_execution.rb
|
|
214
217
|
- lib/noiseless/adapters/execution_modules/typesense_execution.rb
|
|
218
|
+
- lib/noiseless/adapters/indices_api.rb
|
|
215
219
|
- lib/noiseless/adapters/open_search.rb
|
|
216
220
|
- lib/noiseless/adapters/postgresql.rb
|
|
217
221
|
- lib/noiseless/adapters/typesense.rb
|
|
@@ -222,6 +226,7 @@ files:
|
|
|
222
226
|
- lib/noiseless/ast/collapse.rb
|
|
223
227
|
- lib/noiseless/ast/combined_fields.rb
|
|
224
228
|
- lib/noiseless/ast/conversation.rb
|
|
229
|
+
- lib/noiseless/ast/field_value_node.rb
|
|
225
230
|
- lib/noiseless/ast/filter.rb
|
|
226
231
|
- lib/noiseless/ast/hybrid.rb
|
|
227
232
|
- lib/noiseless/ast/image_query.rb
|