mm_es_search 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitignore +4 -0
  2. data/.project +18 -0
  3. data/Gemfile +4 -0
  4. data/Rakefile +1 -0
  5. data/lib/mm_es_search/api/facet/abstract_facet.rb +28 -0
  6. data/lib/mm_es_search/api/facet/date_histogram_facet.rb +11 -0
  7. data/lib/mm_es_search/api/facet/filter_facet.rb +9 -0
  8. data/lib/mm_es_search/api/facet/geo_distance_facet.rb +9 -0
  9. data/lib/mm_es_search/api/facet/histogram_facet.rb +9 -0
  10. data/lib/mm_es_search/api/facet/query_facet.rb +9 -0
  11. data/lib/mm_es_search/api/facet/range_facet.rb +36 -0
  12. data/lib/mm_es_search/api/facet/range_facet_row.rb +97 -0
  13. data/lib/mm_es_search/api/facet/range_item.rb +17 -0
  14. data/lib/mm_es_search/api/facet/statistical_facet.rb +33 -0
  15. data/lib/mm_es_search/api/facet/statistical_facet_result.rb +36 -0
  16. data/lib/mm_es_search/api/facet/terms_facet.rb +62 -0
  17. data/lib/mm_es_search/api/facet/terms_facet_row.rb +35 -0
  18. data/lib/mm_es_search/api/facet/terms_stats_facet.rb +9 -0
  19. data/lib/mm_es_search/api/highlight/result_highlight.rb +40 -0
  20. data/lib/mm_es_search/api/query/abstract_filter.rb +15 -0
  21. data/lib/mm_es_search/api/query/abstract_query.rb +48 -0
  22. data/lib/mm_es_search/api/query/and_filter.rb +9 -0
  23. data/lib/mm_es_search/api/query/bool_filter.rb +11 -0
  24. data/lib/mm_es_search/api/query/bool_query.rb +67 -0
  25. data/lib/mm_es_search/api/query/constant_score_query.rb +31 -0
  26. data/lib/mm_es_search/api/query/custom_filters_score_query.rb +52 -0
  27. data/lib/mm_es_search/api/query/custom_score_query.rb +31 -0
  28. data/lib/mm_es_search/api/query/dismax_query.rb +29 -0
  29. data/lib/mm_es_search/api/query/filtered_query.rb +30 -0
  30. data/lib/mm_es_search/api/query/has_child_filter.rb +11 -0
  31. data/lib/mm_es_search/api/query/has_child_query.rb +25 -0
  32. data/lib/mm_es_search/api/query/has_parent_filter.rb +11 -0
  33. data/lib/mm_es_search/api/query/has_parent_query.rb +25 -0
  34. data/lib/mm_es_search/api/query/match_all_filter.rb +11 -0
  35. data/lib/mm_es_search/api/query/match_all_query.rb +19 -0
  36. data/lib/mm_es_search/api/query/nested_filter.rb +22 -0
  37. data/lib/mm_es_search/api/query/nested_query.rb +62 -0
  38. data/lib/mm_es_search/api/query/not_filter.rb +9 -0
  39. data/lib/mm_es_search/api/query/or_filter.rb +9 -0
  40. data/lib/mm_es_search/api/query/prefix_filter.rb +11 -0
  41. data/lib/mm_es_search/api/query/prefix_query.rb +34 -0
  42. data/lib/mm_es_search/api/query/query_filter.rb +28 -0
  43. data/lib/mm_es_search/api/query/query_string_query.rb +37 -0
  44. data/lib/mm_es_search/api/query/range_filter.rb +11 -0
  45. data/lib/mm_es_search/api/query/range_query.rb +57 -0
  46. data/lib/mm_es_search/api/query/scored_filter.rb +29 -0
  47. data/lib/mm_es_search/api/query/single_bool_filter.rb +66 -0
  48. data/lib/mm_es_search/api/query/term_filter.rb +11 -0
  49. data/lib/mm_es_search/api/query/term_query.rb +34 -0
  50. data/lib/mm_es_search/api/query/terms_filter.rb +11 -0
  51. data/lib/mm_es_search/api/query/terms_query.rb +58 -0
  52. data/lib/mm_es_search/api/query/text_query.rb +42 -0
  53. data/lib/mm_es_search/api/query/top_children_query.rb +28 -0
  54. data/lib/mm_es_search/api/sort/root_sort.rb +36 -0
  55. data/lib/mm_es_search/models/abstract_facet_model.rb +23 -0
  56. data/lib/mm_es_search/models/abstract_query_model.rb +21 -0
  57. data/lib/mm_es_search/models/abstract_range_facet_model.rb +365 -0
  58. data/lib/mm_es_search/models/abstract_search_model.OLD +538 -0
  59. data/lib/mm_es_search/models/abstract_search_model.rb +521 -0
  60. data/lib/mm_es_search/models/abstract_sort_model.rb +13 -0
  61. data/lib/mm_es_search/models/abstract_terms_facet_model.rb +87 -0
  62. data/lib/mm_es_search/models/root_sort_model.rb +20 -0
  63. data/lib/mm_es_search/models/virtual_field_sort.rb +52 -0
  64. data/lib/mm_es_search/utils/facet_row_utils.rb +86 -0
  65. data/lib/mm_es_search/utils/search_logger.rb +10 -0
  66. data/lib/mm_es_search/version.rb +3 -0
  67. data/lib/mm_es_search.rb +124 -0
  68. data/mm_es_search.gemspec +24 -0
  69. 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,13 @@
1
+ module MmEsSearch
2
+ module Models
3
+ class AbstractSortModel
4
+
5
+ include MmEsSearch::Api::Sort
6
+ include MongoMapper::EmbeddedDocument
7
+ plugin MmUsesNoId
8
+
9
+ key :direction, String # "asc"* | "desc"
10
+
11
+ end
12
+ end
13
+ 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