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
@@ -0,0 +1,175 @@
1
+ (function() {
2
+ // Technique from Juriy Zaytsev
3
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
+ function isEventSupported(eventName) {
5
+ var el = document.createElement('div');
6
+ eventName = 'on' + eventName;
7
+ var isSupported = (eventName in el);
8
+ if (!isSupported) {
9
+ el.setAttribute(eventName, 'return;');
10
+ isSupported = typeof el[eventName] == 'function';
11
+ }
12
+ el = null;
13
+ return isSupported;
14
+ }
15
+
16
+ function isForm(element) {
17
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
+ }
19
+
20
+ function isInput(element) {
21
+ if (Object.isElement(element)) {
22
+ var name = element.nodeName.toUpperCase()
23
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
+ }
25
+ else return false
26
+ }
27
+
28
+ var submitBubbles = isEventSupported('submit'),
29
+ changeBubbles = isEventSupported('change')
30
+
31
+ if (!submitBubbles || !changeBubbles) {
32
+ // augment the Event.Handler class to observe custom events when needed
33
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
+ function(init, element, eventName, selector, callback) {
35
+ init(element, eventName, selector, callback)
36
+ // is the handler being attached to an element that doesn't support this event?
37
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
+ // "submit" => "emulated:submit"
40
+ this.eventName = 'emulated:' + this.eventName
41
+ }
42
+ }
43
+ )
44
+ }
45
+
46
+ if (!submitBubbles) {
47
+ // discover forms on the page by observing focus events which always bubble
48
+ document.on('focusin', 'form', function(focusEvent, form) {
49
+ // special handler for the real "submit" event (one-time operation)
50
+ if (!form.retrieve('emulated:submit')) {
51
+ form.on('submit', function(submitEvent) {
52
+ var emulated = form.fire('emulated:submit', submitEvent, true)
53
+ // if custom event received preventDefault, cancel the real one too
54
+ if (emulated.returnValue === false) submitEvent.preventDefault()
55
+ })
56
+ form.store('emulated:submit', true)
57
+ }
58
+ })
59
+ }
60
+
61
+ if (!changeBubbles) {
62
+ // discover form inputs on the page
63
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
+ // special handler for real "change" events
65
+ if (!input.retrieve('emulated:change')) {
66
+ input.on('change', function(changeEvent) {
67
+ input.fire('emulated:change', changeEvent, true)
68
+ })
69
+ input.store('emulated:change', true)
70
+ }
71
+ })
72
+ }
73
+
74
+ function handleRemote(element) {
75
+ var method, url, params;
76
+
77
+ var event = element.fire("ajax:before");
78
+ if (event.stopped) return false;
79
+
80
+ if (element.tagName.toLowerCase() === 'form') {
81
+ method = element.readAttribute('method') || 'post';
82
+ url = element.readAttribute('action');
83
+ params = element.serialize();
84
+ } else {
85
+ method = element.readAttribute('data-method') || 'get';
86
+ url = element.readAttribute('href');
87
+ params = {};
88
+ }
89
+
90
+ new Ajax.Request(url, {
91
+ method: method,
92
+ parameters: params,
93
+ evalScripts: true,
94
+
95
+ onComplete: function(request) { element.fire("ajax:complete", request); },
96
+ onSuccess: function(request) { element.fire("ajax:success", request); },
97
+ onFailure: function(request) { element.fire("ajax:failure", request); }
98
+ });
99
+
100
+ element.fire("ajax:after");
101
+ }
102
+
103
+ function handleMethod(element) {
104
+ var method = element.readAttribute('data-method'),
105
+ url = element.readAttribute('href'),
106
+ csrf_param = $$('meta[name=csrf-param]')[0],
107
+ csrf_token = $$('meta[name=csrf-token]')[0];
108
+
109
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
+ element.parentNode.insert(form);
111
+
112
+ if (method !== 'post') {
113
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
+ form.insert(field);
115
+ }
116
+
117
+ if (csrf_param) {
118
+ var param = csrf_param.readAttribute('content'),
119
+ token = csrf_token.readAttribute('content'),
120
+ field = new Element('input', { type: 'hidden', name: param, value: token });
121
+ form.insert(field);
122
+ }
123
+
124
+ form.submit();
125
+ }
126
+
127
+
128
+ document.on("click", "*[data-confirm]", function(event, element) {
129
+ var message = element.readAttribute('data-confirm');
130
+ if (!confirm(message)) event.stop();
131
+ });
132
+
133
+ document.on("click", "a[data-remote]", function(event, element) {
134
+ if (event.stopped) return;
135
+ handleRemote(element);
136
+ event.stop();
137
+ });
138
+
139
+ document.on("click", "a[data-method]", function(event, element) {
140
+ if (event.stopped) return;
141
+ handleMethod(element);
142
+ event.stop();
143
+ });
144
+
145
+ document.on("submit", function(event) {
146
+ var element = event.findElement(),
147
+ message = element.readAttribute('data-confirm');
148
+ if (message && !confirm(message)) {
149
+ event.stop();
150
+ return false;
151
+ }
152
+
153
+ var inputs = element.select("input[type=submit][data-disable-with]");
154
+ inputs.each(function(input) {
155
+ input.disabled = true;
156
+ input.writeAttribute('data-original-value', input.value);
157
+ input.value = input.readAttribute('data-disable-with');
158
+ });
159
+
160
+ var element = event.findElement("form[data-remote]");
161
+ if (element) {
162
+ handleRemote(element);
163
+ event.stop();
164
+ }
165
+ });
166
+
167
+ document.on("ajax:after", "form", function(event, element) {
168
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
+ inputs.each(function(input) {
170
+ input.value = input.readAttribute('data-original-value');
171
+ input.removeAttribute('data-original-value');
172
+ input.disabled = false;
173
+ });
174
+ });
175
+ })();
File without changes
@@ -0,0 +1,56 @@
1
+ body { background-color: #fff; color: #333; }
2
+
3
+ body, p, ol, ul, td {
4
+ font-family: verdana, arial, helvetica, sans-serif;
5
+ font-size: 13px;
6
+ line-height: 18px;
7
+ }
8
+
9
+ pre {
10
+ background-color: #eee;
11
+ padding: 10px;
12
+ font-size: 11px;
13
+ }
14
+
15
+ a { color: #000; }
16
+ a:visited { color: #666; }
17
+ a:hover { color: #fff; background-color:#000; }
18
+
19
+ div.field, div.actions {
20
+ margin-bottom: 10px;
21
+ }
22
+
23
+ #notice {
24
+ color: green;
25
+ }
26
+
27
+ .field_with_errors {
28
+ padding: 2px;
29
+ background-color: red;
30
+ display: table;
31
+ }
32
+
33
+ #error_explanation {
34
+ width: 450px;
35
+ border: 2px solid red;
36
+ padding: 7px;
37
+ padding-bottom: 0;
38
+ margin-bottom: 20px;
39
+ background-color: #f0f0f0;
40
+ }
41
+
42
+ #error_explanation h2 {
43
+ text-align: left;
44
+ font-weight: bold;
45
+ padding: 5px 5px 5px 15px;
46
+ font-size: 12px;
47
+ margin: -7px;
48
+ margin-bottom: 0px;
49
+ background-color: #c00;
50
+ color: #fff;
51
+ }
52
+
53
+ #error_explanation ul li {
54
+ font-size: 12px;
55
+ list-style: square;
56
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe FulltextSearchable do
4
+ it "should be valid" do
5
+ FulltextSearchable.should be_a(Module)
6
+ end
7
+ end
@@ -0,0 +1,132 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ def thread_wait
5
+ Thread.list.each{|t| t.join if t != Thread.current }
6
+ end
7
+
8
+ describe Blog do
9
+ it "should be valid" do
10
+ Blog.superclass.should == ActiveRecord::Base
11
+ end
12
+
13
+ it "should be checkd with changes" do
14
+ @blog = Blog.new
15
+ @blog.should_receive(:check_fulltext_changes)
16
+ @blog.save
17
+ end
18
+
19
+ context "creation" do
20
+ it "should create fulltext index" do
21
+ FulltextIndex.match('お知らせ').items.should == []
22
+ @blog = Blog.new :title => '題名', :body => '<h1>お知らせ</h1>', :user => FactoryGirl.create(:hanako)
23
+ @blog.save
24
+ @blog.fulltext_index.text.should ==
25
+ "#{FulltextSearchable.to_model_keyword(Blog)} #{FulltextSearchable.to_item_keyword(@blog)} 題名 お知らせ #{FulltextSearchable.to_item_keyword(@blog.user)} 花子"
26
+ FulltextIndex.match('お知らせ').items.should == [@blog]
27
+ end
28
+
29
+ it "should create fulltext index with deep nested models" do
30
+ @user = FactoryGirl.create(:john)
31
+ @user.fulltext_index.text.should == '7d3ecc6a9 7d3ecc6a_1 john 392955b1_1 昨日の天気は a69403a7_1 超寒い!!!11<> 0e4e4340_1 1'
32
+ end
33
+ end
34
+
35
+ context "retrieval" do
36
+ before do
37
+ @blog = FactoryGirl.create(:today)
38
+ end
39
+
40
+ it "should return item" do
41
+ FulltextIndex.match('晴れ').items.should == [@blog]
42
+ end
43
+ end
44
+
45
+ context "updating" do
46
+ before do
47
+ @blog = FactoryGirl.create(:taro).blogs.first
48
+ end
49
+
50
+ it "should update fulltext index" do
51
+ FulltextIndex.match('晴れ 雨').items.should == []
52
+ @blog.body = '<DIV><FONT size="2">&nbsp;</FONT>曇り時々晴れでところにより一時にわか雨です。</DIV>'
53
+ @blog.save
54
+ @blog.fulltext_index.reload.text.should ==
55
+ "#{FulltextSearchable.to_model_keyword(Blog)} #{FulltextSearchable.to_item_keyword(@blog)} 今日の天気は 曇り時々晴れでところにより一時にわか雨です。 #{FulltextSearchable.to_item_keyword(@blog.user)} 太郎"
56
+ FulltextIndex.match('晴れ 雨').items.should == [@blog]
57
+ end
58
+
59
+ it "should work with malformed html" do
60
+ FulltextIndex.match('晴れ 雨').items.should == []
61
+ @blog.body = '<DIV><FONT size="2">&nbsp;曇り<a name="abc">時々晴れで<b>ところにより</FONT>一時超にわか雨</b>です。</DIV>'
62
+ @blog.save
63
+ @blog.fulltext_index.reload.text.should ==
64
+ "#{FulltextSearchable.to_model_keyword(Blog)} #{FulltextSearchable.to_item_keyword(@blog)} 今日の天気は 曇り時々晴れでところにより一時超にわか雨です。 #{FulltextSearchable.to_item_keyword(@blog.user)} 太郎"
65
+ FulltextIndex.match('晴れ 雨').items.should == [@blog]
66
+ end
67
+
68
+ it "should also update fulltext index with update of associated model" do
69
+ @blog.user.update_attributes :name => '東京太郎'
70
+ @blog.fulltext_index.reload.text.should ==
71
+ "#{FulltextSearchable.to_model_keyword(Blog)} #{FulltextSearchable.to_item_keyword(@blog)} 今日の天気は 曇り時々晴れです。 #{FulltextSearchable.to_item_keyword(@blog.user)} 東京太郎"
72
+ FulltextIndex.match('晴れ 東京').items.should == [@blog]
73
+ end
74
+ end
75
+
76
+ context "deletion" do
77
+ before do
78
+ @blog = FactoryGirl.create(:today)
79
+ end
80
+
81
+ it "should destroy fulltext index" do
82
+ @blog.destroy
83
+ lambda do
84
+ @blog.fulltext_index.reload or raise ActiveRecord::RecordNotFound
85
+ end.should raise_error ActiveRecord::RecordNotFound
86
+ FulltextIndex.match('今日').items.should == []
87
+ end
88
+
89
+ it "should not care fulltext index if not exists" do
90
+ FulltextIndex.delete_all
91
+ lambda{ @blog.destroy }.should_not raise_error
92
+ end
93
+ end
94
+
95
+ context "updating asynchronously" do
96
+ before(:all) { FulltextSearchable::Engine.config.async = true }
97
+ after(:all) { FulltextSearchable::Engine.config.async = false }
98
+ before do
99
+ @user = FactoryGirl.create(:user)
100
+ @count = @user.blogs.count + 1
101
+ end
102
+
103
+ it "should update fulltext index" do
104
+ @user.name = 'taro'
105
+ @user.save
106
+ FulltextIndex.match('taro').count.should_not == @count
107
+ thread_wait
108
+ FulltextIndex.match('taro').count.should == @count
109
+ end
110
+ end
111
+
112
+ context "update optimization" do
113
+ before do
114
+ @user = FactoryGirl.create(:taro)
115
+ @blog = @user.blogs.first
116
+ end
117
+
118
+ it "should update related models with change of referenced attribute" do
119
+ @blog.title = @blog.title + 'foo'
120
+ FulltextIndex.should_receive(:update).with(@blog)
121
+ @blog.save
122
+ end
123
+
124
+ it "should not update related models with change of non-referenced attribute" do
125
+ @blog.body = @blog.body + 'foo'
126
+ FulltextIndex.should_not_receive(:update)
127
+ @blog.save
128
+ @blog.fulltext_index.reload.text.should ==
129
+ '392955b1b 392955b1_1 今日の天気は 曇り時々晴れです。foo 7d3ecc6a_1 太郎'
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,9 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe Comment do
5
+ it "should create fulltext index with proc" do
6
+ @comment = FactoryGirl.create(:comment)
7
+ @comment.fulltext_index.text.should == 'a69403a7e a69403a7_1 text from proc'
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe FulltextIndex do
5
+ it "should be valid" do
6
+ FulltextIndex.superclass.should == ActiveRecord::Base
7
+ end
8
+
9
+ context "rebuilding" do
10
+ before do
11
+ @taisyaku = FactoryGirl.create(:taisyaku)
12
+ News.delete_all! @taisyaku.id
13
+ @soneki = FactoryGirl.create(:soneki)
14
+ News.update_all("body = '夕飯はカレーです。'", ['id = ?',@soneki.id])
15
+ end
16
+ it "should update fulltext index" do
17
+ FulltextIndex.all.map{|i| i.text}.should == [
18
+ "1b5f32164 1b5f3216_1 貸借対照表 営業年度の終了時、決算において資産、負債、資本がどれだけあるかを一定のルールにのっとって財務状態を表にしたもの",
19
+ "1b5f32164 1b5f3216_2 損益計算書 営業年度中の売り上げと経費、それを差し引いた利益(損失)を記載して表にしたもの",
20
+ ]
21
+ FulltextIndex.rebuild_all
22
+ FulltextIndex.all.map{|i| i.text}.should == [
23
+ "1b5f32164 1b5f3216_2 損益計算書 夕飯はカレーです。",
24
+ ]
25
+ end
26
+ end
27
+
28
+ context "retrieval" do
29
+ before do
30
+ FactoryGirl.create(:taisyaku)
31
+ FactoryGirl.create(:soneki)
32
+ FactoryGirl.create(:eigyo)
33
+ FactoryGirl.create(:rieki)
34
+ @taro = FactoryGirl.create(:taro)
35
+ @jiro = FactoryGirl.create(:jiro)
36
+ end
37
+ it "should fulltext searchable with '営業'" do
38
+ FulltextIndex.match('営業').items.count.should == 4
39
+ end
40
+
41
+ it "should fulltext searchable with '営業 状態'" do
42
+ FulltextIndex.match('営業 状態').items.count.should == 2
43
+ end
44
+
45
+ it "should fulltext searchable with '営業 状態'(fullwidth-space delimiterd)" do
46
+ FulltextIndex.match('営業 状態').items.count.should == 2
47
+ end
48
+
49
+ it "should not match with html tags" do
50
+ FulltextIndex.match('DIV').items.count.should == 0
51
+ FulltextIndex.match('FONT').items.count.should == 0
52
+ end
53
+
54
+ it "should fulltext searchable with associated model's keyword" do
55
+ FulltextIndex.match('晴れ 太郎').items.count.should == 1
56
+ FulltextIndex.match('今日 太郎').items.count.should == 2
57
+ end
58
+
59
+ it "should fulltext searchable with target model specified" do
60
+ FulltextIndex.match('太郎', :model => News).items.count.should == 0
61
+ FulltextIndex.match('太郎', :model => [Blog, News]).items.count.should == 2
62
+ FulltextIndex.match('太郎', :model => [Blog, News, User]).items.count.should == 3
63
+ end
64
+
65
+ it "should fulltext searchable with target item specified" do
66
+ FulltextIndex.match('天気').items.count.should == 5
67
+ FulltextIndex.match('天気', :with => @taro).items.count.should == 3
68
+ end
69
+
70
+ it "should fulltext searchable with target items and models specified" do
71
+ FulltextIndex.match('天気').items.count.should == 5
72
+ FulltextIndex.match('天気', :with => @jiro, :model => Blog).items.count.should == 1
73
+ FulltextIndex.match('天気', :with => @jiro, :model => [Blog, User]).items.count.should == 2
74
+ FulltextIndex.match('天気', :with => [@taro, @jiro], :model => Blog).items.count.should == 3
75
+ FulltextIndex.match('天気', :with => [@taro, @jiro], :model => [Blog, User]).items.count.should == 5
76
+ end
77
+
78
+ it "should perform workaround with ActiveRecord's string-followed-by-period bug" do
79
+ FulltextIndex.match('ab.').items.should_not raise_error ActiveRecord::EagerLoadPolymorphicError
80
+ end
81
+ end
82
+
83
+ context "optimization" do
84
+ before do
85
+ @taro = FactoryGirl.create(:taro)
86
+ @jiro = FactoryGirl.create(:jiro)
87
+ FactoryGirl.create(:taisyaku)
88
+ end
89
+ it "should utilize groonga_fast_order_limit optization" do
90
+ fast = get_mroonga_status_var('fast_order_limit')
91
+ FulltextIndex.match('天気').limit(1).all
92
+ (get_mroonga_status_var('fast_order_limit') - fast).should == 1
93
+ end
94
+
95
+ it "should utilize groonga_count_skip optization" do
96
+ skip = get_mroonga_status_var('count_skip')
97
+ FulltextIndex.match('天気').count
98
+ (get_mroonga_status_var('count_skip') - skip).should == 1
99
+ end
100
+
101
+ it "should utilize both of optization with pagination" do
102
+ fast = get_mroonga_status_var('fast_order_limit')
103
+ skip = get_mroonga_status_var('count_skip')
104
+ FulltextIndex.match('天気').paginate(:per_page=>1, :page=>3).to_a
105
+ (get_mroonga_status_var('fast_order_limit') - fast).should == 1
106
+ (get_mroonga_status_var('count_skip') - skip).should == 1
107
+ end
108
+
109
+ def get_mroonga_status_var(name)
110
+ ActiveRecord::Base.connection.
111
+ execute("SHOW STATUS LIKE '#{ActiveRecord::Base.connection.mroonga_storage_engine_name}_#{name}';").first.last.to_i
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,100 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe News do
5
+ it "should be valid" do
6
+ News.superclass.should == ActiveRecord::Base
7
+ end
8
+
9
+ it "should not be checkd with changes" do
10
+ @news = News.new
11
+ @news.should_not_receive(:check_fulltext_changes)
12
+ @news.save
13
+ end
14
+
15
+ context "creation" do
16
+ it "should create fulltext index" do
17
+ FulltextIndex.match('例 ダミー').items.should == []
18
+ @news = News.new :title => '例', :body => 'ダミー'
19
+ @news.save
20
+ @news.fulltext_index.text.should ==
21
+ "#{FulltextSearchable.to_model_keyword(News)} #{FulltextSearchable.to_item_keyword(@news)} 例 ダミー"
22
+ FulltextIndex.match('例 ダミー').items.should == [@news]
23
+ end
24
+
25
+ it "should not create fulltext index when soft-deleted initially" do
26
+ FulltextIndex.match('例 ダミー').items.should == []
27
+ @news = News.create! :title => '例', :body => 'ダミー'
28
+ @news.destroy
29
+ @news.fulltext_index.text.should == ''
30
+ end
31
+ end
32
+
33
+ context "retrieval" do
34
+ before do
35
+ @news = FactoryGirl.create(:taisyaku)
36
+ FactoryGirl.create(:soneki)
37
+ end
38
+ it "should return item" do
39
+ FulltextIndex.match('決算').items.should == [@news]
40
+ end
41
+ it "should be fulltext-searched with model restriction" do
42
+ FactoryGirl.create(:day_after_tomorrow)
43
+ FulltextIndex.match('決算').items.count.should == 2
44
+ FulltextIndex.match('決算', :model => News).items.should == [@news]
45
+ News.fulltext_match('決算').items.should == [@news]
46
+ end
47
+ end
48
+
49
+ context "updating" do
50
+ before do
51
+ @news = FactoryGirl.create(:taisyaku)
52
+ end
53
+ it "should update fulltext index" do
54
+ FulltextIndex.match('営業年度 楽しい').items.should == []
55
+ @news.body = "営業年度の終了時、決算において資産、負債、資本がどれだけあるかを一定のルールにのっとって財務状態を表にした楽しいもの"
56
+ @news.save
57
+ @news.fulltext_index.reload.text.should ==
58
+ "#{FulltextSearchable.to_model_keyword(News)} #{FulltextSearchable.to_item_keyword(@news)} 貸借対照表 営業年度の終了時、決算において資産、負債、資本がどれだけあるかを一定のルールにのっとって財務状態を表にした楽しいもの"
59
+ FulltextIndex.match('営業年度 楽しい').items.should == [@news]
60
+ end
61
+ end
62
+
63
+ context "deletion",f:true do
64
+ before do
65
+ @news = FactoryGirl.create(:taisyaku)
66
+ end
67
+ describe "with paranoid removal" do
68
+ it "should nulify fulltext index" do
69
+ @news.destroy
70
+ @news.reload.deleted_at.should_not be_nil
71
+ @news.fulltext_index.should_not be_nil
72
+ @news.fulltext_index.text.should == ''
73
+ FulltextIndex.match('営業年度').items.should == []
74
+ end
75
+ end
76
+ describe "with real removal" do
77
+ it "should destroy fulltext index" do
78
+ @fulltext_index = @news.fulltext_index
79
+ @news.destroy!
80
+ FulltextIndex.find_by__id(@fulltext_index.id).should be_nil
81
+ FulltextIndex.match('営業年度').items.should == []
82
+ end
83
+ end
84
+ end
85
+
86
+ context "recovery" do
87
+ before do
88
+ @news = FactoryGirl.create(:taisyaku)
89
+ @news.destroy
90
+ end
91
+ it "should rebuild fulltext index" do
92
+ @news.fulltext_index.text.should == ''
93
+ @news.recover
94
+ @news.deleted_at.should be_nil
95
+ @news.fulltext_index.reload.text.should ==
96
+ "#{FulltextSearchable.to_model_keyword(News)} #{FulltextSearchable.to_item_keyword(@news)} 貸借対照表 営業年度の終了時、決算において資産、負債、資本がどれだけあるかを一定のルールにのっとって財務状態を表にしたもの"
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,44 @@
1
+ # Configure Rails Envinronment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require 'cover_me'
5
+ CoverMe.config do |c|
6
+ c.at_exit = Proc.new {}
7
+ c.file_pattern = /(#{c.project.root}\/app\/.+\.rb|#{c.project.root}\/lib\/.+\.rb)/ix
8
+ end
9
+
10
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
11
+ require "rails/test_help"
12
+ require "rspec/rails"
13
+
14
+ require "factory_girl"
15
+ require "faker"
16
+
17
+ require "database_cleaner"
18
+
19
+ ActionMailer::Base.delivery_method = :test
20
+ ActionMailer::Base.perform_deliveries = true
21
+ ActionMailer::Base.default_url_options[:host] = "test.com"
22
+
23
+ Rails.backtrace_cleaner.remove_silencers!
24
+
25
+ # Load support files
26
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
27
+
28
+ RSpec.configure do |config|
29
+ # Remove this line if you don't want RSpec's should and should_not
30
+ # methods or matchers
31
+ require 'rspec/expectations'
32
+ config.include RSpec::Matchers
33
+
34
+ # == Mock Framework
35
+ config.mock_with :rspec
36
+ config.before(:suite) do
37
+ DatabaseCleaner.app_root = "#{File.dirname(__FILE__)}/dummy/"
38
+ DatabaseCleaner.strategy = :truncation
39
+ end
40
+ config.before(:each){ DatabaseCleaner.clean }
41
+ # filtering
42
+ config.filter_run :focus => true
43
+ config.run_all_when_everything_filtered = true
44
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+
3
+ FactoryGirl.define do
4
+ factory :today, :class => Blog do
5
+ title '今日の天気は'
6
+ body '<DIV><FONT size="2">&nbsp;</FONT>曇り時々晴れです。</DIV>'
7
+ end
8
+
9
+ factory :tomorrow, :class => Blog do
10
+ title '明日の天気は'
11
+ body '<DIV><FONT size="2">&nbsp;</FONT>雨のち曇りです。</DIV>'
12
+ end
13
+
14
+ factory :day_after_tomorrow, :class => Blog do
15
+ title '明後日の天気は'
16
+ body '<DIV><FONT size="2">決算の&nbsp;</FONT>雪です。</DIV>'
17
+ end
18
+
19
+ factory :yesterday, :class => Blog do
20
+ title '昨日の天気は'
21
+ body '<marquee>雹でした</marquee>'
22
+ comments {[ FactoryGirl.create(:comment) ]}
23
+ end
24
+
25
+ factory :blog do
26
+ title Faker::Lorem.sentence[1..20]
27
+ body Faker::Lorem.paragraph
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # coding: utf-8
2
+
3
+ FactoryGirl.define do
4
+ factory :comment do
5
+ body '<DIV>超寒い!!!11&lt;&gt;</DIV>'
6
+ end
7
+ end