ansr_blacklight 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/Gemfile +3 -0
- data/README.md +37 -0
- data/ansr_blacklight.gemspec +28 -0
- data/lib/ansr_blacklight.rb +57 -0
- data/lib/ansr_blacklight/arel.rb +6 -0
- data/lib/ansr_blacklight/arel/big_table.rb +57 -0
- data/lib/ansr_blacklight/arel/visitors.rb +6 -0
- data/lib/ansr_blacklight/arel/visitors/query_builder.rb +217 -0
- data/lib/ansr_blacklight/arel/visitors/to_no_sql.rb +14 -0
- data/lib/ansr_blacklight/base.rb +21 -0
- data/lib/ansr_blacklight/connection_adapters/no_sql_adapter.rb +38 -0
- data/lib/ansr_blacklight/model/querying.rb +29 -0
- data/lib/ansr_blacklight/relation.rb +50 -0
- data/lib/ansr_blacklight/relation/solr_projection_methods.rb +55 -0
- data/lib/ansr_blacklight/request_builders.rb +141 -0
- data/lib/ansr_blacklight/solr.rb +4 -0
- data/lib/ansr_blacklight/solr/request.rb +46 -0
- data/lib/ansr_blacklight/solr/response.rb +94 -0
- data/lib/ansr_blacklight/solr/response/group.rb +32 -0
- data/lib/ansr_blacklight/solr/response/group_response.rb +50 -0
- data/lib/ansr_blacklight/solr/response/more_like_this.rb +14 -0
- data/lib/ansr_blacklight/solr/response/pagination_methods.rb +35 -0
- data/lib/ansr_blacklight/solr/response/spelling.rb +92 -0
- data/spec/fixtures/config.yml +0 -0
- data/spec/lib/loaded_relation_spec.rb +223 -0
- data/spec/lib/queryable_relation_spec.rb +133 -0
- data/spec/lib/relation/faceting_spec.rb +475 -0
- data/spec/lib/relation/grouping_spec.rb +159 -0
- data/spec/spec_helper.rb +72 -0
- metadata +225 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ansr::Blacklight
|
2
|
+
class Base < Ansr::Base
|
3
|
+
include Ansr::Blacklight::Model::Querying
|
4
|
+
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
def self.solr_search_params_logic
|
8
|
+
@solr_search_params_logic || []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.solr_search_params_logic=(vals)
|
12
|
+
@solr_search_params_logic=vals
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.build_default_scope
|
16
|
+
rel = super
|
17
|
+
solr_search_params_logic.each {|method| rel = self.send(method, rel)}
|
18
|
+
rel
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Ansr::Blacklight::ConnectionAdapters
|
2
|
+
class NoSqlAdapter < Ansr::ConnectionAdapters::NoSqlAdapter
|
3
|
+
|
4
|
+
def self.connection_for(klass)
|
5
|
+
Ansr::Blacklight.solr
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(klass, logger = nil, pool = nil) #:nodoc:
|
9
|
+
super(klass, klass.solr, logger, pool)
|
10
|
+
# the RSolr class has one query method, with the name of the selector the first parm?
|
11
|
+
@method = :send_and_receive
|
12
|
+
@visitor = Ansr::Blacklight::Arel::Visitors::ToNoSql.new(@table)
|
13
|
+
end
|
14
|
+
|
15
|
+
# RSolr
|
16
|
+
def raw_connection
|
17
|
+
@connection
|
18
|
+
end
|
19
|
+
|
20
|
+
def adapter_name
|
21
|
+
'Solr'
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_sql(*args)
|
25
|
+
to_nosql(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute(query, name='ANSR-SOLR')
|
29
|
+
query = query.dup
|
30
|
+
# TODO: execution context to assign :post to params[:method]
|
31
|
+
params = {params: query, method: :get}
|
32
|
+
params[:data] = params.delete(:params) if params[:method] == :post
|
33
|
+
raw_response = eval(@connection.send(@method, query.path, params))
|
34
|
+
Ansr::Blacklight::Solr::Response.new(raw_response, raw_response['params'])
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ansr::Blacklight::Model
|
2
|
+
module Querying
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def solr
|
8
|
+
Ansr::Blacklight.solr
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_default_scope
|
12
|
+
rel = Ansr::Blacklight::Relation.new(model(), table())
|
13
|
+
rel
|
14
|
+
end
|
15
|
+
|
16
|
+
def unique_key
|
17
|
+
table().unique_key
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_connection_handler
|
21
|
+
@default_connection_handler ||= Ansr::Model::ConnectionHandler.new(Ansr::Blacklight::ConnectionAdapters::NoSqlAdapter)
|
22
|
+
end
|
23
|
+
|
24
|
+
def references
|
25
|
+
[]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Ansr::Blacklight
|
2
|
+
class Relation < Ansr::Relation
|
3
|
+
include Ansr::Blacklight::SolrProjectionMethods
|
4
|
+
delegate :blacklight_config, to: :model
|
5
|
+
|
6
|
+
# some compatibility aliases that should go away to properly genericize
|
7
|
+
alias :facets :filters
|
8
|
+
|
9
|
+
delegate :docs, to: :response
|
10
|
+
delegate :params, to: :response
|
11
|
+
|
12
|
+
# overrides for query response handling
|
13
|
+
def docs_from(response)
|
14
|
+
response.docs
|
15
|
+
end
|
16
|
+
|
17
|
+
def filters_from(response)
|
18
|
+
response.facets
|
19
|
+
end
|
20
|
+
|
21
|
+
def count
|
22
|
+
response.total
|
23
|
+
end
|
24
|
+
|
25
|
+
# overrides for weird Blacklight expectations
|
26
|
+
def max_pages
|
27
|
+
if Kaminari.config.respond_to? :max_pages
|
28
|
+
nil
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def limit_value
|
35
|
+
(super || default_limit_value) + 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_arel
|
39
|
+
arel = super
|
40
|
+
solr_props = {}
|
41
|
+
solr_props[:defType] = defType_value if defType_value
|
42
|
+
solr_props[:wt] = wt_value if wt_value
|
43
|
+
unless solr_props.empty?
|
44
|
+
prop_node = Ansr::Arel::Nodes::ProjectionTraits.new arel.grouping(arel.projections), solr_props
|
45
|
+
arel.projections = [prop_node]
|
46
|
+
end
|
47
|
+
arel
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ansr::Blacklight
|
2
|
+
module SolrProjectionMethods
|
3
|
+
def defType_value
|
4
|
+
@values[:defType]
|
5
|
+
end
|
6
|
+
|
7
|
+
def defType_value=(value)
|
8
|
+
raise ImmutableRelation if @loaded
|
9
|
+
@values[:defType] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def defType(value)
|
13
|
+
spawn.defType!(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def defType!(value)
|
17
|
+
self.defType_value= value
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def defType_unscoping
|
22
|
+
end
|
23
|
+
|
24
|
+
def wt_value
|
25
|
+
@values[:wt]
|
26
|
+
end
|
27
|
+
|
28
|
+
def wt_value=(value)
|
29
|
+
raise ImmutableRelation if @loaded
|
30
|
+
@values[:wt] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def wt(value)
|
34
|
+
spawn.wt!(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
def wt!(value)
|
38
|
+
self.wt_value= (value)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def wt_unscoping
|
43
|
+
end
|
44
|
+
|
45
|
+
# omitHeader
|
46
|
+
|
47
|
+
# timeAllowed
|
48
|
+
|
49
|
+
# debug (true, :timing, :query, :results)
|
50
|
+
|
51
|
+
# explainOther
|
52
|
+
|
53
|
+
# debug.explain.structured
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Ansr::Blacklight
|
2
|
+
##
|
3
|
+
# This module contains methods that transform user parameters into parameters that are sent
|
4
|
+
# as a request to Solr when RequestBuilders#solr_search_params is called.
|
5
|
+
#
|
6
|
+
module RequestBuilders
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def local_field_params(facet_field)
|
10
|
+
cf = table[facet_field]
|
11
|
+
if (cf.is_a? Ansr::Arel::ConfiguredField)
|
12
|
+
return cf.config.fetch(:local, {})
|
13
|
+
else
|
14
|
+
return {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
# A helper method used for generating solr LocalParams, put quotes
|
18
|
+
# around the term unless it's a bare-word. Escape internal quotes
|
19
|
+
# if needed.
|
20
|
+
def solr_param_quote(val, options = {})
|
21
|
+
options[:quote] ||= '"'
|
22
|
+
unless val =~ /^[a-zA-Z0-9$_\-\^]+$/
|
23
|
+
val = options[:quote] +
|
24
|
+
# Yes, we need crazy escaping here, to deal with regexp esc too!
|
25
|
+
val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") +
|
26
|
+
options[:quote]
|
27
|
+
end
|
28
|
+
return val
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Take the user-entered query, and put it in the solr params,
|
33
|
+
# including config's "search field" params for current search field.
|
34
|
+
# also include setting spellcheck.q.
|
35
|
+
def add_query_to_solr(field_key, value, opts={})
|
36
|
+
###
|
37
|
+
# Merge in search field configured values, if present, over-writing general
|
38
|
+
# defaults
|
39
|
+
###
|
40
|
+
|
41
|
+
if (::Arel::Nodes::As === field_key)
|
42
|
+
solr_request[:qt] = field_key.right.to_s
|
43
|
+
field_key = field_key.left
|
44
|
+
end
|
45
|
+
|
46
|
+
search_field = table[field_key]
|
47
|
+
##
|
48
|
+
# Create Solr 'q' including the user-entered q, prefixed by any
|
49
|
+
# solr LocalParams in config, using solr LocalParams syntax.
|
50
|
+
# http://wiki.apache.org/solr/LocalParams
|
51
|
+
##
|
52
|
+
if (Ansr::Arel::ConfiguredField === search_field && !search_field.config.empty?)
|
53
|
+
local_params = search_field.config.fetch(:local,{}).merge(opts).collect do |key, val|
|
54
|
+
key.to_s + "=" + solr_param_quote(val, :quote => "'")
|
55
|
+
end.join(" ")
|
56
|
+
solr_request[:q] = local_params.empty? ? value : "{!#{local_params}}#{value}"
|
57
|
+
search_field.config.fetch(:query,{}).each do |k,v|
|
58
|
+
solr_request[k] = v
|
59
|
+
end
|
60
|
+
else
|
61
|
+
solr_request[:q] = value if value
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Set Solr spellcheck.q to be original user-entered query, without
|
66
|
+
# our local params, otherwise it'll try and spellcheck the local
|
67
|
+
# params! Unless spellcheck.q has already been set by someone,
|
68
|
+
# respect that.
|
69
|
+
#
|
70
|
+
# TODO: Change calling code to expect this as a symbol instead of
|
71
|
+
# a string, for consistency? :'spellcheck.q' is a symbol. Right now
|
72
|
+
# rspec tests for a string, and can't tell if other code may
|
73
|
+
# insist on a string.
|
74
|
+
solr_request["spellcheck.q"] = value unless solr_request["spellcheck.q"]
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Add any existing facet limits, stored in app-level HTTP query
|
79
|
+
# as :f, to solr as appropriate :fq query.
|
80
|
+
def add_filter_fq_to_solr(solr_request, user_params)
|
81
|
+
|
82
|
+
# convert a String value into an Array
|
83
|
+
if solr_request[:fq].is_a? String
|
84
|
+
solr_request[:fq] = [solr_request[:fq]]
|
85
|
+
end
|
86
|
+
|
87
|
+
# :fq, map from :f.
|
88
|
+
if ( user_params[:f])
|
89
|
+
f_request_params = user_params[:f]
|
90
|
+
|
91
|
+
f_request_params.each_pair do |facet_field, value_list|
|
92
|
+
opts = local_field_params(facet_field).merge(user_params.fetch(:opts,{}))
|
93
|
+
Array(value_list).each do |value|
|
94
|
+
solr_request.append_filter_query filter_value_to_fq_string(facet_field, value, user_params[:opts])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def with_ex_local_param(ex, value)
|
101
|
+
if ex
|
102
|
+
"{!ex=#{ex}}#{value}"
|
103
|
+
else
|
104
|
+
value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
##
|
111
|
+
# Convert a filter/value pair into a solr fq parameter
|
112
|
+
def filter_value_to_fq_string(facet_key, value, facet_opts=nil)
|
113
|
+
facet_field = table[facet_key]
|
114
|
+
facet_config = (Ansr::Arel::ConfiguredField === facet_field)
|
115
|
+
facet_default = (::Arel.star == facet_key)
|
116
|
+
local_params = local_field_params(facet_key)
|
117
|
+
local_params.merge!(facet_opts) if facet_opts
|
118
|
+
local_params = local_params.collect {|k,v| "#{k.to_s}=#{v.to_s}"}
|
119
|
+
|
120
|
+
prefix = ""
|
121
|
+
prefix = "{!#{local_params.join(" ")}}" unless local_params.empty?
|
122
|
+
|
123
|
+
fq = case
|
124
|
+
when facet_default
|
125
|
+
""
|
126
|
+
when (facet_config and facet_field.date),
|
127
|
+
(value.is_a?(TrueClass) or value.is_a?(FalseClass) or value == 'true' or value == 'false'),
|
128
|
+
(value.is_a?(Integer) or (value.to_i.to_s == value if value.respond_to? :to_i)),
|
129
|
+
(value.is_a?(Float) or (value.to_f.to_s == value if value.respond_to? :to_f))
|
130
|
+
(value.is_a?(DateTime) or value.is_a?(Date) or value.is_a?(Time))
|
131
|
+
"#{prefix}#{facet_field.name}:#{value}"
|
132
|
+
when value.is_a?(Range)
|
133
|
+
"#{prefix}#{facet_field.name}:[#{value.first} TO #{value.last}]"
|
134
|
+
else
|
135
|
+
"{!raw f=#{facet_field.name}#{(" " + local_params.join(" ")) unless local_params.empty?}}#{value}"
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Ansr::Blacklight::Solr
|
2
|
+
class Request < ::HashWithIndifferentAccess
|
3
|
+
attr_accessor :path
|
4
|
+
|
5
|
+
SINGULAR_KEYS = %W{ facet fl q qt rows start spellcheck spellcheck.q sort
|
6
|
+
per_page wt hl group defType}
|
7
|
+
ARRAY_KEYS = %W{facet.field facet.query facet.pivot fq hl.fl }
|
8
|
+
|
9
|
+
def initialize(constructor = {})
|
10
|
+
if constructor.is_a?(Hash)
|
11
|
+
super()
|
12
|
+
update(constructor)
|
13
|
+
else
|
14
|
+
super(constructor)
|
15
|
+
end
|
16
|
+
ARRAY_KEYS.each do |key|
|
17
|
+
self[key] ||= []
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def append_filter_query(query)
|
22
|
+
self['fq'] << query
|
23
|
+
end
|
24
|
+
|
25
|
+
def append_facet_fields(values)
|
26
|
+
(self['facet.field'] += Array(values)).uniq!
|
27
|
+
self['facet'] = true unless values.blank?
|
28
|
+
end
|
29
|
+
|
30
|
+
def append_facet_query(values)
|
31
|
+
self['facet.query'] += Array(values)
|
32
|
+
end
|
33
|
+
|
34
|
+
def append_facet_pivot(query)
|
35
|
+
self['facet.pivot'] << query
|
36
|
+
end
|
37
|
+
|
38
|
+
def append_highlight_field(query)
|
39
|
+
self['hl.fl'] << query
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_hash
|
43
|
+
reject {|key, value| ARRAY_KEYS.include?(key) && value.blank?}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
## copied directly from Blacklight::SolrResponse
|
2
|
+
class Ansr::Blacklight::Solr::Response < HashWithIndifferentAccess
|
3
|
+
|
4
|
+
require 'ansr_blacklight/solr/response/pagination_methods'
|
5
|
+
|
6
|
+
autoload :Spelling, 'ansr_blacklight/solr/response/spelling'
|
7
|
+
autoload :MoreLikeThis, 'ansr_blacklight/solr/response/more_like_this'
|
8
|
+
autoload :GroupResponse, 'ansr_blacklight/solr/response/group_response'
|
9
|
+
autoload :Group, 'ansr_blacklight/solr/response/group'
|
10
|
+
|
11
|
+
include PaginationMethods
|
12
|
+
|
13
|
+
attr_reader :request_params
|
14
|
+
def initialize(data, request_params)
|
15
|
+
super(data)
|
16
|
+
@request_params = request_params
|
17
|
+
extend Spelling
|
18
|
+
extend Ansr::Facets
|
19
|
+
extend Response
|
20
|
+
extend MoreLikeThis
|
21
|
+
end
|
22
|
+
|
23
|
+
def header
|
24
|
+
self['responseHeader']
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(other_hash)
|
28
|
+
other_hash.each_pair { |key, value| self[key] = value }
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def params
|
33
|
+
(header and header['params']) ? header['params'] : request_params
|
34
|
+
end
|
35
|
+
|
36
|
+
def rows
|
37
|
+
params[:rows].to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
def docs
|
41
|
+
@docs ||= begin
|
42
|
+
response['docs'] || []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def spelling
|
47
|
+
self['spelling']
|
48
|
+
end
|
49
|
+
|
50
|
+
def grouped(model)
|
51
|
+
@groups ||= self["grouped"].map do |field, group|
|
52
|
+
# grouped responses can either be grouped by:
|
53
|
+
# - field, where this key is the field name, and there will be a list
|
54
|
+
# of documents grouped by field value, or:
|
55
|
+
# - function, where the key is the function, and the documents will be
|
56
|
+
# further grouped by function value, or:
|
57
|
+
# - query, where the key is the query, and the matching documents will be
|
58
|
+
# in the doclist on THIS object
|
59
|
+
if group["groups"] # field or function
|
60
|
+
GroupResponse.new field, model, group, self
|
61
|
+
else # query
|
62
|
+
Group.new({field => field}, model, group, self)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def group key
|
68
|
+
grouped.select { |x| x.key == key }.first
|
69
|
+
end
|
70
|
+
|
71
|
+
def grouped?
|
72
|
+
self.has_key? "grouped"
|
73
|
+
end
|
74
|
+
|
75
|
+
module Response
|
76
|
+
def response
|
77
|
+
self[:response] || {}
|
78
|
+
end
|
79
|
+
|
80
|
+
# short cut to response['numFound']
|
81
|
+
def total
|
82
|
+
response[:numFound].to_s.to_i
|
83
|
+
end
|
84
|
+
|
85
|
+
def start
|
86
|
+
response[:start].to_s.to_i
|
87
|
+
end
|
88
|
+
|
89
|
+
def empty?
|
90
|
+
total == 0
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|