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.
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