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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +46 -0
  3. data/app/controllers/concerns/blacklight_iiif_search/controller.rb +48 -0
  4. data/app/models/blacklight_iiif_search/iiif_search.rb +36 -0
  5. data/app/models/blacklight_iiif_search/iiif_search_annotation.rb +43 -0
  6. data/app/models/blacklight_iiif_search/iiif_search_response.rb +103 -0
  7. data/app/models/blacklight_iiif_search/iiif_suggest_response.rb +56 -0
  8. data/app/models/blacklight_iiif_search/iiif_suggest_search.rb +37 -0
  9. data/app/models/concerns/blacklight_iiif_search/annotation_behavior.rb +28 -0
  10. data/app/models/concerns/blacklight_iiif_search/ignored.rb +13 -0
  11. data/app/models/concerns/blacklight_iiif_search/search_behavior.rb +15 -0
  12. data/lib/blacklight_iiif_search.rb +14 -0
  13. data/lib/blacklight_iiif_search/engine.rb +14 -0
  14. data/lib/blacklight_iiif_search/routes.rb +12 -0
  15. data/lib/blacklight_iiif_search/version.rb +3 -0
  16. data/lib/generators/blacklight_iiif_search/controller_generator.rb +37 -0
  17. data/lib/generators/blacklight_iiif_search/install_generator.rb +52 -0
  18. data/lib/generators/blacklight_iiif_search/model_generator.rb +17 -0
  19. data/lib/generators/blacklight_iiif_search/routes_generator.rb +26 -0
  20. data/lib/generators/blacklight_iiif_search/solr_generator.rb +87 -0
  21. data/lib/generators/blacklight_iiif_search/templates/iiif_search_builder.rb +15 -0
  22. data/lib/generators/blacklight_iiif_search/templates/solr/lib/solr-tokenizing_suggester-7.x.jar +0 -0
  23. data/lib/railties/blacklight_iiif_search.rake +15 -0
  24. data/spec/controllers/catalog_controller_spec.rb +99 -0
  25. data/spec/fixtures/sample_solr_documents.yml +272 -0
  26. data/spec/iiif_search_shared.rb +49 -0
  27. data/spec/models/blacklight_iiif_search/iiif_search_annotation_spec.rb +36 -0
  28. data/spec/models/blacklight_iiif_search/iiif_search_response_spec.rb +73 -0
  29. data/spec/models/blacklight_iiif_search/iiif_search_spec.rb +23 -0
  30. data/spec/models/blacklight_iiif_search/iiif_suggest_response_spec.rb +43 -0
  31. data/spec/models/blacklight_iiif_search/iiif_suggest_search_spec.rb +36 -0
  32. data/spec/models/concerns/blacklight_iiif_search/annotation_behavior_spec.rb +31 -0
  33. data/spec/models/concerns/blacklight_iiif_search/ignored_spec.rb +10 -0
  34. data/spec/models/concerns/blacklight_iiif_search/search_behavior_spec.rb +12 -0
  35. data/spec/spec_helper.rb +26 -0
  36. data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
  37. 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