blacklight-spotlight 3.0.0.rc3 → 3.0.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/spotlight/admin/reindex_monitor.js +1 -0
  3. data/app/assets/stylesheets/spotlight/browse_group_categories_block.scss +23 -0
  4. data/app/controllers/spotlight/catalog_controller.rb +4 -1
  5. data/app/controllers/spotlight/dashboards_controller.rb +1 -1
  6. data/app/controllers/spotlight/exhibits_controller.rb +1 -1
  7. data/app/helpers/spotlight/application_helper.rb +19 -0
  8. data/app/helpers/spotlight/pages_helper.rb +1 -1
  9. data/app/jobs/concerns/spotlight/job_tracking.rb +47 -0
  10. data/app/jobs/concerns/spotlight/limit_concurrency.rb +33 -0
  11. data/app/jobs/spotlight/add_uploads_from_csv.rb +6 -3
  12. data/app/jobs/spotlight/application_job.rb +8 -0
  13. data/app/jobs/spotlight/cleanup_job_trackers_job.rb +13 -0
  14. data/app/jobs/spotlight/default_thumbnail_job.rb +1 -3
  15. data/app/jobs/spotlight/reindex_exhibit_job.rb +36 -0
  16. data/app/jobs/spotlight/reindex_job.rb +49 -41
  17. data/app/jobs/spotlight/rename_sidecar_field_job.rb +2 -2
  18. data/app/jobs/spotlight/update_job_trackers_job.rb +20 -0
  19. data/app/models/concerns/spotlight/user.rb +2 -1
  20. data/app/models/spotlight/event.rb +13 -0
  21. data/app/models/spotlight/exhibit.rb +4 -14
  22. data/app/models/spotlight/job_tracker.rb +105 -0
  23. data/app/models/spotlight/reindex_progress.rb +44 -27
  24. data/app/models/spotlight/resource.rb +24 -58
  25. data/app/models/spotlight/resources/iiif_harvester.rb +10 -1
  26. data/app/models/spotlight/resources/iiif_manifest.rb +3 -1
  27. data/app/models/spotlight/resources/iiif_service.rb +1 -1
  28. data/app/models/spotlight/resources/json_upload.rb +12 -0
  29. data/app/models/spotlight/resources/upload.rb +25 -2
  30. data/app/models/spotlight/solr_document_sidecar.rb +2 -1
  31. data/app/services/spotlight/etl.rb +7 -0
  32. data/app/services/spotlight/etl/context.rb +52 -0
  33. data/app/services/spotlight/etl/executor.rb +194 -0
  34. data/app/services/spotlight/etl/loaders.rb +12 -0
  35. data/app/services/spotlight/etl/pipeline.rb +81 -0
  36. data/app/services/spotlight/etl/solr_loader.rb +96 -0
  37. data/app/services/spotlight/etl/sources.rb +25 -0
  38. data/app/services/spotlight/etl/step.rb +82 -0
  39. data/app/services/spotlight/etl/transforms.rb +64 -0
  40. data/app/services/spotlight/validity_checker.rb +5 -5
  41. data/app/views/spotlight/dashboards/_reindexing_activity.html.erb +6 -6
  42. data/app/views/spotlight/shared/_locale_picker.html.erb +1 -1
  43. data/app/views/spotlight/sir_trevor/blocks/_browse_group_categories_block.html.erb +4 -3
  44. data/config/locales/spotlight.ar.yml +11 -1
  45. data/config/locales/spotlight.en.yml +3 -2
  46. data/db/migrate/20210122082032_create_job_trackers.rb +22 -0
  47. data/db/migrate/20210126123041_create_events.rb +15 -0
  48. data/lib/generators/spotlight/scaffold_resource_generator.rb +5 -13
  49. data/lib/spotlight/engine.rb +8 -1
  50. data/lib/spotlight/version.rb +1 -1
  51. data/spec/controllers/spotlight/catalog_controller_spec.rb +3 -1
  52. data/spec/examples.txt +1448 -1437
  53. data/spec/factories/job_trackers.rb +9 -0
  54. data/spec/features/add_items_spec.rb +9 -4
  55. data/spec/features/javascript/reindex_monitor_spec.rb +1 -1
  56. data/spec/features/site_users_management_spec.rb +4 -4
  57. data/spec/helpers/spotlight/pages_helper_spec.rb +8 -0
  58. data/spec/jobs/spotlight/reindex_exhibit_job_spec.rb +43 -0
  59. data/spec/jobs/spotlight/reindex_job_spec.rb +30 -59
  60. data/spec/models/spotlight/exhibit_spec.rb +3 -57
  61. data/spec/models/spotlight/reindex_progress_spec.rb +89 -87
  62. data/spec/models/spotlight/resource_spec.rb +69 -90
  63. data/spec/models/spotlight/resources/iiif_harvester_spec.rb +9 -10
  64. data/spec/models/spotlight/solr_document_sidecar_spec.rb +1 -0
  65. data/spec/services/spotlight/etl/context_spec.rb +66 -0
  66. data/spec/services/spotlight/etl/executor_spec.rb +149 -0
  67. data/spec/services/spotlight/etl/pipeline_spec.rb +22 -0
  68. data/spec/services/spotlight/etl/solr_loader_spec.rb +76 -0
  69. data/spec/services/spotlight/etl/step_spec.rb +70 -0
  70. data/spec/spec_helper.rb +2 -5
  71. data/spec/views/spotlight/dashboards/_reindexing_activity.html.erb_spec.rb +22 -19
  72. metadata +55 -15
  73. data/app/models/concerns/spotlight/resources/open_graph.rb +0 -36
  74. data/app/models/spotlight/reindexing_log_entry.rb +0 -42
  75. data/app/services/spotlight/resources/iiif_builder.rb +0 -19
  76. data/app/services/spotlight/solr_document_builder.rb +0 -77
  77. data/app/services/spotlight/upload_solr_document_builder.rb +0 -57
  78. data/spec/factories/reindexing_log_entries.rb +0 -54
  79. data/spec/models/spotlight/reindexing_log_entry_spec.rb +0 -129
  80. data/spec/models/spotlight/resources/open_graph_spec.rb +0 -65
  81. data/spec/services/spotlight/solr_document_builder_spec.rb +0 -66
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8fb1d422302a15b42343a0f7afb8c545869d0c872e355762a7cea998469b12b
4
- data.tar.gz: df63403ced5aa975097224a0e298562d216c427fcab43aba7640eeee18dc8ea9
3
+ metadata.gz: 55231336b3b4a0eae1abdd68bbeb8ede968d5ffbe4359f29f83a3a27124a4b05
4
+ data.tar.gz: b2f2bfc6230b5081493484393a26670961486217b64b7a96b9659979eab64381
5
5
  SHA512:
6
- metadata.gz: 3c6261c2edc4b04d331d0e3961f0a88ce15599314add02061475349e9c86cb5dcb7c1cb5bd9d89100266bfcbab4449160b7b662d66aece1d73e5a721d6d35914
7
- data.tar.gz: 96fad663f5370e18de8a345ed3bbd9f7b6e7b774a76ca2d16342d66bdfa2bde41fe3eaef70b290bde32e35828f50bc3c71f0f7f75df20e8029c61fff29193ab1
6
+ metadata.gz: c3d2a9d1203e0950c79844e4fad0b2083e3cc8d1b63a357ce47a2760ac48b13ec9c2f4e2f0890add8a533d71f61bf7a96a684b74710c564b3e17d919635b592a
7
+ data.tar.gz: 842054ed3d1c5d46c5c6f9bb3180efdf83feee8f1bf44ae607865d6bea2f9d4589e1b19990cbb53a65ba4249fd5794dca213830be71b055a28b69e188b29dde7
@@ -84,6 +84,7 @@ Spotlight.onLoad(function() {
84
84
  }
85
85
 
86
86
  function calculatePercentage(data) {
87
+ if (data.total == 0) return 0;
87
88
  return Math.floor((data.completed / data.total) * 100);
88
89
  }
89
90
 
@@ -4,6 +4,24 @@
4
4
  padding-bottom: $spacer * .75;
5
5
  padding-top: $spacer * .75;
6
6
 
7
+ .browse-categories .browse-category {
8
+ .category-caption {
9
+ z-index: 100;
10
+ }
11
+
12
+ .hover-overlay {
13
+ height: 100%;
14
+ position: absolute;
15
+ width: 100%;
16
+ }
17
+
18
+ &:hover {
19
+ .hover-overlay {
20
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.7));
21
+ }
22
+ }
23
+ }
24
+
7
25
  .spotlight-flexbox.browse-categories .box {
8
26
  display: flex;
9
27
  }
@@ -66,4 +84,9 @@
66
84
  .browse-group-categories-block .blacklight-icon-arrow-alt-circle-left, .blacklight-icon-arrow-alt-circle-right{
67
85
  transform: rotate(180deg);
68
86
  }
87
+ // Hack to override overflow issue not fixed in RTL upstream in tiny-slider
88
+ .tns-visually-hidden {
89
+ left: 0;
90
+ right: -10000em;
91
+ }
69
92
  }
@@ -31,7 +31,10 @@ module Spotlight
31
31
  blacklight_config.view.admin_table.document_actions = []
32
32
  blacklight_config.track_search_session = false
33
33
 
34
- blacklight_config.add_sort_field :timestamp, sort: "#{blacklight_config.index.timestamp_field} desc" unless blacklight_config.sort_fields.key? :timestamp
34
+ unless blacklight_config.sort_fields.key? :timestamp
35
+ blacklight_config.add_sort_field :timestamp, default: true,
36
+ sort: "#{blacklight_config.index.timestamp_field} desc"
37
+ end
35
38
  end
36
39
 
37
40
  before_action only: :edit do
@@ -22,7 +22,7 @@ module Spotlight
22
22
 
23
23
  @pages = @exhibit.pages.recent.limit(5)
24
24
  @solr_documents = load_recent_solr_documents 5
25
- @recent_reindexing = @exhibit.reindexing_log_entries.recent
25
+ @recent_reindexing = @exhibit.job_trackers.recent
26
26
 
27
27
  attach_dashboard_breadcrumbs
28
28
  end
@@ -27,7 +27,7 @@ module Spotlight
27
27
  end
28
28
 
29
29
  def process_import
30
- if @exhibit.import(JSON.parse(import_exhibit_params.read)) && @exhibit.reindex_later
30
+ if @exhibit.import(JSON.parse(import_exhibit_params.read)) && @exhibit.reindex_later(current_user)
31
31
  redirect_to spotlight.exhibit_dashboard_path(@exhibit), notice: t(:'helpers.submit.exhibit.updated', model: @exhibit.class.model_name.human.downcase)
32
32
  else
33
33
  render action: :import
@@ -30,6 +30,25 @@ module Spotlight
30
30
  current_site.title if current_site.title.present?
31
31
  end
32
32
 
33
+ # Returns the url for the current page in the new locale. This may be
34
+ # overridden in downstream applications where our naive use of `url_for`
35
+ # is insufficient to generate the expected routes
36
+ def current_page_for_locale(locale)
37
+ initial_exception = nil
38
+
39
+ ([self] + additional_locale_routing_scopes).each do |scope|
40
+ return scope.public_send(:url_for, params.to_unsafe_h.merge(locale: locale))
41
+ rescue ActionController::UrlGenerationError => e
42
+ initial_exception ||= e
43
+ end
44
+
45
+ raise initial_exception
46
+ end
47
+
48
+ def additional_locale_routing_scopes
49
+ [spotlight, main_app]
50
+ end
51
+
33
52
  # Can search for named routes directly in the main app, omitting
34
53
  # the "main_app." prefix
35
54
  def method_missing(method, *args, &block)
@@ -21,7 +21,7 @@ module Spotlight
21
21
  # a more complete markdown rendered
22
22
  def sir_trevor_markdown(text)
23
23
  clean_text = if text
24
- text.gsub('<br>', "\n").gsub('<p>', '').gsub('</p>', "\n\n")
24
+ text.gsub('<br>', "\n\n").gsub('<p>', '').gsub('</p>', "\n\n")
25
25
  else
26
26
  ''
27
27
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ # Job status tracking
5
+ module JobTracking
6
+ extend ActiveSupport::Concern
7
+ include ActiveJob::Status
8
+
9
+ def self.with_job_tracking
10
+ before_perform :find_or_initialize_job_tracker
11
+ after_perform :finalize_job_tracker
12
+ end
13
+
14
+ def job_tracker
15
+ @job_tracker ||= find_or_initialize_job_tracker
16
+ end
17
+
18
+ private
19
+
20
+ def find_or_initialize_job_tracker
21
+ JobTracker.find_or_create_by(job_id: job_id) do |tracker|
22
+ tracker.job_class = self.class.name
23
+ tracker.status = 'enqueued'
24
+ update_job_tracker_properties(tracker)
25
+ end
26
+ end
27
+
28
+ def finalize_job_tracker
29
+ job_tracker.update(status: 'completed') if job_tracker.status == 'enqueued'
30
+ end
31
+
32
+ def update_job_tracker_properties(tracker)
33
+ tracker.resource = job_tracking_resource
34
+ tracker.on = reports_on_resource || tracker.resource
35
+
36
+ tracker.user = arguments.last[:user] if arguments.last.is_a?(Hash)
37
+ end
38
+
39
+ def job_tracking_resource
40
+ arguments.first
41
+ end
42
+
43
+ def reports_on_resource
44
+ arguments.last[:reports_on] if arguments.last.is_a?(Hash)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ # Job status tracking
5
+ module LimitConcurrency
6
+ extend ActiveSupport::Concern
7
+
8
+ VALIDITY_TOKEN_PARAMETER = 'validity_token'
9
+
10
+ included do
11
+ # The validity checker is a seam for implementations to expire unnecessary
12
+ # indexing tasks if it becomes redundant while waiting in the job queue.
13
+ class_attribute :validity_checker, default: Spotlight::ValidityChecker.new
14
+
15
+ before_enqueue do |job|
16
+ token = job.arguments.last[VALIDITY_TOKEN_PARAMETER] if job.arguments.last.is_a?(Hash)
17
+ token ||= validity_checker.mint(job)
18
+
19
+ job.arguments << {} unless job.arguments.last.is_a? Hash
20
+ job.arguments.last[VALIDITY_TOKEN_PARAMETER] = token
21
+ end
22
+
23
+ before_perform do |job|
24
+ next unless job.arguments.last.is_a?(Hash)
25
+
26
+ token = job.arguments.last.delete(VALIDITY_TOKEN_PARAMETER)
27
+ throw(:abort) unless token.nil? || validity_checker.check(job, validity_token: token)
28
+
29
+ job.arguments.pop if job.arguments.last.empty?
30
+ end
31
+ end
32
+ end
33
+ end
@@ -3,12 +3,11 @@
3
3
  module Spotlight
4
4
  ##
5
5
  # Process a CSV upload into new Spotlight::Resource::Upload objects
6
- class AddUploadsFromCsv < ActiveJob::Base
6
+ class AddUploadsFromCsv < Spotlight::ApplicationJob
7
+ include Spotlight::JobTracking
7
8
  attr_reader :count
8
9
  attr_reader :errors
9
10
 
10
- queue_as :default
11
-
12
11
  after_perform do |job|
13
12
  csv_data, exhibit, user = job.arguments
14
13
  Spotlight::IndexingCompleteMailer.documents_indexed(
@@ -59,5 +58,9 @@ module Spotlight
59
58
  end.compact.to_h
60
59
  end.compact
61
60
  end
61
+
62
+ def job_tracking_resource
63
+ arguments[1]
64
+ end
62
65
  end
63
66
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ # :nodoc:
5
+ class ApplicationJob < ActiveJob::Base
6
+ queue_as :default
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ ###
5
+ # Calls the #set_default_thumbnail method
6
+ # on the object passed in and calls save
7
+ ###
8
+ class CleanupJobTrackersJob < Spotlight::ApplicationJob
9
+ def perform
10
+ Spotlight::JobTracker.where(status: 'completed', updated_at: Time.zone.at(0)...Spotlight::Engine.config.reindex_progress_window.minutes.ago).delete_all
11
+ end
12
+ end
13
+ end
@@ -5,9 +5,7 @@ module Spotlight
5
5
  # Calls the #set_default_thumbnail method
6
6
  # on the object passed in and calls save
7
7
  ###
8
- class DefaultThumbnailJob < ActiveJob::Base
9
- queue_as :default
10
-
8
+ class DefaultThumbnailJob < Spotlight::ApplicationJob
11
9
  def perform(thumbnailable)
12
10
  thumbnailable.set_default_thumbnail
13
11
  thumbnailable.save
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spotlight
4
+ ##
5
+ # Reindex an exhibit by parallelizing resource indexing into multiple batches of reindex jobs
6
+ class ReindexExhibitJob < Spotlight::ApplicationJob
7
+ include Spotlight::JobTracking
8
+ include Spotlight::LimitConcurrency
9
+
10
+ def perform(exhibit, batch_size: Spotlight::Engine.config.reindexing_batch_size, batch_count: Spotlight::Engine.config.reindexing_batch_count, **)
11
+ job_tracker.update(status: 'in_progress')
12
+
13
+ count = exhibit.resources.count
14
+
15
+ # Use the provided batch size, or calculate a reasonable default
16
+ batch_count = (count.to_f / batch_size).ceil if batch_size
17
+ batch_count ||= 1 + Math.log(count).round # e.g. 10 => 3, 100 => 6, 1000 => 8
18
+
19
+ return Spotlight::ReindexJob.perform_now(exhibit, reports_on: job_tracker) if batch_count == 1
20
+
21
+ batch_size ||= (count.to_f / batch_count).ceil
22
+
23
+ perform_later_in_batches(exhibit, of: batch_size)
24
+ end
25
+
26
+ def perform_later_in_batches(exhibit, of:)
27
+ last = 0
28
+ exhibit.resources.select(:id).in_batches(of: of) do |batch|
29
+ last = batch.last.id
30
+ Spotlight::ReindexJob.perform_later(exhibit, reports_on: job_tracker, start: batch.first.id, finish: batch.last.id)
31
+ end
32
+
33
+ Spotlight::ReindexJob.perform_later(exhibit, reports_on: job_tracker, start: last)
34
+ end
35
+ end
36
+ end
@@ -3,72 +3,80 @@
3
3
  module Spotlight
4
4
  ##
5
5
  # Reindex the given resources or exhibits
6
- class ReindexJob < ActiveJob::Base
7
- queue_as :default
8
-
9
- # The validity checker is a seam for implementations to expire unnecessary
10
- # indexing tasks if it becomes redundant while waiting in the job queue.
11
- class_attribute :validity_checker, default: Spotlight::ValidityChecker.new
12
- self.validity_checker ||= Spotlight::ValidityChecker.new if Rails.version < '5.2'
6
+ class ReindexJob < Spotlight::ApplicationJob
7
+ include Spotlight::JobTracking
8
+ include Spotlight::LimitConcurrency
13
9
 
14
10
  before_perform do |job|
15
- job_log_entry = log_entry(job)
16
- next unless job_log_entry
11
+ pagination = job.arguments.last.slice(:start, :finish) if job.arguments.last.is_a? Hash
12
+ pagination ||= {}
17
13
 
18
- items_reindexed_estimate = resource_list(job.arguments.first).sum do |resource|
19
- resource.document_builder.documents_to_index.size
20
- end
21
- job_log_entry.update(items_reindexed_estimate: items_reindexed_estimate)
14
+ progress.total = resource_list(job.arguments.first, **pagination).sum(&:estimated_size)
15
+ end
16
+
17
+ after_perform do
18
+ exhibit&.touch # rubocop:disable Rails/SkipsModelValidations
22
19
  end
23
20
 
24
- around_perform do |job, block|
25
- job_log_entry = log_entry(job)
26
- job_log_entry&.in_progress!
21
+ after_perform :commit
22
+
23
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
24
+ def perform(exhibit_or_resources, start: nil, finish: nil, **)
25
+ job_tracker.update(status: 'in_progress')
26
+
27
+ errors = 0
28
+
29
+ error_handler = lambda do |pipeline, _error_context, exception, _data|
30
+ job_tracker.append_log_entry(type: :error, message: exception.to_s, resource_id: pipeline.source&.id)
31
+ errors += 1
32
+ end
27
33
 
28
- begin
29
- block.call
30
- rescue StandardError
31
- job_log_entry&.failed!
32
- raise
34
+ resource_list(exhibit_or_resources, start: start, finish: finish).each do |resource|
35
+ resource.reindex(touch: false, commit: false, job_tracker: job_tracker, additional_data: job_data, on_error: error_handler) do |*|
36
+ progress&.increment
37
+ end
38
+ rescue StandardError => e
39
+ error_handler.call(Struct.new(:source).new(resource), self, e, nil)
33
40
  end
34
41
 
35
- job_log_entry&.succeeded!
42
+ job_tracker.append_log_entry(type: :info, message: "#{progress.progress} of #{progress.total} (#{errors} errors)")
43
+ job_tracker.update(status: errors.zero? ? 'completed' : 'failed', data: { progress: progress.progress, total: progress.total })
36
44
  end
45
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
37
46
 
38
- def self.perform_later(exhibit_or_resources, log_entry = nil)
39
- validity_token = validity_checker.mint(exhibit_or_resources)
47
+ private
40
48
 
41
- super(exhibit_or_resources, log_entry, validity_token)
49
+ def commit
50
+ Blacklight.default_index.connection.commit
42
51
  end
43
52
 
44
- def perform(exhibit_or_resources, log_entry = nil, validity_token = nil)
45
- return unless still_valid?(exhibit_or_resources, validity_token)
53
+ def job_data
54
+ return unless job_tracker
46
55
 
47
- resource_list(exhibit_or_resources).each do |resource|
48
- resource.reindex(log_entry)
49
- end
56
+ { Spotlight::Engine.config.job_tracker_id_field => job_tracker.top_level_job_tracker.job_id }
50
57
  end
51
58
 
52
- private
53
-
54
- def resource_list(exhibit_or_resources)
59
+ def resource_list(exhibit_or_resources, start: nil, finish: nil)
55
60
  if exhibit_or_resources.is_a?(Spotlight::Exhibit)
56
- exhibit_or_resources.resources.find_each
57
- elsif exhibit_or_resources.is_a?(Enumerable)
58
- exhibit_or_resources
61
+ exhibit_or_resources.resources.find_each(start: start, finish: finish)
59
62
  else
60
63
  Array(exhibit_or_resources)
61
64
  end
62
65
  end
63
66
 
64
- def log_entry(job)
65
- job.arguments.second if job.arguments.second.is_a?(Spotlight::ReindexingLogEntry)
67
+ def job_tracking_resource
68
+ exhibit
66
69
  end
67
70
 
68
- def still_valid?(exhibit_or_resources, validity_token)
69
- return true unless validity_token
71
+ def exhibit
72
+ exhibit_or_resources = arguments.first
70
73
 
71
- validity_checker.check exhibit_or_resources, validity_token
74
+ case exhibit_or_resources
75
+ when Spotlight::Exhibit
76
+ exhibit_or_resources
77
+ when Spotlight::Resource
78
+ exhibit_or_resources.exhibit
79
+ end
72
80
  end
73
81
  end
74
82
  end
@@ -4,8 +4,8 @@ module Spotlight
4
4
  ##
5
5
  # After renaming an exhibit-specific field, we also
6
6
  # need to update the sidecars that may contain that field
7
- class RenameSidecarFieldJob < ActiveJob::Base
8
- queue_as :default
7
+ class RenameSidecarFieldJob < Spotlight::ApplicationJob
8
+ include Spotlight::JobTracking
9
9
 
10
10
  def perform(exhibit, old_field, new_field, old_slug = nil, new_slug = nil)
11
11
  exhibit.solr_document_sidecars.find_each do |s|