noiseless 0.7.1 → 0.8.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 +2 -2
- data/lib/noiseless/adapter.rb +28 -35
- 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 +63 -293
- 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 +5 -5
- 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 +16 -27
- 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 +21 -36
- 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 +2 -2
- data/lib/noiseless/multi_search.rb +2 -4
- data/lib/noiseless/pagination.rb +3 -3
- data/lib/noiseless/query_builder.rb +5 -4
- data/lib/noiseless/railtie.rb +0 -2
- data/lib/noiseless/response/records.rb +2 -1
- 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 +31 -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
|
@@ -224,12 +224,10 @@ module Noiseless
|
|
|
224
224
|
next unless model_class.respond_to?(:where)
|
|
225
225
|
|
|
226
226
|
ids = model_hits.map { |hit| hit["_id"] }
|
|
227
|
-
|
|
227
|
+
records_by_id = model_class.where(id: ids).index_by { it.id.to_s }
|
|
228
228
|
|
|
229
229
|
# Sort by search relevance
|
|
230
|
-
sorted_records = model_hits.filter_map
|
|
231
|
-
loaded_records.find { |record| record.id.to_s == hit["_id"].to_s }
|
|
232
|
-
end
|
|
230
|
+
sorted_records = model_hits.filter_map { |hit| records_by_id[hit["_id"].to_s] }
|
|
233
231
|
|
|
234
232
|
records[model_key] = sorted_records
|
|
235
233
|
end
|
data/lib/noiseless/pagination.rb
CHANGED
|
@@ -75,16 +75,16 @@ module Noiseless
|
|
|
75
75
|
|
|
76
76
|
# Encode cursor for API response
|
|
77
77
|
def encode
|
|
78
|
-
|
|
78
|
+
[ActiveSupport::JSON.encode({ f: field, v: value, d: direction })].pack("m0").tr("+/", "-_")
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Decode cursor from API request
|
|
82
82
|
def self.decode(encoded)
|
|
83
83
|
return nil if encoded.blank?
|
|
84
84
|
|
|
85
|
-
data = JSON.parse(
|
|
85
|
+
data = JSON.parse(encoded.tr("-_", "+/").unpack1("m0"))
|
|
86
86
|
new(field: data["f"], value: data["v"], direction: data["d"]&.to_sym || :asc)
|
|
87
|
-
rescue
|
|
87
|
+
rescue ArgumentError, JSON::ParserError
|
|
88
88
|
nil
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -143,12 +143,13 @@ module Noiseless
|
|
|
143
143
|
# @param text_query [String] The text query for BM25 matching
|
|
144
144
|
# @param embedding [Array<Float>] The query embedding vector
|
|
145
145
|
# @param field [Symbol] The embedding field name
|
|
146
|
+
# @param fields [Array<Symbol>] Fields the text query runs against
|
|
146
147
|
# @param text_weight [Float] Weight for text search score (default: 0.5)
|
|
147
148
|
# @param vector_weight [Float] Weight for vector search score (default: 0.5)
|
|
148
149
|
# @param k [Integer] Number of nearest neighbors (default: 10)
|
|
149
|
-
def hybrid(text_query, embedding, field:, text_weight: 0.5, vector_weight: 0.5, k: 10)
|
|
150
|
+
def hybrid(text_query, embedding, field:, fields: [], text_weight: 0.5, vector_weight: 0.5, k: 10)
|
|
150
151
|
vector_node = AST::Vector.new(field, embedding, k: k)
|
|
151
|
-
@hybrid = AST::Hybrid.new(text_query, vector_node,
|
|
152
|
+
@hybrid = AST::Hybrid.new(text_query, vector_node, fields:, text_weight:, vector_weight:)
|
|
152
153
|
self
|
|
153
154
|
end
|
|
154
155
|
|
|
@@ -188,8 +189,8 @@ module Noiseless
|
|
|
188
189
|
# @param on [Hash] Join conditions
|
|
189
190
|
# @param include_fields [Array] Fields to include from joined collection
|
|
190
191
|
# @param strategy [Symbol] Join strategy :left or :inner
|
|
191
|
-
def join(collection,
|
|
192
|
-
@joins << AST::Join.new(collection,
|
|
192
|
+
def join(collection, include_fields: [], strategy: :left)
|
|
193
|
+
@joins << AST::Join.new(collection, include_fields:, strategy:)
|
|
193
194
|
self
|
|
194
195
|
end
|
|
195
196
|
|
data/lib/noiseless/railtie.rb
CHANGED
|
@@ -75,8 +75,9 @@ module Noiseless
|
|
|
75
75
|
|
|
76
76
|
# Sort records by the order they appear in search results
|
|
77
77
|
sorted_records = []
|
|
78
|
+
records_by_id = loaded_records.index_by { it.id.to_s }
|
|
78
79
|
hits.each_with_index do |hit, hit_index|
|
|
79
|
-
record =
|
|
80
|
+
record = records_by_id[hit["_id"].to_s]
|
|
80
81
|
if record
|
|
81
82
|
sorted_records << record
|
|
82
83
|
@record_hit_map[record] = hit_index
|
|
@@ -31,12 +31,9 @@ module Noiseless
|
|
|
31
31
|
raise ArgumentError, "Unknown operation: #{operation}"
|
|
32
32
|
end
|
|
33
33
|
rescue StandardError => e
|
|
34
|
-
if options[:raise_on_error]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Log error silently
|
|
38
|
-
logger.error "Noiseless: Background job failed for #{model_class_name}##{record_id}: #{e.message}"
|
|
39
|
-
end
|
|
34
|
+
raise if options[:raise_on_error]
|
|
35
|
+
|
|
36
|
+
Rails.logger&.error "Noiseless: Background job failed for #{model_class_name}##{record_id}: #{e.message}"
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
# Minimal object for deleted records
|
|
@@ -58,16 +55,21 @@ module Noiseless
|
|
|
58
55
|
nil
|
|
59
56
|
end
|
|
60
57
|
end
|
|
58
|
+
|
|
59
|
+
# Queued runs raise so the job backend retries instead of recording success.
|
|
60
|
+
module Queued
|
|
61
|
+
def perform(model_class_name, record_id, operation, options = {})
|
|
62
|
+
SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options.merge(raise_on_error: true))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
# ActiveJob integration
|
|
64
68
|
if defined?(ActiveJob::Base)
|
|
65
69
|
class ActiveJobSearchIndexUpdateJob < ActiveJob::Base
|
|
66
|
-
|
|
70
|
+
include SearchIndexUpdateJob::Queued
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options)
|
|
70
|
-
end
|
|
72
|
+
queue_as :default
|
|
71
73
|
end
|
|
72
74
|
end
|
|
73
75
|
|
|
@@ -75,10 +77,7 @@ module Noiseless
|
|
|
75
77
|
if defined?(Sidekiq)
|
|
76
78
|
class SidekiqSearchIndexUpdateJob
|
|
77
79
|
include Sidekiq::Worker
|
|
78
|
-
|
|
79
|
-
def perform(model_class_name, record_id, operation, options = {})
|
|
80
|
-
SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options)
|
|
81
|
-
end
|
|
80
|
+
include SearchIndexUpdateJob::Queued
|
|
82
81
|
end
|
|
83
82
|
end
|
|
84
83
|
end
|
data/lib/noiseless/test_case.rb
CHANGED
data/lib/noiseless/version.rb
CHANGED
data/lib/noiseless.rb
CHANGED
|
@@ -11,7 +11,6 @@ require "singleton"
|
|
|
11
11
|
require "async"
|
|
12
12
|
require "async/http/endpoint"
|
|
13
13
|
require "async/http/client"
|
|
14
|
-
require "async/pool"
|
|
15
14
|
require_relative "noiseless/version"
|
|
16
15
|
|
|
17
16
|
module Noiseless
|
|
@@ -96,11 +95,7 @@ module Noiseless
|
|
|
96
95
|
|
|
97
96
|
# Register all connections statically from YAML - no runtime registration
|
|
98
97
|
config.connections_config.each do |name, params|
|
|
99
|
-
|
|
100
|
-
hosts = params[:hosts] || []
|
|
101
|
-
register_params = { adapter: adapter_name, hosts: hosts, timeout: params[:timeout] }
|
|
102
|
-
register_params[:request_timeout] = params[:request_timeout] if params.key?(:request_timeout)
|
|
103
|
-
connections.register(name, **register_params)
|
|
98
|
+
connections.register(name, **params, hosts: params[:hosts] || [])
|
|
104
99
|
end
|
|
105
100
|
end
|
|
106
101
|
|
|
@@ -123,10 +118,6 @@ module Noiseless
|
|
|
123
118
|
loader.setup
|
|
124
119
|
loader.eager_load if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
|
|
125
120
|
|
|
126
|
-
# Manually require response classes since they're in a subdirectory
|
|
127
|
-
require_relative "noiseless/response"
|
|
128
|
-
require_relative "noiseless/response_factory"
|
|
129
|
-
|
|
130
121
|
# Global registry instance
|
|
131
122
|
def self.registry
|
|
132
123
|
ModelRegistry.instance
|