bento_search 1.7.0.beta.1 → 1.7.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/README.md +16 -7
- data/app/assets/javascripts/bento_search/ajax_load.js +37 -21
- data/app/helpers/bento_search_helper.rb +72 -74
- data/app/models/bento_search/concurrent_searcher.rb +136 -0
- data/app/models/bento_search/multi_searcher.rb +36 -35
- data/app/models/bento_search/search_engine.rb +70 -40
- data/app/search_engines/bento_search/doaj_articles_engine.rb +1 -1
- data/app/search_engines/bento_search/eds_engine.rb +176 -56
- data/app/views/bento_search/_ajax_loading.html.erb +17 -0
- data/app/views/bento_search/_link.html.erb +3 -3
- data/lib/bento_search.rb +12 -0
- data/lib/bento_search/engine.rb +2 -0
- data/lib/bento_search/version.rb +1 -1
- data/test/search_engines/eds_engine_test.rb +91 -59
- data/test/search_engines/search_engine_base_test.rb +11 -0
- data/test/search_engines/search_engine_test.rb +12 -0
- data/test/unit/concurrent_searcher_test.rb +75 -0
- data/test/unit/multi_searcher_test.rb +16 -19
- data/test/vcr_cassettes/eds/FullText_CustomLink.yml +198 -0
- data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1036 -1729
- data/test/vcr_cassettes/eds/catalog_ebook_query.yml +218 -0
- data/test/vcr_cassettes/eds/catalog_query.yml +255 -0
- data/test/vcr_cassettes/eds/get_auth_token.yml +11 -44
- data/test/vcr_cassettes/eds/get_auth_token_failure.yml +10 -7
- data/test/vcr_cassettes/eds/get_with_auth.yml +144 -153
- data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +167 -223
- metadata +15 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65bfa7da3d1e0896569d44cb7875931b92876186
|
4
|
+
data.tar.gz: 222147474c34a761bfc483abd5185a3b4712b6fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee8d316144f84e0469c37d0784d894cda427dc8606ad736af9a0172223c5ace2a61db026fb3d44d2b586791894160dbf5fdce368ae1c5e510314b067ca66bbd0
|
7
|
+
data.tar.gz: 3b31f327569b5e9de6fce19da85b7d6f653dae97c9b3ace77139be4c0048a3dff992a3579b4da9b54a553870c23c4fcc681ce193f2dfe3465b7f5487be2ba3e2
|
data/README.md
CHANGED
@@ -250,12 +250,12 @@ to execute concurrently in seperate threads, so the total wait time is the slowe
|
|
250
250
|
engine, not the sum of the engines.
|
251
251
|
|
252
252
|
You can write your own logic using ruby threads to do this, but
|
253
|
-
BentoSearch provides a multi-searching helper using [
|
253
|
+
BentoSearch provides a multi-searching helper using [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby)
|
254
254
|
to help you do this easily. Say, in a controller:
|
255
255
|
|
256
256
|
~~~~ruby
|
257
257
|
# constructor takes id's registered with BentoSearch.register_engine
|
258
|
-
searcher = BentoSearch::
|
258
|
+
searcher = BentoSearch::ConcurentSearcher.new(:gbs, :scopus, :summon)
|
259
259
|
|
260
260
|
# Call 'search' with any parameters you would give to an_engine.search
|
261
261
|
searcher.search("my query", :semantic_search_field => :author, :sort => "title")
|
@@ -273,13 +273,22 @@ search execute in a seperate thread, so you can continue doing other work
|
|
273
273
|
in the main thread (like search a local store of some kind outside of
|
274
274
|
bento_search)
|
275
275
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
276
|
+
If you are using a Rails previous to 5.x, you will have to add the
|
277
|
+
`concurrent-ruby` gem to your `Gemfile` (It's already a dependency of
|
278
|
+
Rails5).
|
279
|
+
|
280
|
+
If you are using Rails5, ConcurrentSearcher uses new Rails API that
|
281
|
+
should make development-mode class reloading work fine even with
|
282
|
+
the ConcurrentSearcher's concurrency.
|
283
|
+
|
284
|
+
For more info, see [BentoSearch::ConcurrentSearcher](./app/models/bento_search/concurrent_searcher.rb).
|
285
|
+
|
286
|
+
The previous **MultiSearcher** class is now deprecated, ConcurrentSearcher
|
287
|
+
is the replacement, and will likely work as a drop-in replacement.
|
288
|
+
See [CHANGES](./CHANGES.md#17) for more info.
|
289
|
+
|
280
290
|
|
281
291
|
|
282
|
-
For more info, see [BentoSearch::MultiSearcher](./app/models/bento_search/multi_searcher.rb).
|
283
292
|
|
284
293
|
### Delayed results loading via AJAX (actually more like AJAHtml)
|
285
294
|
|
@@ -1,56 +1,72 @@
|
|
1
1
|
var BentoSearch = BentoSearch || {}
|
2
2
|
|
3
|
-
// Pass in a DOM node that has a data-ajax-url attribute.
|
3
|
+
// Pass in a DOM node that has a data-ajax-url attribute.
|
4
4
|
// Will AJAX load bento search results inside that node.
|
5
|
-
//
|
5
|
+
//
|
6
|
+
// optional success_callback function.
|
7
|
+
//
|
8
|
+
// success_callback: will have the container div as 'this', and new content
|
9
|
+
// as first argument (in JQuery wrapper). Called before DOM update happens, so
|
10
|
+
// first arg div is not yet placed in the DOM. Callback can
|
11
|
+
// return `false` to prevent automatic placement in the DOM, if you want
|
12
|
+
// to handle it yourself.
|
13
|
+
//
|
14
|
+
// You can set default success_callback function for all calls with:
|
15
|
+
//
|
16
|
+
// BentoSearch.ajax_load.default_success_callback = function(div) { ...
|
6
17
|
BentoSearch.ajax_load = function(node, success_callback) {
|
18
|
+
// default success_callback
|
19
|
+
if (success_callback === undefined) {
|
20
|
+
success_callback = BentoSearch.ajax_load.default_success_callback;
|
21
|
+
}
|
22
|
+
|
7
23
|
var div = $(node);
|
8
|
-
|
24
|
+
|
9
25
|
if (div.length == 0) {
|
10
26
|
//we've got nothing
|
11
27
|
return
|
12
28
|
}
|
13
|
-
|
29
|
+
|
14
30
|
|
15
31
|
// We find the "waiting"/spinner section already rendered,
|
16
32
|
// and show it. We experimented with generating the spinner/waiting
|
17
33
|
// purely in JS, instead of rendering a hidden one server-side. But
|
18
|
-
// it was too weird and unreliable to do that, sorry.
|
34
|
+
// it was too weird and unreliable to do that, sorry.
|
19
35
|
div.find(".bento_search_ajax_loading").show();
|
20
|
-
|
21
|
-
|
36
|
+
|
37
|
+
|
22
38
|
// Now load the actual external content from html5 data-bento-ajax-url
|
23
39
|
$.ajax({
|
24
|
-
url: div.data("bentoAjaxUrl"),
|
40
|
+
url: div.data("bentoAjaxUrl"),
|
25
41
|
success: function(response, status, xhr) {
|
26
|
-
var do_replace = true;
|
42
|
+
var do_replace = true;
|
27
43
|
if (success_callback) {
|
28
44
|
// We need to make the response into a DOM so the callback
|
29
45
|
// can deal with it better. Wrapped in a div, so it makes
|
30
|
-
// jquery happy even if there isn't a single parent element.
|
46
|
+
// jquery happy even if there isn't a single parent element.
|
31
47
|
response = $("<div>" + response + "</div>");
|
32
|
-
|
33
|
-
do_replace = success_callback.apply(div, [response]);
|
48
|
+
|
49
|
+
do_replace = success_callback.apply(div, [response]);
|
34
50
|
}
|
35
|
-
if (do_replace != false) {
|
51
|
+
if (do_replace != false) {
|
36
52
|
div.replaceWith(response);
|
37
|
-
}
|
53
|
+
}
|
38
54
|
},
|
39
55
|
error: function(xhr, status, errorThrown) {
|
40
56
|
var msg = "Sorry but there was an error: ";
|
41
57
|
div.html(msg + xhr.status + " " + xhr.statusText + ", " + status);
|
42
58
|
}
|
43
59
|
});
|
44
|
-
|
45
|
-
|
60
|
+
|
61
|
+
|
46
62
|
}
|
47
63
|
|
48
64
|
jQuery(document).ready(function($) {
|
49
65
|
//Intentionally wait for window.load, not just onready, to
|
50
|
-
//prevent interfering with rest of page load.
|
51
|
-
$(window).bind("load", function() {
|
66
|
+
//prevent interfering with rest of page load.
|
67
|
+
$(window).bind("load", function() {
|
52
68
|
$("*[data-bento-search-load=ajax_auto]").each(function(i, div) {
|
53
|
-
BentoSearch.ajax_load(div);
|
69
|
+
BentoSearch.ajax_load(div);
|
54
70
|
});
|
55
|
-
});
|
56
|
-
});
|
71
|
+
});
|
72
|
+
});
|
@@ -3,112 +3,110 @@
|
|
3
3
|
require 'nokogiri'
|
4
4
|
|
5
5
|
# Rails helper module provided by BentoSearch meant to be included
|
6
|
-
# in host app's helpers.
|
6
|
+
# in host app's helpers.
|
7
7
|
module BentoSearchHelper
|
8
|
-
|
8
|
+
|
9
9
|
# Renders bento search results on page, or an AJAX loader, etc, as appropriate.
|
10
10
|
# Pass in:
|
11
11
|
# * BentoSearch::SearchResults => will render
|
12
12
|
# * an instantiated BentoSearch::SearchEngine => Will do search and render
|
13
|
-
# * an id that a search engine was registered under with
|
14
|
-
# BentoSearch.register_engine => will do search and render.
|
13
|
+
# * an id that a search engine was registered under with
|
14
|
+
# BentoSearch.register_engine => will do search and render.
|
15
15
|
#
|
16
16
|
# Second arg options hash includes options for bento_search helper,
|
17
17
|
# as well as other options pased on to BentoSearch::Engine.search(options)
|
18
18
|
#
|
19
|
+
# Partial used for display can be configured on engine with
|
20
|
+
# * for_display.error_partial => gets `results` local
|
21
|
+
# * for_display.no_results_partial => gets `results` local
|
22
|
+
# * for_display.item_partial => `:collection => results, :as => :item, :locals => {:results => results}`
|
23
|
+
# * for_display.ajax_loading_partial => local `engine`
|
24
|
+
#
|
25
|
+
# If not specified for a particular engine, the partials listed in BentoSearch.defaults will be used.
|
26
|
+
#
|
19
27
|
# == Options
|
20
28
|
#
|
21
29
|
# load: :ajax_auto, :immediate. :ajax_auto will put a spinner up there,
|
22
30
|
# and load actual results via AJAX request. :immediate preloads
|
23
|
-
# results.
|
31
|
+
# results.
|
24
32
|
#
|
25
33
|
# == Examples
|
26
34
|
#
|
27
35
|
# bento_results( results_obj )
|
28
36
|
# bento_results( engine_obj, :query => "cancer")
|
29
37
|
# bento_results("google_books", :query => "cancer", :load => :ajax_auto)
|
30
|
-
#
|
38
|
+
#
|
31
39
|
def bento_search(search_arg, options = {})
|
32
|
-
|
40
|
+
|
33
41
|
results = search_arg if search_arg.kind_of? BentoSearch::Results
|
34
|
-
|
35
|
-
load_mode = options.delete(:load)
|
36
|
-
|
42
|
+
|
43
|
+
load_mode = options.delete(:load)
|
44
|
+
|
37
45
|
engine = nil
|
38
46
|
unless results
|
39
|
-
# need to load an engine and do a search, or ajax, etc.
|
47
|
+
# need to load an engine and do a search, or ajax, etc.
|
40
48
|
engine = (if search_arg.kind_of? BentoSearch::SearchEngine
|
41
49
|
search_arg
|
42
50
|
else
|
43
51
|
raise ArgumentError.new("Need Results, engine, or registered engine_id as first argument to #bento_search") unless search_arg
|
44
52
|
BentoSearch.get_engine(search_arg.to_s)
|
45
53
|
end)
|
46
|
-
|
54
|
+
|
47
55
|
end
|
48
56
|
|
49
57
|
if (!results && [:ajax_auto, :ajax_triggered].include?(load_mode))
|
50
58
|
raise ArgumentError.new("`:load => :ajax` requires a registered engine with an id") unless engine.configuration.id
|
51
59
|
content_tag(:div,
|
52
60
|
:class => "bento_search_ajax_wait",
|
53
|
-
:"data-bento-search-load" => load_mode.to_s,
|
61
|
+
:"data-bento-search-load" => load_mode.to_s,
|
54
62
|
:"data-bento-ajax-url" => to_bento_search_url( {:engine_id => engine.configuration.id}.merge(options) )) do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
content_tag("noscript") do
|
59
|
-
I18n.t("bento_search.ajax_noscript")
|
60
|
-
end +
|
61
|
-
content_tag(:div,
|
62
|
-
:class => "bento_search_ajax_loading",
|
63
|
-
:style => "display:none") do
|
64
|
-
image_tag("bento_search/large_loader.gif",
|
65
|
-
:alt => I18n.translate("bento_search.ajax_loading")
|
66
|
-
)
|
63
|
+
|
64
|
+
partial = (engine.configuration.for_display.ajax_loading_partial if engine.configuration.for_display) || BentoSearch.defaults.ajax_loading_partial
|
65
|
+
render :partial => partial, locals: { engine: engine }
|
67
66
|
end
|
68
|
-
end
|
69
67
|
else
|
70
68
|
results = engine.search(options) unless results
|
71
69
|
|
72
70
|
if results.failed?
|
73
|
-
partial = (results.display_configuration.error_partial if results.display_configuration) ||
|
71
|
+
partial = (results.display_configuration.error_partial if results.display_configuration) || BentoSearch.defaults.error_partial
|
74
72
|
render :partial => partial, :locals => {:results => results}
|
75
|
-
elsif results.length > 0
|
76
|
-
partial = (results.display_configuration.item_partial if results.display_configuration) ||
|
73
|
+
elsif results.length > 0
|
74
|
+
partial = (results.display_configuration.item_partial if results.display_configuration) || BentoSearch.defaults.item_partial
|
77
75
|
render :partial => partial, :collection => results, :as => :item, :locals => {:results => results}
|
78
76
|
else
|
79
77
|
content_tag(:div, :class=> "bento_search_no_results") do
|
80
|
-
partial = (results.display_configuration.no_results_partial if results.display_configuration) ||
|
78
|
+
partial = (results.display_configuration.no_results_partial if results.display_configuration) || BentoSearch.defaults.no_results_partial
|
81
79
|
render :partial => partial, :locals => {:results => results}
|
82
80
|
end
|
83
81
|
end
|
84
|
-
end
|
82
|
+
end
|
85
83
|
end
|
86
|
-
|
84
|
+
|
87
85
|
# Wrap a ResultItem in a decorator! For now hard-coded to
|
88
86
|
# BentoSearch::StandardDecorator
|
89
|
-
def bento_decorate(result_item)
|
87
|
+
def bento_decorate(result_item)
|
90
88
|
# in a helper method, 'self' is a view_context already I think?
|
91
|
-
decorated = BentoSearch::DecoratorBase.decorate(result_item, self)
|
92
|
-
yield(decorated) if block_given?
|
93
|
-
return decorated
|
89
|
+
decorated = BentoSearch::DecoratorBase.decorate(result_item, self)
|
90
|
+
yield(decorated) if block_given?
|
91
|
+
return decorated
|
94
92
|
end
|
95
|
-
|
96
|
-
|
93
|
+
|
94
|
+
|
97
95
|
##
|
98
96
|
# More methods used by bento standard views, namespaced with bento_, sorry
|
99
97
|
# no great way to take logic out of views into helper methods without
|
100
|
-
# namespacey hack.
|
98
|
+
# namespacey hack.
|
101
99
|
#
|
102
100
|
# You can use these methods in your own custom views, you also should be
|
103
101
|
# able to over-ride them (including calling super) in local helpers to
|
104
|
-
# change behavior in standard views.
|
102
|
+
# change behavior in standard views.
|
105
103
|
#
|
106
104
|
##
|
107
|
-
|
105
|
+
|
108
106
|
# Like rails truncate helper, and taking the same options, but html_safe.
|
109
|
-
#
|
110
|
-
# If input string is NOT marked html_safe?, simply passes to rails truncate helper.
|
111
|
-
# If a string IS marked html_safe?, uses nokogiri to parse it, and truncate
|
107
|
+
#
|
108
|
+
# If input string is NOT marked html_safe?, simply passes to rails truncate helper.
|
109
|
+
# If a string IS marked html_safe?, uses nokogiri to parse it, and truncate
|
112
110
|
# actual displayed text to max_length, while keeping html structure valid.
|
113
111
|
#
|
114
112
|
# Default omission marker is unicode elipsis
|
@@ -118,23 +116,23 @@ module BentoSearchHelper
|
|
118
116
|
def bento_truncate(str, options = {})
|
119
117
|
return str if str.nil? || str.empty?
|
120
118
|
|
121
|
-
options.reverse_merge!(:omission => "…", :length => 280, :separator => ' ')
|
122
|
-
|
119
|
+
options.reverse_merge!(:omission => "…", :length => 280, :separator => ' ')
|
120
|
+
|
123
121
|
# works for non-html of course, but for html a quick check
|
124
122
|
# to avoid expensive nokogiri parse if the whole string, even
|
125
|
-
# with tags, is still less than max length.
|
123
|
+
# with tags, is still less than max length.
|
126
124
|
return str if str.length < options[:length]
|
127
|
-
|
128
|
-
if str.html_safe?
|
125
|
+
|
126
|
+
if str.html_safe?
|
129
127
|
noko = Nokogiri::HTML::DocumentFragment.parse(str)
|
130
128
|
BentoSearch::Util.nokogiri_truncate(noko, options[:length], options[:omission], options[:separator]).inner_html.html_safe
|
131
129
|
else
|
132
130
|
return truncate(str, options)
|
133
131
|
end
|
134
132
|
end
|
135
|
-
|
136
133
|
|
137
|
-
|
134
|
+
|
135
|
+
|
138
136
|
# Deprecated, made more sense to put this in a partial. Just
|
139
137
|
# call partial directly:
|
140
138
|
# <%= render :partial => "bento_search/item_title", :object => item, :as => 'item' %>
|
@@ -142,41 +140,41 @@ module BentoSearchHelper
|
|
142
140
|
render :partial => "bento_search/item_title", :object => item, :as => 'item'
|
143
141
|
end
|
144
142
|
deprecate :bento_item_title
|
145
|
-
|
143
|
+
|
146
144
|
# pass in 0-based rails current collection counter and a BentoSearch::Results,
|
147
|
-
# calculates a user-displayable result set index label.
|
145
|
+
# calculates a user-displayable result set index label.
|
148
146
|
#
|
149
147
|
# Only non-trivial thing is both inputs are allowed to be nil; if either
|
150
|
-
# is nil, nil is returned.
|
148
|
+
# is nil, nil is returned.
|
151
149
|
def bento_item_counter(counter, results)
|
152
150
|
return nil if counter.nil? || results.nil? || results.start.nil?
|
153
|
-
|
151
|
+
|
154
152
|
return counter + results.start + 1
|
155
153
|
end
|
156
|
-
|
157
|
-
|
154
|
+
|
155
|
+
|
158
156
|
# returns a hash of label => key suitable for passing to rails
|
159
|
-
# options_for_select. (Yes, it works backwards from how you'd expect).
|
157
|
+
# options_for_select. (Yes, it works backwards from how you'd expect).
|
160
158
|
# Label is looked up using I18n, at bento_search.sort_keys.*
|
161
159
|
#
|
162
160
|
# If no i18n is found, titleized version of key itself is used as somewhat
|
163
|
-
# reasonable default.
|
161
|
+
# reasonable default.
|
164
162
|
def bento_sort_hash_for(engine)
|
165
|
-
Hash[
|
163
|
+
Hash[
|
166
164
|
engine.sort_definitions.keys.collect do |key|
|
167
165
|
[I18n.translate(key.to_s, :scope => "bento_search.sort_keys", :default => key.to_s.titleize), key.to_s]
|
168
|
-
end
|
169
|
-
]
|
166
|
+
end
|
167
|
+
]
|
170
168
|
end
|
171
|
-
|
169
|
+
|
172
170
|
# Returns a hash of label => key suitable for passing to rails
|
173
171
|
# options_for_select. ONLY includes fields with :semantics set at
|
174
|
-
# present. Key will be _semantic_ key name.
|
172
|
+
# present. Key will be _semantic_ key name.
|
175
173
|
# For engine-specific fields, you're on you're own, sorry!
|
176
174
|
#
|
177
175
|
# If first arg is an engine instance, will
|
178
176
|
# be search fields supported by that engine. If first arg is nil,
|
179
|
-
# will be any field in our i18n lists for search fields.
|
177
|
+
# will be any field in our i18n lists for search fields.
|
180
178
|
#
|
181
179
|
# Can pass in options :only or :except to customize list. Values
|
182
180
|
# in :only and :except will match on internal field names OR
|
@@ -190,25 +188,25 @@ module BentoSearchHelper
|
|
190
188
|
hash = I18n.t("bento_search.search_fields").invert
|
191
189
|
else
|
192
190
|
hash = Hash[ engine.search_field_definitions.collect do |k, defn|
|
193
|
-
if defn[:semantic] && (label = I18n.t(defn[:semantic], :scope => "bento_search.search_fields", :default => defn[:semantic].to_s.titlecase ))
|
191
|
+
if defn[:semantic] && (label = I18n.t(defn[:semantic], :scope => "bento_search.search_fields", :default => defn[:semantic].to_s.titlecase ))
|
194
192
|
[label, defn[:semantic].to_s]
|
195
193
|
end
|
196
194
|
end.compact]
|
197
195
|
end
|
198
|
-
|
199
|
-
# :only/:except
|
200
|
-
if options[:only]
|
196
|
+
|
197
|
+
# :only/:except
|
198
|
+
if options[:only]
|
201
199
|
keys = [options[:only]].flatten.collect(&:to_s)
|
202
|
-
hash.delete_if {|key, value| ! keys.include?(value) }
|
200
|
+
hash.delete_if {|key, value| ! keys.include?(value) }
|
203
201
|
end
|
204
|
-
|
202
|
+
|
205
203
|
if options[:except]
|
206
204
|
keys = [options[:except]].flatten.collect(&:to_s)
|
207
205
|
hash.delete_if {|key, value| keys.include?(value) }
|
208
206
|
end
|
209
|
-
|
207
|
+
|
210
208
|
return hash
|
211
209
|
end
|
212
|
-
|
213
|
-
|
210
|
+
|
211
|
+
|
214
212
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
begin
|
2
|
+
require 'concurrent'
|
3
|
+
|
4
|
+
# Concurrently runs multiple searches in separate threads. Since a search
|
5
|
+
# generally spends most of it's time waiting on foreign API, this is
|
6
|
+
# useful to significantly reduce total latency of running multiple searches,
|
7
|
+
# even in MRI.
|
8
|
+
#
|
9
|
+
# Uses [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby),
|
10
|
+
# already a dependency of Rails 5.x. To use with Rails previous to 5.x,
|
11
|
+
# just add concurrent-ruby to your `Gemfile`:
|
12
|
+
#
|
13
|
+
# gem 'concurrent-ruby', '~> 1.0'
|
14
|
+
#
|
15
|
+
# # Usage
|
16
|
+
#
|
17
|
+
# initialize with id's of registered engines:
|
18
|
+
#
|
19
|
+
# searcher = BentoBox::MultiSearcher.new(:gbs, :scopus)
|
20
|
+
#
|
21
|
+
# start the concurrent searches, params same as engine.search
|
22
|
+
#
|
23
|
+
# searcher.search( query_params )
|
24
|
+
#
|
25
|
+
# retrieve results, blocking until all are completed:
|
26
|
+
#
|
27
|
+
# results = searcher.results
|
28
|
+
#
|
29
|
+
# returns a Hash keyed by engine id, values BentoSearch::Results objects.
|
30
|
+
#
|
31
|
+
# results # => { "gbs" => <BentoSearch::Results ...>, "scopus" => <BentoSearch::Results ...>}
|
32
|
+
#
|
33
|
+
# Calling results more than once will just return the initial results again
|
34
|
+
# (cached), it won't run a search again.
|
35
|
+
#
|
36
|
+
# ## Dev-mode autoloading and concurrency
|
37
|
+
#
|
38
|
+
# In Rails previous to Rails5, you may have to set config.cache_classes=true
|
39
|
+
# even in development to avoid problems. In Rails 5.x, we take advantage of
|
40
|
+
# new api that should allow concurrency-save autoloading. But if you run into
|
41
|
+
# any weird problems (such as a deadlock), `cache_classes = true` and
|
42
|
+
# `eager_load = true` should eliminate them, at the cost of dev-mode
|
43
|
+
# auto-reloading.
|
44
|
+
#
|
45
|
+
#
|
46
|
+
# TODO: have a method that returns Futures instead of only supplying the blocking
|
47
|
+
# results method? Several tricks, including making sure to properly terminate actors.
|
48
|
+
class BentoSearch::ConcurrentSearcher
|
49
|
+
def initialize(*engine_ids)
|
50
|
+
auto_rescued_exceptions = [StandardError]
|
51
|
+
|
52
|
+
@engines = []
|
53
|
+
engine_ids.each do |id|
|
54
|
+
add_engine( BentoSearch.get_engine(id).tap { |e| e.auto_rescued_exceptions = auto_rescued_exceptions + e.auto_rescued_exceptions })
|
55
|
+
end
|
56
|
+
@extra_auto_rescue_exceptions = [StandardError]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Adds an instantiated engine directly, rather than by id from global
|
60
|
+
# registry.
|
61
|
+
def add_engine(engine)
|
62
|
+
unless engine.configuration.id.present?
|
63
|
+
raise ArgumentError.new("ConcurrentSearcher engines need `configuration.id`, this one didn't have one: #{engine}")
|
64
|
+
end
|
65
|
+
@engines << engine
|
66
|
+
end
|
67
|
+
|
68
|
+
# Starts all searches, returns self so you can chain method calls if you like.
|
69
|
+
def search(*search_args)
|
70
|
+
search_args.freeze
|
71
|
+
@futures = @engines.collect do |engine|
|
72
|
+
Concurrent::Future.execute { rails_future_wrap { engine.search(*search_args) } }
|
73
|
+
end
|
74
|
+
return self
|
75
|
+
end
|
76
|
+
|
77
|
+
# Have you called #search yet? You can only call #results if you have.
|
78
|
+
# Will stay true forever, it doesn't tell you if the search is done or not.
|
79
|
+
def search_started?
|
80
|
+
!! @futures
|
81
|
+
end
|
82
|
+
|
83
|
+
# Call after #search. Blocks until each included engine is finished
|
84
|
+
# then returns a Hash keyed by engine registered id, value is a
|
85
|
+
# BentoSearch::Results object.
|
86
|
+
#
|
87
|
+
# If called multiple times, returns the same results each time, does
|
88
|
+
# not re-run searches.
|
89
|
+
#
|
90
|
+
# It is an error to invoke without having previously called #search
|
91
|
+
def results
|
92
|
+
unless search_started?
|
93
|
+
raise ArgumentError, "Can't call ConcurrentSearcher#results before you have executed a #search"
|
94
|
+
end
|
95
|
+
|
96
|
+
@results ||= begin
|
97
|
+
pairs = rails_wait_wrap do
|
98
|
+
@futures.collect { |future| [future.value!.engine_id, future.value!] }
|
99
|
+
end
|
100
|
+
Hash[ pairs ].freeze
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
# In Rails5, future body's need to be wrapped in an executor,
|
107
|
+
# to handle auto-loading right in dev-mode, among other things.
|
108
|
+
# Rails docs coming, see https://github.com/rails/rails/issues/26847
|
109
|
+
@@rails_has_executor = Rails.application.respond_to?(:executor)
|
110
|
+
def rails_future_wrap
|
111
|
+
if @@rails_has_executor
|
112
|
+
Rails.application.executor.wrap { yield }
|
113
|
+
else
|
114
|
+
yield
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# In Rails5, if we are collecting from within an action method
|
119
|
+
# (ie the 'request loop'), as we usually will be, we need to
|
120
|
+
# give up the autoload lock. Rails docs coming, see https://github.com/rails/rails/issues/26847
|
121
|
+
@@rails_needs_interlock_permit = ActiveSupport::Dependencies.respond_to?(:interlock) &&
|
122
|
+
!(Rails.application.config.eager_load && Rails.application.config.cache_classes)
|
123
|
+
def rails_wait_wrap
|
124
|
+
if @@rails_needs_interlock_permit
|
125
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads { yield }
|
126
|
+
else
|
127
|
+
yield
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
rescue LoadError
|
133
|
+
# you can use bento_search without celluloid, just not
|
134
|
+
# this class.
|
135
|
+
$stderr.puts "Tried but could not load BentoSearch::ConcurrentSearcher, concurrent-ruby not available!"
|
136
|
+
end
|