mwmitchell-solr 0.5.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.
- data/LICENSE +201 -0
- data/README.rdoc +91 -0
- data/Rakefile +72 -0
- data/examples/direct.rb +14 -0
- data/examples/http.rb +7 -0
- data/lib/core_ext.rb +8 -0
- data/lib/solr.rb +36 -0
- data/lib/solr/adapter.rb +7 -0
- data/lib/solr/adapter/common_methods.rb +89 -0
- data/lib/solr/adapter/direct.rb +65 -0
- data/lib/solr/adapter/http.rb +55 -0
- data/lib/solr/connection.rb +7 -0
- data/lib/solr/connection/base.rb +122 -0
- data/lib/solr/connection/search_ext.rb +110 -0
- data/lib/solr/indexer.rb +23 -0
- data/lib/solr/mapper.rb +56 -0
- data/lib/solr/mapper/rss.rb +27 -0
- data/lib/solr/message.rb +69 -0
- data/lib/solr/response.rb +143 -0
- data/test/adapter_common_methods_test.rb +49 -0
- data/test/connection_test_methods.rb +82 -0
- data/test/direct_test.rb +20 -0
- data/test/ext_pagination_test.rb +58 -0
- data/test/ext_search_test.rb +9 -0
- data/test/http_test.rb +13 -0
- data/test/indexer_test.rb +14 -0
- data/test/mapper_test.rb +105 -0
- data/test/message_test.rb +70 -0
- data/test/ruby-lang.org.rss.xml +391 -0
- data/test/test_helpers.rb +31 -0
- metadata +91 -0
data/lib/solr/indexer.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
class Solr::Indexer
|
2
|
+
|
3
|
+
attr_reader :solr, :mapper, :opts
|
4
|
+
|
5
|
+
def initialize(solr, mapping_or_mapper, opts={})
|
6
|
+
@solr = solr
|
7
|
+
@mapper = mapping_or_mapper.is_a?(Hash) ? Solr::Mapper::Base.new(mapping_or_mapper) : mapping_or_mapper
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
# data - the raw data to send into the mapper
|
12
|
+
# params - url query params for solr /update handler
|
13
|
+
# commit - boolean; true==commit after adding, false==no commit after adding
|
14
|
+
# block can be used for modifying the "add", "doc" and "field" xml elements (for boosting etc.)
|
15
|
+
def index(data, params={}, &block)
|
16
|
+
docs = data.collect {|d| @mapper.map(d)}
|
17
|
+
@solr.add(docs, params) do |add, doc, field|
|
18
|
+
# check opts for :debug etc.?
|
19
|
+
yield add, doc, field if block_given?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/solr/mapper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Solr::Mapper
|
2
|
+
|
3
|
+
autoload :RSS, 'solr/mapper/rss'
|
4
|
+
|
5
|
+
class UnkownMappingValue < RuntimeError; end
|
6
|
+
|
7
|
+
class Base
|
8
|
+
|
9
|
+
attr_reader :mapping, :opts
|
10
|
+
|
11
|
+
def initialize(mapping={}, opts={}, &block)
|
12
|
+
@mapping = mapping
|
13
|
+
@opts = opts
|
14
|
+
yield @mapping if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
# source - a hash or array of source data
|
18
|
+
# override_mapping - an alternate mapper
|
19
|
+
# returns an array with one or more mapped hashes
|
20
|
+
def map(source, override_mapping=nil)
|
21
|
+
source = [source] if source.is_a?(Hash)
|
22
|
+
m = override_mapping || @mapping
|
23
|
+
source.collect do |src|
|
24
|
+
m.inject({}) do |mapped_data, (field_name, mapped_value)|
|
25
|
+
value = mapped_field_value(src, mapped_value)
|
26
|
+
value.to_s.empty? ? mapped_data : mapped_data.merge!({field_name=>value})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# This is a hook method useful for subclassing
|
34
|
+
def source_field_value(source, field_name)
|
35
|
+
source[field_name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def mapped_field_value(source, mapped_value)
|
39
|
+
case mapped_value
|
40
|
+
when String
|
41
|
+
mapped_value
|
42
|
+
when Symbol
|
43
|
+
source_field_value(source, mapped_value)
|
44
|
+
when Proc
|
45
|
+
mapped_value.call(source, self)
|
46
|
+
when Enumerable
|
47
|
+
mapped_value.collect {|key| source_field_value(source, key)}.flatten
|
48
|
+
else
|
49
|
+
# try to turn it into a string, else raise UnkownMappingValue
|
50
|
+
mapped_value.respond_to?(:to_s) ? mapped_value.to_s : raise(UnkownMappingValue.new(mapped_value))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rss'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
class Solr::Mapper::RSS < Solr::Mapper::Base
|
5
|
+
|
6
|
+
attr_reader :rss
|
7
|
+
|
8
|
+
# rss_file_or_url is file path or url (see open-uri)
|
9
|
+
# override_mapping is an alternate mapping (see Solr::Mapper::Base)
|
10
|
+
# returns array of mapped hashes
|
11
|
+
def map(rss_file_or_url, override_mapping=nil)
|
12
|
+
open(rss_file_or_url) do |feed|
|
13
|
+
@rss = RSS::Parser.parse(feed.read, false)
|
14
|
+
super(rss.items.collect, override_mapping)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# sends methods chain down into the @rss object
|
19
|
+
# example: :'channel.title' == @rss.channel.title
|
20
|
+
# if the method chain doesn't exist, the super #source_field_value method is called
|
21
|
+
def source_field_value(source, method_path)
|
22
|
+
method_path.to_s.split('.').inject(@rss) do |rss, m|
|
23
|
+
rss.respond_to?(m) ? rss.send(m.to_sym) : super(source, method_path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/solr/message.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
# http://builder.rubyforge.org/
|
3
|
+
require 'rubygems'
|
4
|
+
require 'builder'
|
5
|
+
|
6
|
+
class Solr::Message
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def xml
|
11
|
+
Builder::XmlMarkup.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# add({})
|
15
|
+
# add([{}, {}])
|
16
|
+
# add(docs) do |doc|
|
17
|
+
# doc.boost = 10.0
|
18
|
+
# end
|
19
|
+
def add(data, opts={}, &block)
|
20
|
+
data = [data] if data.respond_to?(:each_pair) # if it's a hash, put it in an array
|
21
|
+
xml.add(opts) do |add_xml|
|
22
|
+
data.each do |item|
|
23
|
+
add_xml.doc do |doc_xml|
|
24
|
+
# convert keys into strings and perform an alpha sort (easier testing between ruby and jruby)
|
25
|
+
# but probably not great for performance? whatever...
|
26
|
+
sorted_items = item.inject({}) {|acc,(k,v)| acc.merge({k.to_s=>v})}
|
27
|
+
sorted_items.keys.sort.each do |k|
|
28
|
+
doc_attrs = {:name=>k}
|
29
|
+
yield doc_attrs if block_given?
|
30
|
+
doc_xml.field(sorted_items[k], doc_attrs)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def commit(opts={})
|
38
|
+
xml.commit(opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def optimize(opts={})
|
42
|
+
xml.optimize(opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def rollback
|
46
|
+
xml.rollback
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete_by_id(ids)
|
50
|
+
ids = [ids] unless ids.is_a?(Array)
|
51
|
+
xml.delete do |xml|
|
52
|
+
ids.each do |id|
|
53
|
+
xml.id(id)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete_by_query(queries)
|
59
|
+
queries = [queries] unless queries.is_a?(Array)
|
60
|
+
xml.delete do |xml|
|
61
|
+
queries.each do |query|
|
62
|
+
xml.query(query)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Solr::Response
|
2
|
+
|
3
|
+
# default/base response object
|
4
|
+
class Base
|
5
|
+
|
6
|
+
attr_reader :raw_response, :data, :header, :params, :status, :query_time
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
if data.is_a?(String)
|
10
|
+
@raw_response = data
|
11
|
+
@data = Kernel.eval(@raw_response)
|
12
|
+
else
|
13
|
+
@data = data
|
14
|
+
end
|
15
|
+
@header = @data['responseHeader']
|
16
|
+
@params = @header['params']
|
17
|
+
@status = @header['status']
|
18
|
+
@query_time = @header['QTime']
|
19
|
+
end
|
20
|
+
|
21
|
+
def ok?
|
22
|
+
self.status==0
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
=begin
|
28
|
+
class Document
|
29
|
+
|
30
|
+
attr_reader :data
|
31
|
+
|
32
|
+
def initialize(source_hash)
|
33
|
+
source_hash.each do |k,v|
|
34
|
+
@data[k.to_sym]=v
|
35
|
+
instance_eval <<-EOF
|
36
|
+
def #{k}
|
37
|
+
@data[:#{k}]
|
38
|
+
end
|
39
|
+
EOF
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# doc.has?(:location_facet, 'Clemons')
|
45
|
+
# doc.has?(:id, 'h009', /^u/i)
|
46
|
+
#
|
47
|
+
def has?(k, *values)
|
48
|
+
return if @data[k].nil?
|
49
|
+
target = @data[k]
|
50
|
+
if target.is_a?(Array)
|
51
|
+
values.each do |val|
|
52
|
+
return target.any?{|tv| val.is_a?(Regexp) ? (tv =~ val) : (tv==val)}
|
53
|
+
end
|
54
|
+
else
|
55
|
+
return values.any? {|val| val.is_a?(Regexp) ? (target =~ val) : (target == val)}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
def get(key, default=nil)
|
61
|
+
@data[key] || default
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
=end
|
66
|
+
|
67
|
+
# response for queries
|
68
|
+
class Query < Base
|
69
|
+
|
70
|
+
attr_reader :response, :docs, :num_found, :start
|
71
|
+
|
72
|
+
alias :total :num_found
|
73
|
+
alias :offset :start
|
74
|
+
|
75
|
+
def initialize(data)
|
76
|
+
super(data)
|
77
|
+
@response = @data['response']
|
78
|
+
@docs = @response['docs']
|
79
|
+
@num_found = @response['numFound']
|
80
|
+
@start = @response['start']
|
81
|
+
end
|
82
|
+
|
83
|
+
def per_page
|
84
|
+
@per_page = params['rows'].to_s.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
def current_page
|
88
|
+
@current_page = self.per_page > 0 ? ((self.start / self.per_page).ceil) : 1
|
89
|
+
@current_page == 0 ? 1 : @current_page
|
90
|
+
end
|
91
|
+
|
92
|
+
alias :page :current_page
|
93
|
+
|
94
|
+
def page_count
|
95
|
+
@page_count = self.per_page > 0 ? (self.total / self.per_page.to_f).ceil : 1
|
96
|
+
end
|
97
|
+
|
98
|
+
# supports WillPaginate
|
99
|
+
alias :total_pages :page_count
|
100
|
+
|
101
|
+
alias :pages :page_count
|
102
|
+
|
103
|
+
# supports WillPaginate
|
104
|
+
def previous_page
|
105
|
+
(current_page > 1) ? current_page - 1 : 1
|
106
|
+
end
|
107
|
+
|
108
|
+
# supports WillPaginate
|
109
|
+
def next_page
|
110
|
+
(current_page < page_count) ? current_page + 1 : page_count
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
# response class for update requests
|
116
|
+
class Update < Base
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
# response for /admin/luke
|
121
|
+
class IndexInfo < Base
|
122
|
+
|
123
|
+
attr_reader :index, :directory, :has_deletions, :optimized, :current, :max_doc, :num_docs, :version
|
124
|
+
|
125
|
+
alias :has_deletions? :has_deletions
|
126
|
+
alias :optimized? :optimized
|
127
|
+
alias :current? :current
|
128
|
+
|
129
|
+
def initialize(data)
|
130
|
+
super(data)
|
131
|
+
@index = @data['index']
|
132
|
+
@directory = @data['directory']
|
133
|
+
@has_deletions = @index['hasDeletions']
|
134
|
+
@optimized = @index['optimized']
|
135
|
+
@current = @index['current']
|
136
|
+
@max_doc = @index['maxDoc']
|
137
|
+
@num_docs = @index['numDocs']
|
138
|
+
@version = @index['version']
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helpers')
|
2
|
+
|
3
|
+
class AdapterCommonMethodsTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
class DummyClass
|
6
|
+
include Solr::Adapter::CommonMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@c = DummyClass.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_default_options
|
14
|
+
target = {
|
15
|
+
:select_path => '/select',
|
16
|
+
:update_path => '/update',
|
17
|
+
:luke_path => '/admin/luke'
|
18
|
+
}
|
19
|
+
assert_equal target, @c.default_options
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_build_url
|
23
|
+
m = @c.method(:build_url)
|
24
|
+
assert_equal '/something', m.call('/something')
|
25
|
+
assert_equal '/something?q=Testing', m.call('/something', :q=>'Testing')
|
26
|
+
assert_equal '/something?array=1&array=2&array=3', m.call('/something', :array=>[1, 2, 3])
|
27
|
+
assert_equal '/something?array=1&array=2&array=3&q=A', m.call('/something', :q=>'A', :array=>[1, 2, 3])
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_escape
|
31
|
+
assert_equal '%2B', @c.escape('+')
|
32
|
+
assert_equal 'This+is+a+test', @c.escape('This is a test')
|
33
|
+
assert_equal '%3C%3E%2F%5C', @c.escape('<>/\\')
|
34
|
+
assert_equal '%22', @c.escape('"')
|
35
|
+
assert_equal '%3A', @c.escape(':')
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_hash_to_params
|
39
|
+
my_params = {
|
40
|
+
:z=>'should be last',
|
41
|
+
:q=>'test',
|
42
|
+
:d=>[1, 2, 3, 4],
|
43
|
+
:b=>:zxcv,
|
44
|
+
:x=>['!', '*', nil]
|
45
|
+
}
|
46
|
+
assert_equal 'b=zxcv&d=1&d=2&d=3&d=4&q=test&x=%21&x=%2A&z=should+be+last', @c.hash_to_params(my_params)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# These are all of the test methods used by the various connection + adapter tests
|
2
|
+
# Currently: Direct and HTTP
|
3
|
+
# By sharing these tests, we can make sure the adapters are doing what they're suppossed to
|
4
|
+
# while staying "dry"
|
5
|
+
|
6
|
+
module ConnectionTestMethods
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
response = @solr.delete_by_query('id:[* TO *]')
|
10
|
+
assert_equal 0, @solr.query(:q=>'*:*').docs.size
|
11
|
+
end
|
12
|
+
|
13
|
+
# setting adapter options in Solr.connect method should set them in the adapter
|
14
|
+
def test_set_adapter_options
|
15
|
+
solr = Solr.connect(:http, :select_path=>'/select2')
|
16
|
+
assert_equal '/select2', solr.adapter.opts[:select_path]
|
17
|
+
end
|
18
|
+
|
19
|
+
# setting connection options in Solr.connect method should set them in the connection
|
20
|
+
def test_set_connection_options
|
21
|
+
solr = Solr.connect(:http, {}, :default_wt=>:json)
|
22
|
+
assert_equal :json, solr.opts[:default_wt]
|
23
|
+
end
|
24
|
+
|
25
|
+
# If :wt is NOT :ruby, the format doesn't get wrapped in a Solr::Response class
|
26
|
+
# Raw ruby can be returned by using :wt=>'ruby', not :ruby
|
27
|
+
def test_raw_response_formats
|
28
|
+
ruby_response = @solr.query(:q=>'*:*', :wt=>'ruby')
|
29
|
+
assert ruby_response.is_a?(String)
|
30
|
+
assert ruby_response=~%r('wt'=>'ruby')
|
31
|
+
# xml?
|
32
|
+
xml_response = @solr.query(:q=>'*:*', :wt=>'xml')
|
33
|
+
assert xml_response=~%r(<str name="wt">xml</str>)
|
34
|
+
# json?
|
35
|
+
json_response = @solr.query(:q=>'*:*', :wt=>'json')
|
36
|
+
assert json_response=~%r("wt":"json")
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_query_responses
|
40
|
+
r = @solr.query(:q=>'*:*')
|
41
|
+
assert r.is_a?(Solr::Response::Query)
|
42
|
+
# catch exceptions for bad queries
|
43
|
+
assert_raise Solr::RequestError do
|
44
|
+
@solr.query(:q=>'!')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_add
|
49
|
+
assert_equal 0, @solr.query(:q=>'*:*').total
|
50
|
+
response = @solr.add(:id=>100)
|
51
|
+
assert_equal 1, @solr.query(:q=>'*:*').total
|
52
|
+
assert response.is_a?(Solr::Response::Update)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_delete_by_id
|
56
|
+
@solr.add(:id=>100)
|
57
|
+
total = @solr.query(:q=>'*:*').total
|
58
|
+
assert_equal 1, total
|
59
|
+
delete_response = @solr.delete_by_id(100)
|
60
|
+
assert delete_response.is_a?(Solr::Response::Update)
|
61
|
+
total = @solr.query(:q=>'*:*').total
|
62
|
+
assert_equal 0, total
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_delete_by_query
|
66
|
+
@solr.add(:id=>1, :name=>'BLAH BLAH BLAH')
|
67
|
+
assert_equal 1, @solr.query(:q=>'*:*').total
|
68
|
+
response = @solr.delete_by_query('name:BLAH BLAH BLAH')
|
69
|
+
assert response.is_a?(Solr::Response::Update)
|
70
|
+
assert_equal 0, @solr.query(:q=>'*:*').total
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_index_info
|
74
|
+
response = @solr.index_info
|
75
|
+
assert response.is_a?(Solr::Response::IndexInfo)
|
76
|
+
# make sure the ? methods are true/false
|
77
|
+
assert [true, false].include?(response.current?)
|
78
|
+
assert [true, false].include?(response.optimized?)
|
79
|
+
assert [true, false].include?(response.has_deletions?)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|