diff --git a/.circleci/config.yml b/.circleci/config.yml index 80bb1a737..4bcda6853 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,13 +7,15 @@ jobs: build: docker: # legacy needed for phantomjs - - image: circleci/ruby:2.3-node-browsers-legacy + - image: circleci/ruby:2.4-node-browsers-legacy - image: circleci/redis:4 - image: ualbertalib/docker-fcrepo4:4.7 environment: CATALINA_OPTS: "-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:+DisableExplicitGC" - - image: solr:6 + - image: solr:9 command: bin/solr -cloud -noprompt -f -p 8985 + environment: + - SOLR_MODULES=analysis-extras,extraction # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -63,8 +65,10 @@ jobs: - run: name: Install dependencies command: | - sudo npm install eslint -g - gem update --system + npm install node@12.22.12 + export PATH=/home/circleci/repo/node_modules/node/bin:$PATH + npm install eslint@8.57.1 + gem i rubygems-update -v '<3' && update_rubygems gem update bundler bundle config path $CIRCLE_WORKING_DIRECTORY/vendor/bundle bundle install @@ -100,13 +104,13 @@ jobs: cd solr/config zip -1 -r solr_pumpkin_config.zip ./* curl -H "Content-type:application/octet-stream" --data-binary @solr_pumpkin_config.zip "http://localhost:8985/solr/admin/configs?action=UPLOAD&name=pumpkin" - curl -d action=CREATE -d name=hydra-test -d collection.configName=pumpkin -d numShards=1 http://localhost:8985/solr/admin/collections - ## Solr 7+ (API v2): curl -H 'Content-type: application/json' http://localhost:8985/api/collections/ -d '{create: {name: hydra-test, config: pumpkin, numShards: 1}}' + curl "http://localhost:8985/solr/admin/collections?action=CREATE&name=hydra-test&numShards=1&collection.configName=pumpkin" - run: name: Run Javascript tests command: | - eslint app/assets/javascripts/*.es6 + export PATH=/home/circleci/repo/node_modules/node/bin:$PATH + npx eslint app/assets/javascripts/*.es6 bundle exec rake spec:javascript - run: diff --git a/.rubocop.yml b/.rubocop.yml index c8a26c033..90eeb9f9c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ AllCops: # below added temporarily to hold back effects of version upgrade - 'app/views/search/*' - 'lib/tasks/*' + - 'lib/iu_dev_ops/reindex_solr_to_solr.rb' Rails: Enabled: true diff --git a/.solr_wrapper b/.solr_wrapper index ab5d70452..1207d6252 100644 --- a/.solr_wrapper +++ b/.solr_wrapper @@ -1,7 +1,9 @@ # Place any default configuration for solr_wrapper here -version: 6.2.0 +# version: 9.8 port: 8983 instance_dir: tmp/solr-development +env: + SOLR_MODULES: analysis-extras,extraction collection: persist: true dir: solr/config/ diff --git a/Gemfile b/Gemfile index ab0940693..d4730bca1 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,7 @@ gem 'devise', '>= 4.6.0' gem 'devise-guests', '~> 0.3' gem 'hydra-role-management', '~> 0.2.0' gem 'iiif-presentation', git: 'https://github.com/iiif/osullivan', branch: 'development' -gem 'ldap_groups_lookup', '~> 0.7.0' +gem 'ldap_groups_lookup', '~> 0.11' gem 'pul_metadata_services', git: 'https://github.com/IU-Libraries-Joint-Development/pul_metadata_services.git', branch: :master gem 'rsolr', '~> 1.1.0' gem 'simple_form', '~> 3.2', '< 3.5' @@ -57,7 +57,7 @@ gem 'active_fedora-noid', '~> 2.0.0' gem 'hydra-derivatives' # , github: 'projecthydra/hydra-derivatives', branch: 'master' gem 'hydra-pcdm' # , github: 'projecthydra-labs/hydra-pcdm', branch: 'master' gem 'hydra-works' # , github: 'projecthydra-labs/hydra-works', branch: 'master' -gem 'net-http-persistent', '~> 2.9.4' +gem 'net-http-persistent' gem 'rake', '~> 12.3.3' group :development, :test do @@ -84,6 +84,8 @@ gem 'coveralls', require: false gem 'fcrepo_wrapper', '~> 0.7.0' gem 'solr_wrapper', '~> 0.19.0' +gem 'crack', '< 0.4.6' + group :development do gem 'capistrano', '3.4.0' gem 'capistrano-passenger' @@ -101,7 +103,7 @@ group :test do gem "launchy" gem 'rspecproxies' gem "vcr", '~> 2.9' - gem "webmock", '~> 1.0', require: false + gem "webmock", '~> 2.3', require: false end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 7138aed4b..2b9060cbc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -238,8 +238,8 @@ GEM term-ansicolor (~> 1.3) thor (~> 0.19.1) tins (>= 1.6.0, < 2) - crack (0.4.3) - safe_yaml (~> 1.0.0) + crack (0.4.5) + rexml crass (1.0.6) daemons (1.2.4) dalli (2.7.6) @@ -320,7 +320,7 @@ GEM rubocop (~> 0.20) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashdiff (0.3.0) + hashdiff (1.2.0) hashery (2.1.2) hashie (3.4.6) httmultiparty (0.3.16) @@ -414,7 +414,7 @@ GEM kgio (2.11.0) launchy (2.4.3) addressable (~> 2.3) - ldap_groups_lookup (0.7.0) + ldap_groups_lookup (0.11.0) net-ldap ldp (0.6.4) deprecation @@ -462,8 +462,9 @@ GEM multipart-post (2.0.0) mysql2 (0.4.10) nenv (0.3.0) - net-http-persistent (2.9.4) - net-ldap (0.16.2) + net-http-persistent (4.0.5) + connection_pool (~> 2.2) + net-ldap (0.19.0) net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (3.2.0) @@ -589,6 +590,7 @@ GEM actionpack (>= 4.2.0, < 6.0) railties (>= 4.2.0, < 6.0) retriable (2.1.0) + rexml (3.2.5) rsolr (1.1.2) builder (>= 2.1.2) rspec (3.5.0) @@ -636,7 +638,6 @@ GEM ruby-rc4 (0.1.5) ruby_dep (1.3.1) rubyzip (1.3.0) - safe_yaml (1.0.4) sass (3.4.22) sass-rails (5.0.6) railties (>= 4.0.0, < 6) @@ -739,7 +740,7 @@ GEM binding_of_caller (>= 0.7.2) railties (>= 4.0) sprockets-rails (>= 2.0, < 4.0) - webmock (1.24.6) + webmock (2.3.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff @@ -765,6 +766,7 @@ DEPENDENCIES capybara coffee-rails (~> 4.1.0) coveralls + crack (< 0.4.6) curation_concerns! dalli devise (>= 4.6.0) @@ -787,11 +789,11 @@ DEPENDENCIES jquery-ui-rails kgio launchy - ldap_groups_lookup (~> 0.7.0) + ldap_groups_lookup (~> 0.11) mimemagic (~> 0.3.7) modernizr-rails mysql2 - net-http-persistent (~> 2.9.4) + net-http-persistent okcomputer omniauth-cas openseadragon @@ -833,7 +835,7 @@ DEPENDENCIES uglifier (>= 1.3.0) vcr (~> 2.9) web-console (~> 2.0) - webmock (~> 1.0) + webmock (~> 2.3) RUBY VERSION ruby 2.3.1p112 diff --git a/app/jobs/save_structure_job.rb b/app/jobs/save_structure_job.rb index d08c24a9b..7c25a114c 100644 --- a/app/jobs/save_structure_job.rb +++ b/app/jobs/save_structure_job.rb @@ -3,6 +3,8 @@ class SaveStructureJob < ActiveJob::Base queue_as :default # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/MethodLength def perform(curation_concern, structure) return unless curation_concern.respond_to?(:logical_order) # Remove existing logical order object to avoid accumulation of fragments. @@ -31,4 +33,6 @@ def perform(curation_concern, structure) raise end # rubocop:ensable Metrics/AbcSize + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/MethodLength end diff --git a/bin/ci_kakadu_install.sh b/bin/ci_kakadu_install.sh index 6f00f782f..6db6a9797 100755 --- a/bin/ci_kakadu_install.sh +++ b/bin/ci_kakadu_install.sh @@ -1,8 +1,8 @@ if [ ! -d "kakadu" ]; then mkdir ~/downloads - wget http://kakadusoftware.com/wp-content/uploads/KDU805_Demo_Apps_for_Linux-x86-64_200602.zip -O ~/downloads/kakadu.zip + wget http://kakadusoftware.com/wp-content/uploads/KDU841_Demo_Apps_for_Linux-x86-64_231117.zip -O ~/downloads/kakadu.zip unzip ~/downloads/kakadu.zip - mv KDU805_Demo_Apps_for_Linux-x86-64_200602 kakadu + mv KDU841_Demo_Apps_for_Linux-x86-64_231117 kakadu fi sudo cp kakadu/*.so /usr/lib sudo cp kakadu/* /usr/bin diff --git a/lib/iu_dev_ops/reindex_solr_to_solr.rb b/lib/iu_dev_ops/reindex_solr_to_solr.rb new file mode 100644 index 000000000..ca4db0805 --- /dev/null +++ b/lib/iu_dev_ops/reindex_solr_to_solr.rb @@ -0,0 +1,291 @@ +module IuDevOps + class ReindexSolrToSolr + + # Only url is needed for new solr but other config options can be passed in addition + attr_accessor :old_solr, :new_solr + + def initialize(new_solr_config) + @old_solr = ActiveFedora.solr + @new_solr = ActiveFedora::SolrService.new(new_solr_config) + end + + # Example reindexing a delta via query: "timestamp:[#{(DateTime.now - 1.day).utc.iso8601} TO *]" + def reindex(query: "*", batch_size: 1000) + total_docs = old_solr.conn.get('select', params: {q: query, rows: 0})["response"]["numFound"] + if total_docs == 0 + puts "No documents found to reindex." + return + end + + puts "Starting reindex at #{DateTime.now.utc.iso8601}" + docs_processed = 0 + while docs_processed < total_docs + docs = old_solr.conn.get('select', params: {q: query, rows: batch_size, start: docs_processed})["response"]["docs"] + reconstructed_docs = docs.collect do |doc| + begin + SolrDocReconstructor.new(doc).reconstruct + rescue RuntimeError => e + puts "Error reconstructing #{doc["id"]}...falling back to ActiveFedora method" + puts e.message + ActiveFedora::Base.find(doc["id"]).to_solr + end + end + + new_solr.conn.add(reconstructed_docs, {softCommit: true}) + docs_processed += docs.size + puts "Migrated #{docs_processed} out of #{total_docs}" + end + puts "Committing..." + new_solr.conn.commit + puts "Optimizing..." + new_solr.conn.optimize + puts "Complete" + end + + class SolrDocReconstructor + STORED_DEFINITIONS = ["stored_searchable", "stored_sortable", "displayable", "symbol"] + NON_STORED_DEFINITIONS = ["facetable", "searchable", "sortable"] + + attr_accessor :doc + + def initialize(doc) + @doc = doc + end + + def reconstruct + klass = detect_class(doc) + new_doc = doc.except("timestamp", "score", "_version_") + reconstruct_class(new_doc, klass) + reconstruct_includes(new_doc, klass) + new_doc + end + + private + + def detect_class(doc) + doc["has_model_ssim"].first.safe_constantize || Object + end + + def find_value(field, stored_def, new_doc) + case stored_def + when "stored_searchable" + new_doc["#{field}_tesim"] || new_doc["#{field}_dtsim"] || new_doc["#{field}_isim"] + when "displayable" + new_doc["#{field}_ssm"] + when "symbol" + new_doc["#{field}_ssim"] + when "stored_sortable" + new_doc["#{field}_ssi"] || new_doc["#{field}_dtsi"] + default + nil + end + end + + def find_value_type(value) + case value + when String + :string + when Integer + :integer + when DateTime + :time + when TrueClass, FalseClass + :boolean + when Array + find_value_type(value.first) + end + end + + def set_value(field, non_store_def, value, new_doc) + value_type = find_value_type(value) + case non_store_def + when "facetable" + new_doc["#{field}_sim"] = value + when "searchable" + if value_type == :string + new_doc["#{field}_teim"] = value + elsif value_type == :time + new_doc["#{field}_dtim"] = value + elsif value_type == :integer + new_doc["#{field}_iim"] = value + end + when "unstemmed_searchable" + new_doc["#{field}_tim"] = value + end + end + + # Characterization terms (e.g. width, height) are all stored and defined in CurationConcerns::FileSetIndexer + + # CurationConcerns::RequiredMetadata + REQUIRED_METADATA_FIELDS = { + "title_sim" => "title_tesim" + } + + # CurationConcerns::HumanReadableType + HUMAN_READABLE_FIELDS = { + "human_readable_type_sim" => "human_readable_type_tesim" + } + + # CurationConcerns::BasicMetadata + BASIC_METADATA_FIELDS = { + "resource_type_sim" => "resource_type_tesim", + "creator_sim" => "creator_tesim", + "contributor_sim" => "contributor_tesim", + "keyword_sim" => "keyword_tesim", + "publisher_sim" => "publisher_tesim", + "subject_sim" => "subject_tesim", + "language_sim" => "language_tesim", + "based_near_sim" => "based_near_tesim" + } + + PLUM_SCHEMA_FIELDS = ["sort_title", "portion_note", "description", "identifier", "replaces", "rights_statement", "rights_note", "source_metadata_identifier", "state", "workflow_note", "holding_location", "ocr_language", "nav_date", "pdf_type", "full_text_searchable", "start_canvas", "alternative_title", "digital_date", "usage_right", "volume_and_issue_no", "edition", "series", "coverage", "date", "digital_specifications", "digital_collection", "digital_publisher", "extent", "date_published", "modified", "lccn_call_number", "local_call_number", "physical_description", "abridger", "actor", "adapter", "addressee", "analyst", "animator", "annotator", "appellant", "appellee", "applicant", "architect", "arranger", "art_copyist", "art_director", "artist", "artistic_director", "assignee", "associated_name", "attributed_name", "auctioneer", "author", "author_in_quotations_or_text_abstracts", "author_of_afterword_colophon_etc", "author_of_dialog", "author_of_introduction_etc", "autographer", "bibliographic_antecedent", "binder", "binding_designer", "blurb_writer", "book_designer", "book_producer", "bookjacket_designer", "bookplate_designer", "bookseller", "braille_embosser", "broadcaster", "calligrapher", "cartographer", "caster", "censor", "choreographer", "cinematographer", "client", "collection_registrar", "collector", "collotyper", "colorist", "commentator", "commentator_for_written_text", "compiler", "complainant", "complainant_appellant", "complainant_appellee", "composer", "compositor", "conceptor", "conductor", "conservator", "consultant", "consultant_to_a_project", "contestant", "contestant_appellant", "contestant_appellee", "contestee", "contestee_appellant", "contestee_appellee", "contractor", "copyright_claimant", "copyright_holder", "corrector", "correspondent", "costume_designer", "court_governed", "court_reporter", "cover_designer", "curator", "dancer", "data_contributor", "data_manager", "dedicatee", "dedicator", "defendant", "defendant_appellant", "defendant_appellee", "degree_granting_institution", "degree_supervisor", "delineator", "depicted", "designer", "director", "dissertant", "distribution_place", "distributor", "owning_institution", "draftsman", "dubious_author", "editor", "editor_of_compilation", "editor_of_moving_image_work", "electrician", "electrotyper", "enacting_jurisdiction", "engineer", "engraver", "etcher", "event_place", "expert", "facsimilist", "field_director", "film_distributor", "film_director", "film_editor", "film_producer", "filmmaker", "first_party", "forger", "former_owner", "funding", "geographic_information_specialist", "honoree", "host", "host_institution", "illuminator", "illustrator", "inscriber", "instrumentalist", "interviewee", "interviewer", "author", "issuing_body", "judge", "jurisdiction_governed", "laboratory", "laboratory_director", "landscape_architect", "lead", "lender", "libelant", "libelant_appellant", "libelant_appellee", "libelee", "libelee_appellant", "libelee_appellee", "librettist", "licensee", "licensor", "lighting_designer", "lithographer", "lyricist", "manufacture_place", "manufacturer", "marbler", "markup_editor", "medium", "metadata_contact", "metal_engraver", "minute_taker", "moderator", "monitor", "music_copyist", "musical_director", "musician", "narrator", "onscreen_presenter", "opponent", "organizer", "originator", "other", "owner", "panelist", "papermaker", "patent_applicant", "patent_holder", "patron", "performer", "permitting_agency", "photographer", "plaintiff", "plaintiff_appellant", "plaintiff_appellee", "platemaker", "praeses", "presenter", "printer", "printer_of_plates", "printmaker", "process_contact", "producer", "production_company", "production_designer", "production_manager", "production_personnel", "production_place", "programmer", "project_director", "proofreader", "provider", "publication_place", "publishing_director", "puppeteer", "radio_director", "radio_producer", "recording_engineer", "recordist", "redaktor", "renderer", "reporter", "marc_repository", "research_team_head", "research_team_member", "researcher", "respondent", "respondent_appellant", "respondent_appellee", "responsible_party", "restager", "restorationist", "reviewer", "rubricator", "scenarist", "scientific_advisor", "screenwriter", "scribe", "sculptor", "second_party", "secretary", "seller", "set_designer", "setting", "signer", "singer", "sound_designer", "speaker", "sponsor", "stage_director", "stage_manager", "standards_body", "stereotyper", "storyteller", "supporting_host", "surveyor", "teacher", "technical_director", "television_director", "television_producer", "thesis_advisor", "transcriber", "translator", "type_designer", "typographer", "university_place", "videographer", "voice_actor", "witness", "wood_engraver", "woodcutter", "writer_of_accompanying_material", "writer_of_added_commentary", "writer_of_added_text", "writer_of_added_lyrics", "writer_of_supplementary_textual_content", "writer_of_introduction", "writer_of_preface", "call_number", "published", "responsibility_note"] + + def reconstruct_includes(new_doc, klass) + REQUIRED_METADATA_FIELDS.each { |unstored, stored| new_doc[unstored] = new_doc[stored] } if klass.ancestors.include? CurationConcerns::RequiredMetadata + BASIC_METADATA_FIELDS.each { |unstored, stored| new_doc[unstored] = new_doc[stored] } if klass.ancestors.include? CurationConcerns::BasicMetadata + HUMAN_READABLE_FIELDS.each { |unstored, stored| new_doc[unstored] = new_doc[stored] } if klass.ancestors.include? CurationConcerns::HumanReadableType + new_doc + end + + def reconstruct_class(new_doc, klass) + class_metadata_fields = case klass + when Collection + {} + when FileSet + { + # Hyrax::FileSetIndexer + "file_format_sim" => "file_format_tesim", + "all_text_timv" => "all_text_tsimv" + } + when MultiVolumeWork, ScannedResource + PLUM_SCHEMA_FIELDS.collect { |k| ["#{k}_sim","#{k}_tesim"] }.to_h + end + + class_metadata_fields.each { |unstored, stored| new_doc[unstored] = new_doc[stored] } + + if klass == FileSet + # tsimv doesn't exist so actually need to look up the value from fedora (this is faster than calling update_index + new_doc["all_text_timv"] = FileSet.find(new_doc["id"]).extracted_text&.content + end + + # generic_type_sim + generic_type = case klass + when Collection + "Collection" + when Work + "Work" + else + nil + end + new_doc["generic_type_sim"] = [generic_type] + + new_doc + end + end + + class ReconstructedDocValidator + def self.validate_reconstructed_solr_doc(reconstructed_doc) + original_doc = ActiveFedora::Base.find(reconstructed_doc["id"]).to_solr.stringify_keys!.select {|k,v| v.present?} + original_doc.map do |k,v| + if k.end_with? "m" + original_doc[k] = Array(v) + elsif k =~ /_s[a-z]*$/ + original_doc[k] = v.to_s + end + end + + hash_diff = HashDiff::Comparison.new(original_doc, reconstructed_doc) + missing_or_changed_fields = hash_diff.diff.select {|k,v| !(v[0].nil? || v[0] == HashDiff::NO_VALUE) } + #missing_or_changed_fields.empty? ? true : false + end + end + end + end + + # Taken from HashDiff gem and modified to sort arrays given rdf is non-deterministic as to order of values + module HashDiff + class NO_VALUE; end + + class Comparison + def initialize(left, right) + @left = left + @right = right + end + + attr_reader :left, :right + + def diff + @diff ||= find_differences { |l, r| [l, r] } + end + + def left_diff + @left_diff ||= find_differences { |_, r| r } + end + + def right_diff + @right_diff ||= find_differences { |l, _| l } + end + + protected + + def find_differences(&reporter) + combined_keys.each_with_object({ }, &comparison_strategy(reporter)) + end + + private + + def comparison_strategy(reporter) + lambda do |key, diff| + diff[key] = report_difference(key, reporter) unless equal?(key) + end + end + + def combined_keys + if hash?(left) && hash?(right) then + (left.keys + right.keys).uniq + elsif array?(left) && array?(right) then + (0..[left.size, right.size].max).to_a + else + raise ArgumentError, "Don't know how to extract keys. Neither arrays nor hashes given" + end + end + + def equal?(key) + value_with_default(left, key) == value_with_default(right, key) + end + + def hash?(value) + value.is_a?(Hash) + end + + def array?(value) + value.is_a?(Array) + end + + def comparable_hash?(key) + hash?(left[key]) && hash?(right[key]) + end + + def comparable_array?(key) + array?(left[key]) && array?(right[key]) + end + + def report_difference(key, reporter) + if comparable_hash?(key) + self.class.new(left[key], right[key]).find_differences(&reporter) + elsif comparable_array?(key) + self.class.new(left[key], right[key]).find_differences(&reporter) + else + reporter.call( + value_with_default(left, key), + value_with_default(right, key) + ) + end + end + + def value_with_default(obj, key) + value = obj.fetch(key, NO_VALUE) + value.sort if array?(value) + end + end +end diff --git a/solr/config/schema.xml b/solr/config/schema.xml index 1b930686f..a53b96082 100644 --- a/solr/config/schema.xml +++ b/solr/config/schema.xml @@ -45,46 +45,50 @@ that avoids logging every request --> - + - + - + - - - - - - - - - - - + + + + + + + + + + + - - - - - + + + + + + - - - - - - + @@ -112,7 +113,7 @@ - + @@ -120,7 +121,7 @@ - + @@ -129,7 +130,7 @@ - + @@ -144,7 +145,7 @@ - + @@ -154,7 +155,7 @@ - + @@ -168,23 +169,38 @@ - - + + + + + + + + + + + + + + + + + + --> - + - + @@ -201,7 +217,7 @@ - + @@ -213,7 +229,7 @@ - + @@ -222,7 +238,7 @@ - + @@ -230,15 +246,15 @@ - - + + - + @@ -248,15 +264,24 @@ - - + + - + + + + + + + + + + @@ -264,15 +289,15 @@ - - + + - + @@ -280,15 +305,15 @@ - - + + - + @@ -296,50 +321,46 @@ - - + + - + - + - - - - - - - - - + - + + + + + + - id - - - - - - @@ -350,10 +371,15 @@ destination field is to use the dynamic field syntax. copyField also supports a maxChars to copy setting. --> - + + + - @@ -371,4 +397,13 @@ --> + + + + + diff --git a/solr/config/solrconfig.xml b/solr/config/solrconfig.xml index 48f58041d..87a2fdfb2 100644 --- a/solr/config/solrconfig.xml +++ b/solr/config/solrconfig.xml @@ -28,13 +28,8 @@ that you fully re-index after changing this setting as it can affect both how text is indexed and queried. --> - 5.0.0 + 9.10.0 - - - - - @@ -51,12 +46,28 @@ + *:* + + + ${solr.ulog.dir:} + + + + ${solr.autoCommit.maxTime:15000} + true + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + + + + last_modified @@ -296,5 +408,43 @@ + + + + + + + +