diff --git a/app/helpers/blacklight_config_helper.rb b/app/helpers/blacklight_config_helper.rb index f6a45c3d8..64119397d 100644 --- a/app/helpers/blacklight_config_helper.rb +++ b/app/helpers/blacklight_config_helper.rb @@ -2,6 +2,8 @@ # Used in CatalogController. This module is split out because catalog_controller.rb # contains all the required blacklight configurations and is plenty large enough. +# +# See spec/features/indexing_xxx_spec.rb for relevancy tests of Solr search results module BlacklightConfigHelper # sending these values to Solr as arguments with search requests will override # the default params configured for Solr searching via solrconfig.xml diff --git a/solr_conf/conf/solrconfig.xml b/solr_conf/conf/solrconfig.xml index 0d0d29f79..8b44aa22a 100644 --- a/solr_conf/conf/solrconfig.xml +++ b/solr_conf/conf/solrconfig.xml @@ -21,11 +21,11 @@ ${solr.core1.data.dir:} - ${solr.autoCommit.maxTime:600000} + ${solr.autoCommit.maxTime:500} false - ${solr.autoSoftCommit.maxTime:5000} + ${solr.autoSoftCommit.maxTime:100} diff --git a/spec/factories/items.rb b/spec/factories/items.rb index b3e159134..52ed19a45 100644 --- a/spec/factories/items.rb +++ b/spec/factories/items.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +# Because we are instantiating an immutable cocina-model rather than a database record, +# nested hash structure values (e.g. a title value in a description) +# may require a setter method in ItemMethodSender before they can be passed in FactoryBot.define do factory :persisted_item, class: 'Cocina::Models::RequestDRO' do initialize_with do @@ -8,9 +11,7 @@ 'type' => type, 'label' => label, 'version' => 1, - 'identification' => { - 'sourceId' => "sul:#{SecureRandom.uuid}" - }, + 'identification' => identification, 'administrative' => { 'hasAdminPolicy' => admin_policy_id }, @@ -30,6 +31,12 @@ admin_policy_id { 'druid:hv992ry2431' } label { 'test object' } type { Cocina::Models::ObjectType.object } + source_id { "sul:#{SecureRandom.uuid}" } + identification do + { + 'sourceId' => source_id + } + end factory :agreement do type { Cocina::Models::ObjectType.agreement } diff --git a/spec/features/indexing_identifiers_spec.rb b/spec/features/indexing_identifiers_spec.rb new file mode 100644 index 000000000..6a2d27240 --- /dev/null +++ b/spec/features/indexing_identifiers_spec.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Integration tests for expected behaviors of our Solr indexing choices, through +# our whole stack: tests create cocina objects with factories, write them +# to dor-services-app, index the new objects via dor-indexing-app and then use +# the Argo UI to test Solr behavior such as search results and facet values. +RSpec.describe 'Indexing and search results for identifiers' do + let(:item) { FactoryBot.create_for_repository(:persisted_item) } + let(:blacklight_config) { CatalogController.blacklight_config } + let(:solr_conn) { blacklight_config.repository_class.new(blacklight_config).connection } + let(:solr_id) { item.externalIdentifier } + + before do + sign_in create(:user), groups: ['sdr:administrator-role'] + solr_conn.commit # ensure no deletes are pending + visit '/' + end + + after do + solr_conn.delete_by_id(solr_id) + solr_conn.commit + end + + describe 'identifier searching' do + context 'for druids' do + let(:prefixed_druid) { item.externalIdentifier } + + it 'matches query with bare and prefixed druid' do + [prefixed_druid, prefixed_druid.split(':').last].each do |query| + fill_in 'q', with: query + click_button 'search' + expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + end + end + + context 'for sourceids' do + # SPEC: Source ID: M2549_2022-259_stertzer, where M2549 is the collection number and 2022-259 is the accession number + # sul:M0997_S1_B473_021_0001 (S is for series, B is for box, F is for folder ...) + let(:source_id) { "sul:M2549_2022-259_stertzer_#{SecureRandom.alphanumeric(12)}" } + let(:item) { FactoryBot.create_for_repository(:persisted_item, source_id:) } + + before do + item.identification.sourceId # ensure item is created before searching + end + + it 'matches whole string, including prefix before first colon' do + fill_in 'q', with: source_id + click_button 'search' + # expect a single result, but Solr may not finish commit for previous test delete in time + # expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + + it 'matches without prefix before the first colon' do + fill_in 'q', with: source_id.split(':').last + click_button 'search' + # expect a single result, but Solr may not finish commit for previous test delete in time + # expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + + it 'matches source_id fragments' do + fragments = [ + 'M2549', + '2022-259', # accession number + 'M2549_2022-259', + 'M2549 2022 259', + 'stertzer', + '259_stertzer', + '259-stertzer', + '259 stertzer' + ] + fragments.each do |fragment| + fill_in 'q', with: fragment + click_button 'search' + # expect a single result, but Solr may not finish commit for previous test delete in time + # expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + end + + it 'is not case sensitive' do + fill_in 'q', with: 'm2549 STERTZER' + click_button 'search' + # expect a single result, but Solr may not finish commit for previous test delete in time + # expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + + punctuation_source_ids = [ + 'sulcons:8552-RB_Miscellanies_agabory,Before treatment photos', + 'Archiginnasio:Bassi_Box10_Folder2_Item3.14', + 'Revs:2012-015GHEW-CO-1980-b1_1.16_0007' + ] + punctuation_source_ids.each do |punctuation_source_id| + context "when punctuation in #{punctuation_source_id}" do + let(:source_id) { "#{punctuation_source_id}.#{SecureRandom.alphanumeric(4)}" } + + it 'matches without punctuation' do + fill_in 'q', with: source_id.gsub(/[_\-:.,]/, ' ') + click_button 'search' + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + end + end + end + + context 'for barcodes' do + let(:barcode) { '20503740296' } + let(:item) do + FactoryBot.create_for_repository(:persisted_item, identification: { + sourceId: "sul:#{SecureRandom.uuid}", + barcode: + }) + end + + before do + item.identification.barcode # ensure item is created before searching + end + + it 'matches query with bare and prefixed barcode' do + [barcode, "barcode:#{barcode}"].each do |query| + fill_in 'q', with: query + click_button 'search' + expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + end + end + + context 'for ILS (folio) identifiers' do + let(:catalog_id) { 'a11403803' } + let(:item) do + FactoryBot.create_for_repository(:persisted_item, identification: { + sourceId: "sul:#{SecureRandom.uuid}", + catalogLinks: [{ + catalog: 'folio', + refresh: false, + catalogRecordId: catalog_id + }] + }) + end + + before do + item.identification.catalogLinks # ensure item is created before searching + end + + it 'matches catalog identifier with and without folio prefix' do + [catalog_id, "folio:#{catalog_id}"].each do |query| + fill_in 'q', with: query + click_button 'search' + expect(page).to have_content('1 entry found') + expect(page).to have_css('dd.blacklight-id', text: solr_id) + end + end + end + + context 'for DOIs' do + # is there a reason to tokenize DOIs? + + it 'matches bare and "doi:" prefixed DOIs' do + skip('write this test') + end + end + end +end diff --git a/spec/support/item_method_sender.rb b/spec/support/item_method_sender.rb index 9f8464beb..e9bfebc2c 100644 --- a/spec/support/item_method_sender.rb +++ b/spec/support/item_method_sender.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# Used in conjunction with factories/items.rb to create a Cocina::Models::RequestDRO, +# allowing for customization of fields/values in the immutable cocina-model. class ItemMethodSender def initialize(cocina_request) @cocina_model = cocina_request @@ -7,14 +9,27 @@ def initialize(cocina_request) attr_reader :cocina_model + # @example + # item = create(:persisted_item, title: 'simple title value') def title=(title) @cocina_model = @cocina_model.new(description: { title: [{ value: title }] }) end + # @param [Array] title_values - an array of arbitrarily complex cocina title values + # @example + # item = create(:persisted_item, title_values: [{ value: 'simple title value' }, { value: 'another title' }]) + def title_values=(title_values) + @cocina_model = @cocina_model.new(description: { title: title_values }) + end + + # @example + # item = create(:persisted_item, source_id: 'sul:M932_1623_B1_F1_001') def source_id=(source_id) @cocina_model = @cocina_model.new(identification: @cocina_model.identification.new(sourceId: source_id)) end + # @example + # item = create(:persisted_item, collection_id: 'druid:rt923rd3423') def collection_id=(collection_id) @cocina_model = @cocina_model.new(structural: Cocina::Models::DROStructural.new(isMemberOf: [collection_id])) end