telvue-rsolr 2.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|