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,533 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "mermaid"
5
+ DIAGRAMS_AVAILABLE = true
6
+ rescue LoadError
7
+ DIAGRAMS_AVAILABLE = false
8
+ end
9
+
10
+ module Noiseless
11
+ module Introspection
12
+ class QueryVisualizer
13
+ def self.compare_across_engines(ast_node, **_opts)
14
+ adapters = [
15
+ { name: :elasticsearch, class: Noiseless::Adapters::Elasticsearch },
16
+ { name: :opensearch, class: Noiseless::Adapters::OpenSearch },
17
+ { name: :typesense, class: Noiseless::Adapters::Typesense }
18
+ ]
19
+
20
+ comparison = {
21
+ original_ast: ast_node.to_h,
22
+ engine_translations: {},
23
+ compatibility_analysis: {},
24
+ recommendations: []
25
+ }
26
+
27
+ adapters.each do |adapter_config|
28
+ adapter = adapter_config[:class].new
29
+
30
+ # Get the translated query
31
+ engine_query = adapter.send(:ast_to_hash, ast_node)
32
+
33
+ # Get adapter info
34
+ adapter_info = adapter.adapter_info
35
+
36
+ comparison[:engine_translations][adapter_config[:name]] = {
37
+ engine_query: engine_query,
38
+ adapter_info: adapter_info,
39
+ query_differences: analyze_query_differences(ast_node.to_h, engine_query),
40
+ estimated_performance: estimate_performance(adapter_info, engine_query)
41
+ }
42
+ rescue StandardError => e
43
+ comparison[:engine_translations][adapter_config[:name]] = {
44
+ error: e.message,
45
+ available: false
46
+ }
47
+ end
48
+
49
+ # Analyze compatibility across engines
50
+ comparison[:compatibility_analysis] = analyze_cross_engine_compatibility(comparison[:engine_translations])
51
+
52
+ # Generate recommendations
53
+ comparison[:recommendations] = generate_recommendations(comparison)
54
+
55
+ comparison
56
+ end
57
+
58
+ def self.visualize_ast(ast_node, format: :tree)
59
+ case format
60
+ when :tree
61
+ visualize_as_tree(ast_node.to_h)
62
+ when :json
63
+ JSON.pretty_generate(ast_node.to_h)
64
+ when :yaml
65
+ YAML.dump(ast_node.to_h)
66
+ when :mermaid
67
+ ast_to_mermaid_flowchart(ast_node)
68
+ when :mermaid_class
69
+ ast_to_mermaid_class_diagram(ast_node)
70
+ else
71
+ raise ArgumentError, "Unsupported format: #{format}. Available: :tree, :json, :yaml, :mermaid, :mermaid_class"
72
+ end
73
+ end
74
+
75
+ def self.explain_query_flow(ast_node, adapter)
76
+ explanation = adapter.explain_query(ast_node)
77
+
78
+ flow_diagram = generate_flow_diagram(explanation)
79
+
80
+ {
81
+ explanation: explanation,
82
+ visual_flow: flow_diagram,
83
+ performance_breakdown: format_performance_breakdown(explanation[:performance]),
84
+ optimization_suggestions: suggest_optimizations(explanation)
85
+ }
86
+ end
87
+
88
+ def self.analyze_query_differences(original_ast, engine_query)
89
+ differences = []
90
+
91
+ # Check for field mapping differences
92
+ original_fields = extract_fields_from_ast(original_ast)
93
+ engine_fields = extract_fields_from_query(engine_query)
94
+
95
+ if original_fields != engine_fields
96
+ differences << {
97
+ type: :field_mapping,
98
+ original: original_fields,
99
+ engine: engine_fields,
100
+ impact: :medium
101
+ }
102
+ end
103
+
104
+ # Check for query structure differences
105
+ if has_structural_differences?(original_ast, engine_query)
106
+ differences << {
107
+ type: :structural_change,
108
+ description: "Query structure adapted for engine compatibility",
109
+ impact: :low
110
+ }
111
+ end
112
+
113
+ differences
114
+ end
115
+
116
+ def self.estimate_performance(adapter_info, engine_query)
117
+ base_score = 100
118
+
119
+ # Adjust based on query complexity
120
+ complexity_penalty = calculate_query_complexity(engine_query) * 5
121
+ base_score -= complexity_penalty
122
+
123
+ # Adjust based on adapter capabilities
124
+ if adapter_info[:execution_mode] == :async
125
+ base_score += 10 # Async generally better for I/O
126
+ end
127
+
128
+ # Engine-specific adjustments
129
+ case adapter_info[:engine_name]
130
+ when :typesense
131
+ base_score += 15 # Generally faster for simple queries
132
+ when :elasticsearch, :opensearch
133
+ base_score += 5 # Good for complex queries
134
+ end
135
+
136
+ {
137
+ estimated_score: [base_score, 0].max,
138
+ factors: {
139
+ complexity_penalty: complexity_penalty,
140
+ async_bonus: adapter_info[:execution_mode] == :async ? 10 : 0,
141
+ engine_factor: case adapter_info[:engine_name]
142
+ when :typesense then 15
143
+ when :elasticsearch, :opensearch then 5
144
+ else 0
145
+ end
146
+ }
147
+ }
148
+ end
149
+
150
+ def self.analyze_cross_engine_compatibility(translations)
151
+ available_engines = translations.reject { |_, data| data.key?(:error) }
152
+
153
+ analysis = {
154
+ compatible_engines: available_engines.keys,
155
+ query_variations: {},
156
+ potential_issues: []
157
+ }
158
+
159
+ # Compare query structures across engines
160
+ queries = available_engines.transform_values { |data| data[:engine_query] }
161
+
162
+ if queries.values.uniq.size > 1
163
+ analysis[:query_variations] = queries
164
+ analysis[:potential_issues] << {
165
+ type: :query_structure_differences,
166
+ description: "Engines produce different query structures",
167
+ severity: :medium
168
+ }
169
+ end
170
+
171
+ # Check for feature compatibility
172
+ features_by_engine = available_engines.transform_values do |data|
173
+ data[:adapter_info][:capabilities]
174
+ end
175
+
176
+ common_features = features_by_engine.values.reduce(:&)
177
+ analysis[:common_features] = common_features
178
+
179
+ analysis
180
+ end
181
+
182
+ def self.generate_recommendations(comparison)
183
+ recommendations = []
184
+
185
+ # Performance recommendations
186
+ best_performance = comparison[:engine_translations]
187
+ .reject { |_, data| data.key?(:error) }
188
+ .max_by { |_, data| data[:estimated_performance][:estimated_score] }
189
+
190
+ if best_performance
191
+ recommendations << {
192
+ type: :performance,
193
+ recommendation: "Consider using #{best_performance[0]} for optimal performance",
194
+ score: best_performance[1][:estimated_performance][:estimated_score]
195
+ }
196
+ end
197
+
198
+ # Compatibility recommendations
199
+ if comparison[:compatibility_analysis][:potential_issues].any?
200
+ recommendations << {
201
+ type: :compatibility,
202
+ recommendation: "Query may behave differently across engines",
203
+ issues: comparison[:compatibility_analysis][:potential_issues]
204
+ }
205
+ end
206
+
207
+ recommendations
208
+ end
209
+
210
+ def self.visualize_as_tree(node, depth = 0)
211
+ indent = " " * depth
212
+
213
+ case node
214
+ when Hash
215
+ result = ""
216
+ node.each do |key, value|
217
+ result += "#{indent}#{key}:\n"
218
+ result += visualize_as_tree(value, depth + 1)
219
+ end
220
+ result
221
+ when Array
222
+ result = ""
223
+ node.each_with_index do |item, index|
224
+ result += "#{indent}[#{index}]:\n"
225
+ result += visualize_as_tree(item, depth + 1)
226
+ end
227
+ result
228
+ else
229
+ "#{indent}#{node}\n"
230
+ end
231
+ end
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
+ def self.format_performance_breakdown(performance)
296
+ performance.map do |metric, value|
297
+ {
298
+ metric: metric.to_s.humanize,
299
+ value: value,
300
+ unit: metric.to_s.end_with?("_ms") ? "milliseconds" : "unknown"
301
+ }
302
+ end
303
+ end
304
+
305
+ def self.suggest_optimizations(explanation)
306
+ suggestions = []
307
+
308
+ # Check for slow AST conversion
309
+ if explanation[:performance][:ast_conversion_ms] > 1.0
310
+ suggestions << {
311
+ type: :performance,
312
+ area: :ast_conversion,
313
+ suggestion: "AST conversion is slow. Consider simplifying the query structure.",
314
+ impact: :medium
315
+ }
316
+ end
317
+
318
+ suggestions
319
+ end
320
+
321
+ # Helper methods
322
+ def self.extract_fields_from_ast(ast)
323
+ fields = []
324
+ extract_recursive(ast, fields)
325
+ fields.uniq
326
+ end
327
+
328
+ def self.extract_recursive(node, fields)
329
+ case node
330
+ when Hash
331
+ fields << node["field"] if node["field"]
332
+ fields << node[:field] if node[:field]
333
+ node.each_value { |value| extract_recursive(value, fields) }
334
+ when Array
335
+ node.each { |item| extract_recursive(item, fields) }
336
+ end
337
+ end
338
+
339
+ def self.extract_fields_from_query(_query)
340
+ # This would need to be engine-specific
341
+ # For now, return empty array
342
+ []
343
+ end
344
+
345
+ def self.has_structural_differences?(ast, query)
346
+ # Simple heuristic - if the query has different top-level keys
347
+ ast_keys = ast.keys.sort
348
+ query_keys = query.keys.sort
349
+ ast_keys != query_keys
350
+ end
351
+
352
+ def self.calculate_query_complexity(query)
353
+ complexity_counter = { count: 0 }
354
+ count_recursive(query, complexity_counter)
355
+ complexity_counter[:count]
356
+ end
357
+
358
+ def self.count_recursive(node, complexity)
359
+ case node
360
+ when Hash
361
+ complexity[:count] += node.size
362
+ node.each_value { |value| count_recursive(value, complexity) }
363
+ when Array
364
+ complexity[:count] += node.size
365
+ node.each { |item| count_recursive(item, complexity) }
366
+ end
367
+ 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
+ end
532
+ end
533
+ end