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.
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +20 -0
- data/README +32 -0
- data/Rakefile +186 -0
- data/TODO +0 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/acts_as_searchable.rb +360 -0
- data/lib/vendor/estraierpure.rb +1025 -0
- data/lib/vendor/overview +100 -0
- data/rdoc/classes/ActiveRecord/Acts/Searchable.html +182 -0
- data/rdoc/classes/ActiveRecord/Acts/Searchable/ActMethods.html +233 -0
- data/rdoc/classes/ActiveRecord/Acts/Searchable/ClassMethods.html +387 -0
- data/rdoc/classes/EstraierPure/Condition.html +619 -0
- data/rdoc/classes/EstraierPure/Document.html +551 -0
- data/rdoc/classes/EstraierPure/Node.html +1172 -0
- data/rdoc/classes/EstraierPure/NodeResult.html +248 -0
- data/rdoc/classes/EstraierPure/ResultDocument.html +307 -0
- data/rdoc/created.rid +1 -0
- data/rdoc/files/README.html +165 -0
- data/rdoc/files/lib/acts_as_searchable_rb.html +140 -0
- data/rdoc/files/lib/vendor/estraierpure_rb.html +253 -0
- data/rdoc/fr_class_index.html +34 -0
- data/rdoc/fr_file_index.html +29 -0
- data/rdoc/fr_method_index.html +97 -0
- data/rdoc/index.html +24 -0
- data/rdoc/rdoc-style.css +208 -0
- data/tasks/acts_as_searchable_tasks.rake +21 -0
- data/test/abstract_unit.rb +34 -0
- data/test/acts_as_searchable_test.rb +153 -0
- data/test/database.yml +3 -0
- data/test/fixtures/article.rb +5 -0
- data/test/fixtures/articles.yml +23 -0
- data/test/fixtures/comment.rb +4 -0
- data/test/fixtures/comments.yml +4 -0
- data/test/schema.rb +14 -0
- metadata +96 -0
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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'
|
data/install.rb
ADDED
@@ -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
|