fulltext_searchable 0.1.0

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