rsolr 0.12.0 → 2.6.0
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 +7 -0
- data/.github/workflows/ruby.yml +29 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/CHANGES.txt +63 -260
- data/Gemfile +13 -0
- data/README.rdoc +177 -63
- data/Rakefile +19 -0
- data/lib/rsolr/char.rb +6 -0
- data/lib/rsolr/client.rb +344 -86
- data/lib/rsolr/document.rb +66 -0
- data/lib/rsolr/error.rb +182 -0
- data/lib/rsolr/field.rb +87 -0
- data/lib/rsolr/generator.rb +5 -0
- data/lib/rsolr/json.rb +60 -0
- data/lib/rsolr/response.rb +95 -0
- data/lib/rsolr/uri.rb +25 -0
- data/lib/rsolr/version.rb +7 -0
- data/lib/rsolr/xml.rb +150 -0
- data/lib/rsolr.rb +47 -35
- data/rsolr.gemspec +44 -31
- data/spec/api/client_spec.rb +423 -0
- data/spec/api/document_spec.rb +48 -0
- data/spec/api/error_spec.rb +158 -0
- data/spec/api/json_spec.rb +248 -0
- data/spec/api/pagination_spec.rb +31 -0
- data/spec/api/rsolr_spec.rb +31 -0
- data/spec/api/uri_spec.rb +37 -0
- data/spec/api/xml_spec.rb +255 -0
- data/spec/fixtures/basic_configs/_rest_managed.json +1 -0
- data/spec/fixtures/basic_configs/currency.xml +67 -0
- data/spec/fixtures/basic_configs/lang/stopwords_en.txt +54 -0
- data/spec/fixtures/basic_configs/protwords.txt +21 -0
- data/spec/fixtures/basic_configs/schema.xml +530 -0
- data/spec/fixtures/basic_configs/solrconfig.xml +572 -0
- data/spec/fixtures/basic_configs/stopwords.txt +14 -0
- data/spec/fixtures/basic_configs/synonyms.txt +29 -0
- data/spec/integration/solr5_spec.rb +38 -0
- data/spec/lib/rsolr/client_spec.rb +19 -0
- data/spec/spec_helper.rb +94 -0
- metadata +228 -54
- data/lib/rsolr/connection/net_http.rb +0 -48
- data/lib/rsolr/connection/requestable.rb +0 -43
- data/lib/rsolr/connection/utils.rb +0 -73
- data/lib/rsolr/connection.rb +0 -9
- data/lib/rsolr/message/document.rb +0 -48
- data/lib/rsolr/message/field.rb +0 -20
- data/lib/rsolr/message/generator.rb +0 -89
- data/lib/rsolr/message.rb +0 -8
data/lib/rsolr/error.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSolr::Error
|
4
|
+
|
5
|
+
module URICleanup
|
6
|
+
# Removes username and password from URI object.
|
7
|
+
def clean_uri(uri)
|
8
|
+
uri = uri.dup
|
9
|
+
uri.password = "REDACTED" if uri.password
|
10
|
+
uri.user = "REDACTED" if uri.user
|
11
|
+
uri
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module SolrContext
|
16
|
+
include URICleanup
|
17
|
+
|
18
|
+
attr_accessor :request, :response
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
m = "#{super.to_s}"
|
22
|
+
if response
|
23
|
+
m << " - #{response[:status]} #{Http::STATUS_CODES[response[:status].to_i]}"
|
24
|
+
details = parse_solr_error_response response[:body]
|
25
|
+
m << "\nError: #{details}\n" if details
|
26
|
+
end
|
27
|
+
p = "\nURI: #{clean_uri(request[:uri]).to_s}"
|
28
|
+
p << "\nRequest Headers: #{request[:headers].inspect}" if request[:headers]
|
29
|
+
p << "\nRequest Data: #{request[:data].inspect}" if request[:data]
|
30
|
+
p << "\n"
|
31
|
+
p << "\nBacktrace: " + self.backtrace[0..10].join("\n")
|
32
|
+
m << p
|
33
|
+
m
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def parse_solr_error_response body
|
39
|
+
begin
|
40
|
+
# Default JSON response, try to parse and retrieve error message
|
41
|
+
if response[:headers] && response[:headers]["content-type"].start_with?("application/json")
|
42
|
+
begin
|
43
|
+
parsed_body = JSON.parse(body)
|
44
|
+
info = parsed_body && parsed_body["error"] && parsed_body["error"]["msg"]
|
45
|
+
rescue JSON::ParserError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
return info if info
|
49
|
+
|
50
|
+
# legacy analysis, I think trying to handle wt=ruby responses without
|
51
|
+
# a full parse?
|
52
|
+
if body =~ /<pre>/
|
53
|
+
info = body.scan(/<pre>(.*)<\/pre>/mi)[0]
|
54
|
+
elsif body =~ /'msg'=>/
|
55
|
+
info = body.scan(/'msg'=>(.*)/)[0]
|
56
|
+
end
|
57
|
+
info = info.join if info.respond_to? :join
|
58
|
+
info ||= body # body might not contain <pre> or msg elements
|
59
|
+
|
60
|
+
partial = info.to_s.split("\n")[0..10]
|
61
|
+
partial.join("\n").gsub(">", ">").gsub("<", "<")
|
62
|
+
rescue
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class ConnectionRefused < ::Errno::ECONNREFUSED
|
69
|
+
include URICleanup
|
70
|
+
|
71
|
+
def initialize(request)
|
72
|
+
request[:uri] = clean_uri(request[:uri])
|
73
|
+
|
74
|
+
super(request.inspect)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class Http < RuntimeError
|
79
|
+
|
80
|
+
include SolrContext
|
81
|
+
|
82
|
+
# ripped right from ActionPack
|
83
|
+
# Defines the standard HTTP status codes, by integer, with their
|
84
|
+
# corresponding default message texts.
|
85
|
+
# Source: http://www.iana.org/assignments/http-status-codes
|
86
|
+
STATUS_CODES = {
|
87
|
+
100 => "Continue",
|
88
|
+
101 => "Switching Protocols",
|
89
|
+
102 => "Processing",
|
90
|
+
|
91
|
+
200 => "OK",
|
92
|
+
201 => "Created",
|
93
|
+
202 => "Accepted",
|
94
|
+
203 => "Non-Authoritative Information",
|
95
|
+
204 => "No Content",
|
96
|
+
205 => "Reset Content",
|
97
|
+
206 => "Partial Content",
|
98
|
+
207 => "Multi-Status",
|
99
|
+
226 => "IM Used",
|
100
|
+
|
101
|
+
300 => "Multiple Choices",
|
102
|
+
301 => "Moved Permanently",
|
103
|
+
302 => "Found",
|
104
|
+
303 => "See Other",
|
105
|
+
304 => "Not Modified",
|
106
|
+
305 => "Use Proxy",
|
107
|
+
307 => "Temporary Redirect",
|
108
|
+
|
109
|
+
400 => "Bad Request",
|
110
|
+
401 => "Unauthorized",
|
111
|
+
402 => "Payment Required",
|
112
|
+
403 => "Forbidden",
|
113
|
+
404 => "Not Found",
|
114
|
+
405 => "Method Not Allowed",
|
115
|
+
406 => "Not Acceptable",
|
116
|
+
407 => "Proxy Authentication Required",
|
117
|
+
408 => "Request Timeout",
|
118
|
+
409 => "Conflict",
|
119
|
+
410 => "Gone",
|
120
|
+
411 => "Length Required",
|
121
|
+
412 => "Precondition Failed",
|
122
|
+
413 => "Request Entity Too Large",
|
123
|
+
414 => "Request-URI Too Long",
|
124
|
+
415 => "Unsupported Media Type",
|
125
|
+
416 => "Requested Range Not Satisfiable",
|
126
|
+
417 => "Expectation Failed",
|
127
|
+
422 => "Unprocessable Entity",
|
128
|
+
423 => "Locked",
|
129
|
+
424 => "Failed Dependency",
|
130
|
+
426 => "Upgrade Required",
|
131
|
+
|
132
|
+
500 => "Internal Server Error",
|
133
|
+
501 => "Not Implemented",
|
134
|
+
502 => "Bad Gateway",
|
135
|
+
503 => "Service Unavailable",
|
136
|
+
504 => "Gateway Timeout",
|
137
|
+
505 => "HTTP Version Not Supported",
|
138
|
+
507 => "Insufficient Storage",
|
139
|
+
510 => "Not Extended"
|
140
|
+
}
|
141
|
+
|
142
|
+
def initialize request, response
|
143
|
+
response = response_with_force_encoded_body(response)
|
144
|
+
@request, @response = request, response
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def response_with_force_encoded_body(response)
|
150
|
+
response[:body] = response[:body].force_encoding('UTF-8') if response
|
151
|
+
response
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Thrown if the :wt is :ruby
|
156
|
+
# but the body wasn't succesfully parsed/evaluated
|
157
|
+
class InvalidResponse < Http
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
# Subclasses Rsolr::Error::Http for legacy backwards compatibility
|
162
|
+
# purposes, because earlier RSolr 2 didn't distinguish these
|
163
|
+
# from Http errors.
|
164
|
+
#
|
165
|
+
# In RSolr 3, it could make sense to `< Timeout::Error` instead,
|
166
|
+
# analagous to ConnectionRefused above
|
167
|
+
class Timeout < Http
|
168
|
+
end
|
169
|
+
|
170
|
+
# Thrown if the :wt is :ruby
|
171
|
+
# but the body wasn't succesfully parsed/evaluated
|
172
|
+
class InvalidJsonResponse < InvalidResponse
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
# Thrown if the :wt is :ruby
|
177
|
+
# but the body wasn't succesfully parsed/evaluated
|
178
|
+
class InvalidRubyResponse < InvalidResponse
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
data/lib/rsolr/field.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
module RSolr
|
2
|
+
class Field
|
3
|
+
def self.instance(attrs, value)
|
4
|
+
attrs = attrs.dup
|
5
|
+
field_type = attrs.delete(:type) { value.class.name }
|
6
|
+
|
7
|
+
klass = if field_type.is_a? String
|
8
|
+
class_for_field(field_type)
|
9
|
+
elsif field_type.is_a? Class
|
10
|
+
field_type
|
11
|
+
else
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
klass.new(attrs, value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.class_for_field(field_type)
|
19
|
+
potential_class_name = field_type + 'Field'.freeze
|
20
|
+
search_scope = Module.nesting[1]
|
21
|
+
search_scope.const_defined?(potential_class_name, false) ? search_scope.const_get(potential_class_name) : self
|
22
|
+
end
|
23
|
+
private_class_method :class_for_field
|
24
|
+
|
25
|
+
# "attrs" is a hash for setting the "doc" xml attributes
|
26
|
+
# "value" is the text value for the node
|
27
|
+
attr_accessor :attrs, :source_value
|
28
|
+
|
29
|
+
# "attrs" must be a hash
|
30
|
+
# "value" should be something that responds to #_to_s
|
31
|
+
def initialize(attrs, source_value)
|
32
|
+
@attrs = attrs
|
33
|
+
@source_value = source_value
|
34
|
+
end
|
35
|
+
|
36
|
+
# the value of the "name" attribute
|
37
|
+
def name
|
38
|
+
attrs[:name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def value
|
42
|
+
source_value
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json
|
46
|
+
if attrs[:update]
|
47
|
+
{ attrs[:update] => value }
|
48
|
+
elsif attrs.any? { |k, _| k != :name }
|
49
|
+
hash = attrs.dup
|
50
|
+
hash.delete(:name)
|
51
|
+
hash.merge(value: value)
|
52
|
+
else
|
53
|
+
value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class DateField < Field
|
59
|
+
def value
|
60
|
+
Time.utc(source_value.year, source_value.mon, source_value.mday).iso8601
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TimeField < Field
|
65
|
+
def value
|
66
|
+
source_value.getutc.strftime('%FT%TZ')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class DateTimeField < Field
|
71
|
+
def value
|
72
|
+
source_value.to_time.getutc.iso8601
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class DocumentField < Field
|
77
|
+
def value
|
78
|
+
return RSolr::Document.new(source_value) if source_value.respond_to? :each_pair
|
79
|
+
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
def as_json
|
84
|
+
value.as_json
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/rsolr/json.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module RSolr::JSON
|
4
|
+
class Generator < RSolr::Generator
|
5
|
+
CONTENT_TYPE = 'application/json'.freeze
|
6
|
+
|
7
|
+
def content_type
|
8
|
+
CONTENT_TYPE
|
9
|
+
end
|
10
|
+
|
11
|
+
def add data, add_attrs = {}
|
12
|
+
add_attrs ||= {}
|
13
|
+
data = RSolr::Array.wrap(data)
|
14
|
+
|
15
|
+
if add_attrs.empty? && data.none? { |doc| doc.is_a?(RSolr::Document) && !doc.attrs.empty? }
|
16
|
+
data.map do |doc|
|
17
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
18
|
+
yield doc if block_given?
|
19
|
+
doc.as_json
|
20
|
+
end.to_json
|
21
|
+
else
|
22
|
+
i = 0
|
23
|
+
data.each_with_object({}) do |doc, hash|
|
24
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
25
|
+
yield doc if block_given?
|
26
|
+
hash["add__UNIQUE_RSOLR_SUFFIX_#{i += 1}"] = add_attrs.merge(doc.attrs).merge(doc: doc.as_json)
|
27
|
+
end.to_json.gsub(/__UNIQUE_RSOLR_SUFFIX_\d+/, '')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# generates a commit message
|
32
|
+
def commit(opts = {})
|
33
|
+
opts ||= {}
|
34
|
+
{ commit: opts }.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
# generates a optimize message
|
38
|
+
def optimize(opts = {})
|
39
|
+
opts ||= {}
|
40
|
+
{ optimize: opts }.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
# generates a rollback message
|
44
|
+
def rollback
|
45
|
+
{ rollback: {} }.to_json
|
46
|
+
end
|
47
|
+
|
48
|
+
# generates a delete message
|
49
|
+
# "ids" can be a single value or array of values
|
50
|
+
def delete_by_id(ids)
|
51
|
+
{ delete: ids }.to_json
|
52
|
+
end
|
53
|
+
|
54
|
+
# generates a delete message
|
55
|
+
# "queries" can be a single value or an array of values
|
56
|
+
def delete_by_query(queries)
|
57
|
+
{ delete: { query: queries } }.to_json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module RSolr::Response
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
unless base < Hash
|
5
|
+
raise ArgumentError, "RSolr::Response expects to included only in (sub)classes of Hash; got included in '#{base}' instead."
|
6
|
+
end
|
7
|
+
base.send(:attr_reader, :request, :response)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize_rsolr_response(request, response, result)
|
11
|
+
@request = request
|
12
|
+
@response = response
|
13
|
+
self.merge!(result)
|
14
|
+
if self["response"] && self["response"]["docs"].is_a?(::Array)
|
15
|
+
docs = PaginatedDocSet.new(self["response"]["docs"])
|
16
|
+
docs.per_page = request[:params]["rows"]
|
17
|
+
docs.page_start = request[:params]["start"]
|
18
|
+
docs.page_total = self["response"]["numFound"].to_s.to_i
|
19
|
+
self["response"]["docs"] = docs
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_indifferent_access
|
24
|
+
if defined?(::RSolr::HashWithIndifferentAccessWithResponse)
|
25
|
+
::RSolr::HashWithIndifferentAccessWithResponse.new(request, response, self)
|
26
|
+
else
|
27
|
+
if defined?(ActiveSupport::HashWithIndifferentAccess)
|
28
|
+
RSolr.const_set("HashWithIndifferentAccessWithResponse", Class.new(ActiveSupport::HashWithIndifferentAccess))
|
29
|
+
RSolr::HashWithIndifferentAccessWithResponse.class_eval <<-eos
|
30
|
+
include RSolr::Response
|
31
|
+
def initialize(request, response, result)
|
32
|
+
super()
|
33
|
+
initialize_rsolr_response(request, response, result)
|
34
|
+
end
|
35
|
+
eos
|
36
|
+
::RSolr::HashWithIndifferentAccessWithResponse.new(request, response, self)
|
37
|
+
else
|
38
|
+
raise RuntimeError, "HashWithIndifferentAccess is not currently defined"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# A response module which gets mixed into the solr ["response"]["docs"] array.
|
44
|
+
class PaginatedDocSet < ::Array
|
45
|
+
|
46
|
+
attr_accessor :page_start, :per_page, :page_total
|
47
|
+
if not (Object.const_defined?("RUBY_ENGINE") and Object::RUBY_ENGINE=='rbx')
|
48
|
+
alias_method(:start,:page_start)
|
49
|
+
alias_method(:start=,:page_start=)
|
50
|
+
alias_method(:total,:page_total)
|
51
|
+
alias_method(:total=,:page_total=)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the current page calculated from 'rows' and 'start'
|
55
|
+
def current_page
|
56
|
+
return 1 if start < 1
|
57
|
+
per_page_normalized = per_page < 1 ? 1 : per_page
|
58
|
+
@current_page ||= (start / per_page_normalized).ceil + 1
|
59
|
+
end
|
60
|
+
|
61
|
+
# Calcuates the total pages from 'numFound' and 'rows'
|
62
|
+
def total_pages
|
63
|
+
@total_pages ||= per_page > 0 ? (total / per_page.to_f).ceil : 1
|
64
|
+
end
|
65
|
+
|
66
|
+
# returns the previous page number or 1
|
67
|
+
def previous_page
|
68
|
+
@previous_page ||= (current_page > 1) ? current_page - 1 : 1
|
69
|
+
end
|
70
|
+
|
71
|
+
# returns the next page number or the last
|
72
|
+
def next_page
|
73
|
+
@next_page ||= (current_page == total_pages) ? total_pages : current_page+1
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_next?
|
77
|
+
current_page < total_pages
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_previous?
|
81
|
+
current_page > 1
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
class RSolr::HashWithResponse < Hash
|
89
|
+
include RSolr::Response
|
90
|
+
|
91
|
+
def initialize(request, response, result)
|
92
|
+
super()
|
93
|
+
initialize_rsolr_response(request, response, result || {})
|
94
|
+
end
|
95
|
+
end
|
data/lib/rsolr/uri.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module RSolr::Uri
|
4
|
+
# Creates a Solr based query string.
|
5
|
+
# Keys that have arrays values are set multiple times:
|
6
|
+
# params_to_solr(:q => 'query', :fq => ['a', 'b'])
|
7
|
+
# is converted to:
|
8
|
+
# ?q=query&fq=a&fq=b
|
9
|
+
# @param [boolean] escape false if no URI escaping is to be performed. Default true.
|
10
|
+
# @return [String] Solr query params as a String, suitable for use in a url
|
11
|
+
def self.params_to_solr(params, escape = true)
|
12
|
+
return URI.encode_www_form(params.reject{|k,v| k.to_s.empty? || v.to_s.empty?}) if escape
|
13
|
+
|
14
|
+
# escape = false if we are here
|
15
|
+
mapped = params.map do |k, v|
|
16
|
+
next if v.to_s.empty?
|
17
|
+
if v.class == ::Array
|
18
|
+
params_to_solr(v.map { |x| [k, x] }, false)
|
19
|
+
else
|
20
|
+
"#{k}=#{v}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
mapped.compact.join("&")
|
24
|
+
end
|
25
|
+
end
|
data/lib/rsolr/xml.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
module RSolr::Xml
|
2
|
+
Document = RSolr::Document
|
3
|
+
Field = RSolr::Field
|
4
|
+
|
5
|
+
class Generator < RSolr::Generator
|
6
|
+
class << self
|
7
|
+
attr_accessor :use_nokogiri
|
8
|
+
|
9
|
+
def builder_proc
|
10
|
+
if use_nokogiri
|
11
|
+
require 'nokogiri' unless defined?(::Nokogiri::XML::Builder)
|
12
|
+
:nokogiri_build
|
13
|
+
else
|
14
|
+
require 'builder' unless defined?(::Builder::XmlMarkup)
|
15
|
+
:builder_build
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
self.use_nokogiri = defined?(::Nokogiri::XML::Builder) ? true : false
|
20
|
+
|
21
|
+
CONTENT_TYPE = 'text/xml'.freeze
|
22
|
+
|
23
|
+
def content_type
|
24
|
+
CONTENT_TYPE
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def nokogiri_build &block
|
29
|
+
b = ::Nokogiri::XML::Builder.new do |xml|
|
30
|
+
block_given? ? yield(xml) : xml
|
31
|
+
end
|
32
|
+
'<?xml version="1.0" encoding="UTF-8"?>'+b.to_xml(:indent => 0, :encoding => 'UTF-8', :save_with => ::Nokogiri::XML::Node::SaveOptions::AS_XML | ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
|
33
|
+
end
|
34
|
+
protected :nokogiri_build
|
35
|
+
|
36
|
+
def builder_build &block
|
37
|
+
b = ::Builder::XmlMarkup.new(:indent => 0, :margin => 0, :encoding => 'UTF-8')
|
38
|
+
b.instruct!
|
39
|
+
block_given? ? yield(b) : b
|
40
|
+
end
|
41
|
+
protected :builder_build
|
42
|
+
|
43
|
+
def build &block
|
44
|
+
self.send(self.class.builder_proc,&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
# generates "add" xml for updating solr
|
48
|
+
# "data" can be a hash or an array of hashes.
|
49
|
+
# - each hash should be a simple key=>value pair representing a solr doc.
|
50
|
+
# If a value is an array, multiple fields will be created.
|
51
|
+
#
|
52
|
+
# "add_attrs" can be a hash for setting the add xml element attributes.
|
53
|
+
#
|
54
|
+
# This method can also accept a block.
|
55
|
+
# The value yielded to the block is a Message::Document; for each solr doc in "data".
|
56
|
+
# You can set xml element attributes for each "doc" element or individual "field" elements.
|
57
|
+
#
|
58
|
+
# For example:
|
59
|
+
#
|
60
|
+
# solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
|
61
|
+
# doc_msg.attrs[:boost] = 10.00 # boost the document
|
62
|
+
# nickname = doc_msg.field_by_name(:nickname)
|
63
|
+
# nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# would result in an add element having the attributes boost="10.0"
|
67
|
+
# and a commitWithin="1.0".
|
68
|
+
# Each doc element would have a boost="10.0".
|
69
|
+
# The "nickname" field would have a boost="20.0"
|
70
|
+
# if the doc had a "nickname" field with the value of "Tim".
|
71
|
+
#
|
72
|
+
def add data, add_attrs = nil, &block
|
73
|
+
add_attrs ||= {}
|
74
|
+
data = RSolr::Array.wrap(data)
|
75
|
+
build do |xml|
|
76
|
+
xml.add(add_attrs) do |add_node|
|
77
|
+
data.each do |doc|
|
78
|
+
doc = RSolr::Document.new(doc) if doc.respond_to?(:each_pair)
|
79
|
+
yield doc if block_given?
|
80
|
+
doc_node_builder = to_xml(doc)
|
81
|
+
self.class.use_nokogiri ? add_node.doc_(doc.attrs,&doc_node_builder) : add_node.doc(doc.attrs,&doc_node_builder)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# generates a <commit/> message
|
88
|
+
def commit opts = nil
|
89
|
+
opts ||= {}
|
90
|
+
build {|xml| xml.commit(opts) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# generates a <optimize/> message
|
94
|
+
def optimize opts = nil
|
95
|
+
opts ||= {}
|
96
|
+
build {|xml| xml.optimize(opts) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# generates a <rollback/> message
|
100
|
+
def rollback
|
101
|
+
build {|xml| xml.rollback({}) }
|
102
|
+
end
|
103
|
+
|
104
|
+
# generates a <delete><id>ID</id></delete> message
|
105
|
+
# "ids" can be a single value or array of values
|
106
|
+
def delete_by_id ids
|
107
|
+
ids = RSolr::Array.wrap(ids)
|
108
|
+
build do |xml|
|
109
|
+
xml.delete do |delete_node|
|
110
|
+
ids.each do |id|
|
111
|
+
self.class.use_nokogiri ? delete_node.id_(id) : delete_node.id(id)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# generates a <delete><query>ID</query></delete> message
|
118
|
+
# "queries" can be a single value or an array of values
|
119
|
+
def delete_by_query(queries)
|
120
|
+
queries = RSolr::Array.wrap(queries)
|
121
|
+
build do |xml|
|
122
|
+
xml.delete do |delete_node|
|
123
|
+
queries.each { |query| delete_node.query(query) }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def to_xml(doc)
|
131
|
+
lambda do |doc_node|
|
132
|
+
doc.fields.each do |field_obj|
|
133
|
+
value = field_obj.value
|
134
|
+
|
135
|
+
if field_obj.name.to_s == RSolr::Document::CHILD_DOCUMENT_KEY
|
136
|
+
child_node_builder = to_xml(field_obj.value)
|
137
|
+
self.class.use_nokogiri ? doc_node.doc_(&child_node_builder) : doc_node.doc(&child_node_builder)
|
138
|
+
elsif value.is_a?(Hash) && value.length == 1 && field_obj.attrs[:update].nil?
|
139
|
+
update_attr, real_value = value.first
|
140
|
+
doc_node.field real_value, field_obj.attrs.merge(update: update_attr)
|
141
|
+
elsif value.nil?
|
142
|
+
doc_node.field field_obj.value, field_obj.attrs.merge(null: true)
|
143
|
+
else
|
144
|
+
doc_node.field field_obj.value, field_obj.attrs
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|