mwmitchell-solr-ruby 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,122 @@
1
+ #
2
+ # Connection adapter decorator
3
+ #
4
+ class Solr::Connection::Base
5
+
6
+ attr_reader :adapter, :opts
7
+
8
+ include Solr::Connection::SearchExt
9
+
10
+ # conection is instance of:
11
+ # Solr::Adapter::HTTP
12
+ # Solr::Adapter::Direct (jRuby only)
13
+ def initialize(adapter, opts={})
14
+ @adapter=adapter
15
+ opts[:auto_commit]||=false
16
+ opts[:global_params]||={}
17
+ default_global_params = {
18
+ :wt=>:ruby,
19
+ :echoParams=>'EXPLICIT',
20
+ :debugQuery=>true
21
+ }
22
+ opts[:global_params] = default_global_params.merge(opts[:global_params])
23
+ @opts=opts
24
+ end
25
+
26
+ # sets default params etc.. - could be used as a mapping hook
27
+ # type of request should be passed in here? -> map_params(:query, {})
28
+ def map_params(params)
29
+ opts[:global_params].dup.merge(params).dup
30
+ end
31
+
32
+ # send request to the select handler
33
+ # params is hash with valid solr request params (:q, :fl, :qf etc..)
34
+ # if params[:wt] is not set, the default is :ruby (see opts[:global_params])
35
+ # if :wt is something other than :ruby, the raw response body is returned
36
+ # otherwise, an instance of Solr::Response::Query is returned
37
+ # NOTE: to get raw ruby, use :wt=>'ruby'
38
+ def query(params)
39
+ params = map_params(modify_params_for_pagination(params))
40
+ response = @adapter.query(params)
41
+ params[:wt]==:ruby ? Solr::Response::Query.new(response) : response
42
+ end
43
+
44
+ # Finds a document by its id
45
+ def find_by_id(id, params={})
46
+ params = map_params(params)
47
+ params[:q] = 'id:"#{id}"'
48
+ query params
49
+ end
50
+
51
+ def index_info(params={})
52
+ params = map_params(params)
53
+ response = @adapter.index_info(params)
54
+ params[:wt] == :ruby ? Solr::Response::IndexInfo.new(response) : response
55
+ end
56
+
57
+ # if :ruby is the :wt, then Solr::Response::Base is returned
58
+ # -- there's not really a way to figure out what kind of handler request this is.
59
+
60
+ def update(data, params={}, auto_commit=nil)
61
+ params = map_params(params)
62
+ response = @adapter.update(data, params)
63
+ self.commit if auto_commit.nil? ? @opts[:auto_commit]==true : auto_commit
64
+ params[:wt]==:ruby ? Solr::Response::Update.new(response) : response
65
+ end
66
+
67
+ def add(hash_or_array, opts={}, &block)
68
+ update message.add(hash_or_array, opts, &block)
69
+ end
70
+
71
+ # send </commit>
72
+ def commit(opts={})
73
+ update message.commit, opts, false
74
+ end
75
+
76
+ # send </optimize>
77
+ def optimize(opts={})
78
+ update message.optimize, opts
79
+ end
80
+
81
+ # send </rollback>
82
+ # NOTE: solr 1.4 only
83
+ def rollback(opts={})
84
+ update message.rollback, opts
85
+ end
86
+
87
+ # Delete one or many documents by id
88
+ # solr.delete_by_id 10
89
+ # solr.delete_by_id([12, 41, 199])
90
+ def delete_by_id(ids, opts={})
91
+ update message.delete_by_id(ids), opts
92
+ end
93
+
94
+ # delete one or many documents by query
95
+ # solr.delete_by_query 'available:0'
96
+ # solr.delete_by_query ['quantity:0', 'manu:"FQ"']
97
+ def delete_by_query(queries, opts={})
98
+ update message.delete_by_query(queries), opts
99
+ end
100
+
101
+ protected
102
+
103
+ # shortcut to solr::message
104
+ def message
105
+ Solr::Message
106
+ end
107
+
108
+ def modify_params_for_pagination(params)
109
+ return params unless params[:page]
110
+ params = params.dup # be nice
111
+ params[:per_page]||=10
112
+ params[:rows] = params.delete(:per_page).to_i
113
+ params[:start] = calculate_start(params.delete(:page).to_i, params[:rows])
114
+ params
115
+ end
116
+
117
+ def calculate_start(current_page, per_page)
118
+ page = current_page > 0 ? current_page : 1
119
+ (page - 1) * per_page
120
+ end
121
+
122
+ end
@@ -0,0 +1,110 @@
1
+ module Solr::Connection::SearchExt
2
+
3
+ def search(query, params={})
4
+ if params[:fields].is_a?(Array)
5
+ params[:fl] = params.delete(:fields).join(' ')
6
+ else
7
+ params[:fl] = params.delete :fields
8
+ end
9
+ fq = build_filters(params.delete(:filters)).join(' ') if params[:filters]
10
+ if params[:fq] and fq
11
+ params[:fq] += " AND #{fq}"
12
+ else
13
+ params[:fq] = fq
14
+ end
15
+ facets = params.delete(:facets) if params[:facets]
16
+ if facets
17
+ if facets.is_a?(Array)
18
+ params << {:facet => true}
19
+ params += build_facets(facets)
20
+ elsif facets.is_a?(Hash)
21
+ params << {:facet => true}
22
+ params += build_facet(facets)
23
+ elsif facets.is_a?(String)
24
+ params += facets
25
+ else
26
+ raise 'facets must either be a Hash or an Array'
27
+ end
28
+ end
29
+ params[:qt] ||= :dismax
30
+ self.query params
31
+ end
32
+
33
+ protected
34
+
35
+ # returns the query param
36
+ def build_query(queries)
37
+ query_string = ''
38
+ case queries
39
+ when String
40
+ query_string = queries
41
+ when Array
42
+ query_string = queries.join(' ')
43
+ when Hash
44
+ query_string_array = []
45
+ queries.each do |k,v|
46
+ if v.is_a?(Array) # add a filter for each value
47
+ v.each do |val|
48
+ query_string_array << "#{k}:#{val}"
49
+ end
50
+ elsif v.is_a?(Range)
51
+ query_string_array << "#{k}:[#{v.min} TO #{v.max}]"
52
+ else
53
+ query_string_array << "#{k}:#{v}"
54
+ end
55
+ end
56
+ query_string = query_string_array.join(' ')
57
+ end
58
+ query_string
59
+ end
60
+
61
+ def build_filters(filters)
62
+ params = []
63
+ # handle "ruby-ish" filters
64
+ case filters
65
+ when String
66
+ params << filters
67
+ when Array
68
+ filters.each { |f| params << f }
69
+ when Hash
70
+ filters.each do |k,v|
71
+ if v.is_a?(Array) # add a filter for each value
72
+ v.each do |val|
73
+ params << "#{k}:#{val}"
74
+ end
75
+ elsif v.is_a?(Range)
76
+ params << "#{k}:[#{v.min} TO #{v.max}]"
77
+ else
78
+ params << "#{k}:#{v}"
79
+ end
80
+ end
81
+ end
82
+ params
83
+ end
84
+
85
+ def build_facets(facet_array)
86
+ facet_array.inject([]) do |params, facet_hash|
87
+ params.push build_facet(facet_hash)
88
+ end
89
+ end
90
+
91
+ def build_facet(facet_hash)
92
+ params = []
93
+ facet_name = facet_hash['name'] || facet_hash[:name]
94
+ facet_hash.each do |k,v|
95
+ # handle some cases specially
96
+ if 'field' == k.to_s
97
+ params << {"facet.field" => v}
98
+ elsif 'query' == k.to_s
99
+ q = build_query("facet.query", v)
100
+ params << q
101
+ elsif ['name', :name].include?(k.to_s)
102
+ # do nothing
103
+ else
104
+ params << {"f.#{facet_hash[:field]}.facet.#{k}" => v}
105
+ end
106
+ end
107
+ params
108
+ end
109
+
110
+ end
@@ -0,0 +1,7 @@
1
+ module Solr::Connection
2
+
3
+ autoload :Base, 'solr/connection/base'
4
+ autoload :SearchExt, 'solr/connection/search_ext'
5
+ autoload :PaginationExt, 'solr/connection/pagination_ext'
6
+
7
+ end
@@ -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.rb ADDED
@@ -0,0 +1,36 @@
1
+ # add this directory to the load path if it hasn't already been added
2
+ # load xout and rfuzz libs
3
+ proc {|base, files|
4
+ $: << base unless $:.include?(base) || $:.include?(File.expand_path(base))
5
+ files.each {|f| require f}
6
+ }.call(File.dirname(__FILE__), ['core_ext'])
7
+
8
+ module Solr
9
+
10
+ VERSION = '0.5.0'
11
+
12
+ autoload :Adapter, 'solr/adapter'
13
+ autoload :Message, 'solr/message'
14
+ autoload :Response, 'solr/response'
15
+ autoload :Connection, 'solr/connection'
16
+ autoload :Ext, 'solr/ext'
17
+ autoload :Mapper, 'solr/mapper'
18
+ autoload :Indexer, 'solr/indexer'
19
+
20
+ # factory for creating connections
21
+ # adapter name is either :http or :direct
22
+ # adapter_opts are sent to the adapter instance (:url for http, :dist_dir for :direct etc.)
23
+ # connection_opts are sent to the connection instance (:auto_commit etc.)
24
+ def self.connect(adapter_name, adapter_opts={}, wrapper_opts={})
25
+ types = {
26
+ :http=>'HTTP',
27
+ :direct=>'Direct'
28
+ }
29
+ adapter_class_name = "Solr::Adapter::#{types[adapter_name]}"
30
+ adapter_class = Kernel.eval adapter_class_name
31
+ Solr::Connection::Base.new(adapter_class.new(adapter_opts), wrapper_opts)
32
+ end
33
+
34
+ class RequestError < RuntimeError; end
35
+
36
+ end
data/solr-ruby.gemspec ADDED
@@ -0,0 +1,50 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "solr-ruby"
3
+ s.version = "0.5.0"
4
+ s.date = "2008-12-16"
5
+ s.summary = "Ruby client for Apache Solr"
6
+ s.email = "goodieboy@gmail.com"
7
+ s.homepage = "http://github.com/mwmitchell/solr-ruby"
8
+ s.description = "solr-ruby is a Ruby gem for working with Apache Solr"
9
+ s.has_rdoc = true
10
+ s.authors = ["Matt Mitchell"]
11
+ s.files = [
12
+ "examples/http.rb",
13
+ "examples/direct.rb",
14
+ "lib/solr.rb",
15
+ "lib/core_ext.rb",
16
+ "lib/solr/adapter.rb",
17
+ "lib/solr/adapter/common_methods.rb",
18
+ "lib/solr/adapter/direct.rb",
19
+ "lib/solr/adapter/http.rb",
20
+ "lib/solr/connection.rb",
21
+ "lib/solr/connection/base.rb",
22
+ "lib/solr/connection/search_ext.rb",
23
+ "lib/solr/indexer.rb",
24
+ "lib/mapper.rb",
25
+ "lib/mapper/rss.rb",
26
+ "lib/message.rb",
27
+ "lib/response.rb",
28
+ "LICENSE",
29
+ "Rakefile",
30
+ "README.rdoc",
31
+ "solr-ruby.gemspec"
32
+ ]
33
+ s.test_files = [
34
+ "test/adapter_common_methods_test.rb",
35
+ "test/connection_test_methods.rb",
36
+ "test/direct_test.rb",
37
+ "test/ext_pagination_test.rb",
38
+ "test/ext_params_test.rb",
39
+ "test/ext_search_test.rb",
40
+ "test/http_test.rb",
41
+ "test/indexer_test.rb",
42
+ "test/mapper_test.rb",
43
+ "test/message_test.rb",
44
+ "test/ruby-lang.org.rss.xml",
45
+ "test/test_helpers.rb",
46
+ ]
47
+ s.rdoc_options = ["--main", "README.rdoc"]
48
+ s.extra_rdoc_files = []
49
+ s.add_dependency("builder", [">= 2.1.2"])
50
+ 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
@@ -0,0 +1,20 @@
1
+ if defined?(JRUBY_VERSION)
2
+
3
+ require File.join(File.dirname(__FILE__), 'test_helpers')
4
+
5
+ require 'connection_test_methods'
6
+
7
+ class DirectTest < Test::Unit::TestCase
8
+
9
+ include ConnectionTestMethods
10
+
11
+ def setup
12
+ base = File.expand_path( File.dirname(__FILE__) )
13
+ dist = File.join(base, '..', 'apache-solr')
14
+ home = File.join(dist, 'example', 'solr')
15
+ @solr = Solr.connect(:direct, {:home_dir=>home, :dist_dir=>dist}, :auto_commit=>true)
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helpers')
2
+
3
+ class ExtPaginationTest < Test::Unit::TestCase
4
+
5
+ def create_response(params={})
6
+ response = Solr::Response::Query.new(mock_query_response)
7
+ response.params.merge! params
8
+ response
9
+ end
10
+
11
+ # test the Solr::Connection pagination methods
12
+ def test_connection_calculate_start
13
+ dp = Solr::Connection::Base.new(nil)
14
+ assert_equal 15, dp.send(:calculate_start, 2, 15)
15
+ assert_equal 450, dp.send(:calculate_start, 10, 50)
16
+ assert_equal 0, dp.send(:calculate_start, 0, 50)
17
+ end
18
+
19
+ def test_connection_modify_params_for_pagination
20
+ dp = Solr::Connection::Base.new(nil)
21
+ p = dp.send(:modify_params_for_pagination, {:page=>1})
22
+ assert_equal 0, p[:start]
23
+ assert_equal 10, p[:rows]
24
+ #
25
+ p = dp.send(:modify_params_for_pagination, {:page=>10, :per_page=>100})
26
+ assert_equal 900, p[:start]
27
+ assert_equal 100, p[:rows]
28
+ end
29
+
30
+ def test_math
31
+ response = create_response({'rows'=>5})
32
+ assert_equal response.params['rows'], response.per_page
33
+ assert_equal 26, response.total
34
+ assert_equal 1, response.current_page
35
+ assert_equal 6, response.page_count
36
+
37
+ # now switch the rows (per_page)
38
+ # total and current page should remain the same value
39
+ # page_count should change
40
+
41
+ response = create_response({'rows'=>2})
42
+ assert_equal response.params['rows'], response.per_page
43
+ assert_equal 26, response.total
44
+ assert_equal 1, response.current_page
45
+ assert_equal 13, response.page_count
46
+
47
+ # now switch the start
48
+
49
+ response = create_response({'rows'=>3})
50
+ response.instance_variable_set '@start', 4
51
+ assert_equal response.params['rows'], response.per_page
52
+ assert_equal 26, response.total
53
+ # 2 per page, currently on the 10th item
54
+ assert_equal 1, response.current_page
55
+ assert_equal 9, response.page_count
56
+ end
57
+
58
+ end
@@ -0,0 +1,9 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helpers')
2
+
3
+ class ExtSearchTest < Test::Unit::TestCase
4
+
5
+ def test_mapping_methods
6
+
7
+ end
8
+
9
+ end
data/test/http_test.rb ADDED
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helpers')
2
+
3
+ require 'connection_test_methods'
4
+
5
+ class HTTPTest < Test::Unit::TestCase
6
+
7
+ include ConnectionTestMethods
8
+
9
+ def setup
10
+ @solr = Solr.connect(:http, {}, :auto_commit=>true)
11
+ end
12
+
13
+ end
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helpers')
2
+
3
+ class IndexerTest < Test::Unit::TestCase
4
+
5
+ def test_something
6
+ data = nil
7
+ mapping = {
8
+
9
+ }
10
+ i = Solr::Indexer.new(Solr.connect(:http), mapping)
11
+ i.index([])
12
+ end
13
+
14
+ end