blacklight_iiif_search 0.0.1.pre.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +46 -0
- data/app/controllers/concerns/blacklight_iiif_search/controller.rb +48 -0
- data/app/models/blacklight_iiif_search/iiif_search.rb +36 -0
- data/app/models/blacklight_iiif_search/iiif_search_annotation.rb +43 -0
- data/app/models/blacklight_iiif_search/iiif_search_response.rb +103 -0
- data/app/models/blacklight_iiif_search/iiif_suggest_response.rb +56 -0
- data/app/models/blacklight_iiif_search/iiif_suggest_search.rb +37 -0
- data/app/models/concerns/blacklight_iiif_search/annotation_behavior.rb +28 -0
- data/app/models/concerns/blacklight_iiif_search/ignored.rb +13 -0
- data/app/models/concerns/blacklight_iiif_search/search_behavior.rb +15 -0
- data/lib/blacklight_iiif_search.rb +14 -0
- data/lib/blacklight_iiif_search/engine.rb +14 -0
- data/lib/blacklight_iiif_search/routes.rb +12 -0
- data/lib/blacklight_iiif_search/version.rb +3 -0
- data/lib/generators/blacklight_iiif_search/controller_generator.rb +37 -0
- data/lib/generators/blacklight_iiif_search/install_generator.rb +52 -0
- data/lib/generators/blacklight_iiif_search/model_generator.rb +17 -0
- data/lib/generators/blacklight_iiif_search/routes_generator.rb +26 -0
- data/lib/generators/blacklight_iiif_search/solr_generator.rb +87 -0
- data/lib/generators/blacklight_iiif_search/templates/iiif_search_builder.rb +15 -0
- data/lib/generators/blacklight_iiif_search/templates/solr/lib/solr-tokenizing_suggester-7.x.jar +0 -0
- data/lib/railties/blacklight_iiif_search.rake +15 -0
- data/spec/controllers/catalog_controller_spec.rb +99 -0
- data/spec/fixtures/sample_solr_documents.yml +272 -0
- data/spec/iiif_search_shared.rb +49 -0
- data/spec/models/blacklight_iiif_search/iiif_search_annotation_spec.rb +36 -0
- data/spec/models/blacklight_iiif_search/iiif_search_response_spec.rb +73 -0
- data/spec/models/blacklight_iiif_search/iiif_search_spec.rb +23 -0
- data/spec/models/blacklight_iiif_search/iiif_suggest_response_spec.rb +43 -0
- data/spec/models/blacklight_iiif_search/iiif_suggest_search_spec.rb +36 -0
- data/spec/models/concerns/blacklight_iiif_search/annotation_behavior_spec.rb +31 -0
- data/spec/models/concerns/blacklight_iiif_search/ignored_spec.rb +10 -0
- data/spec/models/concerns/blacklight_iiif_search/search_behavior_spec.rb +12 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 87d986c3e753ea4a1461f64d18602f251d9a62ec
|
4
|
+
data.tar.gz: e2c2ea9cab056c1ff552fff249a112f7c9dd1169
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f24b9614cb7c287fa918cbaac19c6a2269e9811ab619e904eea96b4d0053297a282ddffeed3a166f208b1061889a7f0b9ecc14b50f0a24cde41560fdbc000020
|
7
|
+
data.tar.gz: 667a16c8dda27c737bd29cc54d7a92cdf0226fc1b0c3b533ee46e97561ff5477e571561ca164ca16ddacde376f9605f9f4f47c26d8358c5ce0257f20cb8f4b16
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
9
|
+
rdoc.rdoc_dir = 'rdoc'
|
10
|
+
rdoc.title = 'BlacklightIiifSearch'
|
11
|
+
rdoc.options << '--line-numbers'
|
12
|
+
rdoc.rdoc_files.include('README.rdoc')
|
13
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
14
|
+
end
|
15
|
+
|
16
|
+
Bundler::GemHelper.install_tasks
|
17
|
+
|
18
|
+
Rake::Task.define_task(:environment)
|
19
|
+
|
20
|
+
load 'lib/railties/blacklight_iiif_search.rake'
|
21
|
+
|
22
|
+
task default: :ci
|
23
|
+
|
24
|
+
require 'engine_cart/rake_task'
|
25
|
+
|
26
|
+
require 'solr_wrapper'
|
27
|
+
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new
|
30
|
+
|
31
|
+
require 'rubocop/rake_task'
|
32
|
+
RuboCop::RakeTask.new(:rubocop)
|
33
|
+
|
34
|
+
desc 'Run test suite'
|
35
|
+
task ci: ['engine_cart:generate'] do # TODO: add rubocop
|
36
|
+
SolrWrapper.wrap do |solr|
|
37
|
+
FileUtils.cp File.join(__dir__, 'lib', 'generators', 'blacklight_iiif_search', 'templates', 'solr', 'lib', 'solr-tokenizing_suggester-7.x.jar'),
|
38
|
+
File.join(solr.instance_dir, 'contrib')
|
39
|
+
solr.with_collection do
|
40
|
+
within_test_app do
|
41
|
+
system 'RAILS_ENV=test rake blacklight_iiif_search:index:seed'
|
42
|
+
end
|
43
|
+
Rake::Task['spec'].invoke
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# return a IIIF Content Search response
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
module Controller
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :set_search_builder, only: [:iiif_search]
|
8
|
+
after_action :set_access_headers, only: %i[iiif_search iiif_suggest]
|
9
|
+
end
|
10
|
+
|
11
|
+
def iiif_search
|
12
|
+
_parent_response, @parent_document = fetch(params[:solr_document_id])
|
13
|
+
iiif_search = IiifSearch.new(iiif_search_params, iiif_search_config,
|
14
|
+
@parent_document)
|
15
|
+
@response, _document_list = search_results(iiif_search.solr_params)
|
16
|
+
iiif_search_response = IiifSearchResponse.new(@response,
|
17
|
+
@parent_document,
|
18
|
+
self)
|
19
|
+
render json: iiif_search_response.annotation_list,
|
20
|
+
content_type: 'application/json'
|
21
|
+
end
|
22
|
+
|
23
|
+
def iiif_suggest
|
24
|
+
suggest_search = IiifSuggestSearch.new(params, repository, self)
|
25
|
+
render json: suggest_search.response,
|
26
|
+
content_type: 'application/json'
|
27
|
+
end
|
28
|
+
|
29
|
+
def iiif_search_config
|
30
|
+
blacklight_config.iiif_search || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def iiif_search_params
|
34
|
+
params.permit(:q, :motivation, :date, :user, :solr_document_id, :page)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def set_search_builder
|
40
|
+
blacklight_config.search_builder_class = IiifSearchBuilder
|
41
|
+
end
|
42
|
+
|
43
|
+
# allow apps to load JSON received from a remote server
|
44
|
+
def set_access_headers
|
45
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# IiifSearch
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
class IiifSearch
|
4
|
+
include BlacklightIiifSearch::SearchBehavior
|
5
|
+
|
6
|
+
attr_reader :id, :iiif_config, :parent_document, :q, :page, :rows
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [Hash] params
|
10
|
+
# @param [Hash] iiif_search_config
|
11
|
+
# @param [SolrDocument] parent_document
|
12
|
+
def initialize(params, iiif_search_config, parent_document)
|
13
|
+
@id = params[:solr_document_id]
|
14
|
+
@iiif_config = iiif_search_config
|
15
|
+
@parent_document = parent_document
|
16
|
+
@q = params[:q]
|
17
|
+
@page = params[:page]
|
18
|
+
@rows = 50
|
19
|
+
|
20
|
+
# NOT IMPLEMENTED YET
|
21
|
+
# @motivation = params[:motivation]
|
22
|
+
# @date = params[:date]
|
23
|
+
# @user = params[:user]
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# return a hash of Solr search params
|
28
|
+
# if q is not supplied, have to pass some dummy params
|
29
|
+
# or else all records matching object_relation_solr_params are returned
|
30
|
+
# @return [Hash]
|
31
|
+
def solr_params
|
32
|
+
return { q: 'nil:nil' } unless q
|
33
|
+
{ q: q, f: object_relation_solr_params, rows: rows, page: page }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# corresponds to IIIF Annotation resource
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
class IiifSearchAnnotation
|
4
|
+
include IIIF::Presentation
|
5
|
+
include BlacklightIiifSearch::AnnotationBehavior
|
6
|
+
|
7
|
+
attr_reader :document, :query, :hl_index, :snippet, :controller,
|
8
|
+
:parent_document
|
9
|
+
|
10
|
+
##
|
11
|
+
# @param [SolrDocument] document
|
12
|
+
# @param [String] query
|
13
|
+
# @param [Integer] hl_index
|
14
|
+
# @param [String] snippet
|
15
|
+
# @param [CatalogController] controller
|
16
|
+
# @param [SolrDocument] parent_document
|
17
|
+
def initialize(document, query, hl_index, snippet, controller, parent_document)
|
18
|
+
@document = document
|
19
|
+
@query = query
|
20
|
+
@hl_index = hl_index
|
21
|
+
@snippet = snippet
|
22
|
+
@controller = controller
|
23
|
+
@parent_document = parent_document
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @return [IIIF::Presentation::Annotation]
|
28
|
+
def as_hash
|
29
|
+
annotation = IIIF::Presentation::Annotation.new('@id' => annotation_id)
|
30
|
+
annotation.resource = text_resource_for_annotation if snippet
|
31
|
+
annotation['on'] = canvas_uri_for_annotation
|
32
|
+
annotation
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# @return [IIIF::Presentation::Resource]
|
37
|
+
def text_resource_for_annotation
|
38
|
+
clean_snippet = ActionView::Base.full_sanitizer.sanitize(snippet)
|
39
|
+
IIIF::Presentation::Resource.new('@type' => 'cnt:ContentAsText',
|
40
|
+
'chars' => clean_snippet)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# corresponds to a IIIF Annotation List
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
class IiifSearchResponse
|
4
|
+
include BlacklightIiifSearch::Ignored
|
5
|
+
|
6
|
+
attr_reader :solr_response, :controller, :iiif_config
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [Blacklight::Solr::Response] solr_response
|
10
|
+
# @param [SolrDocument] parent_document
|
11
|
+
# @param [CatalogController] controller
|
12
|
+
def initialize(solr_response, parent_document, controller)
|
13
|
+
@solr_response = solr_response
|
14
|
+
@parent_document = parent_document
|
15
|
+
@controller = controller
|
16
|
+
@iiif_config = controller.iiif_search_config
|
17
|
+
@resources = []
|
18
|
+
@hits = []
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# constructs the IIIF::Presentation::AnnotationList
|
23
|
+
# @return [IIIF::OrderedHash]
|
24
|
+
def annotation_list
|
25
|
+
list_id = controller.request.original_url
|
26
|
+
anno_list = IIIF::Presentation::AnnotationList.new('@id' => list_id)
|
27
|
+
anno_list['@context'] = %w[
|
28
|
+
http://iiif.io/api/presentation/2/context.json
|
29
|
+
http://iiif.io/api/search/1/context.json
|
30
|
+
]
|
31
|
+
anno_list['resources'] = resources
|
32
|
+
anno_list['hits'] = @hits
|
33
|
+
anno_list['within'] = within
|
34
|
+
anno_list['prev'] = paged_url(solr_response.prev_page) if solr_response.prev_page
|
35
|
+
anno_list['next'] = paged_url(solr_response.next_page) if solr_response.next_page
|
36
|
+
anno_list['startIndex'] = 0 unless solr_response.total_pages > 1
|
37
|
+
anno_list.to_ordered_hash(force: true, include_context: false)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Return an array of IiifSearchAnnotation objects
|
42
|
+
# @return [Array]
|
43
|
+
def resources
|
44
|
+
@total = 0
|
45
|
+
solr_response['highlighting'].each do |id, hl_hash|
|
46
|
+
hit = { '@type': 'search:Hit', 'annotations': [] }
|
47
|
+
document = solr_response.documents.select { |v| v[:id] == id }.first
|
48
|
+
if hl_hash.empty?
|
49
|
+
@total += 1
|
50
|
+
annotation = IiifSearchAnnotation.new(document,
|
51
|
+
solr_response.params['q'],
|
52
|
+
0, nil, controller,
|
53
|
+
@parent_document)
|
54
|
+
@resources << annotation.as_hash
|
55
|
+
hit[:annotations] << annotation.annotation_id
|
56
|
+
else
|
57
|
+
hl_hash.each_value do |hl_array|
|
58
|
+
hl_array.each_with_index do |hl, hl_index|
|
59
|
+
@total += 1
|
60
|
+
annotation = IiifSearchAnnotation.new(document,
|
61
|
+
solr_response.params['q'],
|
62
|
+
hl_index, hl, controller,
|
63
|
+
@parent_document)
|
64
|
+
@resources << annotation.as_hash
|
65
|
+
hit[:annotations] << annotation.annotation_id
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@hits << hit
|
70
|
+
end
|
71
|
+
@resources
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# @return [IIIF::Presentation::Layer]
|
76
|
+
def within
|
77
|
+
within_hash = IIIF::Presentation::Layer.new
|
78
|
+
within_hash['ignored'] = ignored
|
79
|
+
if solr_response.total_pages > 1
|
80
|
+
within_hash['first'] = paged_url(1)
|
81
|
+
within_hash['last'] = paged_url(solr_response.total_pages)
|
82
|
+
else
|
83
|
+
within_hash['total'] = @total
|
84
|
+
end
|
85
|
+
within_hash
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# create a URL for the previous/next page of results
|
90
|
+
# @return [String]
|
91
|
+
def paged_url(page_index)
|
92
|
+
controller.solr_document_iiif_search_url(clean_params.merge(page: page_index))
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# remove ignored or irrelevant params from the params hash
|
97
|
+
# @return [ActionController::Parameters]
|
98
|
+
def clean_params
|
99
|
+
remove = ignored.map(&:to_sym)
|
100
|
+
controller.iiif_search_params.except(*%i[page solr_document_id] + remove)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# corresponds to a IIIF search:TermList
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
class IiifSuggestResponse
|
4
|
+
include BlacklightIiifSearch::Ignored
|
5
|
+
|
6
|
+
attr_reader :solr_response, :query, :document_id, :controller, :iiif_config
|
7
|
+
|
8
|
+
##
|
9
|
+
# @param [Blacklight::Solr::Response] solr_response
|
10
|
+
# @param [Hash] params
|
11
|
+
# @param [CatalogController] controller
|
12
|
+
def initialize(solr_response, params, controller)
|
13
|
+
@solr_response = solr_response
|
14
|
+
@query = params[:q]
|
15
|
+
@document_id = params[:solr_document_id]
|
16
|
+
@controller = controller
|
17
|
+
@iiif_config = controller.iiif_search_config
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Constructs the termList as IIIF::Presentation::Resource
|
22
|
+
# @return [IIIF::OrderedHash]
|
23
|
+
def term_list
|
24
|
+
list_id = controller.request.original_url
|
25
|
+
term_list = IIIF::Presentation::Resource.new('@id' => list_id)
|
26
|
+
term_list['@context'] = 'http://iiif.io/api/search/1/context.json'
|
27
|
+
term_list['@type'] = 'search:TermList'
|
28
|
+
term_list['terms'] = terms
|
29
|
+
term_list['ignored'] = ignored
|
30
|
+
term_list.to_ordered_hash(force: true, include_context: false)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Turn solr_response into array of hashes
|
35
|
+
# 'try' chain pattern copied from Blacklight::Suggest::Response#suggestions
|
36
|
+
# @return [Array]
|
37
|
+
def terms
|
38
|
+
terms_for_list = []
|
39
|
+
terms_array = solr_response.try(:[], 'suggest').try(:[], iiif_config[:suggester_name]).try(:[], query).try(:[], 'suggestions') || []
|
40
|
+
terms = terms_array.map { |v| v['term'] }
|
41
|
+
terms.sort.each do |term|
|
42
|
+
term_hash = { match: term, url: iiif_search_url(term) }
|
43
|
+
terms_for_list << term_hash
|
44
|
+
end
|
45
|
+
terms_for_list
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Create a URL corresponding to a IIIF Content Search request for the term
|
50
|
+
# @param [String] term
|
51
|
+
# @return [String]
|
52
|
+
def iiif_search_url(term)
|
53
|
+
controller.solr_document_iiif_search_url(document_id, q: term)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# IiifSuggestSearch
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
class IiifSuggestSearch
|
4
|
+
attr_reader :params, :query, :document_id, :iiif_config, :repository,
|
5
|
+
:controller
|
6
|
+
|
7
|
+
##
|
8
|
+
# @param [Hash] params
|
9
|
+
# @param [Blacklight::AbstractRepository] repository
|
10
|
+
# @param [CatalogController] controller
|
11
|
+
def initialize(params, repository, controller)
|
12
|
+
@params = params
|
13
|
+
@query = params[:q]
|
14
|
+
@document_id = params[:solr_document_id]
|
15
|
+
@iiif_config = controller.iiif_search_config
|
16
|
+
@repository = repository
|
17
|
+
@controller = controller
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Return the termList response
|
22
|
+
# @return [IIIF::OrderedHash]
|
23
|
+
def response
|
24
|
+
response = IiifSuggestResponse.new(suggest_results, params, controller)
|
25
|
+
response.term_list
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Query the suggest handler
|
30
|
+
# @return [RSolr::HashWithResponse]
|
31
|
+
def suggest_results
|
32
|
+
suggest_params = { q: query, :'suggest.cfq' => document_id }
|
33
|
+
repository.connection.send_and_receive(iiif_config[:autocomplete_handler],
|
34
|
+
params: suggest_params)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# customizable behavior for IiifSearchAnnotation
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
module AnnotationBehavior
|
4
|
+
##
|
5
|
+
# Create a URL for the annotation
|
6
|
+
# @return [String]
|
7
|
+
def annotation_id
|
8
|
+
"#{controller.solr_document_url(parent_document[:id])}/canvas/#{document[:id]}/annotation/#{hl_index}"
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Create a URL for the canvas that the annotation refers to
|
13
|
+
# @return [String]
|
14
|
+
def canvas_uri_for_annotation
|
15
|
+
"#{controller.solr_document_url(parent_document[:id])}/canvas/#{document[:id]}" + coordinates
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# return a string like "#xywh=100,100,250,20"
|
20
|
+
# corresponding to coordinates of query term on image
|
21
|
+
# local implementation expected, value returned below is just a placeholder
|
22
|
+
# @return [String]
|
23
|
+
def coordinates
|
24
|
+
return '' unless query
|
25
|
+
'#xywh=0,0,0,0'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# returns ignored params
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
module Ignored
|
4
|
+
##
|
5
|
+
# Return an array of ignored params
|
6
|
+
# @return [Array]
|
7
|
+
def ignored
|
8
|
+
relevant_keys = controller.iiif_search_params.keys
|
9
|
+
relevant_keys.delete('solr_document_id')
|
10
|
+
relevant_keys - iiif_config[:supported_params]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# customizable behavior for IiifSearch
|
2
|
+
module BlacklightIiifSearch
|
3
|
+
module SearchBehavior
|
4
|
+
##
|
5
|
+
# params to limit the search to items that have some relationship
|
6
|
+
# with the parent object (e.g. pages)
|
7
|
+
# for ex., return a {key: value} hash where:
|
8
|
+
# key: solr field for image/file to object relationship
|
9
|
+
# value: identifier of parent
|
10
|
+
# @return [Hash]
|
11
|
+
def object_relation_solr_params
|
12
|
+
{ iiif_config[:object_relation_field] => id }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|