solr_mapper 0.1.3

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,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
+