bento_search 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|