tiny-quick-gem 0.0.1
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 +7 -0
- data/searchkick-6.1.2/CHANGELOG.md +919 -0
- data/searchkick-6.1.2/LICENSE.txt +22 -0
- data/searchkick-6.1.2/README.md +2348 -0
- data/searchkick-6.1.2/lib/searchkick/bulk_reindex_job.rb +19 -0
- data/searchkick-6.1.2/lib/searchkick/controller_runtime.rb +40 -0
- data/searchkick-6.1.2/lib/searchkick/hash_wrapper.rb +41 -0
- data/searchkick-6.1.2/lib/searchkick/index.rb +480 -0
- data/searchkick-6.1.2/lib/searchkick/index_cache.rb +30 -0
- data/searchkick-6.1.2/lib/searchkick/index_options.rb +652 -0
- data/searchkick-6.1.2/lib/searchkick/indexer.rb +43 -0
- data/searchkick-6.1.2/lib/searchkick/log_subscriber.rb +57 -0
- data/searchkick-6.1.2/lib/searchkick/middleware.rb +19 -0
- data/searchkick-6.1.2/lib/searchkick/model.rb +120 -0
- data/searchkick-6.1.2/lib/searchkick/multi_search.rb +46 -0
- data/searchkick-6.1.2/lib/searchkick/process_batch_job.rb +20 -0
- data/searchkick-6.1.2/lib/searchkick/process_queue_job.rb +33 -0
- data/searchkick-6.1.2/lib/searchkick/query.rb +1372 -0
- data/searchkick-6.1.2/lib/searchkick/railtie.rb +7 -0
- data/searchkick-6.1.2/lib/searchkick/record_data.rb +147 -0
- data/searchkick-6.1.2/lib/searchkick/record_indexer.rb +174 -0
- data/searchkick-6.1.2/lib/searchkick/reindex_queue.rb +57 -0
- data/searchkick-6.1.2/lib/searchkick/reindex_v2_job.rb +17 -0
- data/searchkick-6.1.2/lib/searchkick/relation.rb +728 -0
- data/searchkick-6.1.2/lib/searchkick/relation_indexer.rb +184 -0
- data/searchkick-6.1.2/lib/searchkick/reranking.rb +28 -0
- data/searchkick-6.1.2/lib/searchkick/results.rb +359 -0
- data/searchkick-6.1.2/lib/searchkick/script.rb +11 -0
- data/searchkick-6.1.2/lib/searchkick/version.rb +3 -0
- data/searchkick-6.1.2/lib/searchkick/where.rb +11 -0
- data/searchkick-6.1.2/lib/searchkick.rb +388 -0
- data/searchkick-6.1.2/lib/tasks/searchkick.rake +37 -0
- data/tiny-quick-gem.gemspec +12 -0
- metadata +73 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module Searchkick
|
|
2
|
+
class RelationIndexer
|
|
3
|
+
attr_reader :index
|
|
4
|
+
|
|
5
|
+
def initialize(index)
|
|
6
|
+
@index = index
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def reindex(relation, mode:, method_name: nil, ignore_missing: nil, full: false, resume: false, scope: nil, job_options: nil)
|
|
10
|
+
# apply scopes
|
|
11
|
+
if scope
|
|
12
|
+
relation = relation.send(scope)
|
|
13
|
+
elsif relation.respond_to?(:search_import)
|
|
14
|
+
relation = relation.search_import
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# remove unneeded loading for async and queue
|
|
18
|
+
if mode == :async || mode == :queue
|
|
19
|
+
if relation.respond_to?(:primary_key)
|
|
20
|
+
relation = relation.except(:includes, :preload)
|
|
21
|
+
unless mode == :queue && relation.klass.method_defined?(:search_routing)
|
|
22
|
+
relation = relation.except(:select).select(relation.primary_key)
|
|
23
|
+
end
|
|
24
|
+
elsif relation.respond_to?(:only)
|
|
25
|
+
unless mode == :queue && relation.klass.method_defined?(:search_routing)
|
|
26
|
+
relation = relation.only(:_id)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if mode == :async && full
|
|
32
|
+
return full_reindex_async(relation, job_options: job_options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
relation = resume_relation(relation) if resume
|
|
36
|
+
|
|
37
|
+
reindex_options = {
|
|
38
|
+
mode: mode,
|
|
39
|
+
method_name: method_name,
|
|
40
|
+
full: full,
|
|
41
|
+
ignore_missing: ignore_missing,
|
|
42
|
+
job_options: job_options
|
|
43
|
+
}
|
|
44
|
+
record_indexer = RecordIndexer.new(index)
|
|
45
|
+
|
|
46
|
+
in_batches(relation) do |items|
|
|
47
|
+
record_indexer.reindex(items, **reindex_options)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def batches_left
|
|
52
|
+
Searchkick.with_redis { |r| r.call("SCARD", batches_key) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def batch_completed(batch_id)
|
|
56
|
+
Searchkick.with_redis { |r| r.call("SREM", batches_key, [batch_id]) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def resume_relation(relation)
|
|
62
|
+
if relation.respond_to?(:primary_key)
|
|
63
|
+
# use total docs instead of max id since there's not a great way
|
|
64
|
+
# to get the max _id without scripting since it's a string
|
|
65
|
+
where = relation.arel_table[relation.primary_key].gt(index.total_docs)
|
|
66
|
+
relation = relation.where(where)
|
|
67
|
+
else
|
|
68
|
+
raise Error, "Resume not supported for Mongoid"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def in_batches(relation)
|
|
73
|
+
if relation.respond_to?(:find_in_batches)
|
|
74
|
+
klass = relation.klass
|
|
75
|
+
# remove order to prevent possible warnings
|
|
76
|
+
relation.except(:order).find_in_batches(batch_size: batch_size) do |batch|
|
|
77
|
+
# prevent scope from affecting search_data as well as inline jobs
|
|
78
|
+
# Active Record runs relation calls in scoping block
|
|
79
|
+
# https://github.com/rails/rails/blob/main/activerecord/lib/active_record/relation/delegation.rb
|
|
80
|
+
# note: we could probably just call klass.current_scope = nil
|
|
81
|
+
# anywhere in reindex method (after initial all call),
|
|
82
|
+
# but this is more cautious
|
|
83
|
+
previous_scope = klass.current_scope(true)
|
|
84
|
+
if previous_scope
|
|
85
|
+
begin
|
|
86
|
+
klass.current_scope = nil
|
|
87
|
+
yield batch
|
|
88
|
+
ensure
|
|
89
|
+
klass.current_scope = previous_scope
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
yield batch
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
klass = relation.klass
|
|
97
|
+
each_batch(relation, batch_size: batch_size) do |batch|
|
|
98
|
+
# prevent scope from affecting search_data as well as inline jobs
|
|
99
|
+
# note: Model.with_scope doesn't always restore scope, so use custom logic
|
|
100
|
+
previous_scope = Mongoid::Threaded.current_scope(klass)
|
|
101
|
+
if previous_scope
|
|
102
|
+
begin
|
|
103
|
+
Mongoid::Threaded.set_current_scope(nil, klass)
|
|
104
|
+
yield batch
|
|
105
|
+
ensure
|
|
106
|
+
Mongoid::Threaded.set_current_scope(previous_scope, klass)
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
yield batch
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def each_batch(relation, batch_size:)
|
|
116
|
+
# https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
|
|
117
|
+
# use cursor for Mongoid
|
|
118
|
+
items = []
|
|
119
|
+
relation.all.each do |item|
|
|
120
|
+
items << item
|
|
121
|
+
if items.length == batch_size
|
|
122
|
+
yield items
|
|
123
|
+
items = []
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
yield items if items.any?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def batch_size
|
|
130
|
+
@batch_size ||= index.options[:batch_size] || 1000
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def full_reindex_async(relation, job_options: nil)
|
|
134
|
+
batch_id = 1
|
|
135
|
+
class_name = relation.searchkick_options[:class_name]
|
|
136
|
+
starting_id = false
|
|
137
|
+
|
|
138
|
+
if relation.respond_to?(:primary_key)
|
|
139
|
+
primary_key = relation.primary_key
|
|
140
|
+
|
|
141
|
+
starting_id =
|
|
142
|
+
begin
|
|
143
|
+
relation.minimum(primary_key)
|
|
144
|
+
rescue ActiveRecord::StatementInvalid
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if starting_id.nil?
|
|
150
|
+
# no records, do nothing
|
|
151
|
+
elsif starting_id.is_a?(Numeric)
|
|
152
|
+
max_id = relation.maximum(primary_key)
|
|
153
|
+
batches_count = ((max_id - starting_id + 1) / batch_size.to_f).ceil
|
|
154
|
+
|
|
155
|
+
batches_count.times do |i|
|
|
156
|
+
min_id = starting_id + (i * batch_size)
|
|
157
|
+
batch_job(class_name, batch_id, job_options, min_id: min_id, max_id: min_id + batch_size - 1)
|
|
158
|
+
batch_id += 1
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
in_batches(relation) do |items|
|
|
162
|
+
batch_job(class_name, batch_id, job_options, record_ids: items.map(&:id).map { |v| v.instance_of?(Integer) ? v : v.to_s })
|
|
163
|
+
batch_id += 1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def batch_job(class_name, batch_id, job_options, **options)
|
|
169
|
+
job_options ||= {}
|
|
170
|
+
# TODO expire Redis key
|
|
171
|
+
Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
|
|
172
|
+
Searchkick::BulkReindexJob.set(**job_options).perform_later(
|
|
173
|
+
class_name: class_name,
|
|
174
|
+
index_name: index.name,
|
|
175
|
+
batch_id: batch_id,
|
|
176
|
+
**options
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def batches_key
|
|
181
|
+
"searchkick:reindex:#{index.name}:batches"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Searchkick
|
|
2
|
+
module Reranking
|
|
3
|
+
def self.rrf(first_ranking, *rankings, k: 60)
|
|
4
|
+
rankings.unshift(first_ranking)
|
|
5
|
+
rankings.map!(&:to_ary)
|
|
6
|
+
|
|
7
|
+
ranks = []
|
|
8
|
+
results = []
|
|
9
|
+
rankings.each do |ranking|
|
|
10
|
+
ranks << ranking.map.with_index.to_h { |v, i| [v, i + 1] }
|
|
11
|
+
results.concat(ranking)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
results =
|
|
15
|
+
results.uniq.map do |result|
|
|
16
|
+
score =
|
|
17
|
+
ranks.sum do |rank|
|
|
18
|
+
r = rank[result]
|
|
19
|
+
r ? 1.0 / (k + r) : 0.0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{result: result, score: score}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
results.sort_by { |v| -v[:score] }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
module Searchkick
|
|
2
|
+
class Results
|
|
3
|
+
include Enumerable
|
|
4
|
+
extend Forwardable
|
|
5
|
+
|
|
6
|
+
attr_reader :response
|
|
7
|
+
|
|
8
|
+
def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
|
|
9
|
+
|
|
10
|
+
def initialize(klass, response, options = {})
|
|
11
|
+
@klass = klass
|
|
12
|
+
@response = response
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_hit
|
|
17
|
+
return enum_for(:with_hit) unless block_given?
|
|
18
|
+
|
|
19
|
+
build_hits.each do |result|
|
|
20
|
+
yield result
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def missing_records
|
|
25
|
+
@missing_records ||= with_hit_and_missing_records[1]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def suggestions
|
|
29
|
+
if response["suggest"]
|
|
30
|
+
response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
|
|
31
|
+
elsif options[:suggest]
|
|
32
|
+
[]
|
|
33
|
+
else
|
|
34
|
+
raise "Pass `suggest: true` to the search method for suggestions"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def aggregations
|
|
39
|
+
response["aggregations"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def aggs
|
|
43
|
+
@aggs ||= begin
|
|
44
|
+
if aggregations
|
|
45
|
+
aggregations.dup.each do |field, filtered_agg|
|
|
46
|
+
buckets = filtered_agg[field]
|
|
47
|
+
# move the buckets one level above into the field hash
|
|
48
|
+
if buckets
|
|
49
|
+
filtered_agg.delete(field)
|
|
50
|
+
filtered_agg.merge!(buckets)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def took
|
|
58
|
+
response["took"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def error
|
|
62
|
+
response["error"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def model_name
|
|
66
|
+
if klass.nil?
|
|
67
|
+
ActiveModel::Name.new(self.class, nil, 'Result')
|
|
68
|
+
else
|
|
69
|
+
klass.model_name
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entry_name(options = {})
|
|
74
|
+
if options.empty?
|
|
75
|
+
# backward compatibility
|
|
76
|
+
model_name.human.downcase
|
|
77
|
+
else
|
|
78
|
+
default = options[:count] == 1 ? model_name.human : model_name.human.pluralize
|
|
79
|
+
model_name.human(options.reverse_merge(default: default))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def total_count
|
|
84
|
+
if options[:total_entries]
|
|
85
|
+
options[:total_entries]
|
|
86
|
+
elsif response["hits"]["total"].is_a?(Hash)
|
|
87
|
+
response["hits"]["total"]["value"]
|
|
88
|
+
else
|
|
89
|
+
response["hits"]["total"]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
alias_method :total_entries, :total_count
|
|
93
|
+
|
|
94
|
+
def current_page
|
|
95
|
+
options[:page]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def per_page
|
|
99
|
+
options[:per_page]
|
|
100
|
+
end
|
|
101
|
+
alias_method :limit_value, :per_page
|
|
102
|
+
|
|
103
|
+
def padding
|
|
104
|
+
options[:padding]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def total_pages
|
|
108
|
+
(total_count / per_page.to_f).ceil
|
|
109
|
+
end
|
|
110
|
+
alias_method :num_pages, :total_pages
|
|
111
|
+
|
|
112
|
+
def offset_value
|
|
113
|
+
(current_page - 1) * per_page + padding
|
|
114
|
+
end
|
|
115
|
+
alias_method :offset, :offset_value
|
|
116
|
+
|
|
117
|
+
def previous_page
|
|
118
|
+
current_page > 1 ? (current_page - 1) : nil
|
|
119
|
+
end
|
|
120
|
+
alias_method :prev_page, :previous_page
|
|
121
|
+
|
|
122
|
+
def next_page
|
|
123
|
+
current_page < total_pages ? (current_page + 1) : nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def first_page?
|
|
127
|
+
previous_page.nil?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def last_page?
|
|
131
|
+
next_page.nil?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def out_of_range?
|
|
135
|
+
current_page > total_pages
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def hits
|
|
139
|
+
if error
|
|
140
|
+
raise Error, "Query error - use the error method to view it"
|
|
141
|
+
else
|
|
142
|
+
@response["hits"]["hits"]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def highlights(multiple: false)
|
|
147
|
+
hits.map do |hit|
|
|
148
|
+
hit_highlights(hit, multiple: multiple)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def with_highlights(multiple: false)
|
|
153
|
+
return enum_for(:with_highlights, multiple: multiple) unless block_given?
|
|
154
|
+
|
|
155
|
+
with_hit.each do |result, hit|
|
|
156
|
+
yield result, hit_highlights(hit, multiple: multiple)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def with_score
|
|
161
|
+
return enum_for(:with_score) unless block_given?
|
|
162
|
+
|
|
163
|
+
with_hit.each do |result, hit|
|
|
164
|
+
yield result, hit["_score"]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def misspellings?
|
|
169
|
+
@options[:misspellings]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def scroll_id
|
|
173
|
+
@response["_scroll_id"]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def scroll
|
|
177
|
+
raise Error, "Pass `scroll` option to the search method for scrolling" unless scroll_id
|
|
178
|
+
|
|
179
|
+
if block_given?
|
|
180
|
+
records = self
|
|
181
|
+
while records.any?
|
|
182
|
+
yield records
|
|
183
|
+
records = records.scroll
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
records.clear_scroll
|
|
187
|
+
else
|
|
188
|
+
begin
|
|
189
|
+
# TODO Active Support notifications for this scroll call
|
|
190
|
+
params = {
|
|
191
|
+
scroll: options[:scroll],
|
|
192
|
+
body: {scroll_id: scroll_id}
|
|
193
|
+
}
|
|
194
|
+
params[:opaque_id] = options[:opaque_id] if options[:opaque_id]
|
|
195
|
+
Results.new(@klass, Searchkick.client.scroll(params), @options)
|
|
196
|
+
rescue => e
|
|
197
|
+
if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
|
|
198
|
+
raise Error, "Scroll id has expired"
|
|
199
|
+
else
|
|
200
|
+
raise e
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def clear_scroll
|
|
207
|
+
begin
|
|
208
|
+
# try to clear scroll
|
|
209
|
+
# not required as scroll will expire
|
|
210
|
+
# but there is a cost to open scrolls
|
|
211
|
+
Searchkick.client.clear_scroll({body: {scroll_id: scroll_id}})
|
|
212
|
+
rescue => e
|
|
213
|
+
raise e unless Searchkick.transport_error?(e)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
attr_reader :klass, :options
|
|
220
|
+
|
|
221
|
+
def results
|
|
222
|
+
@results ||= with_hit.map(&:first)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def with_hit_and_missing_records
|
|
226
|
+
@with_hit_and_missing_records ||= begin
|
|
227
|
+
missing_records = []
|
|
228
|
+
|
|
229
|
+
if options[:load]
|
|
230
|
+
grouped_hits = hits.group_by { |hit, _| hit["_index"] }
|
|
231
|
+
|
|
232
|
+
# determine models
|
|
233
|
+
index_models = {}
|
|
234
|
+
grouped_hits.each do |index, _|
|
|
235
|
+
models =
|
|
236
|
+
if @klass
|
|
237
|
+
[@klass]
|
|
238
|
+
else
|
|
239
|
+
index_alias = index.split("_")[0..-2].join("_")
|
|
240
|
+
Array((options[:index_mapping] || {})[index_alias])
|
|
241
|
+
end
|
|
242
|
+
raise Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
|
|
243
|
+
index_models[index] = models
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# fetch results
|
|
247
|
+
results = {}
|
|
248
|
+
grouped_hits.each do |index, index_hits|
|
|
249
|
+
results[index] = {}
|
|
250
|
+
index_models[index].each do |model|
|
|
251
|
+
results[index].merge!(results_query(model, index_hits).to_a.index_by { |r| r.id.to_s })
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# sort
|
|
256
|
+
results =
|
|
257
|
+
hits.map do |hit|
|
|
258
|
+
result = results[hit["_index"]][hit["_id"].to_s]
|
|
259
|
+
if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
|
|
260
|
+
if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
|
|
261
|
+
highlights = hit_highlights(hit)
|
|
262
|
+
result.define_singleton_method(:search_highlights) do
|
|
263
|
+
highlights
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
[result, hit]
|
|
268
|
+
end.select do |result, hit|
|
|
269
|
+
unless result
|
|
270
|
+
models = index_models[hit["_index"]]
|
|
271
|
+
missing_records << {
|
|
272
|
+
id: hit["_id"],
|
|
273
|
+
# may be multiple models for inheritance with child models
|
|
274
|
+
# not ideal to return different types
|
|
275
|
+
# but this situation shouldn't be common
|
|
276
|
+
model: models.size == 1 ? models.first : models
|
|
277
|
+
}
|
|
278
|
+
end
|
|
279
|
+
result
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
results =
|
|
283
|
+
hits.map do |hit|
|
|
284
|
+
result =
|
|
285
|
+
if hit["_source"]
|
|
286
|
+
hit.except("_source").merge(hit["_source"])
|
|
287
|
+
elsif hit["fields"]
|
|
288
|
+
hit.except("fields").merge(hit["fields"])
|
|
289
|
+
else
|
|
290
|
+
hit
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
if hit["highlight"] || options[:highlight]
|
|
294
|
+
highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
|
|
295
|
+
options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
|
|
296
|
+
result["highlighted_#{k}"] ||= (highlight[k] || result[k])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
result["id"] ||= result["_id"] # needed for legacy reasons
|
|
301
|
+
[HashWrapper.new(result), hit]
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
[results, missing_records]
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def build_hits
|
|
310
|
+
@build_hits ||= begin
|
|
311
|
+
if missing_records.any?
|
|
312
|
+
Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| "#{Array(v[:model]).map(&:model_name).sort.join("/")} #{v[:id]}" }.join(", ")}")
|
|
313
|
+
end
|
|
314
|
+
with_hit_and_missing_records[0]
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def results_query(records, hits)
|
|
319
|
+
records = Searchkick.scope(records)
|
|
320
|
+
|
|
321
|
+
ids = hits.map { |hit| hit["_id"] }
|
|
322
|
+
if options[:includes] || options[:model_includes]
|
|
323
|
+
included_relations = []
|
|
324
|
+
combine_includes(included_relations, options[:includes])
|
|
325
|
+
combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
|
|
326
|
+
|
|
327
|
+
records = records.includes(included_relations)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
if options[:scope_results]
|
|
331
|
+
records = options[:scope_results].call(records)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
Searchkick.load_records(records, ids)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def combine_includes(result, inc)
|
|
338
|
+
if inc
|
|
339
|
+
if inc.is_a?(Array)
|
|
340
|
+
result.concat(inc)
|
|
341
|
+
else
|
|
342
|
+
result << inc
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def base_field(k)
|
|
348
|
+
k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def hit_highlights(hit, multiple: false)
|
|
352
|
+
if hit["highlight"]
|
|
353
|
+
hit["highlight"].to_h { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, multiple ? v : v.first] }
|
|
354
|
+
else
|
|
355
|
+
{}
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|