bento_search 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +299 -0
- data/Rakefile +40 -0
- data/app/assets/images/bento_search/large_loader.gif +0 -0
- data/app/assets/javascripts/bento_search.js +3 -0
- data/app/assets/javascripts/bento_search/ajax_load.js +22 -0
- data/app/assets/stylesheets/bento_search/bento.css +4 -0
- data/app/controllers/bento_search/bento_search_controller.rb +7 -0
- data/app/controllers/bento_search/search_controller.rb +72 -0
- data/app/helpers/bento_search_helper.rb +138 -0
- data/app/item_decorators/bento_search/only_premade_openurl.rb +16 -0
- data/app/item_decorators/bento_search/openurl_add_other_link.rb +35 -0
- data/app/item_decorators/bento_search/openurl_main_link.rb +30 -0
- data/app/models/bento_search/author.rb +25 -0
- data/app/models/bento_search/link.rb +30 -0
- data/app/models/bento_search/multi_searcher.rb +109 -0
- data/app/models/bento_search/openurl_creator.rb +128 -0
- data/app/models/bento_search/registrar.rb +70 -0
- data/app/models/bento_search/result_item.rb +203 -0
- data/app/models/bento_search/results.rb +54 -0
- data/app/models/bento_search/results/pagination.rb +67 -0
- data/app/models/bento_search/search_engine.rb +219 -0
- data/app/models/bento_search/search_engine/capabilities.rb +65 -0
- data/app/search_engines/bento_search/#Untitled-1# +11 -0
- data/app/search_engines/bento_search/ebsco_host_engine.rb +356 -0
- data/app/search_engines/bento_search/eds_engine.rb +557 -0
- data/app/search_engines/bento_search/google_books_engine.rb +184 -0
- data/app/search_engines/bento_search/primo_engine.rb +231 -0
- data/app/search_engines/bento_search/scopus_engine.rb +295 -0
- data/app/search_engines/bento_search/summon_engine.rb +398 -0
- data/app/search_engines/bento_search/xerxes_engine.rb +168 -0
- data/app/views/bento_search/_link.html.erb +4 -0
- data/app/views/bento_search/_search_error.html.erb +22 -0
- data/app/views/bento_search/_std_item.html.erb +39 -0
- data/app/views/bento_search/search/search.html.erb +1 -0
- data/config/locales/en.yml +25 -0
- data/lib/bento_search.rb +29 -0
- data/lib/bento_search/engine.rb +5 -0
- data/lib/bento_search/routes.rb +45 -0
- data/lib/bento_search/version.rb +3 -0
- data/lib/generators/bento_search/pull_ebsco_dbs_generator.rb +24 -0
- data/lib/generators/bento_search/templates/ebsco_global_var.erb +6 -0
- data/lib/http_client_patch/include_client.rb +86 -0
- data/lib/tasks/bento_search_tasks.rake +4 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +56 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +3100 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/functional/bento_search/search_controller_test.rb +81 -0
- data/test/helper/bento_search_helper_test.rb +125 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/support/mock_engine.rb +23 -0
- data/test/support/test_with_cassette.rb +38 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/#vcr_test.rb# +68 -0
- data/test/unit/ebsco_host_engine_test.rb +134 -0
- data/test/unit/eds_engine_test.rb +105 -0
- data/test/unit/google_books_engine_test.rb +93 -0
- data/test/unit/item_decorators_test.rb +66 -0
- data/test/unit/multi_searcher_test.rb +49 -0
- data/test/unit/openurl_creator_test.rb +111 -0
- data/test/unit/pagination_test.rb +59 -0
- data/test/unit/primo_engine_test.rb +37 -0
- data/test/unit/register_engine_test.rb +50 -0
- data/test/unit/result_item_display_test.rb +39 -0
- data/test/unit/result_item_test.rb +36 -0
- data/test/unit/scopus_engine_test.rb +130 -0
- data/test/unit/search_engine_base_test.rb +178 -0
- data/test/unit/search_engine_test.rb +95 -0
- data/test/unit/summon_engine_test.rb +161 -0
- data/test/unit/xerxes_engine_test.rb +70 -0
- data/test/vcr_cassettes/ebscohost/error_bad_db.yml +45 -0
- data/test/vcr_cassettes/ebscohost/error_bad_password.yml +45 -0
- data/test/vcr_cassettes/ebscohost/get_info.yml +3626 -0
- data/test/vcr_cassettes/ebscohost/live_search.yml +45 -0
- data/test/vcr_cassettes/ebscohost/live_search_smoke_test.yml +1311 -0
- data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1811 -0
- data/test/vcr_cassettes/eds/get_auth_token.yml +75 -0
- data/test/vcr_cassettes/eds/get_auth_token_failure.yml +39 -0
- data/test/vcr_cassettes/eds/get_with_auth.yml +243 -0
- data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +368 -0
- data/test/vcr_cassettes/gbs/error_condition.yml +40 -0
- data/test/vcr_cassettes/gbs/pagination.yml +702 -0
- data/test/vcr_cassettes/gbs/search.yml +340 -0
- data/test/vcr_cassettes/primo/search_smoke_test.yml +1112 -0
- data/test/vcr_cassettes/scopus/bad_api_key_should_return_error_response.yml +60 -0
- data/test/vcr_cassettes/scopus/escaped_chars.yml +187 -0
- data/test/vcr_cassettes/scopus/fielded_search.yml +176 -0
- data/test/vcr_cassettes/scopus/simple_search.yml +227 -0
- data/test/vcr_cassettes/scopus/zero_results_search.yml +67 -0
- data/test/vcr_cassettes/summon/bad_auth.yml +54 -0
- data/test/vcr_cassettes/summon/proper_tags_for_snippets.yml +216 -0
- data/test/vcr_cassettes/summon/search.yml +242 -0
- data/test/vcr_cassettes/xerxes/live_search.yml +2580 -0
- data/test/view/std_item_test.rb +98 -0
- metadata +421 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
# Holds a list of registered search engines with configuration.
|
3
|
+
# There's one global one referened by BentoSearch module, but one
|
4
|
+
# might want to create multiple.
|
5
|
+
class BentoSearch::Registrar
|
6
|
+
class ::BentoSearch::NoSuchEngine < Exception ; end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@registered_engine_confs = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Register a configuration for a BentoSearch search engine.
|
13
|
+
# While some parts of BentoSearch can be used without globally registering
|
14
|
+
# a configuration, it is neccesary for features like AJAX load, and
|
15
|
+
# convenient in other places.
|
16
|
+
#
|
17
|
+
# BentoSearch.register_engine("gbs") do |conf|
|
18
|
+
# conf.engine = "GoogleBooksSearch"
|
19
|
+
# conf.api_key = "my_key"
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# BentoSearch.get_engine("gbs")
|
23
|
+
# => a BentoSearch::GoogleBooksSearch, configured as specified.
|
24
|
+
#
|
25
|
+
# The first parameter identifier, eg "gbs", may be used in some
|
26
|
+
# URLs, for AJAX etc.
|
27
|
+
def register_engine(id, &block)
|
28
|
+
conf = Confstruct::Configuration.new(&block)
|
29
|
+
conf.id = id.to_s
|
30
|
+
|
31
|
+
raise ArgumentError.new("Must supply an `engine` class name") unless conf.engine
|
32
|
+
|
33
|
+
@registered_engine_confs[id] = conf
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get a configured SearchEngine, using configuration and engine
|
37
|
+
# class previously registered for `id` with #register_engine.
|
38
|
+
# Raises a BentoSearch::NoSuchEngine if is is not registered.
|
39
|
+
def get_engine(id)
|
40
|
+
conf = @registered_engine_confs[id.to_s]
|
41
|
+
|
42
|
+
raise BentoSearch::NoSuchEngine.new("No registered engine for identifier '#{id}'") unless conf
|
43
|
+
|
44
|
+
# Figure out which SearchEngine class to instantiate
|
45
|
+
klass = constantize(conf.engine)
|
46
|
+
|
47
|
+
return klass.new( conf )
|
48
|
+
end
|
49
|
+
|
50
|
+
# Mostly just used for testing
|
51
|
+
def reset_engine_registrations!
|
52
|
+
@@registered_engine_confs = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# Turn a string into a constant/class object, lexical lookup
|
58
|
+
# within BentoSearch module. Can use whatever would be legal
|
59
|
+
# in ruby, "A", "A::B", "::A::B" (force top-level lookup), etc.
|
60
|
+
def constantize(klass_string)
|
61
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ klass_string
|
62
|
+
raise NameError, "#{klass_string.inspect} is not a valid constant name!"
|
63
|
+
end
|
64
|
+
|
65
|
+
BentoSearch.module_eval(klass_string, __FILE__, __LINE__)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
module BentoSearch
|
2
|
+
# Data object representing a single hit from a search, normalized
|
3
|
+
# with common data fields. Usually held in a BentoSearch::Results object.
|
4
|
+
#
|
5
|
+
# ANY field can be nil, clients should be aware.
|
6
|
+
class ResultItem
|
7
|
+
include ERB::Util # for html_escape for our presentational stuff
|
8
|
+
include ActionView::Helpers::OutputSafetyHelper # for safe_join
|
9
|
+
|
10
|
+
# Can initialize with a hash of key/values
|
11
|
+
def initialize(args = {})
|
12
|
+
args.each_pair do |key, value|
|
13
|
+
send("#{key}=", value)
|
14
|
+
end
|
15
|
+
|
16
|
+
self.authors ||= []
|
17
|
+
self.other_links ||= []
|
18
|
+
|
19
|
+
self.custom_data ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Array (possibly empty) of BentoSearch::Link objects
|
23
|
+
# representing additional links. Often SearchEngine's themselves
|
24
|
+
# won't include any of these, but Decorators will be used
|
25
|
+
# to add them in.
|
26
|
+
attr_accessor :other_links
|
27
|
+
|
28
|
+
# * dc.title
|
29
|
+
# * schema.org CreativeWork: 'name'
|
30
|
+
attr_accessor :title
|
31
|
+
|
32
|
+
# When an individual seperate subtitle is available.
|
33
|
+
# May also be nil with subtitle in "title" field after colon.
|
34
|
+
#
|
35
|
+
# *
|
36
|
+
attr_accessor :subtitle
|
37
|
+
|
38
|
+
# usually a direct link to the search provider's 'native' page.
|
39
|
+
# Can be changed in actual presentation with a Decorator.
|
40
|
+
# * schema.org CreativeWork: 'url'
|
41
|
+
attr_accessor :link
|
42
|
+
|
43
|
+
# normalized controlled vocab title, important this is supplied
|
44
|
+
# if possible for OpenURL generation and other features.
|
45
|
+
#
|
46
|
+
# schema.org 'type' that's a sub-type of CreativeWork.
|
47
|
+
# should hold a string that, when appended to "http://schema.org/"
|
48
|
+
# is a valid schema.org type uri, that sub-types CreativeWork. Eg.
|
49
|
+
# * Article
|
50
|
+
# * Book
|
51
|
+
# * Movie
|
52
|
+
# * MusicRecording
|
53
|
+
# * Photograph
|
54
|
+
# * SoftwareApplication
|
55
|
+
# * WebPage
|
56
|
+
# * VideoObject
|
57
|
+
# * AudioObject
|
58
|
+
# * SoftwareApplication
|
59
|
+
#
|
60
|
+
#
|
61
|
+
#
|
62
|
+
# OR one of these symbols, sadly not covered by schema.org types:
|
63
|
+
# * :serial (magazine or journal)
|
64
|
+
# * :dissertation (dissertation or thesis)
|
65
|
+
# * :conference_paper # individual paper
|
66
|
+
# * :conference_proceedings # collected proceedings
|
67
|
+
# * :report # white paper or other report.
|
68
|
+
# * :book_item # section or exceprt from book.
|
69
|
+
#
|
70
|
+
# Note: We're re-thinking this, might allow uncontrolled
|
71
|
+
# in here instead.
|
72
|
+
attr_accessor :format
|
73
|
+
|
74
|
+
# uncontrolled presumably english-language format string.
|
75
|
+
# if supplied will be used in display in place of controlled
|
76
|
+
# format.
|
77
|
+
attr_accessor :format_str
|
78
|
+
|
79
|
+
# year published. a ruby int
|
80
|
+
# PART of:.
|
81
|
+
# * schema.org CreativeWork "datePublished", year portion
|
82
|
+
# * dcterms.issued, year portion
|
83
|
+
# * prism:coverDate, year portion
|
84
|
+
attr_accessor :year
|
85
|
+
|
86
|
+
attr_accessor :volume
|
87
|
+
attr_accessor :issue
|
88
|
+
attr_accessor :start_page
|
89
|
+
attr_accessor :end_page
|
90
|
+
|
91
|
+
attr_accessor :journal_title
|
92
|
+
attr_accessor :issn
|
93
|
+
attr_accessor :isbn
|
94
|
+
|
95
|
+
attr_accessor :doi
|
96
|
+
|
97
|
+
# usually used for books rather than articles
|
98
|
+
attr_accessor :publisher
|
99
|
+
|
100
|
+
# an openurl kev-encoded context object. optional,
|
101
|
+
# only if source provides one that may be better
|
102
|
+
# than can be constructed from individual elements above
|
103
|
+
attr_accessor :openurl_kev_co
|
104
|
+
|
105
|
+
# Short summary of item.
|
106
|
+
# Mark .html_safe if it includes html -- creator is responsible
|
107
|
+
# for making sure html is safely sanitizied and/or stripped,
|
108
|
+
# rails ActionView::Helpers::Sanistize #sanitize and #strip_tags
|
109
|
+
# may be helpful.
|
110
|
+
attr_accessor :abstract
|
111
|
+
|
112
|
+
# An array (order matters) of BentoSearch::Author objects
|
113
|
+
# add authors to it with results.authors << Author
|
114
|
+
attr_accessor :authors
|
115
|
+
|
116
|
+
# engine-specific data not suitable for abstract API, usually
|
117
|
+
# for internal use.
|
118
|
+
attr_accessor :custom_data
|
119
|
+
|
120
|
+
|
121
|
+
# Returns a ruby OpenURL::ContextObject (NISO Z39.88).
|
122
|
+
def to_openurl
|
123
|
+
BentoSearch::OpenurlCreator.new(self).to_openurl
|
124
|
+
end
|
125
|
+
|
126
|
+
##################
|
127
|
+
# Presentation related methods.
|
128
|
+
# yes, it really makes sense to include them here, they can be overridden
|
129
|
+
# by decorators.
|
130
|
+
# May extract these to their own base decorator module at some point,
|
131
|
+
# but the OO hieararchy would be basically the same either way.
|
132
|
+
#######################
|
133
|
+
|
134
|
+
|
135
|
+
# How to display a BentoSearch::Author object as a name
|
136
|
+
def author_display(author)
|
137
|
+
if (author.first && author.last)
|
138
|
+
"#{author.last}, #{author.first.slice(0,1)}"
|
139
|
+
elsif author.display
|
140
|
+
author.display
|
141
|
+
elsif author.last
|
142
|
+
author.last
|
143
|
+
else
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Put together title and subtitle if neccesary.
|
149
|
+
def complete_title
|
150
|
+
t = self.title
|
151
|
+
if self.subtitle
|
152
|
+
t = safe_join([t, ": ", self.subtitle], "")
|
153
|
+
end
|
154
|
+
|
155
|
+
if t.blank?
|
156
|
+
t = I18n.translate("bento_search.missing_title")
|
157
|
+
end
|
158
|
+
|
159
|
+
return t
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
# A simple user-displayable citation, _without_ author/title.
|
165
|
+
# the journal, year, vol, iss, page; or publisher and year; etc.
|
166
|
+
# Constructed from individual details. Not formal APA or MLA or anything,
|
167
|
+
# just a rough and ready display.
|
168
|
+
#
|
169
|
+
# TODO: Should this be moved to a rails helper method? Not sure.
|
170
|
+
def published_in
|
171
|
+
result_elements = []
|
172
|
+
|
173
|
+
unless year.blank?
|
174
|
+
# wrap year in a span so we can bold it.
|
175
|
+
result_elements.push "<span class='year'>#{year}</span>"
|
176
|
+
end
|
177
|
+
|
178
|
+
result_elements.push(journal_title) unless journal_title.blank?
|
179
|
+
|
180
|
+
if journal_title.blank? && ! publisher.blank?
|
181
|
+
result_elements.push html_escape publisher
|
182
|
+
end
|
183
|
+
|
184
|
+
if (! volume.blank?) && (! issue.blank?)
|
185
|
+
result_elements.push html_escape "#{volume}(#{issue})"
|
186
|
+
else
|
187
|
+
result_elements.push html_escape volume unless volume.blank?
|
188
|
+
result_elements.push html_escape issue unless issue.blank?
|
189
|
+
end
|
190
|
+
|
191
|
+
if (! start_page.blank?) && (! end_page.blank?)
|
192
|
+
result_elements.push html_escape "pp. #{start_page}-#{end_page}"
|
193
|
+
elsif ! start_page.blank?
|
194
|
+
result_elements.push html_escape "p. #{start_page}"
|
195
|
+
end
|
196
|
+
|
197
|
+
return nil if result_elements.empty?
|
198
|
+
|
199
|
+
return result_elements.join(", ").html_safe
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module BentoSearch
|
2
|
+
# An array-like object (in fact it-subclasses Array) that holds
|
3
|
+
# a page of search results. But also has some meta-data about the
|
4
|
+
# search itself (query, paging, etc).
|
5
|
+
|
6
|
+
# If #error is non-nil, object may not have real results, but
|
7
|
+
# be an error. You can use failed? to see.
|
8
|
+
class Results < ::Array
|
9
|
+
attr_accessor :total_items
|
10
|
+
# 0-based index into total results, used for pagination
|
11
|
+
attr_accessor :start
|
12
|
+
# per_page setting, can be used for pagination.
|
13
|
+
attr_accessor :per_page
|
14
|
+
|
15
|
+
# If error is non-nil, it's an error condition with no real results.
|
16
|
+
# error should be a hash with these (and possibly other) keys, although
|
17
|
+
# none of these are required to be non-nil.
|
18
|
+
# [:status]
|
19
|
+
# A (usually) non-succesful HTTP status code. May be nil.
|
20
|
+
# [:message]
|
21
|
+
# A short message explaining error, usually provided by external
|
22
|
+
# service. NOT suitable for showing to end-users. May be nil.
|
23
|
+
# [:end_user_message]
|
24
|
+
# A message suitable for showing to end-users. May be nil.
|
25
|
+
# [:error_info]
|
26
|
+
# A service-specific way of reporting more error info, for developers,
|
27
|
+
# not suitable for end-users. Might be a string, might be a hash,
|
28
|
+
# depends on the service. may be nil.
|
29
|
+
# [:exception]
|
30
|
+
# Possibly a ruby exception object. may be nil.
|
31
|
+
attr_accessor :error
|
32
|
+
|
33
|
+
# time it took to do search, in seconds.
|
34
|
+
attr_accessor :timing
|
35
|
+
|
36
|
+
# search arguments as normalized by SearchEngine, not neccesarily
|
37
|
+
# directly as input. A hash.
|
38
|
+
attr_accessor :search_args
|
39
|
+
# Registered id of engine used to create these results,
|
40
|
+
# may be nil if used with an unregistered engine.
|
41
|
+
attr_accessor :engine_id
|
42
|
+
|
43
|
+
# Returns a BentoSearch::Results::Pagination, that should be suitable
|
44
|
+
# for passing right to Kaminari (although Kaminari isn't good about doc/specing
|
45
|
+
# it's api, so might break), or convenient methods for your own custom UI.
|
46
|
+
def pagination
|
47
|
+
Pagination.new( total_items, search_args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def failed?
|
51
|
+
! error.nil?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
|
2
|
+
# An object intended to be compatible with kaminari for pagination,
|
3
|
+
# although kaminari doesn't doc/spec exactly what you need, so might
|
4
|
+
# break in future. Could be useful for your own custom pagination too.
|
5
|
+
#
|
6
|
+
# You don't normally create one of these yourself, you get one returned
|
7
|
+
# from Results#pagination
|
8
|
+
class BentoSearch::Results::Pagination
|
9
|
+
|
10
|
+
# first arg is results.total_items, second is result
|
11
|
+
# of normalized_args from searchresults.
|
12
|
+
#
|
13
|
+
# We don't do the page/start normalization calc here,
|
14
|
+
# we count on them both being passed in, already calculated
|
15
|
+
# by normalize_arguments in SearchResults. Expect :page, 0-based
|
16
|
+
# :start, and :per_page
|
17
|
+
def initialize(total, normalized_args)
|
18
|
+
@total_count = total || 0
|
19
|
+
@per_page = normalized_args[:per_page] || 10
|
20
|
+
@current_page = normalized_args[:page] || 1
|
21
|
+
@start_record = (normalized_args[:start] || 0) + 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_page
|
25
|
+
@current_page
|
26
|
+
end
|
27
|
+
|
28
|
+
# 1-based start, suitable for showing to user
|
29
|
+
# Can be 0 for empty result set.
|
30
|
+
def start_record
|
31
|
+
[@start_record, count_records].min
|
32
|
+
end
|
33
|
+
|
34
|
+
# 1-based last record in window, suitable for showing to user.
|
35
|
+
# Can be 0 for empty result set.
|
36
|
+
def end_record
|
37
|
+
[start_record + per_page - 1, count_records].min
|
38
|
+
end
|
39
|
+
|
40
|
+
# If nil is passed in, will normalize to 0 so things doing math
|
41
|
+
# comparisons won't raise.
|
42
|
+
def count_records
|
43
|
+
@total_count
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_pages
|
47
|
+
(@total_count.to_f / @per_page).ceil
|
48
|
+
end
|
49
|
+
# kaminari wants both, weird.
|
50
|
+
alias num_pages total_pages
|
51
|
+
|
52
|
+
|
53
|
+
def first_page?
|
54
|
+
current_page == 1
|
55
|
+
end
|
56
|
+
|
57
|
+
def last_page?
|
58
|
+
current_page >= total_pages
|
59
|
+
end
|
60
|
+
|
61
|
+
def per_page
|
62
|
+
@per_page
|
63
|
+
end
|
64
|
+
# kaminari wants it called this.
|
65
|
+
alias limit_value per_page
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
require 'confstruct'
|
4
|
+
|
5
|
+
|
6
|
+
module BentoSearch
|
7
|
+
# Module mix-in for bento_search search engines.
|
8
|
+
#
|
9
|
+
# ==Using a SearchEngine
|
10
|
+
#
|
11
|
+
# * init/config
|
12
|
+
# * search
|
13
|
+
# * pagination, with max per_page
|
14
|
+
# * search fields, with semantics. ask for supported search fields.
|
15
|
+
#
|
16
|
+
# == Standard config
|
17
|
+
# * item_decorators : Array of Modules that will be decorated. See Decorators section.
|
18
|
+
#
|
19
|
+
# == Implementing a SearchEngine
|
20
|
+
#
|
21
|
+
# `include BentoSearch::SearchEngine`
|
22
|
+
#
|
23
|
+
# a SearchEngine's state should not be search-specific, but
|
24
|
+
# is configuration specific. Don't store anything specific
|
25
|
+
# to a specific search in iVars.
|
26
|
+
#
|
27
|
+
# Do implement `#search(*args)`
|
28
|
+
#
|
29
|
+
# Do use HTTPClient, if possible, for http searches,
|
30
|
+
# using a class-level HTTPClient to maintain persistent connections.
|
31
|
+
#
|
32
|
+
# Other options:
|
33
|
+
# * implement a class-level `self.required_configuration' returning
|
34
|
+
# an array of config keys or dot keypaths, and it'll raise on init
|
35
|
+
# if those config's weren't supplied.
|
36
|
+
# * max per page
|
37
|
+
# * search fields
|
38
|
+
#
|
39
|
+
# Some engines support `:auth => true` for elevated access to affiliated
|
40
|
+
# users.
|
41
|
+
#
|
42
|
+
module SearchEngine
|
43
|
+
DefaultPerPage = 10
|
44
|
+
|
45
|
+
extend ActiveSupport::Concern
|
46
|
+
|
47
|
+
include Capabilities
|
48
|
+
|
49
|
+
included do
|
50
|
+
attr_accessor :configuration
|
51
|
+
end
|
52
|
+
|
53
|
+
# If specific SearchEngine calls initialize, you want to call super
|
54
|
+
# handles configuration loading, mostly. Argument is a
|
55
|
+
# Confstruct::Configuration or Hash.
|
56
|
+
def initialize(aConfiguration = Confstruct::Configuration.new)
|
57
|
+
# init, from copy of default, or new
|
58
|
+
if self.class.default_configuration
|
59
|
+
self.configuration = Confstruct::Configuration.new(self.class.default_configuration)
|
60
|
+
else
|
61
|
+
self.configuration = Confstruct::Configuration.new
|
62
|
+
end
|
63
|
+
# merge in current instance config
|
64
|
+
self.configuration.configure ( aConfiguration )
|
65
|
+
|
66
|
+
# global defaults?
|
67
|
+
self.configuration[:item_decorators] ||= []
|
68
|
+
|
69
|
+
# check for required keys
|
70
|
+
if self.class.required_configuration
|
71
|
+
self.class.required_configuration.each do |required_key|
|
72
|
+
if self.configuration.lookup!(required_key.to_s, "**NOT_FOUND**") == "**NOT_FOUND**"
|
73
|
+
raise ArgumentError.new("#{self.class.name} requires configuration key #{required_key}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
# Calls individual engine #search_implementation.
|
81
|
+
# first normalizes arguments, also adds on standard metadata
|
82
|
+
# to results.
|
83
|
+
def search(*arguments)
|
84
|
+
start_t = Time.now
|
85
|
+
|
86
|
+
arguments = normalized_search_arguments(*arguments)
|
87
|
+
|
88
|
+
results = search_implementation(arguments)
|
89
|
+
|
90
|
+
decorate(results)
|
91
|
+
|
92
|
+
# standard result metadata
|
93
|
+
results.start = arguments[:start] || 0
|
94
|
+
results.per_page = arguments[:per_page]
|
95
|
+
|
96
|
+
results.search_args = arguments
|
97
|
+
results.engine_id = configuration.id
|
98
|
+
|
99
|
+
results.timing = (Time.now - start_t)
|
100
|
+
|
101
|
+
return results
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
def normalized_search_arguments(*orig_arguments)
|
107
|
+
arguments = {}
|
108
|
+
|
109
|
+
# Two-arg style to one hash, if present
|
110
|
+
if (orig_arguments.length > 1 ||
|
111
|
+
(orig_arguments.length == 1 && ! orig_arguments.first.kind_of?(Hash)))
|
112
|
+
arguments[:query] = orig_arguments.delete_at(0)
|
113
|
+
end
|
114
|
+
|
115
|
+
arguments.merge!(orig_arguments.first) if orig_arguments.length > 0
|
116
|
+
|
117
|
+
|
118
|
+
# allow strings for pagination (like from url query), change to
|
119
|
+
# int please.
|
120
|
+
[:page, :per_page, :start].each do |key|
|
121
|
+
arguments.delete(key) if arguments[key].blank?
|
122
|
+
arguments[key] = arguments[key].to_i if arguments[key]
|
123
|
+
end
|
124
|
+
arguments[:per_page] ||= DefaultPerPage
|
125
|
+
|
126
|
+
# illegal arguments
|
127
|
+
if (arguments[:start] && arguments[:page])
|
128
|
+
raise ArgumentError.new("Can't supply both :page and :start")
|
129
|
+
end
|
130
|
+
if ( arguments[:per_page] &&
|
131
|
+
self.max_per_page &&
|
132
|
+
arguments[:per_page] > self.max_per_page)
|
133
|
+
raise ArgumentError.new("#{arguments[:per_page]} is more than maximum :per_page of #{self.max_per_page} for #{self.class}")
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
# Normalize :page to :start, and vice versa
|
138
|
+
if arguments[:page]
|
139
|
+
arguments[:start] = (arguments[:page] - 1) * arguments[:per_page]
|
140
|
+
elsif arguments[:start]
|
141
|
+
arguments[:page] = (arguments[:start] / arguments[:per_page]) + 1
|
142
|
+
end
|
143
|
+
|
144
|
+
# normalize :sort from possibly symbol to string
|
145
|
+
# TODO: raise if unrecognized sort key?
|
146
|
+
if arguments[:sort]
|
147
|
+
arguments[:sort] = arguments[:sort].to_s
|
148
|
+
end
|
149
|
+
|
150
|
+
# translate semantic_search_field to search_field, or raise if
|
151
|
+
# can't.
|
152
|
+
if (semantic = arguments.delete(:semantic_search_field)) && ! semantic.blank?
|
153
|
+
|
154
|
+
mapped = self.semantic_search_map[semantic.to_s]
|
155
|
+
unless mapped
|
156
|
+
raise ArgumentError.new("#{self.class.name} does not know about :semantic_search_field #{semantic}")
|
157
|
+
end
|
158
|
+
arguments[:search_field] = mapped
|
159
|
+
end
|
160
|
+
|
161
|
+
return arguments
|
162
|
+
end
|
163
|
+
alias_method :parse_search_arguments, :normalized_search_arguments
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
# Extend each result with each specified decorator module
|
172
|
+
def decorate(results)
|
173
|
+
results.each do |result|
|
174
|
+
configuration.item_decorators.each do |decorator|
|
175
|
+
result.extend decorator
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
|
181
|
+
module ClassMethods
|
182
|
+
|
183
|
+
# If support fielded search, over-ride to specify fields
|
184
|
+
# supported. Returns a hash, key is engine-specific internal
|
185
|
+
# search field, value is nil or a hash of metadata about
|
186
|
+
# the search field, including semantic mapping.
|
187
|
+
#
|
188
|
+
# def search_field_definitions
|
189
|
+
# { "intitle" => {:semantic => :title}}
|
190
|
+
# end
|
191
|
+
def search_field_definitions
|
192
|
+
{}
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
# Returns list of string internal search_field's that can
|
197
|
+
# be supplied to search(:search_field => x)
|
198
|
+
def search_keys
|
199
|
+
return search_field_definitions.keys
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
# Over-ride returning a hash or Confstruct with
|
205
|
+
# any configuration values you want by default.
|
206
|
+
# actual user-specified config values will be deep-merged
|
207
|
+
# into the defaults.
|
208
|
+
def default_configuration
|
209
|
+
end
|
210
|
+
|
211
|
+
# Over-ride returning an array of symbols for required
|
212
|
+
# configuration keys.
|
213
|
+
def required_configuration
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|