blacklight_iiif_search 0.0.1.pre.alpha

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.
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