telvue-rsolr 2.2.2
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/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/CHANGES.txt +47 -0
- data/Gemfile +5 -0
- data/LICENSE +13 -0
- data/README.rdoc +229 -0
- data/Rakefile +19 -0
- data/lib/rsolr.rb +52 -0
- data/lib/rsolr/char.rb +6 -0
- data/lib/rsolr/client.rb +342 -0
- data/lib/rsolr/document.rb +59 -0
- data/lib/rsolr/error.rb +136 -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/rsolr.gemspec +46 -0
- data/spec/api/client_spec.rb +355 -0
- data/spec/api/document_spec.rb +48 -0
- data/spec/api/error_spec.rb +47 -0
- data/spec/api/json_spec.rb +198 -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 +34 -0
- data/spec/spec_helper.rb +94 -0
- metadata +232 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module RSolr
|
2
|
+
class Document
|
3
|
+
CHILD_DOCUMENT_KEY = '_childDocuments_'.freeze
|
4
|
+
|
5
|
+
# "attrs" is a hash for setting the "doc" xml attributes
|
6
|
+
# "fields" is an array of Field objects
|
7
|
+
attr_accessor :attrs, :fields
|
8
|
+
|
9
|
+
# "doc_hash" must be a Hash/Mash object
|
10
|
+
# If a value in the "doc_hash" is an array,
|
11
|
+
# a field object is created for each value...
|
12
|
+
def initialize(doc_hash = {})
|
13
|
+
@fields = []
|
14
|
+
doc_hash.each_pair do |field, values|
|
15
|
+
add_field(field, values)
|
16
|
+
end
|
17
|
+
@attrs={}
|
18
|
+
end
|
19
|
+
|
20
|
+
# returns an array of fields that match the "name" arg
|
21
|
+
def fields_by_name(name)
|
22
|
+
@fields.select{|f|f.name==name}
|
23
|
+
end
|
24
|
+
|
25
|
+
# returns the *first* field that matches the "name" arg
|
26
|
+
def field_by_name(name)
|
27
|
+
@fields.detect{|f|f.name==name}
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Add a field value to the document. Options map directly to
|
32
|
+
# XML attributes in the Solr <field> node.
|
33
|
+
# See http://wiki.apache.org/solr/UpdateXmlMessages#head-8315b8028923d028950ff750a57ee22cbf7977c6
|
34
|
+
#
|
35
|
+
# === Example:
|
36
|
+
#
|
37
|
+
# document.add_field('title', 'A Title', :boost => 2.0)
|
38
|
+
#
|
39
|
+
def add_field(name, values, options = {})
|
40
|
+
RSolr::Array.wrap(values).each do |v|
|
41
|
+
field_attrs = { name: name }
|
42
|
+
field_attrs[:type] = DocumentField if name.to_s == CHILD_DOCUMENT_KEY
|
43
|
+
|
44
|
+
@fields << RSolr::Field.instance(options.merge(field_attrs), v)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def as_json
|
49
|
+
@fields.group_by(&:name).each_with_object({}) do |(field, values), result|
|
50
|
+
v = values.map(&:as_json)
|
51
|
+
if v.length > 1 && v.first.is_a?(Hash) && v.first.key?(:value)
|
52
|
+
v = v.first.merge(value: v.map { |single| single[:value] })
|
53
|
+
end
|
54
|
+
v = v.first if v.length == 1 && field.to_s != CHILD_DOCUMENT_KEY
|
55
|
+
result[field] = v
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/rsolr/error.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
module RSolr::Error
|
2
|
+
|
3
|
+
module SolrContext
|
4
|
+
|
5
|
+
attr_accessor :request, :response
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
m = "#{super.to_s}"
|
9
|
+
if response
|
10
|
+
m << " - #{response[:status]} #{Http::STATUS_CODES[response[:status].to_i]}"
|
11
|
+
details = parse_solr_error_response response[:body]
|
12
|
+
m << "\nError: #{details}\n" if details
|
13
|
+
end
|
14
|
+
p = "\nURI: #{request[:uri].to_s}"
|
15
|
+
p << "\nRequest Headers: #{request[:headers].inspect}" if request[:headers]
|
16
|
+
p << "\nRequest Data: #{request[:data].inspect}" if request[:data]
|
17
|
+
p << "\n"
|
18
|
+
p << "\nBacktrace: " + self.backtrace[0..10].join("\n")
|
19
|
+
m << p
|
20
|
+
m
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def parse_solr_error_response body
|
26
|
+
begin
|
27
|
+
if body =~ /<pre>/
|
28
|
+
info = body.scan(/<pre>(.*)<\/pre>/mi)[0]
|
29
|
+
elsif body =~ /'msg'=>/
|
30
|
+
info = body.scan(/'msg'=>(.*)/)[0]
|
31
|
+
end
|
32
|
+
info = info.join if info.respond_to? :join
|
33
|
+
info ||= body # body might not contain <pre> or msg elements
|
34
|
+
|
35
|
+
partial = info.to_s.split("\n")[0..10]
|
36
|
+
partial.join("\n").gsub(">", ">").gsub("<", "<")
|
37
|
+
rescue
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class ConnectionRefused < ::Errno::ECONNREFUSED
|
46
|
+
end
|
47
|
+
|
48
|
+
class Http < RuntimeError
|
49
|
+
|
50
|
+
include SolrContext
|
51
|
+
|
52
|
+
# ripped right from ActionPack
|
53
|
+
# Defines the standard HTTP status codes, by integer, with their
|
54
|
+
# corresponding default message texts.
|
55
|
+
# Source: http://www.iana.org/assignments/http-status-codes
|
56
|
+
STATUS_CODES = {
|
57
|
+
100 => "Continue",
|
58
|
+
101 => "Switching Protocols",
|
59
|
+
102 => "Processing",
|
60
|
+
|
61
|
+
200 => "OK",
|
62
|
+
201 => "Created",
|
63
|
+
202 => "Accepted",
|
64
|
+
203 => "Non-Authoritative Information",
|
65
|
+
204 => "No Content",
|
66
|
+
205 => "Reset Content",
|
67
|
+
206 => "Partial Content",
|
68
|
+
207 => "Multi-Status",
|
69
|
+
226 => "IM Used",
|
70
|
+
|
71
|
+
300 => "Multiple Choices",
|
72
|
+
301 => "Moved Permanently",
|
73
|
+
302 => "Found",
|
74
|
+
303 => "See Other",
|
75
|
+
304 => "Not Modified",
|
76
|
+
305 => "Use Proxy",
|
77
|
+
307 => "Temporary Redirect",
|
78
|
+
|
79
|
+
400 => "Bad Request",
|
80
|
+
401 => "Unauthorized",
|
81
|
+
402 => "Payment Required",
|
82
|
+
403 => "Forbidden",
|
83
|
+
404 => "Not Found",
|
84
|
+
405 => "Method Not Allowed",
|
85
|
+
406 => "Not Acceptable",
|
86
|
+
407 => "Proxy Authentication Required",
|
87
|
+
408 => "Request Timeout",
|
88
|
+
409 => "Conflict",
|
89
|
+
410 => "Gone",
|
90
|
+
411 => "Length Required",
|
91
|
+
412 => "Precondition Failed",
|
92
|
+
413 => "Request Entity Too Large",
|
93
|
+
414 => "Request-URI Too Long",
|
94
|
+
415 => "Unsupported Media Type",
|
95
|
+
416 => "Requested Range Not Satisfiable",
|
96
|
+
417 => "Expectation Failed",
|
97
|
+
422 => "Unprocessable Entity",
|
98
|
+
423 => "Locked",
|
99
|
+
424 => "Failed Dependency",
|
100
|
+
426 => "Upgrade Required",
|
101
|
+
|
102
|
+
500 => "Internal Server Error",
|
103
|
+
501 => "Not Implemented",
|
104
|
+
502 => "Bad Gateway",
|
105
|
+
503 => "Service Unavailable",
|
106
|
+
504 => "Gateway Timeout",
|
107
|
+
505 => "HTTP Version Not Supported",
|
108
|
+
507 => "Insufficient Storage",
|
109
|
+
510 => "Not Extended"
|
110
|
+
}
|
111
|
+
|
112
|
+
def initialize request, response
|
113
|
+
@request, @response = request, response
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
# Thrown if the :wt is :ruby
|
119
|
+
# but the body wasn't succesfully parsed/evaluated
|
120
|
+
class InvalidResponse < Http
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
# Thrown if the :wt is :ruby
|
125
|
+
# but the body wasn't succesfully parsed/evaluated
|
126
|
+
class InvalidJsonResponse < InvalidResponse
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
# Thrown if the :wt is :ruby
|
131
|
+
# but the body wasn't succesfully parsed/evaluated
|
132
|
+
class InvalidRubyResponse < InvalidResponse
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
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
|