diff --git a/.github/matrix.json b/.github/matrix.json index fbc27e3094..5f47e044de 100644 --- a/.github/matrix.json +++ b/.github/matrix.json @@ -6,6 +6,7 @@ "api": [null], "additional_engine_cart_rails_options": [""], "additional_name": [""], + "repository": [null], "include": [ { "ruby": "3.4", @@ -25,6 +26,13 @@ "view_component_version": "~> 3.0", "additional_name": "ViewComponent 3" }, + { + "ruby": "3.4", + "rails_version": "8.1.1", + "additional_engine_cart_rails_options": "--css=bootstrap", + "repository": "elasticsearch", + "additional_name": "| Elasticsearch" + }, { "ruby": "3.3", "rails_version": "8.0.3", diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85fdde8642..9202968f92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: ref: required: false type: string - default: '' + default: "" description: The branch or reference to run the workflow against jobs: set_matrix: @@ -35,6 +35,7 @@ jobs: VIEW_COMPONENT_VERSION: ${{ matrix.view_component_version }} BOOTSTRAP_VERSION: ${{ matrix.bootstrap_version }} BLACKLIGHT_API_TEST: ${{ matrix.api }} + BLACKLIGHT_REPOSITORY: ${{ matrix.repository }} ENGINE_CART_RAILS_OPTIONS: "--skip-git --skip-listen --skip-spring --skip-keeps --skip-kamal --skip-solid --skip-coffee --skip-test ${{ matrix.additional_engine_cart_rails_options }}" steps: - uses: actions/checkout@v4 diff --git a/app/components/blacklight/response/facet_group_component.rb b/app/components/blacklight/response/facet_group_component.rb index 70529ff3f7..5fc46f0a56 100644 --- a/app/components/blacklight/response/facet_group_component.rb +++ b/app/components/blacklight/response/facet_group_component.rb @@ -31,7 +31,6 @@ def button_component end def render? - # debugger body.present? end end diff --git a/app/components/blacklight/search_header_component.html.erb b/app/components/blacklight/search_header_component.html.erb index a5e3e6c8cd..4911a6f82a 100644 --- a/app/components/blacklight/search_header_component.html.erb +++ b/app/components/blacklight/search_header_component.html.erb @@ -1,2 +1,2 @@ -<%= render 'did_you_mean' %> -<%= render 'sort_and_per_page' %> \ No newline at end of file +<%= render 'did_you_mean' if did_you_mean? %> +<%= render 'sort_and_per_page' %> diff --git a/app/components/blacklight/search_header_component.rb b/app/components/blacklight/search_header_component.rb index 504f2f6843..0e848273a8 100644 --- a/app/components/blacklight/search_header_component.rb +++ b/app/components/blacklight/search_header_component.rb @@ -2,5 +2,10 @@ module Blacklight class SearchHeaderComponent < Blacklight::Component + # Should we draw the did_you_mean component? + # Currently not supported with elasticsearch + def did_you_mean? + Blacklight.solr? + end end end diff --git a/app/services/blacklight/bookmarks_search_builder.rb b/app/services/blacklight/bookmarks_search_builder.rb index cff2ae9059..bbe2b06b70 100644 --- a/app/services/blacklight/bookmarks_search_builder.rb +++ b/app/services/blacklight/bookmarks_search_builder.rb @@ -10,12 +10,11 @@ class BookmarksSearchBuilder < ::SearchBuilder # # @return [void] def bookmarked(solr_parameters) - solr_parameters[:fq] ||= [] bookmarks = @scope.context.fetch(:bookmarks) return unless bookmarks document_ids = bookmarks.collect { |b| b.document_id.to_s } - solr_parameters[:fq] += ["{!terms f=id}#{document_ids.join(',')}"] + limit_to_specific_records(solr_parameters, document_ids) end self.default_processor_chain += [:bookmarked] end diff --git a/app/services/blacklight/search_service.rb b/app/services/blacklight/search_service.rb index aed3b90626..4cb95a98ff 100644 --- a/app/services/blacklight/search_service.rb +++ b/app/services/blacklight/search_service.rb @@ -34,7 +34,6 @@ def search_results builder = yield(builder) if block_given? response = repository.search(params: builder) - if response.grouped? && grouped_key_for_results response.group(grouped_key_for_results) elsif response.grouped? && response.grouped.length == 1 @@ -74,7 +73,9 @@ def facet_suggest_response(facet_field, facet_suggestion_query, extra_controller def previous_and_next_documents_for_search(index, request_params, extra_controller_params = {}) p = previous_and_next_document_params(index) new_state = request_params.is_a?(Blacklight::SearchState) ? request_params : Blacklight::SearchState.new(request_params, blacklight_config) - query = search_builder.with(new_state).start(p.delete(:start)).rows(p.delete(:rows)).merge(extra_controller_params).merge(p) + builder = search_builder.with(new_state) + builder.processor_chain.delete(:add_aggregation) + query = builder.start(p.delete(:start)).rows(p.delete(:rows)).merge(extra_controller_params).merge(p) response = repository.search(params: query) document_list = response.documents @@ -120,27 +121,7 @@ def solr_opensearch_params(field) solr_params end - ## - # Pagination parameters for selecting the previous and next documents - # out of a result set. - def previous_and_next_document_params(index, window = 1) - solr_params = blacklight_config.document_pagination_params.dup - - if solr_params.empty? - solr_params[:fl] = blacklight_config.document_model.unique_key - end - - if index > 0 - solr_params[:start] = index - window # get one before - solr_params[:rows] = (2 * window) + 1 # and one after - else - solr_params[:start] = 0 # there is no previous doc - solr_params[:rows] = 2 * window # but there should be one after - end - - solr_params[:facet] = false - solr_params - end + delegate :previous_and_next_document_params, to: :search_builder ## # Retrieve a set of documents by id @@ -148,7 +129,6 @@ def previous_and_next_document_params(index, window = 1) # @param [HashWithIndifferentAccess] extra_controller_params def fetch_many(ids, extra_controller_params) extra_controller_params ||= {} - query = search_builder .with(search_state) .where(blacklight_config.document_model.unique_key => ids) diff --git a/blacklight.gemspec b/blacklight.gemspec index e12786ef2a..a9337d96a0 100644 --- a/blacklight.gemspec +++ b/blacklight.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |s| s.add_dependency "zeitwerk" s.add_development_dependency "rsolr", ">= 1.0.6", "< 3" # Library for interacting with rSolr. + s.add_development_dependency "elasticsearch" s.add_development_dependency "rspec-rails", "~> 7.0" s.add_development_dependency "rspec-collection_matchers", ">= 1.0" s.add_development_dependency 'axe-core-rspec' diff --git a/compose.yaml b/compose.yaml index 5244dd084e..312401653b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -36,3 +36,15 @@ services: - /opt/solr/conf - "-Xms256m" - "-Xmx512m" + + elasticsearch: + environment: + - discovery.type=single-node + - xpack.security.enabled=false + image: elasticsearch:8.17.3 + ports: + - 9200:9200 + volumes: + - es_data:/usr/share/elasticsearch/data +volumes: + es_data: diff --git a/lib/blacklight.rb b/lib/blacklight.rb index 0a915e4445..0fce32e496 100644 --- a/lib/blacklight.rb +++ b/lib/blacklight.rb @@ -24,10 +24,15 @@ def self.default_index=(repository) Blacklight::RuntimeRegistry.connection = repository end + # @return [Bool] Are we configured to use solr? + def self.solr? + repository_class == Blacklight::Solr::Repository + end + ## # The configured repository class. By convention, this is # the class Blacklight::(name of the adapter)::Repository, e.g. - # elastic_search => Blacklight::ElasticSearch::Repository + # elasticsearch => Blacklight::ElasticSearch::Repository def self.repository_class if connection_config && !connection_config.key?(:adapter) raise "The value for :adapter was not found in the blacklight.yml config" @@ -36,6 +41,8 @@ def self.repository_class case connection_config&.fetch(:adapter) || 'solr' when 'solr' Blacklight::Solr::Repository + when 'elasticsearch' + Blacklight::Elasticsearch::Repository when /::/ connection_config[:adapter].constantize else diff --git a/lib/blacklight/configuration/facet_field.rb b/lib/blacklight/configuration/facet_field.rb index c81062c219..d110c04cfd 100644 --- a/lib/blacklight/configuration/facet_field.rb +++ b/lib/blacklight/configuration/facet_field.rb @@ -86,8 +86,12 @@ def normalize! blacklight_config = nil super if single && tag.blank? && ex.blank? - self.tag = "#{key}_single" - self.ex = "#{key}_single" + if Blacklight.solr? + self.tag = "#{key}_single" + self.ex = "#{key}_single" + else + Blacklight.logger.warn "the `single' property on the facet configuration (for #{key}) is not yet supported for elasticsearch" + end end self diff --git a/lib/blacklight/elasticsearch/repository.rb b/lib/blacklight/elasticsearch/repository.rb new file mode 100644 index 0000000000..6c3147a4a2 --- /dev/null +++ b/lib/blacklight/elasticsearch/repository.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Blacklight::Elasticsearch + class Repository < Blacklight::AbstractRepository + ## + # Find a single solr document result (by id) using the document configuration + # @param [String] id document's unique key value + # @param [Hash] params additional query parameters + def find id, params = {} + api_response = connection.get(index:, id:) + blacklight_config.response_model.new(api_response, params, document_model: blacklight_config.document_model, blacklight_config: blacklight_config) + rescue Elastic::Transport::Transport::Errors::NotFound + raise Blacklight::Exceptions::RecordNotFound + end + + # Find multiple documents by their ids + # @param [SearchBuilder] search_builder the search builder + def find_many(search_builder) + # TODO: This is hacky, but SearchBuilder#where is currently very coupled to Solr + ids = search_builder.search_state.params['q']['id'] + docs = ids.map { |id| { _index: index, _id: id } } + + api_response = connection.mget(body: { docs: }) + blacklight_config.response_model.new(api_response, search_builder, document_model: blacklight_config.document_model, blacklight_config: blacklight_config) + end + + ## + # Execute a search query against solr + # @param [SearchBuilder] params the search builder + def search params: nil, **kwargs + request_params = params.reverse_merge(kwargs) + api_response = connection.search(index:, body: request_params) + blacklight_config.response_model.new(api_response, params, document_model: blacklight_config.document_model, blacklight_config: blacklight_config) + end + + def seed_index docs + begin + connection.indices.delete(index:) + rescue StandardError + Elastic::Transport::Transport::Errors::NotFound + end + + # TODO: move this to a yaml file + connection.indices.create index:, body: { + settings: { + analysis: { + analyzer: { + foo: { + type: "custom", + tokenizer: "standard", + filter: %w[ + lowercase + asciifolding + ] + } + } + } + }, + mappings: { + properties: { + format: { type: 'keyword' }, + pub_date_ssim: { type: 'integer' }, + subject_ssim: { type: 'keyword' }, + language_ssim: { type: 'keyword' }, + lc_1letter_ssim: { type: 'keyword' }, + subject_geo_ssim: { type: 'keyword' }, + subject_era_ssim: { type: 'keyword' }, + all_text_timv: { type: 'text', analyzer: "foo", search_analyzer: "foo" } + } + } + } + + # Since Elasticsearch doesn't have copy fields, we need to create all_text_timv manually + body = docs.map do |fixture_data| + data = fixture_data.except('id') + data['all_text_timv'] = data.select { |k, _| /_(si|ssim|tsim)\z/.match?(k) }.values.flatten + { index: { _index: index, _id: fixture_data['id'], data: data } } + end + connection.bulk(body:) + connection.indices.refresh(index:) + end + + # @param [Hash] request_params + # @return [Blacklight::Suggest::Response] + def suggestions(request_params) + suggest_results = {} # TODO: implement + Blacklight.logger.warn("suggestions has not yet been implemented for elasticsearch") + Blacklight::Suggest::Response.new suggest_results, request_params, '', '' + end + + private + + def index + @index ||= connection_config.fetch(:index) + end + + # See https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/connecting.html + def build_connection + ::Elasticsearch::Client.new(url: connection_config[:url]) + # cloud_id: '', + # user: '', + # password: '' + end + end +end diff --git a/lib/blacklight/elasticsearch/request.rb b/lib/blacklight/elasticsearch/request.rb new file mode 100644 index 0000000000..d2ef54404c --- /dev/null +++ b/lib/blacklight/elasticsearch/request.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# class Blacklight::Elasticsearch::InvalidParameter < ArgumentError; end + +class Blacklight::Elasticsearch::Request < ActiveSupport::HashWithIndifferentAccess + # This is similar to qf in Solr + cattr_accessor :query_fields, default: %w[all_text_timv] + def initialize(constructor = {}) + if constructor.is_a?(Hash) + super() + update(constructor) + else + super + end + end + + def ids=(ids) + if ids.empty? + match_none + else + append_filter_query({ ids: { values: ids } }) + end + end + + def match_none + must.delete('match_all') + must['match_none'] = {} + end + + # See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html + def match_all + must.delete 'combined_fields' + must['match_all'] = {} + end + + # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html + def append_filter_query(filter_query) + bool['filter'] ||= {} + bool['filter'].merge! filter_query + end + + # See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-combined-fields-query.html + def append_query(query) + return if query.nil? + + # TODO: Perhaps we could use this alternative: + # "_all": "your search text" + must['combined_fields'] = { + 'query' => query, + 'fields' => query_fields, + 'operator' => 'or' + } + end + + def append_facet_fields(value) + self['aggs'] ||= {} + self['aggs']["bl-#{value}"] = { terms: { field: value } } + end + + private + + def bool + self['query'] ||= {} + self['query']['bool'] ||= {} + self['query']['bool'] + end + + def must + bool['must'] ||= {} + bool['must'] + end +end diff --git a/lib/blacklight/elasticsearch/response.rb b/lib/blacklight/elasticsearch/response.rb new file mode 100644 index 0000000000..c058dadc88 --- /dev/null +++ b/lib/blacklight/elasticsearch/response.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +class Blacklight::Elasticsearch::Response < ActiveSupport::HashWithIndifferentAccess + include Blacklight::Response::PaginationMethods + + attr_reader :request_params, :search_builder + attr_accessor :blacklight_config, :options + + delegate :document_factory, to: :blacklight_config + + # @param [Elasticsearch::API::Response] response + # @param [Hash, Blacklight::SearchBuilder] request_params a SearchBuilder or a Hash of parameters + def initialize(api_response, request_params, options = {}) + @search_builder = request_params if request_params.is_a?(Blacklight::SearchBuilder) + + super(ActiveSupport::HashWithIndifferentAccess.new(api_response)) + @request_params = ActiveSupport::HashWithIndifferentAccess.new(request_params) + self.blacklight_config = options[:blacklight_config] + self.options = options + end + + def total + hits[:total][:value] + end + + def start + request_params.fetch('from', 0) + end + + def documents + @documents ||= if self[:_source] # handle a get call + [document_factory.build(self[:_source].merge(id: self[:_id]), self, options)] + elsif self[:docs] # handle mget call + self[:docs].filter_map { |doc| document_factory.build(doc[:_source].merge(id: doc[:_id]), self, options) if doc['found'] } + else # Search call + dig(:hits, :hits).collect { |doc| document_factory.build((doc[:_source] || doc[:fields] || {}).merge(id: doc[:_id]), self, options) } + end + end + alias docs documents + + # def grouped + # @groups ||= self["grouped"].map do |field, group| + # # grouped responses can either be grouped by: + # # - field, where this key is the field name, and there will be a list + # # of documents grouped by field value, or: + # # - function, where the key is the function, and the documents will be + # # further grouped by function value, or: + # # - query, where the key is the query, and the matching documents will be + # # in the doclist on THIS object + # if group["groups"] # field or function + # GroupResponse.new field, group, self + # else # query + # Group.new field, group, self + # end + # end + # end + + # def group key + # grouped.find { |x| x.key == key } + # end + + def aggregations + @aggregations ||= default_aggregations.merge(facet_field_aggregations) # .merge(facet_query_aggregations).merge(facet_pivot_aggregations).merge(json_facet_aggregations) + end + + # This is mostly to follow what the Solr Reponse has. + def params + raise "Elasticsearch doesn't have params" + end + + def grouped? + Array(self[:results]).any? { |result| result[:_group].present? } + end + + def spelling + raise "XXXXXX" + nil + end + + def more_like _document + [] + end + + # TODO: Same implementation as solr, move to mixin? + def export_formats + documents.map { |x| x.export_formats.keys }.flatten.uniq + end + + def rows + search_builder&.rows || hits[:hits].length + end + + private + + # @return [Hash] establish a null object pattern for facet data look-up, allowing + # the response and applied parameters to get passed through even if there was no + # facet data in the response + def default_aggregations + @default_aggregations ||= begin + h = Hash.new { |_hash, key| null_facet_field_object(key) } + h.with_indifferent_access + end + end + + # @return [Blacklight::Solr::Response::FacetField] a "null object" facet field + def null_facet_field_object(key) + Blacklight::Solr::Response::FacetField.new(key, [], { response: self }) + end + + ## + # Convert Solr's facet_field response into + # a hash of Blacklight::Solr::Response::Facet::FacetField objects + def facet_field_aggregations + self['aggregations'].each_with_object({}) do |(aggregation_name, data), hash| + facet_field_name = aggregation_name.delete_prefix('bl-') + items = data['buckets'].map do |bucket| + value = bucket['key'] + hits = bucket['doc_count'] + Blacklight::Solr::Response::Facets::FacetItem.new(value: value, hits: hits) + end + next if items.empty? + + options = {} + facet_field = Blacklight::Solr::Response::Facets::FacetField.new(facet_field_name, items, options) + hash[facet_field_name] = facet_field + + # alias all the possible blacklight config names.. + next unless blacklight_config && !blacklight_config.facet_fields[facet_field_name] + + blacklight_config.facet_fields.select { |_k, v| v.field == facet_field_name }.each_key do |key| + hash[key] = hash[facet_field_name] + end + end + end + + def hits + self[:hits] + end +end diff --git a/lib/blacklight/elasticsearch/search_builder_behavior.rb b/lib/blacklight/elasticsearch/search_builder_behavior.rb new file mode 100644 index 0000000000..49b9cfd573 --- /dev/null +++ b/lib/blacklight/elasticsearch/search_builder_behavior.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +module Blacklight::Elasticsearch + module SearchBuilderBehavior + extend ActiveSupport::Concern + + included do + self.default_processor_chain = [ + # :setup_defaults, + :add_query, + :empty_search_query, + :add_facet_filter, + :add_aggregation, + :add_paging + # :add_sorting_to_solr + ] + end + + # def setup_defaults(_request) + # # No need to set rows, because it's already set in the SearchService + # rows(blacklight_config.default_solr_params[:rows]) if blacklight_config.default_solr_params[:rows] + # end + + def limit_to_specific_records(request, document_ids) + request.ids = document_ids + end + + ## + # Pagination parameters for selecting the previous and next documents + # out of a result set. + def previous_and_next_document_params(index, window = 1) + params = blacklight_config.document_pagination_params.dup + + if params.empty? + params[:fields] = [blacklight_config.document_model.unique_key] + params[:_source] = false + end + + if index > 0 + params[:start] = index - window # get one before + params[:rows] = (2 * window) + 1 # and one after + else + params[:start] = 0 # there is no previous doc + params[:rows] = 2 * window # but there should be one after + end + + params + end + + # Add wildcard search if no search is present (similar to q.alt in solr) + def empty_search_query(request) + request.match_all unless search_state.query_param.present? + end + + def request + Blacklight::Elasticsearch::Request.new + end + + def add_facet_filter(request) + search_state.filters.each do |filter| + if filter.config.filter_query_builder + filter_queries, = filter.config.filter_query_builder.call(self, filter, request) + + Array(filter_queries).each do |filter_query| + request.append_filter_query(filter_query) + end + else + filter.values.compact_blank.each do |value| + value = value.first if value.is_a?(Array) && value.size == 1 + filter_query, = if value.is_a?(Array) + raise "Array of filter values (#{value.inspect}) is not yet supported for elasticsearch" + else + facet_value_to_fq_string(filter.config.key, value) + end + request.append_filter_query filter_query + end + end + end + end + + ## + # Take the user-entered query, and put it in the solr params, + # including config's "search field" params for current search field. + # also include setting spellcheck.q. + def add_query(request) + ## + # Create Solr 'q' including the user-entered q, prefixed by any + # solr LocalParams in config, using solr LocalParams syntax. + # http://wiki.apache.org/solr/LocalParams + ## + return if search_state&.query_param.is_a?(Hash) + + request.append_query search_state.query_param + end + + ## + # Add appropriate facetting directives in, including + # taking account of our facet paging/'more'. This is not + # about solr 'fq', this is about solr facet.* params. + def add_aggregation(request) + facet_fields_to_include_in_request.each do |field_name, facet| + raise 'facet.json is not yet supported in the Elasticsearch configuration' if facet.json + + Blacklight.logger.warn "facet.pivot (for #{field_name}) is not yet supported in the Elasticsearch configuration" if facet.pivot + Blacklight.logger.warn "facet.query (for #{field_name}) is not yet supported in the Elasticsearch configuration" if facet.query + raise "facet.ex (#{facet.ex} for #{field_name}) is not yet supported in the Elasticsearch configuration" if facet.ex + raise 'facet.solr_params is not supported in the Elasticsearch configuration' if facet.solr_params + raise 'facet.sort is not yet supported in the Elasticsearch configuration' if facet.sort + + request.append_facet_fields facet.field + + # if facet.sort + # request[:"f.#{facet.field}.facet.sort"] = facet.sort + # end + + # NOTE: Is this even supported in ES? + # limit = facet_limit_with_pagination(field_name) + # request[:"f.#{facet.field}.facet.limit"] = limit if limit + end + end + + ### + # copy paging params from BL app over to solr, changing + # app level per_page and page to Solr rows and start. + def add_paging(request) + request[:size] = rows + request[:from] = start if start.nonzero? + end + + # ### + # # copy sorting params from BL app over to solr + # def add_sorting_to_solr(solr_parameters) + # solr_parameters[:sort] = sort if sort.present? + # end + + private + + ## + # Convert a facet/value pair into a solr fq parameter + + def facet_value_to_fq_string(facet_field, value, use_local_params: true) + facet_config = blacklight_config.facet_fields[facet_field] + + index_field = facet_config.field if facet_config && !facet_config.query + index_field ||= facet_field + + raise "facet value not found, #{value}" if facet_config&.query && !facet_config.query[value] + + { 'term' => { index_field => convert_to_term_value(value) } } + end + + def convert_to_term_value(value) + case value + when DateTime, Time + value.utc.strftime("%Y-%m-%dT%H:%M:%SZ") + when Date + value.to_time(:local).strftime("%Y-%m-%dT%H:%M:%SZ") + else + value.to_s + end + end + + # TODO: Identical to method in Solr search builder. Perhaps move to blacklight_config? + def facet_fields_to_include_in_request + blacklight_config.facet_fields.select do |_field_name, facet| + facet.include_in_request || (facet.include_in_request.nil? && blacklight_config.add_facet_fields_to_solr_request) + end + end + end +end diff --git a/lib/blacklight/solr/response/pagination_methods.rb b/lib/blacklight/response/pagination_methods.rb similarity index 91% rename from lib/blacklight/solr/response/pagination_methods.rb rename to lib/blacklight/response/pagination_methods.rb index 495bc353fd..036583830f 100644 --- a/lib/blacklight/solr/response/pagination_methods.rb +++ b/lib/blacklight/response/pagination_methods.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Blacklight::Solr::Response::PaginationMethods +module Blacklight::Response::PaginationMethods include Kaminari::PageScopeMethods include Kaminari::ConfigurationMethods::ClassMethods diff --git a/lib/blacklight/solr/repository.rb b/lib/blacklight/solr/repository.rb index 2cb1ad60b1..24293227b9 100644 --- a/lib/blacklight/solr/repository.rb +++ b/lib/blacklight/solr/repository.rb @@ -104,6 +104,11 @@ def build_solr_request(solr_params) end end + def seed_index docs + connection.add docs + connection.commit + end + private ## diff --git a/lib/blacklight/solr/response.rb b/lib/blacklight/solr/response.rb index c8648592f5..2f3b5ec56f 100644 --- a/lib/blacklight/solr/response.rb +++ b/lib/blacklight/solr/response.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Blacklight::Solr::Response < ActiveSupport::HashWithIndifferentAccess - include PaginationMethods + include Blacklight::Response::PaginationMethods include Spelling include Facets include Response diff --git a/lib/blacklight/solr/response/group.rb b/lib/blacklight/solr/response/group.rb index be83a65b96..7e5f30f13d 100644 --- a/lib/blacklight/solr/response/group.rb +++ b/lib/blacklight/solr/response/group.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Blacklight::Solr::Response::Group - include Blacklight::Solr::Response::PaginationMethods + include Blacklight::Response::PaginationMethods attr_reader :key, :group, :response diff --git a/lib/blacklight/solr/response/group_response.rb b/lib/blacklight/solr/response/group_response.rb index 29ec9e2709..b4eea6806d 100644 --- a/lib/blacklight/solr/response/group_response.rb +++ b/lib/blacklight/solr/response/group_response.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Blacklight::Solr::Response::GroupResponse - include Blacklight::Solr::Response::PaginationMethods + include Blacklight::Response::PaginationMethods attr_reader :key, :group, :response diff --git a/lib/blacklight/solr/search_builder_behavior.rb b/lib/blacklight/solr/search_builder_behavior.rb index 2684538f1a..f7224602e2 100644 --- a/lib/blacklight/solr/search_builder_behavior.rb +++ b/lib/blacklight/solr/search_builder_behavior.rb @@ -22,6 +22,33 @@ module SearchBuilderBehavior ] end + def limit_to_specific_records(solr_parameters, document_ids) + solr_parameters[:fq] ||= [] + solr_parameters[:fq] += ["{!terms f=id}#{document_ids.join(',')}"] + end + + ## + # Pagination parameters for selecting the previous and next documents + # out of a result set. + def previous_and_next_document_params(index, window = 1) + solr_params = blacklight_config.document_pagination_params.dup + + if solr_params.empty? + solr_params[:fl] = blacklight_config.document_model.unique_key + end + + if index > 0 + solr_params[:start] = index - window # get one before + solr_params[:rows] = (2 * window) + 1 # and one after + else + solr_params[:start] = 0 # there is no previous doc + solr_params[:rows] = 2 * window # but there should be one after + end + + solr_params[:facet] = false + solr_params + end + #### # Start with general defaults from BL config. Need to use custom # merge to dup values, to avoid later mutating the original by mistake. @@ -325,12 +352,6 @@ def facet_fields_to_include_in_request end end - def search_state - return super if defined?(super) - - @search_state ||= Blacklight::SearchState.new(blacklight_params, blacklight_config) - end - def add_search_field_query_builder_params(solr_parameters) q, additional_parameters = search_field.query_builder.call(self, search_field, solr_parameters) @@ -350,5 +371,9 @@ def add_search_field_with_local_parameters(solr_parameters) # params! solr_parameters["spellcheck.q"] ||= search_state.query_param end + + def request + Blacklight::Solr::Request.new + end end end diff --git a/lib/generators/blacklight/elasticsearch_generator.rb b/lib/generators/blacklight/elasticsearch_generator.rb new file mode 100644 index 0000000000..e338797269 --- /dev/null +++ b/lib/generators/blacklight/elasticsearch_generator.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails/generators' + +module Blacklight + class ElasticsearchGenerator < Rails::Generators::Base + source_root ::File.expand_path('templates', __dir__) + + desc <<-EOF + This generator makes the following changes to your application: + 1. Adds elasticsearch to your Gemfile + 1. Adds config/blacklight.yml + EOF + + # Copy all files in templates/config directory to host config + def create_configuration_file + copy_file "config/blacklight_elasticsearch.yml", "config/blacklight.yml" + gsub_file 'config/blacklight.yml', '__VERSION__', Blacklight::VERSION + end + + def configure_catalog + gsub_file 'app/controllers/catalog_controller.rb', /# config\.repository_class.*/, + 'config.repository_class = Blacklight::Elasticsearch::Repository' + gsub_file 'app/controllers/catalog_controller.rb', /# config\.response_model.*/, + 'config.response_model = Blacklight::Elasticsearch::Response' + end + + def add_rsolr_gem + gem 'elasticsearch', '~> 8.17' + end + + def bundle_install + inside destination_root do + Bundler.with_unbundled_env do + run "bundle install" + end + end + end + end +end diff --git a/lib/generators/blacklight/install_generator.rb b/lib/generators/blacklight/install_generator.rb index 63b8c9ad86..a2be5810b4 100644 --- a/lib/generators/blacklight/install_generator.rb +++ b/lib/generators/blacklight/install_generator.rb @@ -14,23 +14,20 @@ class Install < Rails::Generators::Base class_option :'bootstrap-version', type: :string, default: nil, desc: "Set the generated app's bootstrap version" class_option :'skip-assets', type: :boolean, default: false, desc: "Skip generating javascript and css assets into the application" class_option :'skip-solr', type: :boolean, default: false, desc: "Skip generating solr configurations." + class_option :repository, type: :string, default: ENV['BLACKLIGHT_REPOSITORY'].presence || 'solr', desc: "Which repository to use ('elasticsearch' or 'solr')" desc <<-EOS This generator makes the following changes to your application: 1. Generates blacklight:models - 2. Generates utilities for working with solr - 3. Creates a number of public assets, including images, stylesheets, and javascript - 4. Injects behavior into your user application_controller.rb - 5. Adds example configurations for dealing with MARC-like data - 6. Adds Blacklight routes to your ./config/routes.rb + 2. Creates a number of public assets, including images, stylesheets, and javascript + 3. Injects behavior into your user application_controller.rb + 4. Adds example configurations for dealing with MARC-like data + 5. Adds Blacklight routes to your ./config/routes.rb + 6. Generates utilities for working with solr/elasticsearch Thank you for Installing Blacklight. EOS - def add_solr_wrapper - generate 'blacklight:solr' unless options[:'skip-solr'] - end - # Copy all files in templates/public/ directory to public/ # Call external generator in AssetsGenerator, so we can # leave that callable seperately too. @@ -53,7 +50,7 @@ def generate_blacklight_document end def generate_search_builder - generate 'blacklight:search_builder', search_builder_name + generate 'blacklight:search_builder', search_builder_name, options.fetch(:repository, 'solr') end def generate_blacklight_models @@ -77,6 +74,21 @@ def add_default_catalog_route route("root to: \"#{controller_name}#index\"") end + def add_solr_wrapper + if options[:'skip-solr'] || options[:repository] != 'solr' + say "Skipping solr. #{options.inspect}" + return + end + + generate 'blacklight:solr' + end + + def add_elasticsearch + return unless options[:repository] == 'elasticsearch' + + generate 'blacklight:elasticsearch' + end + def inject_blacklight_i18n_strings copy_file "blacklight.en.yml", "config/locales/blacklight.en.yml" end diff --git a/lib/generators/blacklight/models_generator.rb b/lib/generators/blacklight/models_generator.rb index 1cec1b1b87..40d8c7e014 100644 --- a/lib/generators/blacklight/models_generator.rb +++ b/lib/generators/blacklight/models_generator.rb @@ -13,15 +13,8 @@ class ModelsGenerator < Rails::Generators::Base desc <<-EOS This generator makes the following changes to your application: 1. Creates several database migrations if they do not exist in /db/migrate - 2. Creates config/blacklight.yml with a default configuration EOS - # Copy all files in templates/config directory to host config - def create_configuration_files - copy_file "config/blacklight.yml", "config/blacklight.yml" - gsub_file 'config/blacklight.yml', '__VERSION__', Blacklight::VERSION - end - # Setup the database migrations def copy_migrations rake "blacklight:install:migrations" diff --git a/lib/generators/blacklight/search_builder_generator.rb b/lib/generators/blacklight/search_builder_generator.rb index 4ae43491bd..ec091781b2 100644 --- a/lib/generators/blacklight/search_builder_generator.rb +++ b/lib/generators/blacklight/search_builder_generator.rb @@ -9,13 +9,14 @@ class SearchBuilderGenerator < Rails::Generators::Base source_root File.expand_path('../templates', __FILE__) argument :model_name, type: :string, default: "search_builder" + argument :repository, type: :string, default: "solr" desc <<-EOS This generator makes the following changes to your application: 1. Creates a blacklight search builder in your /app/models directory EOS def create_search_builder - template "search_builder.rb", "app/models/#{model_name}.rb" + template "search_builder_#{repository}.rb", "app/models/#{model_name}.rb" end def create_search_builder_spec diff --git a/lib/generators/blacklight/solr_generator.rb b/lib/generators/blacklight/solr_generator.rb index 041f2c7169..d2f85d86a4 100644 --- a/lib/generators/blacklight/solr_generator.rb +++ b/lib/generators/blacklight/solr_generator.rb @@ -8,12 +8,19 @@ class SolrGenerator < Rails::Generators::Base desc <<-EOF This generator makes the following changes to your application: - 1. Installs solr_wrapper into your application - 2. Copies default blacklight solr config directory into your application - 3. Copies default .solr_wrapper into your application - 4. Adds rsolr to your Gemfile + 1. Creates config/blacklight.yml + 2. Installs solr_wrapper into your application + 3. Copies default blacklight solr config directory into your application + 4. Copies default .solr_wrapper into your application + 5. Adds rsolr to your Gemfile EOF + # Copy all files in templates/config directory to host config + def create_configuration_file + copy_file "config/blacklight_solr.yml", "config/blacklight.yml" + gsub_file 'config/blacklight.yml', '__VERSION__', Blacklight::VERSION + end + def install_solrwrapper gem_group :development, :test do gem 'solr_wrapper', '>= 0.3' @@ -23,6 +30,7 @@ def install_solrwrapper end def copy_solr_conf + raise "XXXXXX" directory 'solr' end diff --git a/lib/generators/blacklight/templates/catalog_controller.rb b/lib/generators/blacklight/templates/catalog_controller.rb index e7820b41bf..736736eff0 100644 --- a/lib/generators/blacklight/templates/catalog_controller.rb +++ b/lib/generators/blacklight/templates/catalog_controller.rb @@ -138,10 +138,17 @@ class <%= controller_name.classify %>Controller < ApplicationController collapsing: true, include_in_advanced_search: false + gte = ->(date) { + if Blacklight.repository_class == Blacklight::Solr::Repository + "pub_date_ssim:[#{date} TO *]" + else + { 'range' => { 'pub_date_ssim' => { 'gte' => date } } } + end + } config.add_facet_field 'example_query_facet_field', label: 'Publish Date', :query => { - :years_5 => { label: 'within 5 Years', fq: "pub_date_ssim:[#{Time.zone.now.year - 5 } TO *]" }, - :years_10 => { label: 'within 10 Years', fq: "pub_date_ssim:[#{Time.zone.now.year - 10 } TO *]" }, - :years_25 => { label: 'within 25 Years', fq: "pub_date_ssim:[#{Time.zone.now.year - 25 } TO *]" } + :years_5 => { label: 'within 5 Years', fq: gte.call(Time.zone.now.year - 5) }, + :years_10 => { label: 'within 10 Years', fq: gte.call(Time.zone.now.year - 10) }, + :years_25 => { label: 'within 25 Years', fq: gte.call(Time.zone.now.year - 25) } } diff --git a/lib/generators/blacklight/templates/config/blacklight_elasticsearch.yml b/lib/generators/blacklight/templates/config/blacklight_elasticsearch.yml new file mode 100644 index 0000000000..411c823f05 --- /dev/null +++ b/lib/generators/blacklight/templates/config/blacklight_elasticsearch.yml @@ -0,0 +1,13 @@ +load_defaults: __VERSION__ +development: + adapter: elasticsearch + index: blacklight + url: http://127.0.0.1:9200 +test: &test + adapter: elasticsearch + index: blacklight + url: http://127.0.0.1:9200 +production: + adapter: elasticsearch + index: blacklight + url: http://127.0.0.1:9200 diff --git a/lib/generators/blacklight/templates/config/blacklight.yml b/lib/generators/blacklight/templates/config/blacklight_solr.yml similarity index 100% rename from lib/generators/blacklight/templates/config/blacklight.yml rename to lib/generators/blacklight/templates/config/blacklight_solr.yml diff --git a/lib/generators/blacklight/templates/search_builder_elasticsearch.rb b/lib/generators/blacklight/templates/search_builder_elasticsearch.rb new file mode 100644 index 0000000000..63bfc0cd2b --- /dev/null +++ b/lib/generators/blacklight/templates/search_builder_elasticsearch.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class <%= model_name.classify %> < Blacklight::SearchBuilder + include Blacklight::Elasticsearch::SearchBuilderBehavior + + ## + # @example Adding a new step to the processor chain + # self.default_processor_chain += [:add_custom_data_to_query] + # + # def add_custom_data_to_query(solr_parameters) + # solr_parameters[:custom] = blacklight_params[:user_value] + # end +end diff --git a/lib/generators/blacklight/templates/search_builder.rb b/lib/generators/blacklight/templates/search_builder_solr.rb similarity index 100% rename from lib/generators/blacklight/templates/search_builder.rb rename to lib/generators/blacklight/templates/search_builder_solr.rb diff --git a/lib/railties/blacklight.rake b/lib/railties/blacklight.rake index c51f5c793f..dd60fff2ed 100644 --- a/lib/railties/blacklight.rake +++ b/lib/railties/blacklight.rake @@ -23,9 +23,7 @@ namespace :blacklight do file = ENV.fetch('FILE') { (app_file && File.exist?(app_file) && app_file) } || File.join(Blacklight.root, 'spec', 'fixtures', 'sample_solr_documents.yml') docs = YAML.safe_load(File.open(file)) - conn = Blacklight.default_index.connection - conn.add docs - conn.commit + Blacklight.default_index.seed_index(docs) end end diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index b2be752b8c..2c32c40591 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -27,12 +27,14 @@ it "has docs and facets for query with results", :integration do get :index, params: { q: user_query } expect(assigns(:response).docs).not_to be_empty + assert_facets_have_values(assigns(:response).aggregations) end it "has docs and facets for existing facet value", :integration do get :index, params: { f: { "format" => 'Book' } } expect(assigns(:response).docs).not_to be_empty + assert_facets_have_values(assigns(:response).aggregations) end @@ -40,6 +42,7 @@ num_per_page = 7 get :index, params: { per_page: num_per_page } expect(assigns(:response).docs).to have(num_per_page).items + assert_facets_have_values(assigns(:response).aggregations) end @@ -48,6 +51,7 @@ get :index, params: { page: page } expect(assigns(:response).docs).not_to be_empty expect(assigns(:response).params[:start].to_i).to eq (page - 1) * controller.blacklight_config[:default_solr_params][:rows] + assert_facets_have_values(assigns(:response).aggregations) end @@ -74,7 +78,7 @@ expect(assigns(:response).docs).to be_empty end - it "has a spelling suggestion for an appropriately poor query", :integration do + it "has a spelling suggestion for an appropriately poor query", :integration, :solr do get :index, params: { q: 'boo' } expect(assigns(:response).spelling.words).not_to be_nil end @@ -102,6 +106,7 @@ it "gets facets when no query", :integration do get :index + assert_facets_have_values(assigns(:response).aggregations) end end @@ -246,6 +251,7 @@ it "redirects to show action for doc id" do put :track, params: { id: doc_id, counter: 3 } + assert_redirected_to(solr_document_path(doc_id)) end @@ -256,16 +262,19 @@ it "redirects to the path given in the redirect param" do put :track, params: { id: doc_id, counter: 3, redirect: '/xyz' } + assert_redirected_to("/xyz") end it "redirects to the path of the uri given in the redirect param" do put :track, params: { id: doc_id, counter: 3, redirect: 'http://localhost:3000/xyz' } + assert_redirected_to("/xyz") end it "keeps querystring on redirect" do put :track, params: { id: doc_id, counter: 3, redirect: 'http://localhost:3000/xyz?locale=pt-BR' } + assert_redirected_to("/xyz?locale=pt-BR") end end @@ -610,7 +619,7 @@ def export_as_mock it "is successful" do get :facet, params: { id: 'format' } expect(response).to be_successful - expect(assigns[:response]).to be_a Blacklight::Solr::Response + expect(assigns[:response]).to respond_to(:documents, :aggregations) expect(assigns[:facet]).to be_a Blacklight::Configuration::FacetField expect(assigns[:display_facet]).to be_a Blacklight::Solr::Response::Facets::FacetField expect(assigns[:pagination]).to be_a Blacklight::Solr::FacetPaginator diff --git a/spec/features/did_you_mean_spec.rb b/spec/features/did_you_mean_spec.rb index 2add8a7505..b03869aa7c 100644 --- a/spec/features/did_you_mean_spec.rb +++ b/spec/features/did_you_mean_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe "Did You Mean" do +RSpec.describe "Did You Mean", :solr do before { visit root_path } describe "searching all fields" do diff --git a/spec/integration/generators/blacklight/solr_generator_spec.rb b/spec/integration/generators/blacklight/solr_generator_spec.rb index b7feaf4836..7df91aa60b 100644 --- a/spec/integration/generators/blacklight/solr_generator_spec.rb +++ b/spec/integration/generators/blacklight/solr_generator_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'generators/blacklight/solr_generator' -RSpec.describe Blacklight::SolrGenerator do +RSpec.describe Blacklight::SolrGenerator, :solr do let(:destination) { Dir.mktmpdir } describe "#solr_wrapper_config" do diff --git a/spec/models/blacklight/search_builder_spec.rb b/spec/models/blacklight/search_builder_spec.rb index 0fb0c0c4eb..df4ca875ff 100644 --- a/spec/models/blacklight/search_builder_spec.rb +++ b/spec/models/blacklight/search_builder_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe Blacklight::SearchBuilder, :api do +RSpec.describe SearchBuilder, :api do subject(:builder) { described_class.new processor_chain, scope } let(:processor_chain) { [] } @@ -8,7 +8,7 @@ let(:scope) { double blacklight_config: blacklight_config, search_state_class: nil } context "with default processor chain" do - subject { described_class.new scope } + subject { Blacklight::SearchBuilder.new scope } it "uses the class-level default_processor_chain" do expect(subject.processor_chain).to eq [] @@ -19,7 +19,7 @@ let(:state_class) { Class.new(Blacklight::SearchState) } let(:scope) { double blacklight_config: blacklight_config, search_state_class: state_class } - it "uses the class-level default_processor_chain" do + it "uses the search_state_class in the scope" do expect(subject.search_state).to be_a state_class end end diff --git a/spec/models/blacklight/solr/repository_spec.rb b/spec/models/blacklight/solr/repository_spec.rb index ad4056efa1..aec19ab8a5 100644 --- a/spec/models/blacklight/solr/repository_spec.rb +++ b/spec/models/blacklight/solr/repository_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe Blacklight::Solr::Repository, :api do +RSpec.describe Blacklight::Solr::Repository, :api, :solr do subject(:repository) do described_class.new blacklight_config end diff --git a/spec/requests/load_suggestions_spec.rb b/spec/requests/load_suggestions_spec.rb index 8a1bd15647..97979bf150 100644 --- a/spec/requests/load_suggestions_spec.rb +++ b/spec/requests/load_suggestions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'GET /catalog/suggest' do +RSpec.describe 'GET /catalog/suggest', :solr do it 'returns suggestions' do get '/catalog/suggest?q=new' expect(response.body).to eq <<-RESULT diff --git a/spec/services/blacklight/search_service_spec.rb b/spec/services/blacklight/search_service_spec.rb index dfae13eaef..f2e09468d3 100644 --- a/spec/services/blacklight/search_service_spec.rb +++ b/spec/services/blacklight/search_service_spec.rb @@ -14,7 +14,7 @@ let(:context) { { whatever: :value } } let(:search_state) { Blacklight::SearchState.new(user_params, blacklight_config) } let(:service) { described_class.new(config: blacklight_config, search_state: search_state, **context) } - let(:repository) { Blacklight::Solr::Repository.new(blacklight_config) } + let(:repository) { Blacklight.repository_class.new(blacklight_config) } let(:user_params) { {} } let(:blacklight_config) { CatalogController.blacklight_config.deep_copy } @@ -62,7 +62,7 @@ end end - describe "for a query returning a grouped response" do + describe "for a query returning a grouped response", :solr do let(:user_params) { { q: all_docs_query } } before do @@ -75,7 +75,7 @@ end end - describe "for a query returning multiple groups", :integration do + describe "for a query returning multiple groups", :integration, :solr do let(:user_params) { { q: all_docs_query } } before do @@ -179,7 +179,7 @@ describe 'Paging', :integration do let(:user_params) { { q: all_docs_query } } - it 'starts with first results by default' do + it 'starts with first results by default', :solr do (solr_response,) = service.search_results expect(solr_response.params[:start].to_i).to eq 0 end @@ -309,7 +309,7 @@ end # SPECS FOR SPELLING SUGGESTIONS VIA SEARCH - describe "Searches should return spelling suggestions", :integration do + describe "Searches should return spelling suggestions", :integration, :solr do context "for just-poor-enough-query term" do let(:user_params) { { q: 'boo' } } @@ -380,7 +380,6 @@ it "returns the previous and next documents for a search" do _response, docs = service.previous_and_next_documents_for_search(4, q: '') - expect(docs.first.id).to eq @full_response.documents[3].id expect(docs.last.id).to eq @full_response.documents[5].id end @@ -410,11 +409,18 @@ end it 'allows the query parameters to be customized using configuration' do - blacklight_config.document_pagination_params[:fl] = 'id,format' + if defined?(RSolr) + blacklight_config.document_pagination_params[:fl] = 'id,format' - _response, docs = service.previous_and_next_documents_for_search(0, q: '') + _response, docs = service.previous_and_next_documents_for_search(0, q: '') - expect(docs.last.to_h).to eq @full_response.documents[1].to_h.slice('id', 'format') + expect(docs.last.to_h).to eq @full_response.documents[1].to_h.slice('id', 'format') + else + blacklight_config.document_pagination_params = { fields: %w[id format], _source: false } + + _response, docs = service.previous_and_next_documents_for_search(0, q: '') + expect(docs.last.to_h).to eq @full_response.documents[1].to_h.slice('id', 'format') + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6bd0dc6cb2..807efed5ed 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,6 +21,7 @@ require 'equivalent-xml' require 'axe-rspec' +require 'elasticsearch' require 'blacklight' Capybara.javascript_driver = :headless_chrome @@ -48,6 +49,9 @@ # When we're testing the API, only run the api tests config.filter_run api: true if ENV['BLACKLIGHT_API_TEST'].present? + # Don't run 'solr' tests when configured for elasticsearch' + config.filter_run_excluding solr: true unless Blacklight.solr? + if Rails.version.to_f >= 7.1 config.fixture_paths = [Rails.root.join("spec/fixtures")] else diff --git a/spec/views/catalog/index.atom.builder_spec.rb b/spec/views/catalog/index.atom.builder_spec.rb index 3909eaee01..6335740090 100644 --- a/spec/views/catalog/index.atom.builder_spec.rb +++ b/spec/views/catalog/index.atom.builder_spec.rb @@ -15,7 +15,7 @@ let(:blacklight_config) { CatalogController.blacklight_config.deep_copy } let(:search_builder) { Blacklight::SearchBuilder.new(view) } - let(:response) { Blacklight::Solr::Response.new({ response: { numFound: 30 } }, search_builder) } + let(:response) { blacklight_config.response_model.new({ response: { numFound: 30 } }, search_builder) } before do allow(view).to receive_messages(action_name: 'index', blacklight_config: blacklight_config) diff --git a/tasks/blacklight.rake b/tasks/blacklight.rake index e78888a758..5242edffc5 100644 --- a/tasks/blacklight.rake +++ b/tasks/blacklight.rake @@ -25,14 +25,15 @@ def with_solr(&block) if ENV['SOLR_ENV'] == 'docker-compose' yield elsif system('docker compose version') + container = ENV['BLACKLIGHT_REPOSITORY'].presence || 'solr' # We're not running `docker compose up' but still want to use a docker instance of solr. begin - puts "Starting Solr" - system_with_error_handling "docker compose up -d solr" + puts "Starting #{container}" + system_with_error_handling "docker compose up -d #{container}" yield ensure - puts "Stopping Solr" - system_with_error_handling "docker compose stop solr" + puts "Stopping #{container}" + system_with_error_handling "docker compose stop #{container}" end else SolrWrapper.wrap do |solr| diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000..c33e42cd61 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,181 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher@^2.4.1": + version "2.5.1" + resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" + integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.1" + "@parcel/watcher-darwin-arm64" "2.5.1" + "@parcel/watcher-darwin-x64" "2.5.1" + "@parcel/watcher-freebsd-x64" "2.5.1" + "@parcel/watcher-linux-arm-glibc" "2.5.1" + "@parcel/watcher-linux-arm-musl" "2.5.1" + "@parcel/watcher-linux-arm64-glibc" "2.5.1" + "@parcel/watcher-linux-arm64-musl" "2.5.1" + "@parcel/watcher-linux-x64-glibc" "2.5.1" + "@parcel/watcher-linux-x64-musl" "2.5.1" + "@parcel/watcher-win32-arm64" "2.5.1" + "@parcel/watcher-win32-ia32" "2.5.1" + "@parcel/watcher-win32-x64" "2.5.1" + +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@rollup/rollup-darwin-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz" + integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== + +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +bootstrap@^5.3.5: + version "5.3.7" + resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz" + integrity sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw== + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +immutable@^5.0.2: + version "5.1.3" + resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz" + integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +rollup-plugin-includepaths@^0.2.4: + version "0.2.4" + resolved "https://registry.npmjs.org/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.4.tgz" + integrity sha512-iZen+XKVExeCzk7jeSZPJKL7B67slZNr8GXSC5ROBXtDGXDBH8wdjMfdNW5hf9kPt+tHyIvWh3wlE9bPrZL24g== + +rollup@^4.24.0: + version "4.24.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz" + integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.24.0" + "@rollup/rollup-android-arm64" "4.24.0" + "@rollup/rollup-darwin-arm64" "4.24.0" + "@rollup/rollup-darwin-x64" "4.24.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.0" + "@rollup/rollup-linux-arm-musleabihf" "4.24.0" + "@rollup/rollup-linux-arm64-gnu" "4.24.0" + "@rollup/rollup-linux-arm64-musl" "4.24.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0" + "@rollup/rollup-linux-riscv64-gnu" "4.24.0" + "@rollup/rollup-linux-s390x-gnu" "4.24.0" + "@rollup/rollup-linux-x64-gnu" "4.24.0" + "@rollup/rollup-linux-x64-musl" "4.24.0" + "@rollup/rollup-win32-arm64-msvc" "4.24.0" + "@rollup/rollup-win32-ia32-msvc" "4.24.0" + "@rollup/rollup-win32-x64-msvc" "4.24.0" + fsevents "~2.3.2" + +sass@^1.80.3: + version "1.89.2" + resolved "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz" + integrity sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + +"source-map-js@>=0.6.2 <2.0.0": + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0"