acts_as_searchable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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