noiseless 0.7.2 → 0.9.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/README.md +56 -2
- data/lib/noiseless/adapter.rb +29 -41
- data/lib/noiseless/adapters/elasticsearch.rb +0 -1
- data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +8 -6
- data/lib/noiseless/adapters/execution_modules/es_compatible_execution.rb +65 -17
- data/lib/noiseless/adapters/execution_modules/http_transport.rb +29 -18
- data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +102 -114
- data/lib/noiseless/adapters/execution_modules/pgvector_support.rb +42 -47
- data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +65 -327
- data/lib/noiseless/adapters/execution_modules/postgresql_query.rb +269 -0
- data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +172 -296
- data/lib/noiseless/adapters/execution_modules/typesense_query.rb +143 -0
- data/lib/noiseless/adapters/indices_api.rb +3 -1
- data/lib/noiseless/adapters/open_search.rb +30 -88
- data/lib/noiseless/adapters/postgresql.rb +6 -16
- data/lib/noiseless/adapters/typesense.rb +1 -2
- data/lib/noiseless/ast/aggregation.rb +0 -23
- data/lib/noiseless/ast/aggregation_builder.rb +28 -0
- data/lib/noiseless/ast/hybrid.rb +4 -2
- data/lib/noiseless/ast/join.rb +4 -5
- data/lib/noiseless/ast/vector.rb +25 -4
- data/lib/noiseless/bulk_importer.rb +17 -32
- data/lib/noiseless/callbacks.rb +4 -30
- data/lib/noiseless/connection_manager.rb +9 -3
- data/lib/noiseless/controller_runtime.rb +37 -0
- data/lib/noiseless/document_manager.rb +22 -41
- data/lib/noiseless/embedding.rb +33 -0
- data/lib/noiseless/instrumentation.rb +2 -136
- data/lib/noiseless/introspection/mermaid_diagrams.rb +143 -0
- data/lib/noiseless/introspection/query_visualizer.rb +3 -228
- data/lib/noiseless/introspection.rb +1 -1
- data/lib/noiseless/log_subscriber.rb +103 -0
- data/lib/noiseless/model.rb +19 -71
- data/lib/noiseless/model_registry.rb +1 -7
- data/lib/noiseless/multi_search.rb +23 -113
- data/lib/noiseless/pagination.rb +14 -40
- data/lib/noiseless/query_builder.rb +16 -16
- data/lib/noiseless/query_delegation.rb +15 -0
- data/lib/noiseless/railtie.rb +0 -2
- data/lib/noiseless/record_class_resolver.rb +15 -0
- data/lib/noiseless/response/records.rb +9 -3
- data/lib/noiseless/runtime_reset_middleware.rb +1 -1
- data/lib/noiseless/search_index_update_job.rb +13 -14
- data/lib/noiseless/test_case.rb +0 -1
- data/lib/noiseless/version.rb +1 -1
- data/lib/noiseless.rb +1 -10
- metadata +33 -13
- data/lib/tasks/benchmark.rake +0 -35
|
@@ -64,9 +64,9 @@ module Noiseless
|
|
|
64
64
|
when :yaml
|
|
65
65
|
YAML.dump(ast_node.to_h)
|
|
66
66
|
when :mermaid
|
|
67
|
-
ast_to_mermaid_flowchart(ast_node)
|
|
67
|
+
MermaidDiagrams.ast_to_mermaid_flowchart(ast_node)
|
|
68
68
|
when :mermaid_class
|
|
69
|
-
ast_to_mermaid_class_diagram(ast_node)
|
|
69
|
+
MermaidDiagrams.ast_to_mermaid_class_diagram(ast_node)
|
|
70
70
|
else
|
|
71
71
|
raise ArgumentError, "Unsupported format: #{format}. Available: :tree, :json, :yaml, :mermaid, :mermaid_class"
|
|
72
72
|
end
|
|
@@ -75,7 +75,7 @@ module Noiseless
|
|
|
75
75
|
def self.explain_query_flow(ast_node, adapter)
|
|
76
76
|
explanation = adapter.explain_query(ast_node)
|
|
77
77
|
|
|
78
|
-
flow_diagram = generate_flow_diagram(explanation)
|
|
78
|
+
flow_diagram = MermaidDiagrams.generate_flow_diagram(explanation)
|
|
79
79
|
|
|
80
80
|
{
|
|
81
81
|
explanation: explanation,
|
|
@@ -230,68 +230,6 @@ module Noiseless
|
|
|
230
230
|
end
|
|
231
231
|
end
|
|
232
232
|
|
|
233
|
-
def self.generate_mermaid_diagram(ast_node)
|
|
234
|
-
diagram = "graph TD\n".dup
|
|
235
|
-
|
|
236
|
-
add_node = lambda do |diagram, node, parent_id, counter|
|
|
237
|
-
case node
|
|
238
|
-
when Hash
|
|
239
|
-
node.each do |key, value|
|
|
240
|
-
current_id = "N#{counter[:count]}"
|
|
241
|
-
counter[:count] += 1
|
|
242
|
-
diagram << " #{current_id}[#{key}]\n"
|
|
243
|
-
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
244
|
-
add_node.call(diagram, value, current_id, counter)
|
|
245
|
-
end
|
|
246
|
-
when Array
|
|
247
|
-
node.each_with_index do |item, index|
|
|
248
|
-
current_id = "N#{counter[:count]}"
|
|
249
|
-
counter[:count] += 1
|
|
250
|
-
diagram << " #{current_id}[Item #{index}]\n"
|
|
251
|
-
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
252
|
-
add_node.call(diagram, item, current_id, counter)
|
|
253
|
-
end
|
|
254
|
-
else
|
|
255
|
-
current_id = "N#{counter[:count]}"
|
|
256
|
-
counter[:count] += 1
|
|
257
|
-
diagram << " #{current_id}[#{node}]\n"
|
|
258
|
-
diagram << " #{parent_id} --> #{current_id}\n" if parent_id
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
counter = { count: 0 }
|
|
263
|
-
root_id = "N#{counter[:count]}"
|
|
264
|
-
counter[:count] += 1
|
|
265
|
-
diagram << " #{root_id}[Root]\n"
|
|
266
|
-
|
|
267
|
-
add_node.call(diagram, ast_node.to_h, root_id, counter)
|
|
268
|
-
diagram
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def self.generate_flow_diagram(explanation)
|
|
272
|
-
# Create a sequence diagram showing the query execution flow
|
|
273
|
-
diagram = "sequenceDiagram\n".dup
|
|
274
|
-
diagram << " participant Client\n"
|
|
275
|
-
diagram << " participant Adapter\n"
|
|
276
|
-
diagram << " participant Engine\n"
|
|
277
|
-
|
|
278
|
-
explanation[:execution_plan].each do |step|
|
|
279
|
-
diagram << case step[:description]
|
|
280
|
-
when /validate/i
|
|
281
|
-
" Client->>Adapter: #{step[:description]}\n"
|
|
282
|
-
when /convert/i, /format/i
|
|
283
|
-
" Adapter->>Adapter: #{step[:description]}\n"
|
|
284
|
-
when /execute/i, /query/i
|
|
285
|
-
" Adapter->>Engine: #{step[:description]}\n"
|
|
286
|
-
else
|
|
287
|
-
" Engine->>Adapter: #{step[:description]}\n"
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
diagram << " Adapter->>Client: Return results\n"
|
|
292
|
-
diagram
|
|
293
|
-
end
|
|
294
|
-
|
|
295
233
|
def self.format_performance_breakdown(performance)
|
|
296
234
|
performance.map do |metric, value|
|
|
297
235
|
{
|
|
@@ -365,169 +303,6 @@ module Noiseless
|
|
|
365
303
|
node.each { |item| count_recursive(item, complexity) }
|
|
366
304
|
end
|
|
367
305
|
end
|
|
368
|
-
|
|
369
|
-
# New methods using your diagram/mermaid gems
|
|
370
|
-
def self.ast_to_mermaid_flowchart(ast_node)
|
|
371
|
-
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
372
|
-
|
|
373
|
-
diagram = Diagrams::FlowchartDiagram.new(version: "1.0")
|
|
374
|
-
|
|
375
|
-
# Convert AST structure to flowchart nodes and edges
|
|
376
|
-
add_ast_node_to_flowchart(diagram, ast_node, "root")
|
|
377
|
-
|
|
378
|
-
diagram.to_mermaid
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def self.ast_to_mermaid_class_diagram(ast_node)
|
|
382
|
-
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
383
|
-
|
|
384
|
-
diagram = Diagrams::ClassDiagram.new(version: "1.0")
|
|
385
|
-
|
|
386
|
-
# Create a class representation of the AST structure
|
|
387
|
-
root_class = Diagrams::Elements::ClassEntity.new(
|
|
388
|
-
name: ast_node.class.name.split("::").last,
|
|
389
|
-
attributes: ast_node.instance_variables.map do |var|
|
|
390
|
-
"#{var.to_s.delete('@')}: #{ast_node.instance_variable_get(var).class.name.split('::').last}"
|
|
391
|
-
end,
|
|
392
|
-
methods: ast_node.public_methods(false).map { |method| "+#{method}()" }
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
diagram.add_class(root_class)
|
|
396
|
-
|
|
397
|
-
# Add child nodes as related classes
|
|
398
|
-
add_ast_children_to_class_diagram(diagram, ast_node, root_class.name)
|
|
399
|
-
|
|
400
|
-
diagram.to_mermaid
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def self.adapter_capability_matrix_to_mermaid
|
|
404
|
-
return "# Mermaid diagrams require 'diagrams' and 'mermaid' gems\n# Add to Gemfile: gem 'diagrams'; gem 'mermaid'" unless DIAGRAMS_AVAILABLE
|
|
405
|
-
|
|
406
|
-
# Create an ER diagram showing adapter capabilities
|
|
407
|
-
diagram = Diagrams::ERDiagram.new
|
|
408
|
-
|
|
409
|
-
# Add adapter entities
|
|
410
|
-
diagram.add_entity(
|
|
411
|
-
name: "ADAPTER",
|
|
412
|
-
attributes: [
|
|
413
|
-
{ type: "string", name: "type", keys: [:PK] },
|
|
414
|
-
{ type: "string", name: "execution_mode" },
|
|
415
|
-
{ type: "string", name: "engine_name" }
|
|
416
|
-
]
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
diagram.add_entity(
|
|
420
|
-
name: "CAPABILITY",
|
|
421
|
-
attributes: [
|
|
422
|
-
{ type: "string", name: "name", keys: [:PK] },
|
|
423
|
-
{ type: "string", name: "description" }
|
|
424
|
-
]
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
diagram.add_entity(
|
|
428
|
-
name: "ADAPTER_CAPABILITY",
|
|
429
|
-
attributes: [
|
|
430
|
-
{ type: "string", name: "adapter_type", keys: %i[PK FK] },
|
|
431
|
-
{ type: "string", name: "capability_name", keys: %i[PK FK] }
|
|
432
|
-
]
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
# Add relationships
|
|
436
|
-
diagram.add_relationship(
|
|
437
|
-
entity1: "ADAPTER",
|
|
438
|
-
entity2: "ADAPTER_CAPABILITY",
|
|
439
|
-
cardinality1: :ONE_ONLY,
|
|
440
|
-
cardinality2: :ZERO_OR_MORE,
|
|
441
|
-
label: "has"
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
diagram.add_relationship(
|
|
445
|
-
entity1: "CAPABILITY",
|
|
446
|
-
entity2: "ADAPTER_CAPABILITY",
|
|
447
|
-
cardinality1: :ONE_ONLY,
|
|
448
|
-
cardinality2: :ZERO_OR_MORE,
|
|
449
|
-
label: "provided by"
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
diagram.to_mermaid
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def self.add_ast_node_to_flowchart(diagram, node, node_id)
|
|
456
|
-
# Add current node
|
|
457
|
-
flowchart_node = Diagrams::Elements::Node.new(
|
|
458
|
-
id: node_id,
|
|
459
|
-
label: node.class.name.split("::").last.to_s
|
|
460
|
-
)
|
|
461
|
-
diagram.add_node(flowchart_node)
|
|
462
|
-
|
|
463
|
-
# Add child nodes and connect them
|
|
464
|
-
return unless node.respond_to?(:instance_variables)
|
|
465
|
-
|
|
466
|
-
node.instance_variables.each_with_index do |var, _index|
|
|
467
|
-
child_value = node.instance_variable_get(var)
|
|
468
|
-
|
|
469
|
-
if child_value.is_a?(Noiseless::AST::Node)
|
|
470
|
-
child_id = "#{node_id}_#{var.to_s.delete('@')}"
|
|
471
|
-
add_ast_node_to_flowchart(diagram, child_value, child_id)
|
|
472
|
-
|
|
473
|
-
edge = Diagrams::Elements::Edge.new(
|
|
474
|
-
source_id: node_id,
|
|
475
|
-
target_id: child_id,
|
|
476
|
-
label: var.to_s.delete("@")
|
|
477
|
-
)
|
|
478
|
-
diagram.add_edge(edge)
|
|
479
|
-
elsif child_value.is_a?(Array) && child_value.any?(Noiseless::AST::Node)
|
|
480
|
-
child_value.each_with_index do |item, item_index|
|
|
481
|
-
next unless item.is_a?(Noiseless::AST::Node)
|
|
482
|
-
|
|
483
|
-
child_id = "#{node_id}_#{var.to_s.delete('@')}_#{item_index}"
|
|
484
|
-
add_ast_node_to_flowchart(diagram, item, child_id)
|
|
485
|
-
|
|
486
|
-
edge = Diagrams::Elements::Edge.new(
|
|
487
|
-
source_id: node_id,
|
|
488
|
-
target_id: child_id,
|
|
489
|
-
label: "#{var.to_s.delete('@')}[#{item_index}]"
|
|
490
|
-
)
|
|
491
|
-
diagram.add_edge(edge)
|
|
492
|
-
end
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
def self.add_ast_children_to_class_diagram(diagram, node, parent_class_name)
|
|
498
|
-
return unless node.respond_to?(:instance_variables)
|
|
499
|
-
|
|
500
|
-
node.instance_variables.each do |var|
|
|
501
|
-
child_value = node.instance_variable_get(var)
|
|
502
|
-
|
|
503
|
-
next unless child_value.is_a?(Noiseless::AST::Node)
|
|
504
|
-
|
|
505
|
-
child_class_name = child_value.class.name.split("::").last
|
|
506
|
-
|
|
507
|
-
# Add child class if not already added
|
|
508
|
-
unless diagram.classes.any? { |c| c.name == child_class_name }
|
|
509
|
-
child_class = Diagrams::Elements::ClassEntity.new(
|
|
510
|
-
name: child_class_name,
|
|
511
|
-
attributes: child_value.instance_variables.map do |cv|
|
|
512
|
-
"#{cv.to_s.delete('@')}: #{child_value.instance_variable_get(cv).class.name.split('::').last}"
|
|
513
|
-
end
|
|
514
|
-
)
|
|
515
|
-
diagram.add_class(child_class)
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
# Add relationship
|
|
519
|
-
relationship = Diagrams::Elements::Relationship.new(
|
|
520
|
-
source_class_name: parent_class_name,
|
|
521
|
-
target_class_name: child_class_name,
|
|
522
|
-
type: "composition",
|
|
523
|
-
label: var.to_s.delete("@")
|
|
524
|
-
)
|
|
525
|
-
diagram.add_relationship(relationship)
|
|
526
|
-
|
|
527
|
-
# Recursively add children
|
|
528
|
-
add_ast_children_to_class_diagram(diagram, child_value, child_class_name)
|
|
529
|
-
end
|
|
530
|
-
end
|
|
531
306
|
end
|
|
532
307
|
end
|
|
533
308
|
end
|
|
@@ -45,7 +45,7 @@ module Noiseless
|
|
|
45
45
|
# Engine-specific capabilities
|
|
46
46
|
case self.class.name
|
|
47
47
|
when /OpenSearch/
|
|
48
|
-
capabilities += %i[
|
|
48
|
+
capabilities += %i[point_in_time_pagination search_templates] if respond_to?(:each_page)
|
|
49
49
|
when /Typesense/
|
|
50
50
|
capabilities += %i[typo_tolerance faceted_search] if respond_to?(:faceted_search)
|
|
51
51
|
when /Elasticsearch/
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
# Log subscriber for Rails integration
|
|
5
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
6
|
+
def search(event)
|
|
7
|
+
return unless logger.debug?
|
|
8
|
+
|
|
9
|
+
indexes = event.payload[:indexes]&.join(", ") || "unknown"
|
|
10
|
+
duration = event.duration.round(2)
|
|
11
|
+
|
|
12
|
+
debug "Noiseless Search (#{duration}ms) indexes=[#{indexes}] #{query_summary(event.payload[:query])}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def bulk(event)
|
|
16
|
+
return unless logger.debug?
|
|
17
|
+
|
|
18
|
+
actions_count = event.payload[:actions_count] || 0
|
|
19
|
+
duration = event.duration.round(2)
|
|
20
|
+
|
|
21
|
+
debug "Noiseless Bulk (#{duration}ms) actions=#{actions_count}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def index_document(event)
|
|
25
|
+
return unless logger.debug?
|
|
26
|
+
|
|
27
|
+
index = event.payload[:index]
|
|
28
|
+
id = event.payload[:id]
|
|
29
|
+
duration = event.duration.round(2)
|
|
30
|
+
|
|
31
|
+
debug "Noiseless Index Document (#{duration}ms) index=#{index} id=#{id}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_document(event)
|
|
35
|
+
return unless logger.debug?
|
|
36
|
+
|
|
37
|
+
index = event.payload[:index]
|
|
38
|
+
id = event.payload[:id]
|
|
39
|
+
changes_count = event.payload[:changes_count] || 0
|
|
40
|
+
duration = event.duration.round(2)
|
|
41
|
+
|
|
42
|
+
debug "Noiseless Update Document (#{duration}ms) index=#{index} id=#{id} changes=#{changes_count}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_document(event)
|
|
46
|
+
return unless logger.debug?
|
|
47
|
+
|
|
48
|
+
index = event.payload[:index]
|
|
49
|
+
id = event.payload[:id]
|
|
50
|
+
duration = event.duration.round(2)
|
|
51
|
+
|
|
52
|
+
debug "Noiseless Delete Document (#{duration}ms) index=#{index} id=#{id}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create_index(event)
|
|
56
|
+
return unless logger.debug?
|
|
57
|
+
|
|
58
|
+
index = event.payload[:index]
|
|
59
|
+
duration = event.duration.round(2)
|
|
60
|
+
|
|
61
|
+
debug "Noiseless Create Index (#{duration}ms) index=#{index}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def delete_index(event)
|
|
65
|
+
return unless logger.debug?
|
|
66
|
+
|
|
67
|
+
index = event.payload[:index]
|
|
68
|
+
duration = event.duration.round(2)
|
|
69
|
+
|
|
70
|
+
debug "Noiseless Delete Index (#{duration}ms) index=#{index}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def query_summary(query)
|
|
76
|
+
return "empty" unless query.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
parts = []
|
|
79
|
+
|
|
80
|
+
if query[:query]&.dig(:bool, :must)&.any?
|
|
81
|
+
must_count = query[:query][:bool][:must].size
|
|
82
|
+
parts << "must:#{must_count}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if query[:query]&.dig(:bool, :filter)&.any?
|
|
86
|
+
filter_count = query[:query][:bool][:filter].size
|
|
87
|
+
parts << "filter:#{filter_count}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if query[:sort]&.any?
|
|
91
|
+
sort_count = query[:sort].size
|
|
92
|
+
parts << "sort:#{sort_count}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if query[:from] || query[:size]
|
|
96
|
+
parts << "from:#{query[:from] || 0}"
|
|
97
|
+
parts << "size:#{query[:size] || 20}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
parts.join(" ")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/noiseless/model.rb
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
module Noiseless
|
|
4
4
|
class Model
|
|
5
5
|
extend DSL::ClassMethods
|
|
6
|
+
extend QueryDelegation
|
|
7
|
+
|
|
8
|
+
delegate_query_methods :match, :multi_match, :filter, :range, :sort, :limit, :offset, :paginate,
|
|
9
|
+
:aggregation, :geo_distance, :indexes, :remove_duplicates, :facet_sample_slope, :pinned_hits,
|
|
10
|
+
to: :@builder
|
|
11
|
+
|
|
12
|
+
# Explicit mapping: index names may be aliases, not record class names.
|
|
13
|
+
# String names are resolved on read so Rails can reload the record class.
|
|
14
|
+
def self.record_class(value = nil)
|
|
15
|
+
unless value.nil?
|
|
16
|
+
raise ArgumentError, "record_class must be a Class or a class name String" unless value.is_a?(Class) || value.is_a?(String)
|
|
17
|
+
|
|
18
|
+
return @record_class = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
configured = @record_class
|
|
22
|
+
configured ||= superclass.record_class unless self == Model
|
|
23
|
+
configured.is_a?(String) ? configured.constantize : configured
|
|
24
|
+
end
|
|
6
25
|
|
|
7
26
|
def initialize
|
|
8
27
|
@builder = QueryBuilder.new(self.class)
|
|
@@ -23,77 +42,6 @@ module Noiseless
|
|
|
23
42
|
end
|
|
24
43
|
end
|
|
25
44
|
|
|
26
|
-
# Instance methods that delegate to the query builder
|
|
27
|
-
def match(field, value, **)
|
|
28
|
-
@builder.match(field, value, **)
|
|
29
|
-
self
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def multi_match(query, fields, **)
|
|
33
|
-
@builder.multi_match(query, fields, **)
|
|
34
|
-
self
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def filter(field, value, **)
|
|
38
|
-
@builder.filter(field, value, **)
|
|
39
|
-
self
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def range(field, gte: nil, lte: nil, gt: nil, lt: nil)
|
|
43
|
-
@builder.range(field, gte: gte, lte: lte, gt: gt, lt: lt)
|
|
44
|
-
self
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def sort(field, direction = :asc, **)
|
|
48
|
-
@builder.sort(field, direction, **)
|
|
49
|
-
self
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def limit(size)
|
|
53
|
-
@builder.limit(size)
|
|
54
|
-
self
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def offset(from)
|
|
58
|
-
@builder.offset(from)
|
|
59
|
-
self
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def paginate(page: nil, per_page: nil)
|
|
63
|
-
@builder.paginate(page: page, per_page: per_page)
|
|
64
|
-
self
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def aggregation(name, type, **)
|
|
68
|
-
@builder.aggregation(name, type, **)
|
|
69
|
-
self
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def geo_distance(field, lat:, lon:, distance:, **)
|
|
73
|
-
@builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
|
|
74
|
-
self
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def indexes(names)
|
|
78
|
-
@builder.indexes(names)
|
|
79
|
-
self
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def remove_duplicates(value: true)
|
|
83
|
-
@builder.remove_duplicates(value: value)
|
|
84
|
-
self
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def facet_sample_slope(value)
|
|
88
|
-
@builder.facet_sample_slope(value)
|
|
89
|
-
self
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def pinned_hits(value)
|
|
93
|
-
@builder.pinned_hits(value)
|
|
94
|
-
self
|
|
95
|
-
end
|
|
96
|
-
|
|
97
45
|
def execute(connection: nil, response_type: nil)
|
|
98
46
|
client = Noiseless.connections.client(connection || self.class.connection)
|
|
99
47
|
ast = @builder.to_ast
|
|
@@ -17,7 +17,7 @@ module Noiseless
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
# Index models by their search index names
|
|
20
|
-
index_names =
|
|
20
|
+
index_names = QueryBuilder.indexes_for(model_class)
|
|
21
21
|
index_names.each do |index_name|
|
|
22
22
|
@models_by_index[index_name.to_sym] ||= []
|
|
23
23
|
@models_by_index[index_name.to_sym] << model_class
|
|
@@ -67,11 +67,5 @@ module Noiseless
|
|
|
67
67
|
@models.clear
|
|
68
68
|
@models_by_index.clear
|
|
69
69
|
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def default_index_name(model_class)
|
|
74
|
-
model_class.name.demodulize.underscore.pluralize
|
|
75
|
-
end
|
|
76
70
|
end
|
|
77
71
|
end
|
|
@@ -2,72 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Noiseless
|
|
4
4
|
class MultiSearch
|
|
5
|
+
extend QueryDelegation
|
|
6
|
+
|
|
7
|
+
delegate_query_methods :match, :multi_match, :filter, :sort, :limit, :offset, :aggregation,
|
|
8
|
+
:geo_distance, :remove_duplicates, :facet_sample_slope, :pinned_hits,
|
|
9
|
+
to: :@builder
|
|
10
|
+
|
|
5
11
|
def initialize(models: nil, indexes: nil, connection: nil)
|
|
6
12
|
@models = resolve_models(models)
|
|
7
13
|
@indexes = resolve_indexes(indexes)
|
|
8
14
|
@connection = connection || Noiseless.config.default_connection
|
|
9
|
-
@builder = QueryBuilder.new(nil)
|
|
15
|
+
@builder = QueryBuilder.new(nil).indexes(@indexes)
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
def search(&block)
|
|
13
19
|
yield(@builder) if block
|
|
14
20
|
|
|
15
21
|
client = Noiseless.connections.client(@connection)
|
|
16
|
-
ast = @builder.to_ast
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
bool: ast.bool,
|
|
22
|
-
sort: ast.sort,
|
|
23
|
-
paginate: ast.paginate,
|
|
24
|
-
vector: ast.vector,
|
|
25
|
-
collapse: ast.collapse,
|
|
26
|
-
search_after: ast.search_after,
|
|
27
|
-
aggregations: ast.aggregations,
|
|
28
|
-
hybrid: ast.hybrid,
|
|
29
|
-
pipeline: ast.pipeline,
|
|
30
|
-
image_query: ast.image_query,
|
|
31
|
-
conversation: ast.conversation,
|
|
32
|
-
joins: ast.joins,
|
|
33
|
-
remove_duplicates: ast.remove_duplicates,
|
|
34
|
-
facet_sample_slope: ast.facet_sample_slope,
|
|
35
|
-
pinned_hits: ast.pinned_hits
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
raw_response = client.search(ast_with_indexes)
|
|
39
|
-
MultiSearchResponse.new(raw_response, @models, @indexes)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Delegate query building methods to the internal builder
|
|
43
|
-
def match(field, value, **)
|
|
44
|
-
@builder.match(field, value, **)
|
|
45
|
-
self
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def multi_match(query, fields, **)
|
|
49
|
-
@builder.multi_match(query, fields, **)
|
|
50
|
-
self
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def filter(field, value, **)
|
|
54
|
-
@builder.filter(field, value, **)
|
|
55
|
-
self
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def sort(field, direction = :asc, **)
|
|
59
|
-
@builder.sort(field, direction, **)
|
|
60
|
-
self
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def limit(size)
|
|
64
|
-
@builder.limit(size)
|
|
65
|
-
self
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def offset(from)
|
|
69
|
-
@builder.offset(from)
|
|
70
|
-
self
|
|
22
|
+
ast = @builder.indexes(@indexes).to_ast
|
|
23
|
+
response = Sync { client.search(ast, response_type: :results).wait }
|
|
24
|
+
MultiSearchResponse.new(response.response, @models, @indexes).tap do |result|
|
|
25
|
+
result.include_pagination_info(Pagination.query_options(ast.paginate))
|
|
26
|
+
end
|
|
71
27
|
end
|
|
72
28
|
|
|
73
29
|
def paginate(page, per_page)
|
|
@@ -75,31 +31,6 @@ module Noiseless
|
|
|
75
31
|
self
|
|
76
32
|
end
|
|
77
33
|
|
|
78
|
-
def aggregation(name, type, **)
|
|
79
|
-
@builder.aggregation(name, type, **)
|
|
80
|
-
self
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def geo_distance(field, lat:, lon:, distance:, **)
|
|
84
|
-
@builder.geo_distance(field, lat: lat, lon: lon, distance: distance, **)
|
|
85
|
-
self
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def remove_duplicates(value: true)
|
|
89
|
-
@builder.remove_duplicates(value: value)
|
|
90
|
-
self
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def facet_sample_slope(value)
|
|
94
|
-
@builder.facet_sample_slope(value)
|
|
95
|
-
self
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def pinned_hits(value)
|
|
99
|
-
@builder.pinned_hits(value)
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
|
|
103
34
|
private
|
|
104
35
|
|
|
105
36
|
def resolve_models(models)
|
|
@@ -125,9 +56,7 @@ module Noiseless
|
|
|
125
56
|
def resolve_indexes(indexes)
|
|
126
57
|
case indexes
|
|
127
58
|
when nil
|
|
128
|
-
@models.flat_map
|
|
129
|
-
Array(model.search_index || default_index_name(model))
|
|
130
|
-
end.uniq
|
|
59
|
+
@models.flat_map { |model| QueryBuilder.indexes_for(model) }.uniq
|
|
131
60
|
when Array
|
|
132
61
|
indexes.map(&:to_s)
|
|
133
62
|
when String, Symbol
|
|
@@ -136,10 +65,6 @@ module Noiseless
|
|
|
136
65
|
[]
|
|
137
66
|
end
|
|
138
67
|
end
|
|
139
|
-
|
|
140
|
-
def default_index_name(model_class)
|
|
141
|
-
model_class.name.demodulize.underscore.pluralize
|
|
142
|
-
end
|
|
143
68
|
end
|
|
144
69
|
|
|
145
70
|
# Multi-search response class
|
|
@@ -149,6 +74,10 @@ module Noiseless
|
|
|
149
74
|
@models = models
|
|
150
75
|
@indexes = indexes
|
|
151
76
|
@results_by_model = nil
|
|
77
|
+
@models_by_index = {}
|
|
78
|
+
models.each do |model|
|
|
79
|
+
QueryBuilder.indexes_for(model).each { |index| @models_by_index[index] ||= model }
|
|
80
|
+
end
|
|
152
81
|
end
|
|
153
82
|
|
|
154
83
|
def each(&)
|
|
@@ -203,17 +132,8 @@ module Noiseless
|
|
|
203
132
|
end
|
|
204
133
|
|
|
205
134
|
def determine_model_class(hit)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# First, try to find models registered for this specific index
|
|
209
|
-
models_for_index = Noiseless.registry.models_for_index(index_name)
|
|
210
|
-
return models_for_index.first if models_for_index.size == 1
|
|
211
|
-
|
|
212
|
-
# If multiple models or none found, try to infer from index name
|
|
213
|
-
@models.find do |model|
|
|
214
|
-
model_indexes = Array(model.search_index || default_index_name(model))
|
|
215
|
-
model_indexes.include?(index_name)
|
|
216
|
-
end
|
|
135
|
+
# Only group into requested models, using the same index rules as search.
|
|
136
|
+
@models_by_index[hit["_index"]]
|
|
217
137
|
end
|
|
218
138
|
|
|
219
139
|
def load_records_by_model
|
|
@@ -221,24 +141,14 @@ module Noiseless
|
|
|
221
141
|
|
|
222
142
|
results_by_model.each do |model_key, model_hits|
|
|
223
143
|
model_class = @models.find { |m| m.name.to_sym == model_key }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
ids = model_hits.map { |hit| hit["_id"] }
|
|
227
|
-
loaded_records = model_class.where(id: ids).to_a
|
|
228
|
-
|
|
229
|
-
# Sort by search relevance
|
|
230
|
-
sorted_records = model_hits.filter_map do |hit|
|
|
231
|
-
loaded_records.find { |record| record.id.to_s == hit["_id"].to_s }
|
|
232
|
-
end
|
|
144
|
+
record_class = RecordClassResolver.resolve(model_class)
|
|
145
|
+
next unless record_class.respond_to?(:where)
|
|
233
146
|
|
|
234
|
-
|
|
147
|
+
raw = { "hits" => { "hits" => model_hits, "total" => model_hits.size } }
|
|
148
|
+
records[model_key] = Response::Records.new(raw, record_class).to_a
|
|
235
149
|
end
|
|
236
150
|
|
|
237
151
|
records
|
|
238
152
|
end
|
|
239
|
-
|
|
240
|
-
def default_index_name(model_class)
|
|
241
|
-
model_class.name.demodulize.underscore.pluralize
|
|
242
|
-
end
|
|
243
153
|
end
|
|
244
154
|
end
|