Skip to content

Extract FilterQueryBuilder to its own class #3599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/blacklight/search_state/pivot_filter_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@ def initialize(value: nil, fq: {}, **_args) # rubocop:disable Naming/MethodParam
end
end

class QueryBuilder
class QueryBuilder < Solr::AbstractFilterQueryBuilder
# @return [Array] filter_query, subqueries
def self.call(search_builder, filter, solr_parameters)
def call(filter, solr_parameters)
existing = solr_parameters['fq']&.dup || []
queries = []
filter.values.compact_blank.each do |value|
queries << search_builder.send(:facet_value_to_fq_string, filter.pivot.first, value.value)
queries << facet_value_to_fq_string(filter.pivot.first, value.value)
value.fq.each do |entry|
k, v = entry
queries << search_builder.send(:facet_value_to_fq_string, k, v) if v
queries << facet_value_to_fq_string(k, v) if v
end
queries.uniq!
end
Expand Down
77 changes: 77 additions & 0 deletions lib/blacklight/solr/abstract_filter_query_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module Blacklight::Solr
class AbstractFilterQueryBuilder
def initialize(blacklight_config:)
@blacklight_config = blacklight_config
end

attr_reader :blacklight_config

private

def facet_inclusive_value_to_fq_string(facet_field, values)
return if values.blank?

return facet_value_to_fq_string(facet_field, values.first) if values.length == 1

facet_config = blacklight_config.facet_fields[facet_field]

local_params = []
local_params << "tag=#{facet_config.tag}" if facet_config&.tag

solr_filters = values.each_with_object({}).with_index do |(v, h), index|
h["f_inclusive.#{facet_field}.#{index}"] = facet_value_to_fq_string(facet_field, v, use_local_params: false)
end

filter_query = solr_filters.keys.map do |k|
"{!query v=$#{k}}"
end.join(' OR ')

["{!lucene#{" #{local_params.join(' ')}" unless local_params.empty?}}#{filter_query}", solr_filters]
end

##
# Convert a facet/value pair into a solr fq parameter
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def facet_value_to_fq_string(facet_field, value, use_local_params: true)
facet_config = blacklight_config.facet_fields[facet_field]

solr_field = facet_config.field if facet_config && !facet_config.query
solr_field ||= facet_field

local_params = []
local_params << "tag=#{facet_config.tag}" if use_local_params && facet_config&.tag

if facet_config&.query
if facet_config.query[value]
facet_config.query[value][:fq]
else
# exclude all documents if the custom facet key specified was not found
'-*:*'
end
elsif value.is_a?(Range)
prefix = "{!#{local_params.join(' ')}}" unless local_params.empty?
start = value.begin || '*'
finish = value.end || '*'
"#{prefix}#{solr_field}:[#{start} TO #{finish}]"
elsif value == Blacklight::SearchState::FilterField::MISSING
"-#{solr_field}:[* TO *]"
else
"{!term f=#{solr_field}#{" #{local_params.join(' ')}" unless local_params.empty?}}#{convert_to_term_value(value)}"
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

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
end
end
20 changes: 20 additions & 0 deletions lib/blacklight/solr/default_filter_query_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Blacklight::Solr
class DefaultFilterQueryBuilder < AbstractFilterQueryBuilder
def call(filter, _solr_parameters)
filter_queries = []
all_subqueries = {}
filter.values.compact_blank.each do |value|
filter_query, subqueries = if value.is_a?(Array)
facet_inclusive_value_to_fq_string(filter.key, value.compact_blank)
else
facet_value_to_fq_string(filter.config.key, value)
end
filter_queries << filter_query
all_subqueries.merge!(subqueries) if subqueries
end
[filter_queries, all_subqueries]
end
end
end
92 changes: 11 additions & 81 deletions lib/blacklight/solr/search_builder_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,25 +128,19 @@ def add_facet_fq_to_solr(solr_parameters)
end

search_state.filters.each do |filter|
if filter.config.filter_query_builder
filter_query, subqueries = filter.config.filter_query_builder.call(self, filter, solr_parameters)

Array(filter_query).each do |fq|
solr_parameters.append_filter_query(fq)
end
solr_parameters.merge!(subqueries) if subqueries
filter_query_builder_class_or_proc = filter.config.filter_query_builder || DefaultFilterQueryBuilder
if filter_query_builder_class_or_proc.is_a?(Class)
filter_query_builder = filter_query_builder_class_or_proc.new(blacklight_config: blacklight_config)
filter_query, subqueries = filter_query_builder.call(filter, solr_parameters)
else
filter.values.compact_blank.each do |value|
filter_query, subqueries = if value.is_a?(Array)
facet_inclusive_value_to_fq_string(filter.key, value.compact_blank)
else
facet_value_to_fq_string(filter.config.key, value)
end

solr_parameters.append_filter_query filter_query
solr_parameters.merge!(subqueries) if subqueries
end
# TODO: Maybe deprecate proc?
filter_query, subqueries = filter_query_builder_class_or_proc.call(self, filter, solr_parameters)
end

Array(filter_query).each do |fq|
solr_parameters.append_filter_query(fq)
end
solr_parameters.merge!(subqueries) if subqueries
end
end

Expand Down Expand Up @@ -338,70 +332,6 @@ def solr_param_quote(val, options = {})

private

##
# Convert a facet/value pair into a solr fq parameter
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def facet_value_to_fq_string(facet_field, value, use_local_params: true)
facet_config = blacklight_config.facet_fields[facet_field]

solr_field = facet_config.field if facet_config && !facet_config.query
solr_field ||= facet_field

local_params = []
local_params << "tag=#{facet_config.tag}" if use_local_params && facet_config&.tag

if facet_config&.query
if facet_config.query[value]
facet_config.query[value][:fq]
else
# exclude all documents if the custom facet key specified was not found
'-*:*'
end
elsif value.is_a?(Range)
prefix = "{!#{local_params.join(' ')}}" unless local_params.empty?
start = value.begin || '*'
finish = value.end || '*'
"#{prefix}#{solr_field}:[#{start} TO #{finish}]"
elsif value == Blacklight::SearchState::FilterField::MISSING
"-#{solr_field}:[* TO *]"
else
"{!term f=#{solr_field}#{" #{local_params.join(' ')}" unless local_params.empty?}}#{convert_to_term_value(value)}"
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

def facet_inclusive_value_to_fq_string(facet_field, values)
return if values.blank?

return facet_value_to_fq_string(facet_field, values.first) if values.length == 1

facet_config = blacklight_config.facet_fields[facet_field]

local_params = []
local_params << "tag=#{facet_config.tag}" if facet_config&.tag

solr_filters = values.each_with_object({}).with_index do |(v, h), index|
h["f_inclusive.#{facet_field}.#{index}"] = facet_value_to_fq_string(facet_field, v, use_local_params: false)
end

filter_query = solr_filters.keys.map do |k|
"{!query v=$#{k}}"
end.join(' OR ')

["{!lucene#{" #{local_params.join(' ')}" unless local_params.empty?}}#{filter_query}", solr_filters]
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

##
# The key to use to retrieve the grouped field to display
def grouped_key_for_results
Expand Down
72 changes: 72 additions & 0 deletions spec/models/blacklight/solr/default_filter_query_builder_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

RSpec.describe Blacklight::Solr::DefaultFilterQueryBuilder do
subject { described_class.new(blacklight_config: blacklight_config) }

let(:blacklight_config) { CatalogController.blacklight_config.deep_copy }

describe "#facet_value_to_fq_string" do
it "uses the configured field name" do
blacklight_config.add_facet_field :facet_key, field: "facet_name"
expect(subject.send(:facet_value_to_fq_string, "facet_key", "my value")).to eq "{!term f=facet_name}my value"
end

it "uses the raw handler for strings" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "my value")).to eq "{!term f=facet_name}my value"
end

it "passes booleans through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", true)).to eq '{!term f=facet_name}true'
end

it "passes boolean-like strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "true")).to eq '{!term f=facet_name}true'
end

it "passes integers through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1)).to eq '{!term f=facet_name}1'
end

it "passes integer-like strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "1")).to eq '{!term f=facet_name}1'
expect(subject.send(:facet_value_to_fq_string, "facet_name", -1)).to eq '{!term f=facet_name}-1'
end

it "passes floats through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1.11)).to eq '{!term f=facet_name}1.11'
end

it "passes floats in strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "1.11")).to eq '{!term f=facet_name}1.11'
end

context 'date handling' do
before { allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(date: true, query: nil, tag: nil, field: 'facet_name')) }

it "passes date-type fields through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "2012-01-01")).to eq '{!term f=facet_name}2012-01-01'
expect(subject.send(:facet_value_to_fq_string, "facet_name", "2003-04-09T00:00:00Z")).to eq '{!term f=facet_name}2003-04-09T00:00:00Z'
end

it "formats Date objects correctly" do
allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(date: nil, query: nil, tag: nil, field: 'facet_name'))
d = DateTime.parse("2003-04-09T00:00:00")
expect(subject.send(:facet_value_to_fq_string, "facet_name", d)).to eq '{!term f=facet_name}2003-04-09T00:00:00Z'
end
end

it "handles range requests" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1..5)).to eq "facet_name:[1 TO 5]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1..nil)).to eq "facet_name:[1 TO *]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", nil..5)).to eq "facet_name:[* TO 5]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", nil..nil)).to eq "facet_name:[* TO *]"
end

it "adds tag local parameters" do
allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(query: nil, tag: 'asdf', date: nil, field: 'facet_name'))

expect(subject.send(:facet_value_to_fq_string, "facet_name", true)).to eq "{!term f=facet_name tag=asdf}true"
expect(subject.send(:facet_value_to_fq_string, "facet_name", "my value")).to eq "{!term f=facet_name tag=asdf}my value"
end
end
end
65 changes: 0 additions & 65 deletions spec/models/blacklight/solr/search_builder_behavior_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -523,71 +523,6 @@
end
end

describe "#facet_value_to_fq_string" do
it "uses the configured field name" do
blacklight_config.add_facet_field :facet_key, field: "facet_name"
expect(subject.send(:facet_value_to_fq_string, "facet_key", "my value")).to eq "{!term f=facet_name}my value"
end

it "uses the raw handler for strings" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "my value")).to eq "{!term f=facet_name}my value"
end

it "passes booleans through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", true)).to eq '{!term f=facet_name}true'
end

it "passes boolean-like strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "true")).to eq '{!term f=facet_name}true'
end

it "passes integers through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1)).to eq '{!term f=facet_name}1'
end

it "passes integer-like strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "1")).to eq '{!term f=facet_name}1'
expect(subject.send(:facet_value_to_fq_string, "facet_name", -1)).to eq '{!term f=facet_name}-1'
end

it "passes floats through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1.11)).to eq '{!term f=facet_name}1.11'
end

it "passes floats in strings through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "1.11")).to eq '{!term f=facet_name}1.11'
end

context 'date handling' do
before { allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(date: true, query: nil, tag: nil, field: 'facet_name')) }

it "passes date-type fields through" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", "2012-01-01")).to eq '{!term f=facet_name}2012-01-01'
expect(subject.send(:facet_value_to_fq_string, "facet_name", "2003-04-09T00:00:00Z")).to eq '{!term f=facet_name}2003-04-09T00:00:00Z'
end

it "formats Date objects correctly" do
allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(date: nil, query: nil, tag: nil, field: 'facet_name'))
d = DateTime.parse("2003-04-09T00:00:00")
expect(subject.send(:facet_value_to_fq_string, "facet_name", d)).to eq '{!term f=facet_name}2003-04-09T00:00:00Z'
end
end

it "handles range requests" do
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1..5)).to eq "facet_name:[1 TO 5]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", 1..nil)).to eq "facet_name:[1 TO *]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", nil..5)).to eq "facet_name:[* TO 5]"
expect(subject.send(:facet_value_to_fq_string, "facet_name", nil..nil)).to eq "facet_name:[* TO *]"
end

it "adds tag local parameters" do
allow(blacklight_config.facet_fields).to receive(:[]).with('facet_name').and_return(double(query: nil, tag: 'asdf', date: nil, field: 'facet_name'))

expect(subject.send(:facet_value_to_fq_string, "facet_name", true)).to eq "{!term f=facet_name tag=asdf}true"
expect(subject.send(:facet_value_to_fq_string, "facet_name", "my value")).to eq "{!term f=facet_name tag=asdf}my value"
end
end

describe "#add_facet_fq_to_solr" do
it "converts a String fq into an Array" do
solr_parameters = { fq: 'a string' }
Expand Down