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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Aggregations
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize(aggs_hash)
|
|
9
|
+
@aggs_hash = aggs_hash || {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@aggs_hash[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
delegate :keys, to: :@aggs_hash
|
|
17
|
+
|
|
18
|
+
def each(&)
|
|
19
|
+
return enum_for(__method__) unless block_given?
|
|
20
|
+
|
|
21
|
+
@aggs_hash.each(&)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
delegate :empty?, to: :@aggs_hash
|
|
25
|
+
|
|
26
|
+
delegate :size, to: :@aggs_hash
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
@aggs_hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Handle method conflicts with Enumerable methods
|
|
33
|
+
def method_missing(method_name, *args, &)
|
|
34
|
+
if @aggs_hash.key?(method_name.to_s)
|
|
35
|
+
@aggs_hash[method_name.to_s]
|
|
36
|
+
else
|
|
37
|
+
super
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
42
|
+
@aggs_hash.key?(method_name.to_s) || super
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Empty < Base
|
|
6
|
+
EMPTY_RESPONSE = {
|
|
7
|
+
"hits" => { "total" => { "value" => 0 }, "hits" => [] },
|
|
8
|
+
"took" => 0
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(model_class = nil)
|
|
12
|
+
super(EMPTY_RESPONSE, model_class)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each
|
|
16
|
+
return enum_for(__method__) unless block_given?
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Records < Base
|
|
6
|
+
def initialize(raw_response, model_class)
|
|
7
|
+
super
|
|
8
|
+
@records = nil
|
|
9
|
+
@record_hit_map = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def each(&)
|
|
13
|
+
return enum_for(__method__) unless block_given?
|
|
14
|
+
|
|
15
|
+
records.each(&)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each_with_hit
|
|
19
|
+
return enum_for(__method__) unless block_given?
|
|
20
|
+
|
|
21
|
+
records.each_with_index do |record, index|
|
|
22
|
+
hit = hits[record_hit_map[record] || index]
|
|
23
|
+
yield(record, hit)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def map_with_hit(&)
|
|
28
|
+
return enum_for(__method__) unless block_given?
|
|
29
|
+
|
|
30
|
+
each_with_hit.map(&)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def records
|
|
34
|
+
@records ||= load_records_with_pagination
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
delegate :first, to: :records
|
|
38
|
+
|
|
39
|
+
delegate :last, to: :records
|
|
40
|
+
|
|
41
|
+
delegate :[], to: :records
|
|
42
|
+
|
|
43
|
+
def to_a
|
|
44
|
+
records
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def load_records_with_pagination
|
|
50
|
+
records = load_records
|
|
51
|
+
|
|
52
|
+
# Wrap in PaginatedArray for pagination metadata
|
|
53
|
+
current_page = @from && @per_page ? (@from / @per_page) + 1 : 1
|
|
54
|
+
per_page = @per_page || Pagination::DEFAULT_PER_PAGE
|
|
55
|
+
|
|
56
|
+
Pagination::PaginatedArray.new(
|
|
57
|
+
records,
|
|
58
|
+
current_page: current_page,
|
|
59
|
+
per_page: per_page,
|
|
60
|
+
total_count: total
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def load_records
|
|
65
|
+
return [] if hits.empty? || !model_class.respond_to?(:where)
|
|
66
|
+
|
|
67
|
+
# Extract IDs from hits
|
|
68
|
+
ids = hits.map { |hit| hit["_id"] }
|
|
69
|
+
|
|
70
|
+
# Load records from database
|
|
71
|
+
loaded_records = model_class.where(id: ids).to_a
|
|
72
|
+
|
|
73
|
+
# Create mapping from record to hit index for correlation
|
|
74
|
+
@record_hit_map = {}
|
|
75
|
+
|
|
76
|
+
# Sort records by the order they appear in search results
|
|
77
|
+
sorted_records = []
|
|
78
|
+
hits.each_with_index do |hit, hit_index|
|
|
79
|
+
record = loaded_records.find { |r| r.id.to_s == hit["_id"].to_s }
|
|
80
|
+
if record
|
|
81
|
+
sorted_records << record
|
|
82
|
+
@record_hit_map[record] = hit_index
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sorted_records
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def record_hit_map
|
|
90
|
+
@record_hit_map ||= {}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Results < Base
|
|
6
|
+
def each
|
|
7
|
+
return enum_for(__method__) unless block_given?
|
|
8
|
+
|
|
9
|
+
hits.each do |hit|
|
|
10
|
+
yield Result.new(hit)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def sources
|
|
15
|
+
@sources ||= hits.map { |hit| hit["_source"] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
alias records sources
|
|
19
|
+
|
|
20
|
+
def each_source(&)
|
|
21
|
+
return enum_for(__method__) unless block_given?
|
|
22
|
+
|
|
23
|
+
sources.each(&)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def map_source(&)
|
|
27
|
+
return enum_for(__method__) unless block_given?
|
|
28
|
+
|
|
29
|
+
sources.map(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def first
|
|
33
|
+
hit = hits.first
|
|
34
|
+
hit ? Result.new(hit) : nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def last
|
|
38
|
+
hit = hits.last
|
|
39
|
+
hit ? Result.new(hit) : nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def [](index)
|
|
43
|
+
hit = hits[index]
|
|
44
|
+
hit ? Result.new(hit) : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_a
|
|
48
|
+
hits.map { |hit| Result.new(hit) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def map
|
|
52
|
+
return enum_for(__method__) unless block_given?
|
|
53
|
+
|
|
54
|
+
results = []
|
|
55
|
+
each do |result|
|
|
56
|
+
results << yield(result)
|
|
57
|
+
end
|
|
58
|
+
results
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def select
|
|
62
|
+
return enum_for(__method__) unless block_given?
|
|
63
|
+
|
|
64
|
+
results = []
|
|
65
|
+
each do |result|
|
|
66
|
+
results << result if yield(result)
|
|
67
|
+
end
|
|
68
|
+
results
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ids
|
|
72
|
+
@ids ||= hits.map { |hit| hit["_id"] }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def scores
|
|
76
|
+
@scores ||= hits.map { |hit| hit["_score"] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Make results enumerable with source property
|
|
80
|
+
def find
|
|
81
|
+
return enum_for(__method__) unless block_given?
|
|
82
|
+
|
|
83
|
+
hits.each do |hit|
|
|
84
|
+
result = Result.new(hit)
|
|
85
|
+
return result if yield(result)
|
|
86
|
+
end
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Result
|
|
91
|
+
attr_reader :source
|
|
92
|
+
|
|
93
|
+
def initialize(hit)
|
|
94
|
+
@hit = hit
|
|
95
|
+
@source = hit["_source"]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def id
|
|
99
|
+
@hit["_id"]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def score
|
|
103
|
+
@hit["_score"]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
delegate :[], to: :@source
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Suggestions
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
def initialize(suggestions_hash)
|
|
9
|
+
@suggestions_hash = suggestions_hash || {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@suggestions_hash[key.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
delegate :keys, to: :@suggestions_hash
|
|
17
|
+
|
|
18
|
+
def each(&)
|
|
19
|
+
return enum_for(__method__) unless block_given?
|
|
20
|
+
|
|
21
|
+
@suggestions_hash.each(&)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
delegate :empty?, to: :@suggestions_hash
|
|
25
|
+
|
|
26
|
+
delegate :size, to: :@suggestions_hash
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
@suggestions_hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def terms
|
|
33
|
+
@terms ||= extract_terms
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def extract_terms
|
|
39
|
+
terms = []
|
|
40
|
+
@suggestions_hash.each_value do |suggestion|
|
|
41
|
+
next unless suggestion.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
suggestion.each do |item|
|
|
44
|
+
next unless item.is_a?(Hash) && item["options"]
|
|
45
|
+
|
|
46
|
+
item["options"].each do |option|
|
|
47
|
+
terms << option["text"] if option["text"]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
terms.uniq
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
module Response
|
|
5
|
+
class Base
|
|
6
|
+
include Enumerable
|
|
7
|
+
include Pagination::ResponsePagination
|
|
8
|
+
|
|
9
|
+
def initialize(raw_response, model_class = nil)
|
|
10
|
+
@raw_response = raw_response
|
|
11
|
+
@model_class = model_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def total
|
|
15
|
+
case @raw_response.dig("hits", "total")
|
|
16
|
+
when Hash
|
|
17
|
+
@raw_response.dig("hits", "total", "value") || 0
|
|
18
|
+
when Integer
|
|
19
|
+
@raw_response.dig("hits", "total") || 0
|
|
20
|
+
else
|
|
21
|
+
0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hits
|
|
26
|
+
@hits ||= @raw_response.dig("hits", "hits") || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def took
|
|
30
|
+
@raw_response["took"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def aggregations
|
|
34
|
+
@aggregations ||= Aggregations.new(@raw_response["aggregations"] || {})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def suggestions
|
|
38
|
+
@suggestions ||= Suggestions.new(@raw_response["suggest"] || {})
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each(&)
|
|
42
|
+
raise NotImplementedError, "Subclasses must implement #each"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty?
|
|
46
|
+
total.zero?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
delegate :size, to: :hits
|
|
50
|
+
|
|
51
|
+
def length
|
|
52
|
+
size
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def include_pagination_info(query_hash)
|
|
56
|
+
@from = query_hash[:from] || 0
|
|
57
|
+
@per_page = query_hash[:size] || 20
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compatibility methods for CommonShare
|
|
61
|
+
def response
|
|
62
|
+
@raw_response
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def result
|
|
66
|
+
# Alias for results - controllers expect .result
|
|
67
|
+
results
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def results
|
|
71
|
+
# For Results class, return self. For Records, return a Results view
|
|
72
|
+
if is_a?(Results)
|
|
73
|
+
self
|
|
74
|
+
else
|
|
75
|
+
Results.new(@raw_response, @model_class)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def count
|
|
80
|
+
size
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def total_count
|
|
84
|
+
total
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def each_with_index(&)
|
|
88
|
+
return enum_for(__method__) unless block_given?
|
|
89
|
+
|
|
90
|
+
each.with_index(&)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
attr_reader :raw_response, :model_class
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
class ResponseFactory
|
|
5
|
+
def self.create(raw_response, model_class: nil, response_type: nil, query_hash: nil)
|
|
6
|
+
# Auto-detect response type based on model class and preferences
|
|
7
|
+
response_type ||= detect_response_type(model_class)
|
|
8
|
+
|
|
9
|
+
response = case response_type
|
|
10
|
+
when :records
|
|
11
|
+
Response::Records.new(raw_response, model_class)
|
|
12
|
+
else # :results or unknown
|
|
13
|
+
Response::Results.new(raw_response, model_class)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Include pagination information if query hash is provided
|
|
17
|
+
response.include_pagination_info(query_hash) if query_hash
|
|
18
|
+
|
|
19
|
+
response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.detect_response_type(model_class)
|
|
23
|
+
# If model_class responds to ActiveRecord-like methods, default to :records
|
|
24
|
+
if model_class.respond_to?(:where) &&
|
|
25
|
+
model_class.respond_to?(:find)
|
|
26
|
+
:records
|
|
27
|
+
else
|
|
28
|
+
:results
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
class RuntimeResetMiddleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
# Reset runtime tracking at the beginning of each request
|
|
11
|
+
Thread.current[:noiseless_runtime] = 0
|
|
12
|
+
@app.call(env)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noiseless
|
|
4
|
+
class SearchIndexUpdateJob
|
|
5
|
+
def self.perform_later(model_class_name, record_id, operation, options = {})
|
|
6
|
+
if defined?(ActiveJob::Base)
|
|
7
|
+
ActiveJobSearchIndexUpdateJob.perform_later(model_class_name, record_id, operation, options)
|
|
8
|
+
elsif defined?(Sidekiq)
|
|
9
|
+
SidekiqSearchIndexUpdateJob.perform_async(model_class_name, record_id, operation, options)
|
|
10
|
+
else
|
|
11
|
+
# Fallback to immediate execution
|
|
12
|
+
perform_now(model_class_name, record_id, operation, options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.perform_now(model_class_name, record_id, operation, options = {})
|
|
17
|
+
model_class = model_class_name.constantize
|
|
18
|
+
|
|
19
|
+
case operation
|
|
20
|
+
when "update"
|
|
21
|
+
record = model_class.find(record_id)
|
|
22
|
+
record.document_manager.update_document(**options)
|
|
23
|
+
when "delete"
|
|
24
|
+
# For delete operations, we need to construct a minimal object
|
|
25
|
+
# since the record might already be deleted from the database
|
|
26
|
+
document_manager = DocumentManager.new(
|
|
27
|
+
DeletedRecord.new(model_class, record_id)
|
|
28
|
+
)
|
|
29
|
+
document_manager.delete_document(**options)
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, "Unknown operation: #{operation}"
|
|
32
|
+
end
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
if options[:raise_on_error]
|
|
35
|
+
raise e
|
|
36
|
+
elsif (logger = Rails.logger)
|
|
37
|
+
# Log error silently
|
|
38
|
+
logger.error "Noiseless: Background job failed for #{model_class_name}##{record_id}: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Minimal object for deleted records
|
|
43
|
+
class DeletedRecord
|
|
44
|
+
def initialize(model_class, record_id)
|
|
45
|
+
@model_class = model_class
|
|
46
|
+
@record_id = record_id
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def id
|
|
50
|
+
@record_id
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def class
|
|
54
|
+
@model_class
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_search_document
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ActiveJob integration
|
|
64
|
+
if defined?(ActiveJob::Base)
|
|
65
|
+
class ActiveJobSearchIndexUpdateJob < ActiveJob::Base
|
|
66
|
+
queue_as :default
|
|
67
|
+
|
|
68
|
+
def perform(model_class_name, record_id, operation, options = {})
|
|
69
|
+
SearchIndexUpdateJob.perform_now(model_class_name, record_id, operation, options)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Sidekiq integration
|
|
75
|
+
if defined?(Sidekiq)
|
|
76
|
+
class SidekiqSearchIndexUpdateJob
|
|
77
|
+
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
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|