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.
@@ -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
@@ -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
@@ -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