acts_as_searchable 0.1.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,3 @@
1
+ *0.1.0*
2
+
3
+ * Initial release
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005 Rick Olson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,32 @@
1
+ = acts_as_searchable
2
+
3
+ This library adds fulltext searching capabilities based on Hyper Estraier (http://hyperestraier.sourceforge.net/) to
4
+ an ActiveRecord model.
5
+
6
+ == Pre-requisites
7
+
8
+ A working Hyper Estraier instance, setup instructions:
9
+
10
+ * http://hyperestraier.sourceforge.net/nguide-en.html
11
+
12
+ == Resources
13
+
14
+ Install
15
+
16
+ * gem install acts_as_searchable
17
+
18
+ Bugtracking
19
+
20
+ * http://trac.poocs.net/projects/plugins
21
+
22
+ Rubyforge project
23
+
24
+ * http://rubyforge.org/projects/ar-searchable
25
+
26
+ RDocs
27
+
28
+ * http://ar-searchable.rubyforge.org
29
+
30
+ Subversion
31
+
32
+ * svn://poocs.net/plugins/acts_as_searchable
@@ -0,0 +1,186 @@
1
+ require 'rubygems'
2
+
3
+ Gem::manage_gems
4
+
5
+ require 'rake/rdoctask'
6
+ require 'rake/packagetask'
7
+ require 'rake/gempackagetask'
8
+ require 'rake/testtask'
9
+ require 'rake/contrib/rubyforgepublisher'
10
+
11
+ PKG_NAME = 'acts_as_searchable'
12
+ PKG_VERSION = '0.1.0'
13
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+ RUBY_FORGE_PROJECT = 'ar-searchable'
15
+ RUBY_FORGE_USER = 'scoop'
16
+
17
+ desc 'Default: run unit tests.'
18
+ task :default => :test
19
+
20
+ desc 'Test the acts_as_searchable plugin.'
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'lib'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = true
25
+ end
26
+
27
+ desc 'Generate documentation for the acts_as_searchable plugin.'
28
+ Rake::RDocTask.new(:rdoc) do |rdoc|
29
+ rdoc.rdoc_dir = 'rdoc'
30
+ rdoc.title = 'ActsAsSearchable'
31
+ rdoc.options << '--line-numbers' << '--inline-source'
32
+ rdoc.rdoc_files.include('README')
33
+ rdoc.rdoc_files.include('lib/**/*.rb')
34
+ end
35
+
36
+ spec = Gem::Specification.new do |s|
37
+ s.name = PKG_NAME
38
+ s.version = PKG_VERSION
39
+ s.platform = Gem::Platform::RUBY
40
+ s.summary = "acts_as_searchable adds fulltext searching capabilities based on Hyper Estraier to an ActiveRecord module."
41
+ s.files = Dir.glob('**/*', File::FNM_DOTMATCH).reject do |f|
42
+ [ /\.$/, /sqlite$/, /\.log$/, /^pkg/, /\.svn/,
43
+ /\~$/, /\/\._/, /\/#/ ].any? {|regex| f =~ regex }
44
+ end
45
+ #s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG)
46
+ s.files.delete "acts_as_searchable_plugin.sqlite.db"
47
+ s.files.delete "acts_as_searchable_plugin.sqlite3.db"
48
+ s.require_path = 'lib'
49
+ s.autorequire = 'acts_as_searchable'
50
+ s.has_rdoc = true
51
+ s.test_files = Dir['test/**/*_test.rb']
52
+ s.author = "Patrick Lenz"
53
+ s.email = "patrick@lenz.sh"
54
+ s.homepage = "http://trac.poocs.net/projects/plugins"
55
+ end
56
+
57
+ Rake::GemPackageTask.new(spec) do |pkg|
58
+ pkg.need_tar = true
59
+ end
60
+
61
+ desc "Publish the API documentation"
62
+ task :pdoc => [:rdoc] do
63
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
64
+ end
65
+
66
+ desc 'Publish the gem and API docs'
67
+ task :publish => [:pdoc, :rubyforge_upload]
68
+
69
+ desc "Publish the release files to RubyForge."
70
+ task :rubyforge_upload => :package do
71
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
72
+
73
+ if RUBY_FORGE_PROJECT then
74
+ require 'net/http'
75
+ require 'open-uri'
76
+
77
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
78
+ project_data = open(project_uri) { |data| data.read }
79
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
80
+ raise "Couldn't get group id" unless group_id
81
+
82
+ # This echos password to shell which is a bit sucky
83
+ if ENV["RUBY_FORGE_PASSWORD"]
84
+ password = ENV["RUBY_FORGE_PASSWORD"]
85
+ else
86
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
87
+ password = STDIN.gets.chomp
88
+ end
89
+
90
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
91
+ data = [
92
+ "login=Login",
93
+ "form_loginname=#{RUBY_FORGE_USER}",
94
+ "form_pw=#{password}"
95
+ ].join("&")
96
+
97
+ headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
98
+
99
+ http.post("/account/login.php", data, headers)
100
+ end
101
+
102
+ cookie = login_response["set-cookie"]
103
+ raise "Login failed" unless cookie
104
+ headers = { "Cookie" => cookie }
105
+
106
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
107
+ release_data = open(release_uri, headers) { |data| data.read }
108
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
109
+ raise "Couldn't get package id" unless package_id
110
+
111
+ first_file = true
112
+ release_id = ""
113
+
114
+ files.each do |filename|
115
+ basename = File.basename(filename)
116
+ file_ext = File.extname(filename)
117
+ file_data = File.open(filename, "rb") { |file| file.read }
118
+
119
+ puts "Releasing #{basename}..."
120
+
121
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
122
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
123
+ type_map = {
124
+ ".zip" => "3000",
125
+ ".tgz" => "3110",
126
+ ".gz" => "3110",
127
+ ".gem" => "1400"
128
+ }; type_map.default = "9999"
129
+ type = type_map[file_ext]
130
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
131
+
132
+ query_hash = if first_file then
133
+ {
134
+ "group_id" => group_id,
135
+ "package_id" => package_id,
136
+ "release_name" => PKG_FILE_NAME,
137
+ "release_date" => release_date,
138
+ "type_id" => type,
139
+ "processor_id" => "8000", # Any
140
+ "release_notes" => "",
141
+ "release_changes" => "",
142
+ "preformatted" => "1",
143
+ "submit" => "1"
144
+ }
145
+ else
146
+ {
147
+ "group_id" => group_id,
148
+ "release_id" => release_id,
149
+ "package_id" => package_id,
150
+ "step2" => "1",
151
+ "type_id" => type,
152
+ "processor_id" => "8000", # Any
153
+ "submit" => "Add This File"
154
+ }
155
+ end
156
+
157
+ data = [
158
+ "--" + boundary,
159
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
160
+ "Content-Type: application/octet-stream",
161
+ "Content-Transfer-Encoding: binary",
162
+ "", file_data, "",
163
+ query_hash.collect do |name, value|
164
+ [ "--" + boundary,
165
+ "Content-Disposition: form-data; name='#{name}'",
166
+ "", value, "" ]
167
+ end
168
+ ].flatten.join("\x0D\x0A")
169
+
170
+ release_headers = headers.merge(
171
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
172
+ )
173
+
174
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
175
+ http.post(target, data, release_headers)
176
+ end
177
+
178
+ if first_file then
179
+ release_id = release_response.body[/release_id=(\d+)/, 1]
180
+ raise("Couldn't get release id") unless release_id
181
+ end
182
+
183
+ first_file = false
184
+ end
185
+ end
186
+ end
data/TODO ADDED
File without changes
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_searchable'
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,360 @@
1
+ # Copyright (c) 2006 Patrick Lenz
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ #
22
+ # Thanks: Rick Olson (technoweenie) for his numerous plugins that served
23
+ # as an example
24
+
25
+ require 'vendor/estraierpure'
26
+
27
+ module ActiveRecord #:nodoc:
28
+ module Acts #:nodoc:
29
+ # Specify this act if you want to provide fulltext search capabilities to your model via Hyper Estraier. This
30
+ # assumes a setup and running Hyper Estraier node accessible through the HTTP API provided by the EstraierPure
31
+ # Ruby module (which is bundled with this plugin).
32
+ #
33
+ # The act supplies appropriate hooks to insert, update and remove documents from the index when you update your
34
+ # model data, create new objects or remove them from your database. For the initial indexing a convenience
35
+ # class method <tt>reindex!</tt> is provided.
36
+ #
37
+ # Example:
38
+ #
39
+ # class Article < ActiveRecord::Base
40
+ # acts_as_searchable
41
+ # end
42
+ #
43
+ # Article.reindex!
44
+ #
45
+ # As soon as your model data has been indexed you can make use of the <tt>fulltext_search</tt> class method
46
+ # to search the index and get back instantiated matches.
47
+ #
48
+ # results = Article.fulltext_search('rails')
49
+ # results.size # => 3
50
+ #
51
+ # results.first.class # => Article
52
+ # results.first.body # => "Ruby on Rails is an open-source web framework"
53
+ #
54
+ # Connectivity configuration can be either inherited from conventions or setup globally in the Rails
55
+ # database configuration file <tt>config/database.yml</tt>.
56
+ #
57
+ # Example:
58
+ #
59
+ # development:
60
+ # adapter: mysql
61
+ # database: rails_development
62
+ # host: localhost
63
+ # user: root
64
+ # password:
65
+ # estraier:
66
+ # host: localhost
67
+ # user: admin
68
+ # password: admin
69
+ # port: 1978
70
+ # node: development
71
+ #
72
+ # That way you can configure separate connections for each environment. The values shown above represent the
73
+ # defaults. If you don't need to change any of these it is safe to not specify the <tt>estraier</tt> hash
74
+ # at all.
75
+ #
76
+ # See ActiveRecord::Acts::Searchable::ClassMethods#acts_as_searchable for per-model configuration options
77
+ #
78
+ module Searchable
79
+ def self.included(base) #:nodoc:
80
+ base.extend ClassMethods
81
+ end
82
+
83
+ module ClassMethods
84
+ VALID_FULLTEXT_OPTIONS = [:limit, :offset, :order, :attributes, :raw_matches, :find]
85
+
86
+ # == Configuration options
87
+ #
88
+ # * <tt>searchable_fields</tt> - Fields to provide searching and indexing for (default: 'body')
89
+ # * <tt>attributes</tt> - Additional attributes to store in Hyper Estraier with the appropriate method supplying the value
90
+ # * <tt>if_changed</tt> - Extra list of attributes to add to the list of attributes that trigger an index update when changed
91
+ #
92
+ # Examples:
93
+ #
94
+ # acts_as_searchable :attributes => { :title => nil, :blog => :blog_title }, :searchable_fields => [ :title, :body ]
95
+ #
96
+ # This would store the return value of the <tt>title</tt> method in the <tt>title</tt> attribute and the return value of the
97
+ # <tt>blog_title</tt> method in the <tt>blog</tt> attribute. The contents of the <tt>title</tt> and <tt>body</tt> columns
98
+ # would end up being indexed for searching.
99
+ #
100
+ # == Attribute naming
101
+ #
102
+ # Attributes that match the reserved names of the Hyper Estraier system attributes are mapped automatically. This is something
103
+ # to keep in mind for custom ordering options or additional query constraints in <tt>fulltext_search</tt>
104
+ # For a list of these attributes see <tt>EstraierPure::SYSTEM_ATTRIBUTES</tt> or visit:
105
+ #
106
+ # http://hyperestraier.sourceforge.net/uguide-en.html#attributes
107
+ #
108
+ # From the example above:
109
+ #
110
+ # Model.fulltext_search('query', :order => '@title STRA') # Returns results ordered by title in ascending order
111
+ # Model.fulltext_search('query', :attributes => 'blog STREQ poocs.net') # Returns results with a blog attribute of 'poocs.net'
112
+ #
113
+ def acts_as_searchable(options = {})
114
+ return if self.included_modules.include?(ActiveRecord::Acts::Searchable::ActMethods)
115
+
116
+ send :include, ActiveRecord::Acts::Searchable::ActMethods
117
+
118
+ cattr_accessor :searchable_fields, :attributes_to_store, :if_changed, :estraier_connection, :estraier_node,
119
+ :estraier_host, :estraier_port, :estraier_user, :estraier_password
120
+
121
+ self.estraier_node = estraier_config['node'] || RAILS_ENV
122
+ self.estraier_host = estraier_config['host'] || 'localhost'
123
+ self.estraier_port = estraier_config['port'] || 1978
124
+ self.estraier_user = estraier_config['user'] || 'admin'
125
+ self.estraier_password = estraier_config['password'] || 'admin'
126
+ self.searchable_fields = options[:searchable_fields] || [ :body ]
127
+ self.attributes_to_store = options[:attributes] || {}
128
+ self.if_changed = options[:if_changed] || []
129
+
130
+ send :attr_accessor, :changed_attributes
131
+
132
+ class_eval do
133
+ after_update :update_index
134
+ after_create :add_to_index
135
+ after_destroy :remove_from_index
136
+ after_save :clear_changed_attributes
137
+
138
+ (if_changed + searchable_fields + attributes_to_store.collect { |attribute, method| method or attribute }).each do |attr_name|
139
+ define_method("#{attr_name}=") do |value|
140
+ write_changed_attribute attr_name, value
141
+ end
142
+ end
143
+
144
+ connect_estraier
145
+ end
146
+ end
147
+
148
+ # Perform a fulltext search against the Hyper Estraier index.
149
+ #
150
+ # Options taken:
151
+ # * <tt>limit</tt> - Maximum number of records to retrieve (default: <tt>100</tt>)
152
+ # * <tt>offset</tt> - Number of records to skip (default: <tt>0</tt>)
153
+ # * <tt>order</tt> - Hyper Estraier expression to sort the results (example: <tt>@title STRA</tt>, default: ordering by score)
154
+ # * <tt>attributes</tt> - String to append to Hyper Estraier search query
155
+ # * <tt>raw_matches</tt> - Returns raw Hyper Estraier documents instead of instantiated AR objects
156
+ # * <tt>find</tt> - Options to pass on to the <tt>ActiveRecord::Base#find</tt> call
157
+ #
158
+ # Examples:
159
+ #
160
+ # Article.fulltext_search("biscuits AND gravy")
161
+ # Article.fulltext_search("biscuits AND gravy", :limit => 15, :offset => 14)
162
+ # Article.fulltext_search("biscuits AND gravy", :attributes => "tag STRINC food")
163
+ # Article.fulltext_search("biscuits AND gravy", :attributes => ["tag STRINC food", "@title STRBW Biscuit"])
164
+ # Article.fulltext_search("biscuits AND gravy", :order => "@title STRA")
165
+ # Article.fulltext_search("biscuits AND gravy", :raw_matches => true)
166
+ # Article.fulltext_search("biscuits AND gravy", :find => { :order => :title, :include => :comments })
167
+ #
168
+ # Consult the Hyper Estraier documentation on proper query syntax:
169
+ #
170
+ # http://hyperestraier.sourceforge.net/uguide-en.html#searchcond
171
+ #
172
+ def fulltext_search(query = "", options = {})
173
+ options.reverse_merge!(:limit => 100, :offset => 0)
174
+ options.assert_valid_keys(VALID_FULLTEXT_OPTIONS)
175
+
176
+ find_options = options[:find] || {}
177
+ [ :limit, :offset ].each { |k| find_options.delete(k) } unless find_options.blank?
178
+
179
+ cond = EstraierPure::Condition.new
180
+ cond.set_phrase query
181
+ cond.add_attr("type STREQ #{self.to_s}")
182
+ [options[:attributes]].flatten.reject { |a| a.blank? }.each do |attr|
183
+ cond.add_attr attr
184
+ end
185
+ cond.set_max options[:limit]
186
+ cond.set_skip options[:offset]
187
+ cond.set_order options[:order] if options[:order]
188
+
189
+ matches = nil
190
+ seconds = Benchmark.realtime do
191
+ result = estraier_connection.search(cond, 1);
192
+ return [] unless result
193
+
194
+ matches = get_docs_from(result)
195
+ return matches if options[:raw_matches]
196
+ end
197
+
198
+ logger.debug(
199
+ connection.send(:format_log_entry,
200
+ "#{self.to_s} seach for '#{query}' (#{sprintf("%f", seconds)})",
201
+ "Condition: #{cond.to_s}"
202
+ )
203
+ )
204
+
205
+ matches.blank? ? [] : find(matches.collect { |m| m.attr('db_id') }, find_options)
206
+ end
207
+
208
+ # Clear all entries from index
209
+ def clear_index!
210
+ estraier_index.each { |d| estraier_connection.out_doc(d.attr('@id')) unless d.nil? }
211
+ end
212
+
213
+ # Peform a full re-index of the model data for this model
214
+ def reindex!
215
+ find(:all).each { |r| r.update_index(true) }
216
+ end
217
+
218
+ def estraier_index #:nodoc:
219
+ cond = EstraierPure::Condition::new
220
+ cond.add_attr("type STREQ #{self.to_s}")
221
+ result = estraier_connection.search(cond, 1)
222
+ docs = get_docs_from(result)
223
+ docs
224
+ end
225
+
226
+ def get_docs_from(result) #:nodoc:
227
+ docs = []
228
+ for i in 0...result.doc_num
229
+ docs << result.get_doc(i)
230
+ end
231
+ docs
232
+ end
233
+
234
+ protected
235
+
236
+ def connect_estraier #:nodoc:
237
+ self.estraier_connection = EstraierPure::Node::new
238
+ self.estraier_connection.set_url("http://#{self.estraier_host}:#{self.estraier_port}/node/#{self.estraier_node}")
239
+ self.estraier_connection.set_auth(self.estraier_user, self.estraier_password)
240
+ end
241
+
242
+ def estraier_config #:nodoc:
243
+ configurations[RAILS_ENV]['estraier'] or {}
244
+ end
245
+ end
246
+
247
+ module ActMethods
248
+ def self.included(base) #:nodoc:
249
+ base.extend ClassMethods
250
+ end
251
+
252
+ # Update index for current instance
253
+ def update_index(force = false)
254
+ return unless changed? or force
255
+ remove_from_index
256
+ add_to_index
257
+ end
258
+
259
+ # Retrieve index record for current model object
260
+ def estraier_doc
261
+ cond = EstraierPure::Condition::new
262
+ cond.add_attr("db_id STREQ #{self.id}")
263
+ cond.add_attr("type STREQ #{self.class.to_s}")
264
+ result = self.estraier_connection.search(cond, 1)
265
+ return unless result and result.doc_num > 0
266
+ get_doc_from(result)
267
+ end
268
+
269
+ # If called with no parameters, gets whether the current model has changed and needs to updated in the index.
270
+ # If called with a single parameter, gets whether the parameter has changed.
271
+ def changed?(attr_name = nil)
272
+ changed_attributes and (attr_name.nil? ?
273
+ (not changed_attributes.length.zero?) : (changed_attributes.include?(attr_name.to_s)) )
274
+ end
275
+
276
+ protected
277
+
278
+ def clear_changed_attributes #:nodoc:
279
+ self.changed_attributes = []
280
+ end
281
+
282
+ def write_changed_attribute(attr_name, attr_value) #:nodoc:
283
+ (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) or self.send(attr_name) == attr_value
284
+ write_attribute(attr_name.to_s, attr_value)
285
+ end
286
+
287
+ def add_to_index #:nodoc:
288
+ seconds = Benchmark.realtime { estraier_connection.put_doc(document_object) }
289
+ logger.debug "#{self.class.to_s} [##{id}] Adding to index (#{sprintf("%f", seconds)})"
290
+
291
+ end
292
+
293
+ def remove_from_index #:nodoc:
294
+ return unless doc = estraier_doc
295
+ seconds = Benchmark.realtime { self.estraier_connection.out_doc(doc.attr('@id')) }
296
+ logger.debug "#{self.class.to_s} [##{id}] Removing from index (#{sprintf("%f", seconds)})"
297
+ end
298
+
299
+ def get_doc_from(result) #:nodoc:
300
+ self.class.get_docs_from(result).first
301
+ end
302
+
303
+ def document_object #:nodoc:
304
+ doc = EstraierPure::Document::new
305
+ doc.add_attr('db_id', "#{id}")
306
+ doc.add_attr('type', "#{self.class.to_s}")
307
+ doc.add_attr('@uri', "/#{self.class.to_s}/#{id}")
308
+
309
+ unless attributes_to_store.blank?
310
+ attributes_to_store.each do |attribute, method|
311
+ value = send(method || attribute)
312
+ value = value.xmlschema if value.is_a?(Time)
313
+ doc.add_attr(attribute_name(attribute), send(method || attribute).to_s)
314
+ end
315
+ end
316
+
317
+ searchable_fields.each do |f|
318
+ doc.add_text send(f)
319
+ end
320
+
321
+ doc
322
+ end
323
+
324
+ def attribute_name(attribute)
325
+ EstraierPure::SYSTEM_ATTRIBUTES.include?(attribute.to_s) ? "@#{attribute}" : "#{attribute}"
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Searchable
333
+
334
+ module EstraierPure
335
+ unless defined?(SYSTEM_ATTRIBUTES)
336
+ SYSTEM_ATTRIBUTES = %w( uri digest cdate mdate adate title author type lang genre size weight misc )
337
+ end
338
+
339
+ class Node
340
+ def list
341
+ return false unless @url
342
+ turl = @url + "/list"
343
+ reqheads = [ "Content-Type: application/x-www-form-urlencoded" ]
344
+ reqheads.push("Authorization: Basic " + Utility::base_encode(@auth)) if @auth
345
+ reqbody = ""
346
+ resbody = StringIO::new
347
+ rv = Utility::shuttle_url(turl, @pxhost, @pxport, @timeout, reqheads, reqbody, nil, resbody)
348
+ @status = rv
349
+ return nil if rv != 200
350
+ lines = resbody.string.split(/\n/)
351
+ lines.collect { |l| val = l.split(/\t/) and { :id => val[0], :uri => val[1], :digest => val[2] } }
352
+ end
353
+ end
354
+
355
+ class Condition
356
+ def to_s
357
+ "phrase: %s, attrs: %s, max: %s, options: %s, order: %s, skip: %s" % [ phrase, attrs * ', ', max, options, order, skip ]
358
+ end
359
+ end
360
+ end