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.
- checksums.yaml +4 -4
- data/LICENSE.txt +28 -0
- data/README.md +214 -0
- data/lib/application_search.rb +15 -0
- data/lib/noiseless/adapter.rb +313 -0
- data/lib/noiseless/adapters/elasticsearch.rb +70 -0
- data/lib/noiseless/adapters/execution_modules/elasticsearch_execution.rb +188 -0
- data/lib/noiseless/adapters/execution_modules/opensearch_execution.rb +377 -0
- data/lib/noiseless/adapters/execution_modules/pgvector_support.rb +219 -0
- data/lib/noiseless/adapters/execution_modules/postgresql_execution.rb +461 -0
- data/lib/noiseless/adapters/execution_modules/typesense_execution.rb +472 -0
- data/lib/noiseless/adapters/open_search.rb +208 -0
- data/lib/noiseless/adapters/postgresql.rb +171 -0
- data/lib/noiseless/adapters/typesense.rb +70 -0
- data/lib/noiseless/adapters.rb +14 -0
- data/lib/noiseless/ast/aggregation.rb +56 -0
- data/lib/noiseless/ast/bool.rb +16 -0
- data/lib/noiseless/ast/bulk.rb +18 -0
- data/lib/noiseless/ast/collapse.rb +16 -0
- data/lib/noiseless/ast/combined_fields.rb +33 -0
- data/lib/noiseless/ast/conversation.rb +29 -0
- data/lib/noiseless/ast/filter.rb +15 -0
- data/lib/noiseless/ast/hybrid.rb +35 -0
- data/lib/noiseless/ast/image_query.rb +29 -0
- data/lib/noiseless/ast/join.rb +31 -0
- data/lib/noiseless/ast/match.rb +15 -0
- data/lib/noiseless/ast/multi_match.rb +24 -0
- data/lib/noiseless/ast/paginate.rb +15 -0
- data/lib/noiseless/ast/prefix.rb +15 -0
- data/lib/noiseless/ast/range.rb +18 -0
- data/lib/noiseless/ast/root.rb +69 -0
- data/lib/noiseless/ast/search_after.rb +14 -0
- data/lib/noiseless/ast/sort.rb +15 -0
- data/lib/noiseless/ast/vector.rb +27 -0
- data/lib/noiseless/ast/wildcard.rb +15 -0
- data/lib/noiseless/ast.rb +30 -0
- data/lib/noiseless/bulk_importer.rb +195 -0
- data/lib/noiseless/callbacks.rb +138 -0
- data/lib/noiseless/connection_manager.rb +26 -0
- data/lib/noiseless/document_manager.rb +137 -0
- data/lib/noiseless/dsl.rb +107 -0
- data/lib/noiseless/generators/application_search_generator.rb +24 -0
- data/lib/noiseless/instrumentation.rb +174 -0
- data/lib/noiseless/introspection/console.rb +228 -0
- data/lib/noiseless/introspection/query_visualizer.rb +533 -0
- data/lib/noiseless/introspection.rb +221 -0
- data/lib/noiseless/mapping.rb +253 -0
- data/lib/noiseless/mapping_definition_processor.rb +231 -0
- data/lib/noiseless/model.rb +111 -0
- data/lib/noiseless/model_registry.rb +77 -0
- data/lib/noiseless/multi_search.rb +244 -0
- data/lib/noiseless/pagination.rb +375 -0
- data/lib/noiseless/query_builder.rb +284 -0
- data/lib/noiseless/railtie.rb +35 -0
- data/lib/noiseless/response/aggregations.rb +46 -0
- data/lib/noiseless/response/empty.rb +20 -0
- data/lib/noiseless/response/records.rb +94 -0
- data/lib/noiseless/response/results.rb +110 -0
- data/lib/noiseless/response/suggestions.rb +55 -0
- data/lib/noiseless/response.rb +98 -0
- data/lib/noiseless/response_factory.rb +32 -0
- data/lib/noiseless/runtime_reset_middleware.rb +15 -0
- data/lib/noiseless/search_index_update_job.rb +84 -0
- data/lib/noiseless/test_case.rb +230 -0
- data/lib/noiseless/test_helper.rb +295 -0
- data/lib/noiseless/version.rb +2 -2
- data/lib/noiseless.rb +130 -2
- data/lib/tasks/benchmark.rake +35 -0
- data/lib/tasks/release.rake +22 -0
- data/lib/tasks/test.rake +11 -0
- metadata +260 -14
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module DSL
|
|
5
|
+
module ClassMethods
|
|
6
|
+
def search_index(*names)
|
|
7
|
+
@index_names = names.flatten.map(&:to_s) if names.any?
|
|
8
|
+
@index_names
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def index_name(name = nil)
|
|
12
|
+
@index_name = name.to_s if name
|
|
13
|
+
@index_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def searchable_fields(*fields)
|
|
17
|
+
@searchable_fields = fields if fields.any?
|
|
18
|
+
@searchable_fields
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def adapter(name = nil)
|
|
22
|
+
@adapter_name = name if name
|
|
23
|
+
@adapter_name || Noiseless.config.default_adapter
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def connection(name = nil)
|
|
27
|
+
@connection_name = name if name
|
|
28
|
+
@connection_name || Noiseless.config.default_connection
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def mapping(&block)
|
|
32
|
+
@mapping_block = block if block
|
|
33
|
+
@mapping_block
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def import(*, **)
|
|
37
|
+
BulkImporter.new(self).import(*, **)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def import_scoped(scope, **)
|
|
41
|
+
BulkImporter.new(self).import_scoped(scope, **)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reindex(**)
|
|
45
|
+
BulkImporter.new(self).reindex(**)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bulk_importer(connection: nil)
|
|
49
|
+
BulkImporter.new(self, connection: connection)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def searchable(**)
|
|
53
|
+
include Callbacks unless included_modules.include?(Callbacks)
|
|
54
|
+
include DSL::InstanceMethods unless included_modules.include?(DSL::InstanceMethods)
|
|
55
|
+
|
|
56
|
+
auto_index(true, **)
|
|
57
|
+
|
|
58
|
+
# Register the model in the global registry
|
|
59
|
+
Noiseless.register_model(self, searchable: true, **)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def multi_search(models: nil, indexes: nil, connection: nil, &block)
|
|
63
|
+
search_instance = MultiSearch.new(
|
|
64
|
+
models: models || [self],
|
|
65
|
+
indexes: indexes,
|
|
66
|
+
connection: connection || self.connection
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if block
|
|
70
|
+
search_instance.search(&block)
|
|
71
|
+
else
|
|
72
|
+
search_instance
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def page(num = nil)
|
|
77
|
+
Pagination::SearchPaginator.new(self, page: num)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def per(num)
|
|
81
|
+
Pagination::SearchPaginator.new(self, per_page: num)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
module InstanceMethods
|
|
86
|
+
def index_document(**)
|
|
87
|
+
DocumentManager.new(self).index_document(**)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def update_document(**)
|
|
91
|
+
DocumentManager.new(self).update_document(**)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete_document(**)
|
|
95
|
+
DocumentManager.new(self).delete_document(**)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def document_exists?
|
|
99
|
+
DocumentManager.new(self).document_exists?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def document_manager(connection: nil)
|
|
103
|
+
DocumentManager.new(self, connection: connection)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Noiseless
|
|
6
|
+
module Generators
|
|
7
|
+
class ApplicationSearchGenerator < Rails::Generators::Base
|
|
8
|
+
desc "Generate ApplicationSearch class for your application"
|
|
9
|
+
|
|
10
|
+
source_root File.expand_path("templates", __dir__)
|
|
11
|
+
|
|
12
|
+
def create_application_search
|
|
13
|
+
create_file "app/search/application_search.rb", <<~RUBY
|
|
14
|
+
# frozen_string_literal: true
|
|
15
|
+
|
|
16
|
+
# Base class for all search models
|
|
17
|
+
class ApplicationSearch < Noiseless::Model
|
|
18
|
+
# Inherits static and dynamic search methods using default_connection
|
|
19
|
+
end
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
# Instrumentation via ActiveSupport
|
|
5
|
+
module Instrumentation
|
|
6
|
+
def instrument(event, payload = {})
|
|
7
|
+
start_time = Time.current
|
|
8
|
+
payload = payload.merge(
|
|
9
|
+
adapter: self.class.name,
|
|
10
|
+
connection: connection_info,
|
|
11
|
+
start_time: start_time
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
result = ActiveSupport::Notifications.instrument("noiseless.#{event}", payload) do
|
|
15
|
+
yield if block_given?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Update runtime tracking for Rails
|
|
19
|
+
add_to_runtime(Time.current - start_time) if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
20
|
+
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def connection_info
|
|
27
|
+
{
|
|
28
|
+
hosts: @hosts&.take(3), # Limit to first 3 hosts for brevity
|
|
29
|
+
adapter_class: self.class.name
|
|
30
|
+
}
|
|
31
|
+
rescue StandardError
|
|
32
|
+
{ adapter_class: self.class.name }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_to_runtime(duration)
|
|
36
|
+
Thread.current[:noiseless_runtime] ||= 0
|
|
37
|
+
Thread.current[:noiseless_runtime] += duration * 1000 # Convert to milliseconds
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Log subscriber for Rails integration
|
|
42
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
43
|
+
def search(event)
|
|
44
|
+
return unless logger.debug?
|
|
45
|
+
|
|
46
|
+
indexes = event.payload[:indexes]&.join(", ") || "unknown"
|
|
47
|
+
duration = event.duration.round(2)
|
|
48
|
+
|
|
49
|
+
debug "Noiseless Search (#{duration}ms) indexes=[#{indexes}] #{query_summary(event.payload[:query])}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bulk(event)
|
|
53
|
+
return unless logger.debug?
|
|
54
|
+
|
|
55
|
+
actions_count = event.payload[:actions_count] || 0
|
|
56
|
+
duration = event.duration.round(2)
|
|
57
|
+
|
|
58
|
+
debug "Noiseless Bulk (#{duration}ms) actions=#{actions_count}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def index_document(event)
|
|
62
|
+
return unless logger.debug?
|
|
63
|
+
|
|
64
|
+
index = event.payload[:index]
|
|
65
|
+
id = event.payload[:id]
|
|
66
|
+
duration = event.duration.round(2)
|
|
67
|
+
|
|
68
|
+
debug "Noiseless Index Document (#{duration}ms) index=#{index} id=#{id}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_document(event)
|
|
72
|
+
return unless logger.debug?
|
|
73
|
+
|
|
74
|
+
index = event.payload[:index]
|
|
75
|
+
id = event.payload[:id]
|
|
76
|
+
changes_count = event.payload[:changes_count] || 0
|
|
77
|
+
duration = event.duration.round(2)
|
|
78
|
+
|
|
79
|
+
debug "Noiseless Update Document (#{duration}ms) index=#{index} id=#{id} changes=#{changes_count}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def delete_document(event)
|
|
83
|
+
return unless logger.debug?
|
|
84
|
+
|
|
85
|
+
index = event.payload[:index]
|
|
86
|
+
id = event.payload[:id]
|
|
87
|
+
duration = event.duration.round(2)
|
|
88
|
+
|
|
89
|
+
debug "Noiseless Delete Document (#{duration}ms) index=#{index} id=#{id}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_index(event)
|
|
93
|
+
return unless logger.debug?
|
|
94
|
+
|
|
95
|
+
index = event.payload[:index]
|
|
96
|
+
duration = event.duration.round(2)
|
|
97
|
+
|
|
98
|
+
debug "Noiseless Create Index (#{duration}ms) index=#{index}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def delete_index(event)
|
|
102
|
+
return unless logger.debug?
|
|
103
|
+
|
|
104
|
+
index = event.payload[:index]
|
|
105
|
+
duration = event.duration.round(2)
|
|
106
|
+
|
|
107
|
+
debug "Noiseless Delete Index (#{duration}ms) index=#{index}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def query_summary(query)
|
|
113
|
+
return "empty" unless query.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
parts = []
|
|
116
|
+
|
|
117
|
+
if query[:query]&.dig(:bool, :must)&.any?
|
|
118
|
+
must_count = query[:query][:bool][:must].size
|
|
119
|
+
parts << "must:#{must_count}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if query[:query]&.dig(:bool, :filter)&.any?
|
|
123
|
+
filter_count = query[:query][:bool][:filter].size
|
|
124
|
+
parts << "filter:#{filter_count}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if query[:sort]&.any?
|
|
128
|
+
sort_count = query[:sort].size
|
|
129
|
+
parts << "sort:#{sort_count}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if query[:from] || query[:size]
|
|
133
|
+
parts << "from:#{query[:from] || 0}"
|
|
134
|
+
parts << "size:#{query[:size] || 20}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
parts.join(" ")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Runtime tracking for Rails
|
|
142
|
+
module ControllerRuntime
|
|
143
|
+
extend ActiveSupport::Concern
|
|
144
|
+
|
|
145
|
+
protected
|
|
146
|
+
|
|
147
|
+
def append_info_to_payload(payload)
|
|
148
|
+
super
|
|
149
|
+
payload[:noiseless_runtime] = noiseless_runtime
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def cleanup_view_runtime
|
|
153
|
+
runtime_before_render = noiseless_runtime
|
|
154
|
+
runtime = super
|
|
155
|
+
runtime_after_render = noiseless_runtime
|
|
156
|
+
runtime + runtime_after_render - runtime_before_render
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def noiseless_runtime
|
|
162
|
+
Thread.current[:noiseless_runtime] ||= 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
module ClassMethods
|
|
166
|
+
def log_process_action(payload)
|
|
167
|
+
messages = super
|
|
168
|
+
runtime = payload[:noiseless_runtime]
|
|
169
|
+
messages << ("Noiseless: %.1fms" % runtime) if runtime&.positive?
|
|
170
|
+
messages
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Introspection
|
|
5
|
+
class Console
|
|
6
|
+
def self.inspect_adapter(adapter)
|
|
7
|
+
puts "[INSPECT] Adapter Introspection"
|
|
8
|
+
puts "=" * 50
|
|
9
|
+
|
|
10
|
+
info = adapter.adapter_info
|
|
11
|
+
|
|
12
|
+
puts "Adapter Type: #{info[:adapter_type]}"
|
|
13
|
+
puts "Execution Mode: #{info[:execution_mode]}"
|
|
14
|
+
puts "Engine Name: #{info[:engine_name]}"
|
|
15
|
+
puts "Capabilities: #{info[:capabilities].join(', ')}"
|
|
16
|
+
|
|
17
|
+
puts "\nExecution Modules:"
|
|
18
|
+
info[:execution_module].each do |mod|
|
|
19
|
+
puts " - #{mod[:name]}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
puts "\n#{'=' * 50}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.compare_query_across_engines(ast_node, **)
|
|
26
|
+
puts "[COMPARE] Cross-Engine Query Comparison"
|
|
27
|
+
puts "=" * 60
|
|
28
|
+
|
|
29
|
+
comparison = QueryVisualizer.compare_across_engines(ast_node, **)
|
|
30
|
+
|
|
31
|
+
puts "Original AST:"
|
|
32
|
+
puts JSON.pretty_generate(comparison[:original_ast])
|
|
33
|
+
|
|
34
|
+
puts "\nEngine Translations:"
|
|
35
|
+
comparison[:engine_translations].each do |engine, data|
|
|
36
|
+
puts "\n#{engine.to_s.humanize}:"
|
|
37
|
+
if data[:error]
|
|
38
|
+
puts " [ERROR] #{data[:error]}"
|
|
39
|
+
else
|
|
40
|
+
puts " [OK] Available"
|
|
41
|
+
puts " Performance Score: #{data[:estimated_performance][:estimated_score]}"
|
|
42
|
+
puts " Query Differences: #{data[:query_differences].size} found"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "\nCompatibility Analysis:"
|
|
47
|
+
analysis = comparison[:compatibility_analysis]
|
|
48
|
+
puts " Compatible Engines: #{analysis[:compatible_engines].join(', ')}"
|
|
49
|
+
puts " Common Features: #{analysis[:common_features]&.join(', ') || 'None'}"
|
|
50
|
+
|
|
51
|
+
if analysis[:potential_issues].any?
|
|
52
|
+
puts " [WARN] Potential Issues:"
|
|
53
|
+
analysis[:potential_issues].each do |issue|
|
|
54
|
+
puts " - #{issue[:description]} (#{issue[:severity]})"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
puts "\nRecommendations:"
|
|
59
|
+
comparison[:recommendations].each do |rec|
|
|
60
|
+
puts " [TIP] #{rec[:recommendation]}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
puts "\n#{'=' * 60}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.visualize_ast(ast_node, format: :tree)
|
|
67
|
+
puts "[AST] Visualization (#{format})"
|
|
68
|
+
puts "=" * 40
|
|
69
|
+
|
|
70
|
+
visualization = QueryVisualizer.visualize_ast(ast_node, format: format)
|
|
71
|
+
puts visualization
|
|
72
|
+
|
|
73
|
+
puts "=" * 40
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.explain_query_execution(ast_node, adapter)
|
|
77
|
+
puts "[EXPLAIN] Query Execution Explanation"
|
|
78
|
+
puts "=" * 50
|
|
79
|
+
|
|
80
|
+
flow_info = QueryVisualizer.explain_query_flow(ast_node, adapter)
|
|
81
|
+
explanation = flow_info[:explanation]
|
|
82
|
+
|
|
83
|
+
puts "Adapter: #{explanation[:adapter][:adapter_type]} (#{explanation[:adapter][:execution_mode]})"
|
|
84
|
+
puts "Engine: #{explanation[:adapter][:engine_name]}"
|
|
85
|
+
|
|
86
|
+
puts "\nExecution Plan:"
|
|
87
|
+
explanation[:execution_plan].each_with_index do |step, index|
|
|
88
|
+
puts " #{index + 1}. #{step[:description]}"
|
|
89
|
+
puts " Cost: #{step[:estimated_cost]}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
puts "\nPerformance Breakdown:"
|
|
93
|
+
flow_info[:performance_breakdown].each do |metric|
|
|
94
|
+
puts " #{metric[:metric]}: #{metric[:value]} #{metric[:unit]}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if flow_info[:optimization_suggestions].any?
|
|
98
|
+
puts "\nOptimization Suggestions:"
|
|
99
|
+
flow_info[:optimization_suggestions].each do |suggestion|
|
|
100
|
+
puts " [TIP] #{suggestion[:suggestion]} (Impact: #{suggestion[:impact]})"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
puts "\n#{'=' * 50}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.profile_query_performance(ast_node, adapter, iterations: 10)
|
|
108
|
+
puts "[PROFILE] Query Performance Profile"
|
|
109
|
+
puts "=" * 50
|
|
110
|
+
|
|
111
|
+
puts "Running #{iterations} iterations on #{adapter.adapter_info[:adapter_type]} (#{adapter.adapter_info[:execution_mode]})..."
|
|
112
|
+
|
|
113
|
+
profile = adapter.profile_query(ast_node, iterations: iterations)
|
|
114
|
+
summary = profile[:summary]
|
|
115
|
+
|
|
116
|
+
puts "\nPerformance Summary:"
|
|
117
|
+
puts " Minimum: #{summary[:min_ms]}ms"
|
|
118
|
+
puts " Maximum: #{summary[:max_ms]}ms"
|
|
119
|
+
puts " Average: #{summary[:avg_ms]}ms"
|
|
120
|
+
puts " Median: #{summary[:median_ms]}ms"
|
|
121
|
+
puts " Std Dev: #{summary[:std_dev_ms]}ms"
|
|
122
|
+
|
|
123
|
+
# Show distribution
|
|
124
|
+
puts "\nTime Distribution:"
|
|
125
|
+
bins = create_histogram_bins(profile[:measurements].pluck(:total_time_ms))
|
|
126
|
+
bins.each do |bin|
|
|
127
|
+
bar = "█" * (bin[:count] * 50 / iterations)
|
|
128
|
+
puts " #{bin[:range]}: #{bar} (#{bin[:count]})"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
puts "\n#{'=' * 50}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.compatibility_matrix
|
|
135
|
+
puts "[MATRIX] Adapter Compatibility Matrix"
|
|
136
|
+
puts "=" * 60
|
|
137
|
+
|
|
138
|
+
matrix = Noiseless::Adapter.new.compatibility_matrix
|
|
139
|
+
|
|
140
|
+
matrix.each do |adapter_key, info|
|
|
141
|
+
status = info[:available] ? "[OK]" : "[X]"
|
|
142
|
+
name = adapter_key.to_s.humanize
|
|
143
|
+
|
|
144
|
+
puts "#{status} #{name}"
|
|
145
|
+
if info[:available]
|
|
146
|
+
puts " Engine: #{info[:engine_name]}"
|
|
147
|
+
puts " Capabilities: #{info[:capabilities].join(', ')}"
|
|
148
|
+
else
|
|
149
|
+
puts " Error: #{info[:error]}"
|
|
150
|
+
end
|
|
151
|
+
puts
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
puts "=" * 60
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.interactive_mode
|
|
158
|
+
puts "[INTERACTIVE] Noiseless Interactive Introspection Mode"
|
|
159
|
+
puts "Type 'help' for available commands or 'exit' to quit"
|
|
160
|
+
puts "=" * 60
|
|
161
|
+
|
|
162
|
+
loop do
|
|
163
|
+
print "noiseless> "
|
|
164
|
+
input = gets.chomp.strip
|
|
165
|
+
|
|
166
|
+
case input
|
|
167
|
+
when "help"
|
|
168
|
+
show_help
|
|
169
|
+
when "adapters"
|
|
170
|
+
compatibility_matrix
|
|
171
|
+
when "exit", "quit"
|
|
172
|
+
puts "Goodbye!"
|
|
173
|
+
break
|
|
174
|
+
when /^profile\s+(.+)/
|
|
175
|
+
# TODO: Parse query and run profile
|
|
176
|
+
puts "Profile command not yet implemented"
|
|
177
|
+
when /^compare\s+(.+)/
|
|
178
|
+
# TODO: Parse query and run comparison
|
|
179
|
+
puts "Compare command not yet implemented"
|
|
180
|
+
else
|
|
181
|
+
puts "Unknown command: #{input}. Type 'help' for available commands."
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.create_histogram_bins(values, bin_count: 5)
|
|
187
|
+
return [] if values.empty?
|
|
188
|
+
|
|
189
|
+
min_val = values.min
|
|
190
|
+
max_val = values.max
|
|
191
|
+
bin_size = (max_val - min_val) / bin_count.to_f
|
|
192
|
+
|
|
193
|
+
bins = Array.new(bin_count) do |i|
|
|
194
|
+
range_start = min_val + (i * bin_size)
|
|
195
|
+
range_end = min_val + ((i + 1) * bin_size)
|
|
196
|
+
{
|
|
197
|
+
range: "#{range_start.round(2)}-#{range_end.round(2)}ms",
|
|
198
|
+
count: 0
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
values.each do |value|
|
|
203
|
+
bin_index = ((value - min_val) / bin_size).floor
|
|
204
|
+
bin_index = [bin_index, bin_count - 1].min # Ensure we don't exceed bounds
|
|
205
|
+
bins[bin_index][:count] += 1
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
bins
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def self.show_help
|
|
212
|
+
puts <<~HELP
|
|
213
|
+
Available Commands:
|
|
214
|
+
|
|
215
|
+
help - Show this help message
|
|
216
|
+
adapters - Show adapter compatibility matrix
|
|
217
|
+
exit/quit - Exit interactive mode
|
|
218
|
+
|
|
219
|
+
Coming Soon:
|
|
220
|
+
profile <query> - Profile query performance
|
|
221
|
+
compare <query> - Compare query across engines
|
|
222
|
+
explain <query> - Explain query execution
|
|
223
|
+
visualize <query> - Visualize AST structure
|
|
224
|
+
HELP
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|