fulltext_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.
Files changed (73) hide show
  1. data/.document +5 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.md +4 -0
  7. data/Rakefile +1 -0
  8. data/app/models/fulltext_index.rb +163 -0
  9. data/fulltext_searchable.gemspec +33 -0
  10. data/lib/fulltext_searchable/active_record.rb +208 -0
  11. data/lib/fulltext_searchable/engine.rb +4 -0
  12. data/lib/fulltext_searchable/mysql2_adapter.rb +64 -0
  13. data/lib/fulltext_searchable/version.rb +3 -0
  14. data/lib/fulltext_searchable.rb +50 -0
  15. data/lib/rails/generators/fulltext_searchable/fulltext_searchable_generator.rb +46 -0
  16. data/lib/rails/generators/fulltext_searchable/templates/initializer.rb +7 -0
  17. data/lib/rails/generators/fulltext_searchable/templates/migration.rb +9 -0
  18. data/lib/rails/generators/fulltext_searchable/templates/schema.rb +5 -0
  19. data/lib/tasks/fulltext_searchable.rake +27 -0
  20. data/spec/dummy/Rakefile +7 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/models/blog.rb +7 -0
  24. data/spec/dummy/app/models/comment.rb +6 -0
  25. data/spec/dummy/app/models/news.rb +4 -0
  26. data/spec/dummy/app/models/reply.rb +4 -0
  27. data/spec/dummy/app/models/user.rb +5 -0
  28. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  29. data/spec/dummy/config/application.rb +44 -0
  30. data/spec/dummy/config/boot.rb +10 -0
  31. data/spec/dummy/config/database.yml.example +19 -0
  32. data/spec/dummy/config/environment.rb +5 -0
  33. data/spec/dummy/config/environments/development.rb +26 -0
  34. data/spec/dummy/config/environments/production.rb +49 -0
  35. data/spec/dummy/config/environments/test.rb +35 -0
  36. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/dummy/config/initializers/fulltext_searchable.rb +7 -0
  38. data/spec/dummy/config/initializers/inflections.rb +10 -0
  39. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  40. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  41. data/spec/dummy/config/initializers/session_store.rb +8 -0
  42. data/spec/dummy/config/locales/en.yml +5 -0
  43. data/spec/dummy/config/routes.rb +66 -0
  44. data/spec/dummy/config.ru +4 -0
  45. data/spec/dummy/db/migrate/20110119090740_create_blogs.rb +15 -0
  46. data/spec/dummy/db/migrate/20110119090753_create_news.rb +15 -0
  47. data/spec/dummy/db/migrate/20110124031824_create_users.rb +13 -0
  48. data/spec/dummy/db/migrate/20110203091209_create_comments.rb +14 -0
  49. data/spec/dummy/db/migrate/20110215091428_create_fulltext_indices_table.rb +9 -0
  50. data/spec/dummy/public/404.html +26 -0
  51. data/spec/dummy/public/422.html +26 -0
  52. data/spec/dummy/public/500.html +26 -0
  53. data/spec/dummy/public/favicon.ico +0 -0
  54. data/spec/dummy/public/javascripts/application.js +2 -0
  55. data/spec/dummy/public/javascripts/controls.js +965 -0
  56. data/spec/dummy/public/javascripts/dragdrop.js +974 -0
  57. data/spec/dummy/public/javascripts/effects.js +1123 -0
  58. data/spec/dummy/public/javascripts/prototype.js +6001 -0
  59. data/spec/dummy/public/javascripts/rails.js +175 -0
  60. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  61. data/spec/dummy/public/stylesheets/scaffold.css +56 -0
  62. data/spec/dummy/script/rails +6 -0
  63. data/spec/fulltext_searchable_spec.rb +7 -0
  64. data/spec/models/blog_spec.rb +132 -0
  65. data/spec/models/comment_spec.rb +9 -0
  66. data/spec/models/fulltext_index_spec.rb +114 -0
  67. data/spec/models/news_spec.rb +100 -0
  68. data/spec/spec_helper.rb +44 -0
  69. data/spec/support/factories/blogs.rb +29 -0
  70. data/spec/support/factories/comments.rb +7 -0
  71. data/spec/support/factories/news.rb +23 -0
  72. data/spec/support/factories/users.rb +27 -0
  73. metadata +364 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ .bundle/
2
+ Gemfile.lock
3
+ log/*.log
4
+ pkg/
5
+ spec/dummy/db/*
6
+ spec/dummy/config/database.yml
7
+ spec/dummy/log/*.log
8
+ spec/dummy/tmp/
9
+ coverage*
10
+ rdoc/
11
+ *.swp
12
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org"
2
+
3
+ if ENV['RAILS_VER'] == '3.0'
4
+ gem "rails", "~> 3.0.5"
5
+ gem "mysql2", "~> 0.2.6"
6
+ gem 'will_paginate', :git => 'git://github.com/mislav/will_paginate.git', :branch => 'rails3'
7
+ else
8
+ gem "rails", "~> 3.0"
9
+ gem "mysql2"
10
+ gem 'will_paginate', "~> 3.0.4"
11
+ end
12
+
13
+ if ENV['PARANOID'] == 'original'
14
+ gem 'acts_as_paranoid'
15
+ else
16
+ gem 'rails3_acts_as_paranoid', :git => 'git://github.com/mshibuya/rails3_acts_as_paranoid.git'
17
+ end
18
+ gem "debugger"
19
+
20
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Mitsuhiro Shibuya
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.md ADDED
@@ -0,0 +1,4 @@
1
+ FulltextSearchable
2
+ ===
3
+
4
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,163 @@
1
+ # coding: utf-8
2
+
3
+ require 'digest/md5'
4
+
5
+ ##
6
+ # == 概要
7
+ # 全文検索インデックスとして機能するモデル。
8
+ #
9
+ class FulltextIndex < ActiveRecord::Base
10
+ self.table_name = FulltextSearchable::TABLE_NAME
11
+ self.primary_key = :_id
12
+ after_create :set_grn_insert_id
13
+
14
+ ## :nodoc:
15
+ # Association
16
+ belongs_to :item, :polymorphic => true
17
+
18
+ class << self
19
+ cattr_accessor :target_models
20
+
21
+ ##
22
+ # 全文検索で絞り込む。
23
+ #
24
+ # ==== phrase
25
+ # 検索する単語。スペース区切りで複数指定。
26
+ #
27
+ # ==== options
28
+ # ===== model
29
+ # 検索対象にするモデルを指定。
30
+ # 例: FulltextIndex.match('天気', :model => User)
31
+ # ===== with
32
+ # 検索対象にしたいレコード(AR object)を指定する。
33
+ # 例: FulltextIndex.match('天気', :with => @user)
34
+ #
35
+ def match(phrase, options={})
36
+ options = options.symbolize_keys
37
+ if phrase.is_a? String
38
+ phrase = phrase.split(/[\s ]/)
39
+ end
40
+
41
+ # モデルで絞り込む
42
+ model_keywords = []
43
+ if options[:model]
44
+ Array.wrap(options.delete(:model)).each do |t|
45
+ if t.is_a?(Class) && indexed?(t)
46
+ model_keywords.push(FulltextSearchable.to_model_keyword(t))
47
+ end
48
+ end
49
+ end
50
+ # レコードで絞り込む
51
+ item_keywords = []
52
+ if options[:with]
53
+ Array.wrap(options.delete(:with)).each do |t|
54
+ if indexed?(t.class)
55
+ item_keywords.push(FulltextSearchable.to_item_keyword(t))
56
+ end
57
+ end
58
+ end
59
+ [model_keywords, item_keywords].each do |keywords|
60
+ case keywords.count
61
+ when 0
62
+ when 1
63
+ phrase.unshift(keywords.first)
64
+ else
65
+ phrase.unshift("(#{keywords.join(' ')})")
66
+ end
67
+ end
68
+ phrase.map!{|i| '+' + i }
69
+
70
+ if connection.mroonga_match_against?
71
+ where("MATCH(`text`) AGAINST(? IN BOOLEAN MODE)",phrase.join(' ')).
72
+ order(sanitize_sql_array(["MATCH(`text`) AGAINST(? IN BOOLEAN MODE)",phrase.join(' ')]))
73
+ else
74
+ where("MATCH(`text`) AGAINST(? IN BOOLEAN MODE)",phrase.join(' ')).
75
+ order('`_score` DESC')
76
+ end
77
+ end
78
+
79
+ ##
80
+ # 結果を取得し、インデックス元モデルの配列に変換。
81
+ #
82
+ def items(*args)
83
+ begin
84
+ # ActiveRecordの参照テーブル判定処理がおかしいので、SQLクエリ内にピリオドが現れる際に
85
+ # ピリオドの前の文字列を参照テーブル名と誤認し、eager loadをjoinの方針に切り替えて
86
+ # polymorphic associationをjoinでeager loadできず例外を吐く。
87
+ # なのでrescueしてeager loadなしで再度クエリを試みておく。
88
+ select('`_id`, `item_type`, `item_id`').
89
+ includes(:item).all(*args).map{|i| i.item }
90
+ rescue ActiveRecord::EagerLoadPolymorphicError
91
+ # eager loadなしで試みる
92
+ select('`_id`, `item_type`, `item_id`').all(*args).map{|i| i.item }
93
+ end
94
+ end
95
+ ##
96
+ # 特定レコードの更新をインデックスに非同期で反映する。
97
+ #
98
+ def update(item)
99
+ l = lambda do
100
+ self.match(FulltextSearchable.to_item_keyword(item)).
101
+ includes(:item).all.each do |record|
102
+ next unless record.item
103
+ record.text = record.item.fulltext_keywords
104
+ record.save
105
+ end
106
+ end
107
+ if FulltextSearchable::Engine.config.respond_to?(:async) &&
108
+ FulltextSearchable::Engine.config.async
109
+ Thread.new{ l.call }
110
+ else
111
+ l.call
112
+ end
113
+ end
114
+ ##
115
+ # 全文検索インデックスを再構築する。
116
+ #
117
+ def rebuild_all
118
+ if connection.tables.include?(FulltextSearchable::TABLE_NAME)
119
+ connection.drop_table FulltextSearchable::TABLE_NAME
120
+ end
121
+ connection.create_fulltext_index_table
122
+
123
+ ActiveRecord::Base.descendants.each do |model|
124
+ next unless model.table_exists? &&
125
+ model.fulltext_searchable?
126
+ n = 0
127
+ depends = model.fulltext_dependent_models
128
+ begin
129
+ rows = model.unscoped.includes(depends).offset(n).
130
+ limit(FulltextSearchable::PROCESS_UNIT).order(:id).all
131
+ rows.each do |r|
132
+ index = self.find_or_initialize_by_key(create_key(r))
133
+ index.update_attributes :text => r.fulltext_keywords, :item => r
134
+ end
135
+ n += rows.count
136
+ end while rows.count >= FulltextSearchable::PROCESS_UNIT
137
+ end
138
+ end
139
+ ##
140
+ # key用文字列を生成する。
141
+ #
142
+ def create_key(item)
143
+ "#{item.class.name}_#{'% 10d' % item.id}"
144
+ end
145
+
146
+ protected
147
+
148
+ ##
149
+ # 全文検索対応モデルかどうかを判定し、返す。
150
+ #
151
+ def indexed?(klass)
152
+ klass.respond_to?(:fulltext_searchable?) && klass.fulltext_searchable?
153
+ end
154
+ end
155
+
156
+ protected
157
+ ##
158
+ # 互換のためレコード作成時に主キーをセット。
159
+ #
160
+ def set_grn_insert_id
161
+ self.id = connection.execute('SELECT last_insert_grn_id();').to_a.first.first
162
+ end
163
+ end
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fulltext_searchable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fulltext_searchable"
8
+ spec.version = FulltextSearchable::VERSION
9
+ spec.authors = ["M.Shibuya"]
10
+ spec.email = ["mit.shibuya@gmail.com"]
11
+ spec.summary = %q{Rails engine that provides fulltext-search capability using mroonga}
12
+ spec.description = %q{Rails engine that provides fulltext-search capability using mroonga(formerly. groonga storage engine). Requires Rails ~> 3.0}
13
+ spec.homepage = "https://github.com/greenbell/fulltext_searchable"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activerecord", "~> 3.0"
22
+ spec.add_dependency "mysql2"
23
+ spec.add_dependency "htmlentities"
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "cover_me"
26
+ spec.add_development_dependency "database_cleaner"
27
+ spec.add_development_dependency "factory_girl"
28
+ spec.add_development_dependency "faker"
29
+ spec.add_development_dependency "jeweler"
30
+ spec.add_development_dependency "rake"
31
+ spec.add_development_dependency "rdoc"
32
+ spec.add_development_dependency "rspec-rails"
33
+ end
@@ -0,0 +1,208 @@
1
+ # coding: utf-8
2
+ require 'htmlentities'
3
+
4
+ module FulltextSearchable
5
+ ##
6
+ # == 概要
7
+ # ActiveRecord::Baseを拡張するモジュール
8
+ #
9
+ module ActiveRecord # :nodoc:
10
+ def self.included(base) # :nodoc:
11
+ base.extend(ClassMethods)
12
+ end
13
+ #
14
+ # ActiveRecord::Baseにextendされるモジュール
15
+ #
16
+ module ClassMethods
17
+ ##
18
+ # 全文検索機能を有効にする。
19
+ #
20
+ # ==== columns
21
+ # 全文検索の対象とするカラムを指定。
22
+ #
23
+ # ==== 例:
24
+ # fulltext_searchable :title, :body
25
+ #
26
+ def fulltext_searchable(columns=[], options={}, &block)
27
+ options = options.symbolize_keys
28
+ cattr_accessor :fulltext_columns,
29
+ :fulltext_keyword_proc, :fulltext_referenced_columns
30
+
31
+ referenced = options.delete(:referenced)
32
+ self.fulltext_columns = Array.wrap(columns)
33
+ self.fulltext_referenced_columns = Array.wrap(referenced) if referenced
34
+ self.fulltext_keyword_proc = block
35
+
36
+ class_eval <<-EOV
37
+ has_one :fulltext_index, {
38
+ :as => :item,
39
+ :conditions => proc{ {:key => FulltextIndex.create_key(self) } }
40
+ }
41
+
42
+ include FulltextSearchable::ActiveRecord::Behaviors
43
+
44
+ after_commit :save_fulltext_index
45
+ after_destroy :destroy_fulltext_index
46
+ EOV
47
+ if self.fulltext_referenced_columns
48
+ class_eval <<-EOV
49
+ before_save :check_fulltext_changes
50
+ EOV
51
+ end
52
+ end
53
+ ##
54
+ # 全文検索対応モデルかどうかを返す。
55
+ #
56
+ def fulltext_searchable?
57
+ self.ancestors.include?(
58
+ ::FulltextSearchable::ActiveRecord::Behaviors)
59
+ end
60
+ end
61
+ #
62
+ # 各モデルにincludeされるモジュール
63
+ #
64
+ module Behaviors
65
+ extend ActiveSupport::Concern
66
+
67
+ module ClassMethods
68
+ ##
69
+ # 各モデルに対し全文検索を行う。
70
+ #
71
+ def fulltext_match(phrase)
72
+ FulltextIndex.match(phrase, :model => self)
73
+ end
74
+
75
+ ##
76
+ # eager loadのために全文検索対応モデルが依存する他のモデルを返す。
77
+ #
78
+ def fulltext_dependent_models(columns=nil)
79
+ columns ||= fulltext_columns
80
+ if columns.is_a? Hash
81
+ columns = Array.wrap(columns)
82
+ end
83
+ if columns.is_a? Array
84
+ result = []
85
+ columns.flatten!
86
+ columns.each do |i|
87
+ if i.is_a?(Hash)
88
+ i.each do |k,v|
89
+ if v.is_a?(Hash) || v.is_a?(Array)
90
+ r = fulltext_dependent_models(v)
91
+ if r
92
+ result.push({k=>r})
93
+ else
94
+ result.push(k)
95
+ end
96
+ elsif v.to_s.downcase != 'html'
97
+ result.push(k)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ case result.count
103
+ when 0
104
+ nil
105
+ when 1
106
+ result.first
107
+ else
108
+ result
109
+ end
110
+ else
111
+ nil
112
+ end
113
+ end
114
+ end
115
+
116
+ ##
117
+ # レコードの内容を全文検索インデックス用に変換
118
+ #
119
+ def fulltext_keywords
120
+ # 論理削除されていたら空に
121
+ return '' if self.respond_to?(:deleted?) && self.deleted?
122
+ [
123
+ FulltextSearchable.to_model_keyword(self.class.name),
124
+ FulltextSearchable.to_item_keyword(self),
125
+ ].
126
+ tap{|a| a.push(fulltext_keyword_proc.call) if fulltext_keyword_proc }.
127
+ concat(collect_fulltext_keywords(self, fulltext_columns)).
128
+ flatten.join(' ')
129
+ end
130
+
131
+ private
132
+
133
+ ##
134
+ # before_saveにフック。全文検索対象カラムが変更されているかどうか調べる。
135
+ #
136
+ def check_fulltext_changes
137
+ @fulltext_change = fulltext_referenced_columns &&
138
+ fulltext_referenced_columns.any?{|i| changes[i]}
139
+ true
140
+ end
141
+ ##
142
+ # after_commitにフック。
143
+ #
144
+ def save_fulltext_index
145
+ if self.fulltext_index
146
+ if !fulltext_index.text.empty? && (!defined?(@fulltext_change) || @fulltext_change)
147
+ FulltextIndex.update(self)
148
+ else
149
+ self.fulltext_index.text = fulltext_keywords
150
+ self.fulltext_index.save
151
+ end
152
+ else
153
+ self.create_fulltext_index(
154
+ :key => FulltextIndex.create_key(self),
155
+ :text => fulltext_keywords
156
+ )
157
+ end
158
+ end
159
+ ##
160
+ # after_destroyにフック。全文検索インデックスを削除
161
+ #
162
+ def destroy_fulltext_index
163
+ return unless fulltext_index
164
+ unless persisted?
165
+ fulltext_index.destroy
166
+ else
167
+ fulltext_index.update_attributes(:text => '')
168
+ end
169
+ end
170
+
171
+ def collect_fulltext_keywords(target, columns)
172
+ result = []
173
+ return result unless target
174
+ if columns.is_a? Hash
175
+ columns = Array.wrap(columns)
176
+ end
177
+ unless columns.is_a? Array
178
+ return result.push(target.send(columns).to_s)
179
+ end
180
+ columns.flatten!
181
+ columns.each do |column|
182
+ if column.is_a? Hash
183
+ column.each do |k,v|
184
+ if v.to_s.downcase == 'html'
185
+ result.push(
186
+ HTMLEntities.new(:xhtml1).decode(
187
+ target.send(k.to_s).to_s.gsub(/<[^>]*>/ui,'')
188
+ ).gsub(/[ \s]+/u, ' ') # contains &nbsp;
189
+ )
190
+ else
191
+ Array.wrap(target.send(k)).each do |t|
192
+ result.concat([
193
+ FulltextSearchable.to_item_keyword(t),
194
+ collect_fulltext_keywords(t, v)
195
+ ])
196
+ end
197
+ end
198
+ end
199
+ else
200
+ result.push(collect_fulltext_keywords(target, column))
201
+ end
202
+ end
203
+ result.flatten
204
+ end
205
+ end
206
+ end
207
+ end
208
+
@@ -0,0 +1,4 @@
1
+ module FulltextSearchable
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ module FulltextSearchable
4
+ module Mysql2Adapter
5
+ def create_fulltext_index_table
6
+ execute( <<SQL
7
+ CREATE TABLE IF NOT EXISTS `#{::FulltextSearchable::TABLE_NAME}` (
8
+ `_id` INT(11),
9
+ `key` VARCHAR(32),
10
+ `item_type` VARCHAR(255),
11
+ `item_id` INT(11),
12
+ `text` TEXT,
13
+ #{"`_score` FLOAT," unless mroonga_match_against?}
14
+ PRIMARY KEY(`key`),
15
+ #{"UNIQUE " if mroonga_unique_hash_index_safe?}INDEX(`_id`) USING HASH,
16
+ FULLTEXT INDEX (`text`)
17
+ ) ENGINE = #{mroonga_storage_engine_name} COLLATE utf8_unicode_ci;
18
+ SQL
19
+ )
20
+ end
21
+
22
+ def mroonga_match_against?
23
+ mroonga_version >= '1.2'
24
+ end
25
+
26
+ def mroonga_version
27
+ execute("SHOW VARIABLES LIKE '#{mroonga_storage_engine_name}_version';").map(&:last).first ||
28
+ '0.0' # older than 1.0
29
+ end
30
+
31
+ def mroonga_storage_engine_name
32
+ @mroonga_storage_engine_name ||=
33
+ execute('SHOW ENGINES;').map(&:first).find{|name| name =~ /.+roonga/} or
34
+ raise "mroonga or groonga storage engine is not installed"
35
+ end
36
+
37
+ private
38
+
39
+ ##
40
+ # mroonga on MacOS X fails truncation/deletion of unique hash indexed table.
41
+ # As a workaround, we actually temporary table with unique hash index and
42
+ # see if it is safe to truncate it.
43
+ #
44
+ def mroonga_unique_hash_index_safe?
45
+ temporary_table_name = ::FulltextSearchable::TABLE_NAME + '_temp_' + Time.now.to_i.to_s
46
+ execute( <<SQL
47
+ CREATE TABLE IF NOT EXISTS `#{temporary_table_name}` (
48
+ `_id` INT(11),
49
+ UNIQUE INDEX(`_id`) USING HASH
50
+ ) ENGINE = #{mroonga_storage_engine_name} COLLATE utf8_unicode_ci;
51
+ SQL
52
+ )
53
+ safe = true
54
+ begin
55
+ execute("TRUNCATE TABLE `#{temporary_table_name}`;")
56
+ rescue
57
+ safe = false
58
+ end
59
+ drop_table temporary_table_name
60
+ safe
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,3 @@
1
+ module FulltextSearchable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+ $:.unshift(File.dirname(__FILE__))
3
+
4
+ require 'digest/md5'
5
+ ##
6
+ # == 概要
7
+ # モデルを全文検索対応にするプラグイン
8
+ #
9
+ module FulltextSearchable
10
+ require 'fulltext_searchable/engine' if defined?(Rails)
11
+
12
+ # 再構築タスク時の一回の処理レコード数
13
+ PROCESS_UNIT = 1000
14
+ TABLE_NAME = 'fulltext_indices'
15
+
16
+ class << self
17
+ def to_model_keyword(model)
18
+ '' + Digest::MD5.hexdigest('FulltextSearchable_'+model.to_s)[0,9] + ''
19
+ end
20
+ def to_item_keyword(item)
21
+ '' + Digest::MD5.hexdigest('FulltextSearchable_'+item.class.to_s)[0,8] + '_' + item.id.to_s + ''
22
+ end
23
+ end
24
+
25
+ autoload :ActiveRecord, 'fulltext_searchable/active_record'
26
+ autoload :Mysql2Adapter, 'fulltext_searchable/mysql2_adapter'
27
+ end
28
+
29
+ ActiveSupport.on_load(:active_record) do
30
+ ActiveRecord::Base.class_eval { include FulltextSearchable::ActiveRecord }
31
+
32
+ require 'active_record/connection_adapters/mysql2_adapter'
33
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.
34
+ send(:include, FulltextSearchable::Mysql2Adapter)
35
+
36
+ ActiveRecord::SchemaDumper.class_eval do
37
+ def table_with_grn(table, stream)
38
+ if table.to_s == ::FulltextSearchable::TABLE_NAME
39
+ tbl = StringIO.new
40
+ tbl.puts " create_fulltext_index_table"
41
+ tbl.puts ""
42
+ tbl.rewind
43
+ stream.print tbl.read
44
+ else
45
+ table_without_grn(table, stream)
46
+ end
47
+ end
48
+ alias_method_chain :table, :grn
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class FulltextSearchableGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ def self.source_root
8
+ File.join(File.dirname(__FILE__), 'templates')
9
+ end
10
+
11
+ def self.next_migration_number(dirname) #:nodoc:
12
+ if ActiveRecord::Base.timestamped_migrations
13
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
14
+ else
15
+ "%.3d" % (current_migration_number(dirname) + 1)
16
+ end
17
+ end
18
+
19
+
20
+ # Every method that is declared below will be automatically executed when the generator is run
21
+
22
+ def create_migration_file
23
+ f = File.open File.join(File.dirname(__FILE__), 'templates', 'schema.rb')
24
+ schema = f.read; f.close
25
+
26
+ schema.gsub!(/ActiveRecord::Schema.*\n/, '')
27
+ schema.gsub!(/^end\n*$/, '')
28
+
29
+ f = File.open File.join(File.dirname(__FILE__), 'templates', 'migration.rb')
30
+ migration = f.read; f.close
31
+ migration.gsub!(/SCHEMA_AUTO_INSERTED_HERE/, schema)
32
+
33
+ tmp = File.open "tmp/~migration_ready.rb", "w"
34
+ tmp.write migration
35
+ tmp.close
36
+
37
+ migration_template File.expand_path(tmp.path),
38
+ 'db/migrate/create_fulltext_indices_table.rb'
39
+ remove_file 'tmp/~migration_ready.rb'
40
+ end
41
+
42
+ def copy_initializer_file
43
+ copy_file 'initializer.rb', 'config/initializers/fulltext_searchable.rb'
44
+ end
45
+
46
+ end
@@ -0,0 +1,7 @@
1
+ module FulltextSearchable
2
+ class Engine < Rails::Engine
3
+
4
+ # if set to true, updates are processed asynchronously(in another thread)
5
+ config.async = false
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class CreateFulltextIndicesTable < ActiveRecord::Migration
2
+ def self.up
3
+ SCHEMA_AUTO_INSERTED_HERE
4
+ end
5
+
6
+ def self.down
7
+ drop_table :fulltext_indices
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+
3
+ create_fulltext_index_table
4
+
5
+ end