mm_es_search 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.
- data/.gitignore +4 -0
- data/.project +18 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/lib/mm_es_search/api/facet/abstract_facet.rb +28 -0
- data/lib/mm_es_search/api/facet/date_histogram_facet.rb +11 -0
- data/lib/mm_es_search/api/facet/filter_facet.rb +9 -0
- data/lib/mm_es_search/api/facet/geo_distance_facet.rb +9 -0
- data/lib/mm_es_search/api/facet/histogram_facet.rb +9 -0
- data/lib/mm_es_search/api/facet/query_facet.rb +9 -0
- data/lib/mm_es_search/api/facet/range_facet.rb +36 -0
- data/lib/mm_es_search/api/facet/range_facet_row.rb +97 -0
- data/lib/mm_es_search/api/facet/range_item.rb +17 -0
- data/lib/mm_es_search/api/facet/statistical_facet.rb +33 -0
- data/lib/mm_es_search/api/facet/statistical_facet_result.rb +36 -0
- data/lib/mm_es_search/api/facet/terms_facet.rb +62 -0
- data/lib/mm_es_search/api/facet/terms_facet_row.rb +35 -0
- data/lib/mm_es_search/api/facet/terms_stats_facet.rb +9 -0
- data/lib/mm_es_search/api/highlight/result_highlight.rb +40 -0
- data/lib/mm_es_search/api/query/abstract_filter.rb +15 -0
- data/lib/mm_es_search/api/query/abstract_query.rb +48 -0
- data/lib/mm_es_search/api/query/and_filter.rb +9 -0
- data/lib/mm_es_search/api/query/bool_filter.rb +11 -0
- data/lib/mm_es_search/api/query/bool_query.rb +67 -0
- data/lib/mm_es_search/api/query/constant_score_query.rb +31 -0
- data/lib/mm_es_search/api/query/custom_filters_score_query.rb +52 -0
- data/lib/mm_es_search/api/query/custom_score_query.rb +31 -0
- data/lib/mm_es_search/api/query/dismax_query.rb +29 -0
- data/lib/mm_es_search/api/query/filtered_query.rb +30 -0
- data/lib/mm_es_search/api/query/has_child_filter.rb +11 -0
- data/lib/mm_es_search/api/query/has_child_query.rb +25 -0
- data/lib/mm_es_search/api/query/has_parent_filter.rb +11 -0
- data/lib/mm_es_search/api/query/has_parent_query.rb +25 -0
- data/lib/mm_es_search/api/query/match_all_filter.rb +11 -0
- data/lib/mm_es_search/api/query/match_all_query.rb +19 -0
- data/lib/mm_es_search/api/query/nested_filter.rb +22 -0
- data/lib/mm_es_search/api/query/nested_query.rb +62 -0
- data/lib/mm_es_search/api/query/not_filter.rb +9 -0
- data/lib/mm_es_search/api/query/or_filter.rb +9 -0
- data/lib/mm_es_search/api/query/prefix_filter.rb +11 -0
- data/lib/mm_es_search/api/query/prefix_query.rb +34 -0
- data/lib/mm_es_search/api/query/query_filter.rb +28 -0
- data/lib/mm_es_search/api/query/query_string_query.rb +37 -0
- data/lib/mm_es_search/api/query/range_filter.rb +11 -0
- data/lib/mm_es_search/api/query/range_query.rb +57 -0
- data/lib/mm_es_search/api/query/scored_filter.rb +29 -0
- data/lib/mm_es_search/api/query/single_bool_filter.rb +66 -0
- data/lib/mm_es_search/api/query/term_filter.rb +11 -0
- data/lib/mm_es_search/api/query/term_query.rb +34 -0
- data/lib/mm_es_search/api/query/terms_filter.rb +11 -0
- data/lib/mm_es_search/api/query/terms_query.rb +58 -0
- data/lib/mm_es_search/api/query/text_query.rb +42 -0
- data/lib/mm_es_search/api/query/top_children_query.rb +28 -0
- data/lib/mm_es_search/api/sort/root_sort.rb +36 -0
- data/lib/mm_es_search/models/abstract_facet_model.rb +23 -0
- data/lib/mm_es_search/models/abstract_query_model.rb +21 -0
- data/lib/mm_es_search/models/abstract_range_facet_model.rb +365 -0
- data/lib/mm_es_search/models/abstract_search_model.OLD +538 -0
- data/lib/mm_es_search/models/abstract_search_model.rb +521 -0
- data/lib/mm_es_search/models/abstract_sort_model.rb +13 -0
- data/lib/mm_es_search/models/abstract_terms_facet_model.rb +87 -0
- data/lib/mm_es_search/models/root_sort_model.rb +20 -0
- data/lib/mm_es_search/models/virtual_field_sort.rb +52 -0
- data/lib/mm_es_search/utils/facet_row_utils.rb +86 -0
- data/lib/mm_es_search/utils/search_logger.rb +10 -0
- data/lib/mm_es_search/version.rb +3 -0
- data/lib/mm_es_search.rb +124 -0
- data/mm_es_search.gemspec +24 -0
- metadata +132 -0
@@ -0,0 +1,521 @@
|
|
1
|
+
module MmEsSearch
|
2
|
+
module Models
|
3
|
+
|
4
|
+
module AbstractSearchModel
|
5
|
+
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include MmEsSearch::Api::Query
|
8
|
+
include MmEsSearch::Api::Sort
|
9
|
+
include MmEsSearch::Api::Facet
|
10
|
+
include MmEsSearch::Api::Highlight
|
11
|
+
include MmEsSearch::Models
|
12
|
+
include MmEsSearch::Utils
|
13
|
+
|
14
|
+
included do
|
15
|
+
|
16
|
+
NUM_TOP_RESULTS ||= 50
|
17
|
+
RESULT_REUSE_PERIOD ||= 30.seconds
|
18
|
+
|
19
|
+
#one :query_object, :class_name => 'MmEsSearch::Models::AbstractQueryModel'
|
20
|
+
#one :sort_object, :class_name => 'MmEsSearch::Models::AbstractSortModel'
|
21
|
+
#one :highlight_object, :class_name => 'MmEsSearch::Api::Highlight::ResultHighlight'
|
22
|
+
#many :facets, :class_name => 'MmEsSearch::Models::AbstractFacetModel'
|
23
|
+
|
24
|
+
# key :result_total, Integer
|
25
|
+
# key :result_ids, Array
|
26
|
+
# key :highlights, Array
|
27
|
+
key :facet_status, Symbol
|
28
|
+
key :debug, Boolean
|
29
|
+
|
30
|
+
attr_accessor :results, :response #:query_string
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
def run(options = {})
|
39
|
+
|
40
|
+
process_run_options(options)
|
41
|
+
|
42
|
+
if can_reuse_results?
|
43
|
+
|
44
|
+
puts "INFO: reusing previous results"
|
45
|
+
extract_page_results_from_top_results
|
46
|
+
page_results
|
47
|
+
|
48
|
+
else
|
49
|
+
|
50
|
+
@results = nil
|
51
|
+
|
52
|
+
if @facet_mode == :auto
|
53
|
+
remove_optional_facets
|
54
|
+
@auto_explore_needed = true
|
55
|
+
# #NOTE hack for debugging
|
56
|
+
# @auto_explore_needed = false
|
57
|
+
build_type_facet unless type_facet.present?
|
58
|
+
end
|
59
|
+
|
60
|
+
facets.each(&:prepare_for_new_data)
|
61
|
+
|
62
|
+
if @facet_mode
|
63
|
+
self.facet_status = :in_progress
|
64
|
+
else
|
65
|
+
self.facet_status = :none_requested
|
66
|
+
end
|
67
|
+
|
68
|
+
execute_query :main
|
69
|
+
process_query_results
|
70
|
+
route_facet_query_results
|
71
|
+
|
72
|
+
if have_pending_facets?
|
73
|
+
self.facet_status = :pending
|
74
|
+
elsif all_facets_finished?
|
75
|
+
self.facet_status = :complete
|
76
|
+
end
|
77
|
+
|
78
|
+
# #NOTE HACK while investigating search
|
79
|
+
# self.facet_status = :complete
|
80
|
+
|
81
|
+
save if @autosave
|
82
|
+
|
83
|
+
case @return
|
84
|
+
when :raw_response
|
85
|
+
@response
|
86
|
+
when :ids
|
87
|
+
page_result_ids
|
88
|
+
when :results
|
89
|
+
page_results
|
90
|
+
@results #here as a reminder that this collection is memoized
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_facets
|
98
|
+
|
99
|
+
puts "STARTED RUNNING FACETS"
|
100
|
+
|
101
|
+
time = Benchmark.measure do
|
102
|
+
|
103
|
+
sanity_check = 0
|
104
|
+
while have_pending_facets? and sanity_check < 10
|
105
|
+
#binding.pry
|
106
|
+
facet_parent_queries.each do |parent_query|
|
107
|
+
execute_query parent_query, :for_facets_only
|
108
|
+
route_facet_query_results
|
109
|
+
end
|
110
|
+
sanity_check += 1
|
111
|
+
end
|
112
|
+
|
113
|
+
self.facet_status = :complete
|
114
|
+
|
115
|
+
#NOTE: this can throw a stack overflow if using Fibres to call run_facets async
|
116
|
+
#this appears to be due to the limited 4k stack of a Fibre
|
117
|
+
#and the fact that saving calls a gazillion methods
|
118
|
+
#for this reason I use the "defer" method in Celluloid
|
119
|
+
#as this gives async without using fibres... or something...
|
120
|
+
#... well it works, whatever it does...
|
121
|
+
save if @autosave
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
puts "ENDED RUNNING FACETS #{time.inspect}"
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def process_run_options(options = {})
|
131
|
+
#set instance variables for important options e.g. page, per_page
|
132
|
+
validate_options(options)
|
133
|
+
options = options.symbolize_keys.reverse_merge(default_run_options)
|
134
|
+
options.each do |key, value|
|
135
|
+
instance_variable_set "@#{key}", value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def validate_options(options)
|
140
|
+
valid_options = default_run_options.keys.to_set
|
141
|
+
valid_options += valid_options.map(&:to_s)
|
142
|
+
unless valid_options.superset?(options.keys.to_set)
|
143
|
+
raise "invalid options passed"
|
144
|
+
end
|
145
|
+
true
|
146
|
+
end
|
147
|
+
|
148
|
+
def default_run_options
|
149
|
+
@default_run_options ||= {
|
150
|
+
:target => :es,
|
151
|
+
:force_refresh => false,
|
152
|
+
:page => 1,
|
153
|
+
:per_page => 10,
|
154
|
+
:fields => [],
|
155
|
+
:return => :results,
|
156
|
+
:sorted => true,
|
157
|
+
:highlight => true,
|
158
|
+
:facet_mode => :auto,
|
159
|
+
:autosave => false
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
def page
|
164
|
+
@page ||= 1
|
165
|
+
end
|
166
|
+
|
167
|
+
def per_page
|
168
|
+
@per_page ||= 10
|
169
|
+
end
|
170
|
+
|
171
|
+
def have_previous_results?
|
172
|
+
top_result_ids.present?
|
173
|
+
end
|
174
|
+
|
175
|
+
def previous_results_fresh?
|
176
|
+
return false unless have_previous_results? and last_run_at.present?
|
177
|
+
(Time.now - last_run_at) < RESULT_REUSE_PERIOD
|
178
|
+
end
|
179
|
+
|
180
|
+
def requested_page_in_top_results_range?
|
181
|
+
page_range.last <= NUM_TOP_RESULTS
|
182
|
+
end
|
183
|
+
|
184
|
+
def page_range
|
185
|
+
lower_index = (page - 1) * per_page
|
186
|
+
upper_index = lower_index + per_page
|
187
|
+
range = lower_index...upper_index
|
188
|
+
end
|
189
|
+
|
190
|
+
def new_results_requested?
|
191
|
+
@force_refresh || @raw_es_response
|
192
|
+
end
|
193
|
+
|
194
|
+
def can_reuse_results?
|
195
|
+
!new_results_requested? && previous_results_fresh? && requested_page_in_top_results_range?
|
196
|
+
end
|
197
|
+
|
198
|
+
def extract_page_results_from_top_results
|
199
|
+
self.page_result_ids = top_result_ids[page_range]
|
200
|
+
end
|
201
|
+
|
202
|
+
def page_results
|
203
|
+
|
204
|
+
#fetch records from db in one call and then reorder to match search result ordering
|
205
|
+
return paginate_records([]) unless page_result_ids.present?
|
206
|
+
return @results if @results.present?
|
207
|
+
|
208
|
+
#NOTE: I use #find_with_fields to avoid redefining the standard MM #find method
|
209
|
+
# this can be trivially implemented with the plucky #where and #fields methods
|
210
|
+
# but is directly implemented in MmUsesUuid
|
211
|
+
unordered_records = target_collection.find_with_fields page_result_ids, :fields => @fields
|
212
|
+
|
213
|
+
if unordered_records.is_a?(Array)
|
214
|
+
records = unordered_records.reorder_by(page_result_ids.map(&:to_s), &Proc.new {|r| r.id.to_s})
|
215
|
+
elsif unordered_records.nil?
|
216
|
+
records = []
|
217
|
+
else
|
218
|
+
records = [unordered_records]
|
219
|
+
end
|
220
|
+
|
221
|
+
paginate_records(records)
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
def paginate_records(records)
|
226
|
+
@results = WillPaginate::Collection.new(page, per_page, result_total || 0)
|
227
|
+
@results.replace(records)
|
228
|
+
@results
|
229
|
+
end
|
230
|
+
|
231
|
+
def prepare_facet_queries_for_query(query_name)
|
232
|
+
@facet_es_queries = {}
|
233
|
+
(facets << self).each do |facet| #NOTE we add self, as search object manages exploratory facet queries
|
234
|
+
queries = facet.es_facet_queries_for_query(query_name)
|
235
|
+
@facet_es_queries.merge!(queries) if queries.present?
|
236
|
+
end
|
237
|
+
@facet_es_queries
|
238
|
+
end
|
239
|
+
|
240
|
+
def process_facet_results(results, target_object = nil)
|
241
|
+
results.each do |label, result|
|
242
|
+
(target_object || self).send "handle_#{label}", result
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def execute_query(query_name, for_facets_only = false)
|
247
|
+
|
248
|
+
case @target
|
249
|
+
when :es
|
250
|
+
|
251
|
+
prepare_facet_queries_for_query query_name unless @facet_mode == :none
|
252
|
+
|
253
|
+
if for_facets_only
|
254
|
+
page = 1
|
255
|
+
per_page = 0
|
256
|
+
request = es_request query_name, :sorted => false, :highlight => false
|
257
|
+
elsif requested_page_in_top_results_range?
|
258
|
+
page = 1
|
259
|
+
per_page = NUM_TOP_RESULTS
|
260
|
+
request = es_request query_name
|
261
|
+
else
|
262
|
+
page = self.page
|
263
|
+
per_page = self.per_page
|
264
|
+
request = es_request query_name
|
265
|
+
end
|
266
|
+
|
267
|
+
@search_log.info(request.except(:query_dsl).to_json) if debug_on?
|
268
|
+
|
269
|
+
@response = target_collection.search_hits(
|
270
|
+
request,
|
271
|
+
:page => page,
|
272
|
+
:per_page => per_page,
|
273
|
+
:ids_only => true,
|
274
|
+
:type => es_type_for_query(query_name)
|
275
|
+
)
|
276
|
+
|
277
|
+
@response
|
278
|
+
|
279
|
+
when :mongo
|
280
|
+
|
281
|
+
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_main_query_if_missing
|
287
|
+
self.query_object ||= build_main_query_object
|
288
|
+
end
|
289
|
+
|
290
|
+
def es_request(query_name, options = {})
|
291
|
+
|
292
|
+
request = {}
|
293
|
+
|
294
|
+
if query_name == :main
|
295
|
+
|
296
|
+
build_main_query_if_missing
|
297
|
+
query = @sorted ? sorted_query : unsorted_query
|
298
|
+
request.merge!(:sort => sort_object.to_es_query) if @sorted and sort_object.is_a?(RootSortModel)
|
299
|
+
request.merge!(:highlight => highlight_object.to_es_query) if @highlight and highlight_object.present?
|
300
|
+
|
301
|
+
else
|
302
|
+
|
303
|
+
filters = [send("build_#{query_name}_query_object").to_filter]
|
304
|
+
query = build_filtered_query(MatchAllQuery.new, filters)
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
request.merge!(:query => query.to_es_query, :query_dsl => false)
|
309
|
+
request.merge!(:facets => @facet_es_queries) if @facet_es_queries.present?
|
310
|
+
request
|
311
|
+
|
312
|
+
end
|
313
|
+
|
314
|
+
def process_query_results
|
315
|
+
|
316
|
+
case @response.hits.first
|
317
|
+
when ElasticSearch::Api::Hit
|
318
|
+
ids = @response.hits.map(&:_id)
|
319
|
+
else
|
320
|
+
ids = @response.hits
|
321
|
+
end
|
322
|
+
|
323
|
+
if requested_page_in_top_results_range?
|
324
|
+
self.top_result_ids = ids
|
325
|
+
extract_page_results_from_top_results
|
326
|
+
else
|
327
|
+
self.top_result_ids = []
|
328
|
+
self.page_result_ids = ids
|
329
|
+
end
|
330
|
+
|
331
|
+
self.result_total = @response.total_entries
|
332
|
+
self.highlights = @response.response['hits']['hits'].map {|hit| hit['highlight']} if highlight_object.present?
|
333
|
+
|
334
|
+
self.last_run_at = Time.now.utc
|
335
|
+
|
336
|
+
end
|
337
|
+
|
338
|
+
def route_facet_query_results
|
339
|
+
|
340
|
+
facet_results = @response.facets
|
341
|
+
return unless facet_results.present?
|
342
|
+
|
343
|
+
grouped_queries = Hash.new { |hash, id| hash[id] = {} }
|
344
|
+
facet_results.each_with_object(grouped_queries) do |(label, result), hsh|
|
345
|
+
label_parts = label.split('_')
|
346
|
+
id_prefix = label_parts.shift.to_i
|
347
|
+
trimmed_label = label_parts.join('_')
|
348
|
+
hsh[id_prefix].merge!(trimmed_label => result)
|
349
|
+
end
|
350
|
+
|
351
|
+
grouped_queries.each do |obj_id, results|
|
352
|
+
query_owner = ObjectSpace._id2ref(obj_id)
|
353
|
+
query_owner.process_facet_results results
|
354
|
+
end
|
355
|
+
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
def have_pending_facets?
|
360
|
+
facets.any? { |f| f.current_state != :ready_for_display } || (@auto_explore_needed and type_facet_positively_set?)
|
361
|
+
end
|
362
|
+
|
363
|
+
def all_facets_finished?
|
364
|
+
facets.all? { |f| f.current_state == :ready_for_display }
|
365
|
+
end
|
366
|
+
|
367
|
+
def prefix_label(label)
|
368
|
+
AbstractFacetModel.prefix_label(self, label)
|
369
|
+
end
|
370
|
+
|
371
|
+
def type_facet
|
372
|
+
facets.detect {|facet| facet.virtual_field == type_field}
|
373
|
+
end
|
374
|
+
|
375
|
+
def type_facet_positively_set?
|
376
|
+
return false unless type_facet.present?
|
377
|
+
type_facet.positively_checked_rows.present?
|
378
|
+
end
|
379
|
+
|
380
|
+
def used_facets
|
381
|
+
facets.select(&:used?)
|
382
|
+
end
|
383
|
+
|
384
|
+
def unused_facets
|
385
|
+
facets.select(&:unused?)
|
386
|
+
end
|
387
|
+
|
388
|
+
def required_facets
|
389
|
+
facets.select(&:required?)
|
390
|
+
end
|
391
|
+
|
392
|
+
def used_or_required_facets
|
393
|
+
facets.select(&:used_or_required?)
|
394
|
+
end
|
395
|
+
|
396
|
+
def remove_optional_facets
|
397
|
+
facets.each do |f|
|
398
|
+
remove_facet f unless f.used? or f.required?
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def combine_queries(scored, unscored)
|
403
|
+
query = if scored.empty? and unscored.empty?
|
404
|
+
MatchAllQuery.new
|
405
|
+
elsif scored.empty?
|
406
|
+
ConstantScoreQuery.new(
|
407
|
+
:boost => 1,
|
408
|
+
:query => BoolQuery.new(
|
409
|
+
:musts => unscored
|
410
|
+
)
|
411
|
+
)
|
412
|
+
elsif unscored.empty?
|
413
|
+
if scored.length > 1
|
414
|
+
BoolQuery.new(
|
415
|
+
:musts => scored
|
416
|
+
)
|
417
|
+
else
|
418
|
+
scored.first
|
419
|
+
end
|
420
|
+
else
|
421
|
+
# mod_scored = scored.map {|query| q = query.dup; q.boost = 1e100; q }
|
422
|
+
mod_unscored = unscored.map {|query| q = query.dup; q.boost = 0; q }
|
423
|
+
BoolQuery.new(
|
424
|
+
:musts => scored + mod_unscored
|
425
|
+
)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
def unsorted_query
|
430
|
+
build_main_query_if_missing
|
431
|
+
unscored_queries, filters = sort_query_and_facets_as_filters #NOTE: we put non-RootSortModel sorts in as filters as these typically restrict results
|
432
|
+
query = combine_queries([], unscored_queries)
|
433
|
+
build_filtered_query(query, filters)
|
434
|
+
end
|
435
|
+
|
436
|
+
def sorted_query
|
437
|
+
build_main_query_if_missing
|
438
|
+
if (sort_object.nil? and query_object.nil?) or sort_object.is_a?(RootSortModel)
|
439
|
+
unsorted_query
|
440
|
+
else
|
441
|
+
if sort_object.nil?
|
442
|
+
query = query_object.to_query
|
443
|
+
filters = facets_as_filters
|
444
|
+
else
|
445
|
+
unscored_queries, filters = query_and_facets_as_filters
|
446
|
+
query = combine_queries([sort_object.to_query], unscored_queries)
|
447
|
+
end
|
448
|
+
build_filtered_query(query, filters)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
def sort_query_and_facets_as_filters
|
453
|
+
unscored_queries, filters = query_and_facets_as_filters
|
454
|
+
filters << sort_object.to_filter unless (sort_object.nil? or sort_object.is_a?(RootSortModel))
|
455
|
+
return unscored_queries, filters
|
456
|
+
end
|
457
|
+
|
458
|
+
def query_and_facets_as_filters
|
459
|
+
filters = facets_as_filters
|
460
|
+
unscored_queries = []
|
461
|
+
query_as_filter = query_object.present? ? query_object.to_filter : nil
|
462
|
+
if query_as_filter
|
463
|
+
filters << query_as_filter
|
464
|
+
elsif query_object.present?
|
465
|
+
unscored_queries << query_object.to_query
|
466
|
+
end
|
467
|
+
return unscored_queries, filters
|
468
|
+
end
|
469
|
+
|
470
|
+
def facets_as_filters
|
471
|
+
used_facets.map(&:to_filter).compact.flatten
|
472
|
+
end
|
473
|
+
|
474
|
+
def build_filtered_query(query, filters)
|
475
|
+
if filters.nil? or filters.empty?
|
476
|
+
query
|
477
|
+
else
|
478
|
+
FilteredQuery.new(
|
479
|
+
:query => query,
|
480
|
+
:filter => AndFilter.new(
|
481
|
+
:filters => filters
|
482
|
+
)
|
483
|
+
)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
def debug_on?
|
488
|
+
on = !!debug
|
489
|
+
prepare_log if on and @search_log.nil?
|
490
|
+
on
|
491
|
+
end
|
492
|
+
|
493
|
+
def debug_on
|
494
|
+
self.debug = true
|
495
|
+
prepare_log unless @search_log
|
496
|
+
return self
|
497
|
+
end
|
498
|
+
|
499
|
+
def prepare_log
|
500
|
+
logfile = File.open(Rails.root.to_s + '/log/search.log', 'a')
|
501
|
+
logfile.sync = true
|
502
|
+
@search_log = SearchLogger.new(logfile)
|
503
|
+
#@search_log.info "#{self.class.name} now logging\n"
|
504
|
+
end
|
505
|
+
|
506
|
+
def debug_off
|
507
|
+
self.debug = nil
|
508
|
+
@search_log = nil
|
509
|
+
return self
|
510
|
+
end
|
511
|
+
|
512
|
+
def target_collection
|
513
|
+
#we assume name is of form klass.name + "Search"
|
514
|
+
klass_match = self.class.name.match(/(?<klass>\w*)(?=Search)/)
|
515
|
+
raise "expected the class name '#{self.class.name}' to be of form 'SomethingSearch' so that we can extract 'Something' as the target collection" unless klass_match[:klass]
|
516
|
+
klass_match[:klass].constantize
|
517
|
+
end
|
518
|
+
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module MmEsSearch
|
2
|
+
module Models
|
3
|
+
|
4
|
+
module AbstractTermsFacetModel
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
|
9
|
+
DEFAULT_NUM_RESULTS ||= 10
|
10
|
+
|
11
|
+
# redefinition to include default
|
12
|
+
many :rows, :class_name => 'MmEsSearch::Api::Facet::TermsFacetRow'
|
13
|
+
key :exclude, Array
|
14
|
+
key :other, Integer
|
15
|
+
|
16
|
+
aasm_initial_state -> facet do
|
17
|
+
if facet.valid?
|
18
|
+
if facet.rows.present?
|
19
|
+
:ready_for_display
|
20
|
+
else
|
21
|
+
:need_row_data
|
22
|
+
end
|
23
|
+
else
|
24
|
+
:missing_required_fields
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
aasm_event :typed_facet_initialized do
|
29
|
+
transitions :to => :need_row_data, :from => [:ready_for_initialization]
|
30
|
+
end
|
31
|
+
|
32
|
+
aasm_event :prepare_for_new_data, :after => :prune_unchecked_rows do
|
33
|
+
transitions :to => :need_row_data, :from => [:ready_for_display, :need_row_data]
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
|
40
|
+
def new(params = {})
|
41
|
+
new_instance = super(params)
|
42
|
+
new_instance.typed_facet_initialized
|
43
|
+
new_instance
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module InstanceMethods
|
49
|
+
include MmEsSearch::Api::Facet
|
50
|
+
|
51
|
+
def result_name
|
52
|
+
'terms'
|
53
|
+
end
|
54
|
+
|
55
|
+
def row_class
|
56
|
+
TermsFacetRow
|
57
|
+
end
|
58
|
+
|
59
|
+
def build_facet_rows(result)
|
60
|
+
self.other = result['other']
|
61
|
+
super(result)
|
62
|
+
end
|
63
|
+
|
64
|
+
# def facet_filter
|
65
|
+
# #create this to provide additional constraints
|
66
|
+
# end
|
67
|
+
|
68
|
+
def to_facet
|
69
|
+
TermsFacet.new(
|
70
|
+
default_params.merge(
|
71
|
+
:label => prefix_label('display_result'),
|
72
|
+
:size => (@num_result_rows || self.class::DEFAULT_NUM_RESULTS) + checked_rows.length,
|
73
|
+
:exclude => exclude,
|
74
|
+
:facet_filter => facet_filter
|
75
|
+
)
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def required_row_fields
|
80
|
+
['term']
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module MmEsSearch
|
2
|
+
module Models
|
3
|
+
class RootSortModel < AbstractSortModel
|
4
|
+
|
5
|
+
key :field, String
|
6
|
+
|
7
|
+
# def to_query
|
8
|
+
# end
|
9
|
+
|
10
|
+
def to_mongo_query
|
11
|
+
RootSort.new(:field => field, :direction => direction).to_mongo_query
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_es_query
|
15
|
+
RootSort.new(:field => field, :direction => direction).to_es_query
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module MmEsSearch
|
2
|
+
module Models
|
3
|
+
|
4
|
+
class VirtualFieldSort < AbstractSortModel
|
5
|
+
|
6
|
+
key :virtual_field, String
|
7
|
+
key :data_type, String
|
8
|
+
|
9
|
+
def to_es_query
|
10
|
+
to_query.to_es_query
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_query
|
14
|
+
NestedQuery.new(
|
15
|
+
:score_mode => "max",
|
16
|
+
:path => path,
|
17
|
+
:query => CustomScoreQuery.new(
|
18
|
+
:script => sort_script,
|
19
|
+
:query => TermQuery.new(
|
20
|
+
:path => path,
|
21
|
+
:field => field,
|
22
|
+
:value => virtual_field
|
23
|
+
)
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_filter
|
29
|
+
NestedFilter.new(
|
30
|
+
:path => path,
|
31
|
+
:query => TermFilter.new(
|
32
|
+
:path => path,
|
33
|
+
:field => field,
|
34
|
+
:value => virtual_field
|
35
|
+
)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def sort_script
|
40
|
+
sort_field = self.sort_field
|
41
|
+
mod_path = path.gsub(/\.[0-9]+/,'')
|
42
|
+
case direction
|
43
|
+
when "asc", "ascending", nil
|
44
|
+
"0 - doc['#{mod_path}.#{sort_field}'].value"
|
45
|
+
when "desc", "descending"
|
46
|
+
"0 + doc['#{mod_path}.#{sort_field}'].value"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|