solr_mapper 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,225 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module SolrMapper
16
+ module SolrDocument
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
19
+ base.solr_fields = []
20
+ base.has_many_relationships = {}
21
+ end
22
+
23
+ module ClassMethods
24
+ attr_accessor :base_url
25
+ attr_accessor :per_page
26
+ attr_accessor :solr_fields
27
+
28
+ def execute_read(opts)
29
+ eval(RestClient.get(build_url('select', opts.merge(:wt => 'ruby'))))
30
+ end
31
+
32
+ def execute_write(data, opts = nil)
33
+ send_update(data, opts)
34
+
35
+ # make an xml commit message Solr will be happy with
36
+ commit_message = ''
37
+ builder = Builder::XmlMarkup.new(:target => commit_message, :indent => 2)
38
+ builder.commit('waitFlush' => true, 'waitSearcher' => true)
39
+
40
+ send_update(commit_message)
41
+ end
42
+
43
+ def build_url(path, opts = nil)
44
+ qs = build_qs(opts)
45
+ qs = '?' + qs unless qs.empty?
46
+
47
+ URI::escape("#{base_url}/#{path}#{qs}")
48
+ end
49
+
50
+ def build_qs(opts)
51
+ uri = ''
52
+
53
+ if opts
54
+ opts.each_pair do |k, v|
55
+ uri << "#{k}=#{v}&"
56
+ end
57
+ end
58
+
59
+ uri.chop
60
+ end
61
+
62
+ def search(values, opts = {})
63
+ puts "Warning, the search method is deprecated and will be removed in a future release."
64
+ puts "Instead use 'query' which can accept a string."
65
+
66
+ query(values, opts)
67
+ end
68
+
69
+ def query(values, opts = {})
70
+ results, _ = query_counted(values, opts)
71
+ results
72
+ end
73
+
74
+ def query_counted(values, opts = {})
75
+ if values.kind_of?(Hash)
76
+ search_string = ''
77
+
78
+ values.each_pair do |k, v|
79
+ search_string << "#{k}:#{v} "
80
+ end
81
+
82
+ search_string = search_string.chop
83
+ else
84
+ search_string = values.to_s
85
+ end
86
+
87
+ response = execute_read(opts.merge(:q => search_string))
88
+ return map(response), response['response']['numFound']
89
+ end
90
+
91
+ def find(id)
92
+ result = query(:id => id)
93
+ return result[0] if result.count > 0
94
+ end
95
+
96
+ # return will_paginate pages for search queries
97
+ def paginate(search_query, opts = {})
98
+ opts[:page] ||= 1
99
+ opts[:page] = opts[:page].to_i if opts[:page].respond_to?(:to_i)
100
+ opts[:rows] ||= per_page || 10
101
+ opts[:start] = (opts[:page] - 1) * opts[:rows]
102
+
103
+ WillPaginate::Collection.create(opts[:page], opts[:rows]) do |pager|
104
+ results, count = query_counted(search_query, opts)
105
+
106
+ if results
107
+ results.compact!
108
+
109
+ pager.total_entries = count
110
+ pager.replace(results)
111
+ return pager
112
+ end
113
+ end
114
+ end
115
+
116
+ protected
117
+ def map(docs)
118
+ objs = []
119
+
120
+ docs['response']['docs'].each do |doc|
121
+ obj = self.new
122
+
123
+ doc.each_pair do |k, v|
124
+ k = '_id' if k.to_s == 'id'
125
+
126
+ unless obj.class.method_defined?(k)
127
+ class_eval { attr_accessor k }
128
+ solr_fields << k.to_s unless solr_fields.include?(k.to_s)
129
+ end
130
+
131
+ obj.instance_variable_set("@#{k}", v)
132
+ end
133
+
134
+ objs << obj
135
+ end
136
+
137
+ objs
138
+ end
139
+
140
+ def send_update(data, opts = nil)
141
+ solr_resource = RestClient::Resource.new(build_url('update', opts))
142
+ solr_resource.post(data, {:content_type => 'text/xml'})
143
+ end
144
+ end
145
+
146
+ attr_accessor :_id
147
+
148
+ def method_missing(m, *args, &block)
149
+ method_name = m.to_s
150
+ assignment = method_name.match(/\=$/)
151
+ method_name = method_name.gsub(/\=/, '') if assignment
152
+ self.class.class_eval { attr_accessor method_name }
153
+ send "#{method_name}=", args[0] if assignment
154
+ end
155
+
156
+ def save()
157
+ self.class.execute_write(to_solr_xml, {:overwrite => true})
158
+ end
159
+
160
+ def destroy
161
+ # make an xml delete message that Solr will be happy with
162
+ delete_message = ''
163
+ builder = Builder::XmlMarkup.new(:target => delete_message, :indent => 2)
164
+
165
+ builder.delete do |delete|
166
+ delete.id(_id)
167
+ end
168
+
169
+ self.class.execute_write(delete_message)
170
+ end
171
+
172
+ def to_solr_xml
173
+ output = ''
174
+ builder = Builder::XmlMarkup.new(:target => output, :indent => 2)
175
+
176
+ builder.add do |add|
177
+ add.doc do |doc|
178
+ self.class.solr_fields.each do |field_name|
179
+ field_name = field_name.to_s
180
+
181
+ if field_name == '_id'
182
+ solr_field_name = 'id'
183
+ else
184
+ solr_field_name = field_name
185
+ end
186
+
187
+ val = instance_variable_get("@#{field_name}")
188
+
189
+ if val
190
+ if val.kind_of? Array
191
+ val.each do |child|
192
+ doc.field({:name => solr_field_name}, child)
193
+ end
194
+ else
195
+ doc.field({:name => solr_field_name}, val)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ output
203
+ end
204
+
205
+ def update_attributes_values(data)
206
+ data.each_pair do |k, v|
207
+ instance_variable_set("@#{k}", v)
208
+ self.class.solr_fields << k.to_s unless self.class.solr_fields.include?(k.to_s)
209
+ end
210
+ end
211
+
212
+ def update_attributes(data)
213
+ update_attributes_values(data)
214
+ save()
215
+ end
216
+
217
+ def to_param
218
+ instance_variable_get('@_id').to_s
219
+ end
220
+
221
+ def initialize(data = {})
222
+ update_attributes_values(data)
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{solr_mapper}
5
+ s.version = "0.1.3"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Chris Umbel"]
9
+ s.date = %q{2010-10-26}
10
+ s.description = %q{Object Document Mapper for the Apache Foundation's Solr search platform}
11
+ s.email = %q{chrisu@dvdempire.com}
12
+ s.extra_rdoc_files = ["LICENSE.txt", "README.rdoc", "lib/solr_mapper.rb", "lib/solr_mapper/calculations.rb", "lib/solr_mapper/locators.rb", "lib/solr_mapper/relations.rb", "lib/solr_mapper/solr_document.rb"]
13
+ s.files = ["LICENSE.txt", "Manifest", "README.rdoc", "Rakefile", "lib/solr_mapper.rb", "lib/solr_mapper/calculations.rb", "lib/solr_mapper/locators.rb", "lib/solr_mapper/relations.rb", "lib/solr_mapper/solr_document.rb", "solr_mapper.gemspec", "spec/base.rb", "spec/calculations_spec.rb", "spec/locators_spec.rb", "spec/misc_spec.rb", "spec/paginated_spec.rb", "spec/query_spec.rb", "spec/relation_spec.rb", "spec/solr/stuff/config/schema.xml", "spec/solr/thing/config/schema.xml", "spec/solr/widget/conf/schema.xml", "spec/url_spec.rb"]
14
+ s.homepage = %q{http://github.com/skunkworx/solr_mapper}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Solr_mapper", "--main", "README.rdoc"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{solr_mapper}
18
+ s.rubygems_version = %q{1.3.7}
19
+ s.summary = %q{Object Document Mapper for the Apache Foundation's Solr search platform}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<rest-client>, [">= 0"])
27
+ s.add_runtime_dependency(%q<uuid>, [">= 0"])
28
+ s.add_runtime_dependency(%q<will_paginate>, [">= 0"])
29
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<rest-client>, [">= 0"])
32
+ s.add_dependency(%q<uuid>, [">= 0"])
33
+ s.add_dependency(%q<will_paginate>, [">= 0"])
34
+ s.add_dependency(%q<activesupport>, [">= 0"])
35
+ end
36
+ else
37
+ s.add_dependency(%q<rest-client>, [">= 0"])
38
+ s.add_dependency(%q<uuid>, [">= 0"])
39
+ s.add_dependency(%q<will_paginate>, [">= 0"])
40
+ s.add_dependency(%q<activesupport>, [">= 0"])
41
+ end
42
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'rubygems'
16
+ require 'uuid'
17
+ require 'rest_client'
18
+ require File.dirname(__FILE__) + '/../lib/solr_mapper'
19
+
20
+ include SolrMapper
21
+
22
+ class Stuff
23
+ include SolrDocument
24
+ @base_url = 'http://localhost:8080/solr_stuff'
25
+ @per_page = 10
26
+
27
+ belongs_to :thing
28
+ end
29
+
30
+ class Widget
31
+ include SolrDocument
32
+ @base_url = 'http://localhost:8080/solr_widget'
33
+ @per_page = 10
34
+
35
+ belongs_to :thing
36
+ end
37
+
38
+ class Thing
39
+ include SolrDocument
40
+ @base_url = 'http://localhost:8080/solr_thing'
41
+ @per_page = 10
42
+
43
+ has_many :stuffs
44
+ has_one :widget
45
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require File.dirname(__FILE__) + '/base'
16
+ require 'rest_client'
17
+
18
+
19
+ describe SolrMapper::SolrDocument do
20
+ before(:each) do
21
+ url = Thing::base_url
22
+ resource = RestClient::Resource.new("#{url}/update")
23
+ resource.post("<delete><query>id:[* TO *]</query></delete>", {:content_type => 'text/xml'})
24
+ resource.post("<commit/>", {:content_type => 'text/xml'})
25
+
26
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add60', :content => 'sample content 1'})
27
+ thing.save
28
+
29
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add61', :content => 'sample content 2'})
30
+ thing.save
31
+ end
32
+
33
+ it "should count all records" do
34
+ Thing.count.should == 2
35
+
36
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add62', :content => 'sample content 2'})
37
+ thing.save
38
+
39
+ Thing.count.should == 3
40
+ end
41
+
42
+ it "should count records by query" do
43
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add62', :content => 'test'})
44
+ thing.save
45
+
46
+ Thing.count('test').should == 1
47
+ Thing.count('sample').should == 2
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require File.dirname(__FILE__) + '/base'
16
+
17
+ describe SolrMapper::SolrDocument do
18
+ before(:all) do
19
+ url = Thing::base_url
20
+ resource = RestClient::Resource.new("#{url}/update")
21
+ resource.post("<delete><query>id:[* TO *]</query></delete>", {:content_type => 'text/xml'})
22
+ resource.post("<commit/>", {:content_type => 'text/xml'})
23
+
24
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add60', :name => 'A', :content => 'sample content 1'})
25
+ thing.save
26
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add61', :name => 'B', :content => 'sample content 2'})
27
+ thing.save
28
+ end
29
+
30
+ it "should return the first item with no query" do
31
+ thing = Thing.first()
32
+ thing.content.should_not be(nil)
33
+ end
34
+
35
+ it "should return the first item with a search string" do
36
+ thing = Thing.first('*:*', {:sort => 'name desc'})
37
+ thing.content.should_not be(nil)
38
+ thing.content.should == 'sample content 2'
39
+ end
40
+ end
data/spec/misc_spec.rb ADDED
@@ -0,0 +1,34 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require File.dirname(__FILE__) + '/base'
16
+
17
+ include SolrMapper
18
+
19
+ describe SolrDocument do
20
+ before(:all) do
21
+ url = Thing::base_url
22
+ resource = RestClient::Resource.new("#{url}/update")
23
+ resource.post("<delete><query>id:[* TO *]</query></delete>", {:content_type => 'text/xml'})
24
+ resource.post("<commit/>", {:content_type => 'text/xml'})
25
+
26
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add60', :name => 'A', :content => 'sample content 1'})
27
+ thing.save
28
+ end
29
+
30
+ it "should match its ID to to_param" do
31
+ thing = Thing.first()
32
+ thing._id.should == thing.to_param()
33
+ end
34
+ end
@@ -0,0 +1,100 @@
1
+ # Copyright 2010 The Skunkworx.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require File.dirname(__FILE__) + '/base'
16
+
17
+ require 'rest_client'
18
+
19
+ describe SolrMapper::SolrDocument do
20
+ before(:all) do
21
+ url = Thing::base_url
22
+ resource = RestClient::Resource.new("#{url}/update")
23
+ resource.post("<delete><query>id:[* TO *]</query></delete>", {:content_type => 'text/xml'})
24
+ resource.post("<commit/>", {:content_type => 'text/xml'})
25
+
26
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add60', :content => 'sample content 1'})
27
+ thing.save
28
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add61', :content => 'sample content 2'})
29
+ thing.save
30
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add62', :content => 'sample content 3'})
31
+ thing.save
32
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add63', :content => 'sample content 4'})
33
+ thing.save
34
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add64', :content => 'sample content 5'})
35
+ thing.save
36
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add65', :content => 'sample content 6'})
37
+ thing.save
38
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add66', :content => 'sample content 7'})
39
+ thing.save
40
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add67', :content => 'sample content 8'})
41
+ thing.save
42
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add68', :content => 'sample content 9'})
43
+ thing.save
44
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add69', :content => 'sample content 10'})
45
+ thing.save
46
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add70', :content => 'sample content 11'})
47
+ thing.save
48
+ thing = Thing.new({:_id => '04787560-bc23-012d-817c-60334b2add11', :content => 'sample content 12'})
49
+ thing.save
50
+ end
51
+
52
+ it "should have correct default counts" do
53
+ page = Thing.paginate('*:*')
54
+ page.per_page.should == 10
55
+ page.total_entries.should == 12
56
+ page.total_pages.should == 2
57
+ page.current_page.should == 1
58
+ end
59
+
60
+ it "should have correct specified counts by search string" do
61
+ page1 = Thing.paginate('*:*', :rows => 5)
62
+ page2 = Thing.paginate('*:*', :rows => 5, :page => 2)
63
+ page3 = Thing.paginate('*:*', :rows => 5, :page => 3)
64
+
65
+ page1.total_pages.should == 3
66
+ page2.total_pages.should == 3
67
+ page3.total_pages.should == 3
68
+ page1.count.should == 5
69
+ page2.count.should == 5
70
+ page3.count.should == 2
71
+ end
72
+
73
+ it "should have correct specified counts by query" do
74
+ page1 = Thing.paginate({'*' => '*'}, {:rows => 5})
75
+ page2 = Thing.paginate({'*' => '*'}, {:rows => 5, :page => 2})
76
+ page3 = Thing.paginate({'*' => '*'}, {:rows => 5, :page => 3})
77
+
78
+ page1.total_pages.should == 3
79
+ page2.total_pages.should == 3
80
+ page3.total_pages.should == 3
81
+ page1.count.should == 5
82
+ page2.count.should == 5
83
+ page3.count.should == 2
84
+ end
85
+
86
+ it "should have a small page 2 bisect a page 1 twice its size by search string" do
87
+ big_page1 = Thing.paginate('*:*')
88
+ small_page2 = Thing.paginate('*:*', :rows => 5, :page => 2)
89
+
90
+ big_page1[5]._id.should == small_page2[0]._id
91
+ end
92
+
93
+ it "should have a small page 2 bisect a page 1 twice its size by query" do
94
+ big_page1 = Thing.paginate('*' => '*')
95
+ small_page2 = Thing.paginate({'*' => '*'}, {:rows => 5, :page => 2})
96
+
97
+ big_page1[5]._id.should == small_page2[0]._id
98
+ end
99
+ end
100
+