Skip to content

Commit f06ef50

Browse files
committed
Run on elasticsearch
1 parent 615f36e commit f06ef50

31 files changed

+901
-74
lines changed

.github/workflows/ruby.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,15 @@ jobs:
4141
bootstrap_version: [null]
4242
view_component_version: ["~> 3.12"]
4343
api: [null]
44+
repository: [null]
4445
additional_engine_cart_rails_options: [""]
4546
additional_name: [""]
4647
include:
48+
- ruby: "3.4"
49+
rails_version: "8.0.1"
50+
additional_engine_cart_rails_options: --css=bootstrap
51+
repository: elasticsearch
52+
additional_name: "/ elasticsearch"
4753
- ruby: "3.3"
4854
rails_version: "8.0.1"
4955
additional_engine_cart_rails_options: --css=bootstrap
@@ -76,6 +82,7 @@ jobs:
7682
VIEW_COMPONENT_VERSION: ${{ matrix.view_component_version }}
7783
BOOTSTRAP_VERSION: ${{ matrix.bootstrap_version }}
7884
BLACKLIGHT_API_TEST: ${{ matrix.api }}
85+
BLACKLIGHT_REPOSITORY: ${{ matrix.repository }}
7986
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 }}"
8087
steps:
8188
- uses: actions/checkout@v4

app/services/blacklight/search_service.rb

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -118,35 +118,14 @@ def solr_opensearch_params(field)
118118
solr_params
119119
end
120120

121-
##
122-
# Pagination parameters for selecting the previous and next documents
123-
# out of a result set.
124-
def previous_and_next_document_params(index, window = 1)
125-
solr_params = blacklight_config.document_pagination_params.dup
126-
127-
if solr_params.empty?
128-
solr_params[:fl] = blacklight_config.document_model.unique_key
129-
end
130-
131-
if index > 0
132-
solr_params[:start] = index - window # get one before
133-
solr_params[:rows] = (2 * window) + 1 # and one after
134-
else
135-
solr_params[:start] = 0 # there is no previous doc
136-
solr_params[:rows] = 2 * window # but there should be one after
137-
end
138-
139-
solr_params[:facet] = false
140-
solr_params
141-
end
121+
delegate :previous_and_next_document_params, to: :search_builder
142122

143123
##
144124
# Retrieve a set of documents by id
145125
# @param [Array] ids
146126
# @param [HashWithIndifferentAccess] extra_controller_params
147127
def fetch_many(ids, extra_controller_params)
148128
extra_controller_params ||= {}
149-
150129
query = search_builder
151130
.with(search_state)
152131
.where(blacklight_config.document_model.unique_key => ids)

blacklight.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Gem::Specification.new do |s|
3535
s.add_dependency "zeitwerk"
3636

3737
s.add_development_dependency "rsolr", ">= 1.0.6", "< 3" # Library for interacting with rSolr.
38+
s.add_development_dependency "elasticsearch"
3839
s.add_development_dependency "rspec-rails", "~> 7.0"
3940
s.add_development_dependency "rspec-collection_matchers", ">= 1.0"
4041
s.add_development_dependency 'axe-core-rspec'

compose.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ services:
3636
- /opt/solr/conf
3737
- "-Xms256m"
3838
- "-Xmx512m"
39+
40+
elasticsearch:
41+
environment:
42+
- discovery.type=single-node
43+
- xpack.security.enabled=false
44+
image: elasticsearch:8.17.3
45+
ports:
46+
- 9200:9200
47+
volumes:
48+
- es_data:/usr/share/elasticsearch/data
49+
volumes:
50+
es_data:

lib/blacklight.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ def self.default_index=(repository)
2727
##
2828
# The configured repository class. By convention, this is
2929
# the class Blacklight::(name of the adapter)::Repository, e.g.
30-
# elastic_search => Blacklight::ElasticSearch::Repository
30+
# elasticsearch => Blacklight::ElasticSearch::Repository
3131
def self.repository_class
3232
case connection_config[:adapter]
3333
when 'solr'
3434
Blacklight::Solr::Repository
35+
when 'elasticsearch'
36+
Blacklight::Elasticsearch::Repository
3537
when /::/
3638
connection_config[:adapter].constantize
3739
else
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module Blacklight::Elasticsearch
4+
class Repository < Blacklight::AbstractRepository
5+
##
6+
# Find a single solr document result (by id) using the document configuration
7+
# @param [String] id document's unique key value
8+
# @param [Hash] params additional solr query parameters
9+
def find id, params = {}
10+
api_response = connection.get(index:, id:)
11+
blacklight_config.response_model.new(api_response, params, document_model: blacklight_config.document_model, blacklight_config: blacklight_config)
12+
rescue Elastic::Transport::Transport::Errors::NotFound
13+
# solr_response = blacklight_config.response_model.new(res, solr_params, document_model: blacklight_config.document_model, blacklight_config: blacklight_config)
14+
15+
# doc_params = params.reverse_merge(blacklight_config.default_document_solr_params)
16+
# .reverse_merge(qt: blacklight_config.document_solr_request_handler)
17+
# .merge(blacklight_config.document_unique_id_param => id)
18+
19+
# solr_response = send_and_receive blacklight_config.document_solr_path || blacklight_config.solr_path, doc_params
20+
raise Blacklight::Exceptions::RecordNotFound
21+
end
22+
23+
# Find multiple documents by their ids
24+
# @param [Hash] _params query parameters
25+
def find_many(search_builder)
26+
# TODO: This is hacky, but SearchBuilder#where is currently very coupled to Solr
27+
ids = search_builder.search_state.params['q']['id']
28+
docs = ids.map { |id| { _index: index, _id: id } }
29+
30+
api_response = connection.mget(body: { docs: })
31+
blacklight_config.response_model.new(api_response, search_builder, document_model: blacklight_config.document_model, blacklight_config: blacklight_config)
32+
end
33+
34+
##
35+
# Execute a search query against solr
36+
# @param [Hash] params solr query parameters
37+
# @param [String] path solr request handler path
38+
def search path: nil, params: nil, **kwargs
39+
request_params = params.reverse_merge(kwargs)
40+
api_response = connection.search(index:, body: request_params)
41+
blacklight_config.response_model.new(api_response, params, document_model: blacklight_config.document_model, blacklight_config: blacklight_config)
42+
# request_params = (params || pos_params).reverse_merge(kwargs).reverse_merge({ qt: blacklight_config.qt })
43+
44+
# send_and_receive(path || default_search_path(request_params), request_params)
45+
end
46+
47+
def seed_index docs
48+
body = docs.map { |data| { index: { _index: index, _id: data['id'], data: data.except('id') } } }
49+
connection.bulk(body:)
50+
connection.indices.refresh(index:)
51+
end
52+
53+
def suggestions
54+
[]
55+
end
56+
57+
private
58+
59+
def index
60+
@index ||= connection_config.fetch(:index)
61+
end
62+
63+
# See https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/current/connecting.html
64+
def build_connection
65+
::Elasticsearch::Client.new(url: connection_config[:url])
66+
# cloud_id: '<CloudID>',
67+
# user: '<Username>',
68+
# password: '<Password>'
69+
end
70+
end
71+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
# class Blacklight::Elasticsearch::InvalidParameter < ArgumentError; end
4+
5+
class Blacklight::Elasticsearch::Request < ActiveSupport::HashWithIndifferentAccess
6+
# This is similar to qf in Solr
7+
cattr_accessor :query_fields, default: %w[ id
8+
full_title_tsim
9+
short_title_tsim
10+
alternative_title_tsim
11+
active_fedora_model_ssi
12+
title_tsim
13+
author_tsim
14+
subject_tsim
15+
all_text_timv]
16+
def initialize(constructor = {})
17+
if constructor.is_a?(Hash)
18+
super()
19+
update(constructor)
20+
else
21+
super
22+
end
23+
end
24+
25+
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-all-query.html
26+
def match_all
27+
must['match_all'] = {}
28+
end
29+
30+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
31+
def append_filter_query(filter_query)
32+
bool['filter'] = filter_query
33+
end
34+
35+
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-combined-fields-query.html
36+
def append_query(query)
37+
return if query.nil?
38+
39+
must['combined_fields'] = {
40+
'query' => query,
41+
'fields' => query_fields,
42+
'operator' => 'or'
43+
}
44+
end
45+
46+
private
47+
48+
def bool
49+
self['query'] ||= {}
50+
self['query']['bool'] ||= {}
51+
end
52+
53+
def must
54+
bool['must'] ||= {}
55+
end
56+
end
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# frozen_string_literal: true
2+
3+
class Blacklight::Elasticsearch::Response < ActiveSupport::HashWithIndifferentAccess
4+
include Blacklight::Response::PaginationMethods
5+
6+
attr_reader :request_params, :search_builder
7+
attr_accessor :blacklight_config, :options
8+
9+
delegate :document_factory, to: :blacklight_config
10+
11+
# @param [Elasticsearch::API::Response] response
12+
def initialize(api_response, request_params, options = {})
13+
@search_builder = request_params if request_params.is_a?(Blacklight::SearchBuilder)
14+
15+
super(ActiveSupport::HashWithIndifferentAccess.new(api_response))
16+
@request_params = ActiveSupport::HashWithIndifferentAccess.new(request_params)
17+
self.blacklight_config = options[:blacklight_config]
18+
self.options = options
19+
end
20+
21+
def total
22+
hits[:total][:value]
23+
end
24+
25+
def start
26+
0
27+
end
28+
29+
# def header
30+
# self['responseHeader'] || {}
31+
# end
32+
33+
def documents
34+
@documents ||= if self[:_source] # handle a get call
35+
[document_factory.build(self[:_source].merge(id: self[:_id]), self, options)]
36+
elsif self[:docs] # handle mget call
37+
self[:docs].collect { |doc| document_factory.build(doc[:_source].merge(id: doc[:_id]), self, options) }
38+
else # Search call
39+
dig(:hits, :hits).collect { |doc| document_factory.build(doc[:_source].merge(id: doc[:_id]), self, options) }
40+
end
41+
end
42+
alias docs documents
43+
44+
# def grouped
45+
# @groups ||= self["grouped"].map do |field, group|
46+
# # grouped responses can either be grouped by:
47+
# # - field, where this key is the field name, and there will be a list
48+
# # of documents grouped by field value, or:
49+
# # - function, where the key is the function, and the documents will be
50+
# # further grouped by function value, or:
51+
# # - query, where the key is the query, and the matching documents will be
52+
# # in the doclist on THIS object
53+
# if group["groups"] # field or function
54+
# GroupResponse.new field, group, self
55+
# else # query
56+
# Group.new field, group, self
57+
# end
58+
# end
59+
# end
60+
61+
# def group key
62+
# grouped.find { |x| x.key == key }
63+
# end
64+
65+
def aggregations
66+
# @aggregations ||= default_aggregations.merge(facet_field_aggregations).merge(facet_query_aggregations).merge(facet_pivot_aggregations).merge(json_facet_aggregations)
67+
{}
68+
end
69+
70+
def grouped?
71+
false
72+
end
73+
74+
def spelling
75+
nil
76+
end
77+
78+
def more_like _document
79+
[]
80+
end
81+
82+
# TODO: Same implementation as solr, move to mixin?
83+
def export_formats
84+
documents.map { |x| x.export_formats.keys }.flatten.uniq
85+
end
86+
87+
def rows
88+
search_builder&.rows || hits[:hits].length
89+
end
90+
91+
private
92+
93+
def hits
94+
self[:hits]
95+
end
96+
end

0 commit comments

Comments
 (0)