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,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