search_do 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,335 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ require 'digest/sha1'
4
+
5
+ describe Story, "extended by acts_as_searchable_enhance" do
6
+ it "should respond_to :fulltext_search" do
7
+ Story.should respond_to(:fulltext_search)
8
+ end
9
+
10
+ it "should have callbak :update_index" do
11
+ Story.after_update.should include(:update_index)
12
+ end
13
+
14
+ it "should have callbak :add_to_index" do
15
+ Story.after_create.should include(:add_to_index)
16
+ end
17
+
18
+ describe "separate node by model classname" do
19
+ before(:all) do
20
+ OtherKlass = Class.new(ActiveRecord::Base)
21
+ OtherKlass.class_eval do
22
+ acts_as_searchable :ignore_timestamp => true
23
+ end
24
+ end
25
+
26
+ it "OtherKlass.search_backend.node_name.should == 'aas_e_test_other_klasses'" do
27
+ OtherKlass.search_backend.node_name.should == 'aas_e_test_other_klasses'
28
+ end
29
+
30
+ it "search_backend.node_name should == 'aas_e_test_stories'" do
31
+ Story.search_backend.node_name.should == 'aas_e_test_stories'
32
+ end
33
+ end
34
+
35
+ describe "matched_ids" do
36
+ fixtures :stories
37
+ before do
38
+ @story_ids = Story.find(:all, :limit=>2).map(&:id)
39
+
40
+ @mock_results = @story_ids.map{|id| mock("ResultDocument_#{id}", :attr => id) }
41
+ nres = EstraierPure::NodeResult.new(@mock_results, {})
42
+ Story.search_backend.connection.stub!(:search).and_return(nres)
43
+ end
44
+
45
+ it "finds all story ids" do
46
+ Story.matched_ids("hoge").should == @story_ids
47
+ end
48
+
49
+ it "calls EstraierPure::Node#search()" do
50
+ Story.search_backend.connection.should_receive(:search)
51
+ Story.matched_ids("hoge")
52
+ end
53
+ end
54
+
55
+ describe "remove from index" do
56
+ fixtures :stories
57
+ before do
58
+ stories = Story.find(:all, :limit=>2)
59
+ @story = stories.first
60
+ mock_results = stories.map{|s| mock("ResultDocument_#{s.id}", :attr => s.id) }
61
+
62
+ nres = EstraierPure::NodeResult.new(mock_results, {})
63
+ Story.search_backend.connection.stub!(:search).and_return(nres)
64
+ end
65
+
66
+ it "should call EstraierPure::Node#delete_from_index" do
67
+ Story.search_backend.connection.should_receive(:out_doc)
68
+ @story.remove_from_index
69
+ end
70
+ end
71
+
72
+ describe "add_snippets" do
73
+ before :each do
74
+ @stories = Story.find :all
75
+ count = 0
76
+ @ids_and_raw = @stories.map {|story| [story.id,fake_raw(count+=1)]}
77
+ end
78
+
79
+ it "adds snippets to objects" do
80
+ Story.send(:add_snippets,@stories,@ids_and_raw)
81
+ @stories.map(&:snippet).should == ['snip1','snip2']
82
+ end
83
+
84
+ it "adds snippets to right objects" do
85
+ Story.send(:add_snippets,@stories.reverse,@ids_and_raw)
86
+ @stories.map(&:snippet).should == ['snip1','snip2']
87
+ end
88
+ end
89
+
90
+ describe :snippet_to_html do
91
+ it "surrounds snippets with bold" do
92
+ Story.send(:snippet_to_html,[['x',true],['y',false]]).should == "<b>x</b>y"
93
+ end
94
+
95
+ it "removes tags html" do#since they would get broken up
96
+ Story.send(:snippet_to_html,[['ab</b>ab',true],['y',false]]).should == "<b>abab</b>y"
97
+ Story.send(:snippet_to_html,[['ab<br>ab',true],['y',false]]).should == "<b>abab</b>y"
98
+ end
99
+
100
+ it "strips tags" do
101
+ Story.send(:snippet_to_html,[['ab<a>x</a>ab',true],['y',false]]).should == "<b>abxab</b>y"
102
+ end
103
+ end
104
+
105
+ describe "fulltext_search" do
106
+ #matched_ids_and_raw => [:id,raw] and find_option=>{:condition => 'id = :id'}
107
+
108
+ fixtures :stories
109
+ before do
110
+ stories = Story.find(:all)
111
+ @story = stories.first
112
+ fake_results = stories.map{|story| [story.id,fake_raw]}
113
+ Story.stub!(:matched_ids_and_raw).and_return fake_results
114
+ end
115
+
116
+ def fulltext_search(query='hoge')
117
+ finder_opt = {:conditions => ["id = ?", @story.id]}
118
+ Story.fulltext_search(query, :find => finder_opt)
119
+ end
120
+
121
+ it "finds story" do
122
+ fulltext_search.should == [@story]
123
+ end
124
+
125
+ it "adds snippets" do
126
+ fulltext_search[0].snippet.should == 'snip'
127
+ end
128
+
129
+ it "does not add snippets if query is empty" do
130
+ fulltext_search('')[0].snippet.should be_nil
131
+ end
132
+
133
+ it "does not add snippet when object does not respond to snippet=" do
134
+ @story.stub!(:respond_to?).with(:html_snippet=)
135
+ @story.should_receive(:respond_to?).with(:snippet=).and_return false
136
+ Story.should_receive(:find).and_return [@story]
137
+ fulltext_search[0].snippet.should be_blank
138
+ end
139
+
140
+ it "calls matched_ids_and_raw" do
141
+ Story.should_receive(:matched_ids_and_raw).and_return([[@story.id,fake_raw]])
142
+ fulltext_search
143
+ end
144
+ end
145
+
146
+ describe "paginate_by_fulltext_search" do
147
+ before do
148
+ Story.stub!(:fulltext_search).and_return [Story.first]
149
+ Story.stub!(:count_fulltext).and_return 1
150
+ end
151
+
152
+ it "has total_entries" do
153
+ Story.paginate_by_fulltext_search('',:page=>1,:per_page=>1).total_entries.should == 1
154
+ end
155
+
156
+ it "translates paginate terms to limit and offset and removes page/per_page" do
157
+ Story.should_receive(:fulltext_search).with('x',{:limit=>3,:offset=>6}).and_return []
158
+ Story.paginate_by_fulltext_search('x',:page=>3,:per_page=>3)
159
+ end
160
+
161
+ it "uses search word and attributes for count query" do
162
+ Story.should_receive(:count_fulltext).with('x',:attributes=>'something').and_return 1
163
+ Story.paginate_by_fulltext_search('x',:page=>1,:per_page=>1,:attributes=>'something')
164
+ end
165
+
166
+ it "calculates total_entries from search results" do
167
+ Story.should_receive(:count_fulltext).never
168
+ Story.paginate_by_fulltext_search('',:page=>1,:per_page=>2).total_entries.should == 1
169
+ end
170
+ end
171
+
172
+ describe "new interface Model.find_fulltext(query, options={})" do
173
+ fixtures :stories
174
+
175
+ before do
176
+ Story.stub!(:matched_ids).and_return([102, 101, 110])
177
+ end
178
+
179
+ it "find_fulltext('hoge', :order=>'updated_at DESC') should == [stories(:sanshiro), stories(:neko)]" do
180
+ Story.find_fulltext('hoge', :order=>"updated_at DESC").should == Story.find([102,101]).reverse
181
+ end
182
+ end
183
+
184
+ describe "search using real HyperEstraier (End-to-End test)" do
185
+ fixtures :stories
186
+ before(:all) do
187
+ Story.clear_index!
188
+ #Story.delete_all
189
+ Story.create!(:title=>"むかしむかし", :body=>"あるところにおじいさんとおばあさんが")
190
+ Story.create!(:title=>"i am so blue", :body=>"testing makes me happy",:non_column=>'non column value')
191
+ Story.reindex!
192
+ # waiting Estraier sync index, adjust 'cachernum' in ${estraier}/_conf if need
193
+ sleep 1
194
+ end
195
+
196
+ before do
197
+ @story = Story.find_by_title("むかしむかし")
198
+ end
199
+
200
+ it "finds a indexed object" do
201
+ Story.fulltext_search('むかしむかし').should == [@story]
202
+ end
203
+
204
+ it "counts correctly using count_fulltext" do
205
+ Story.count_fulltext('むかしむかし').should == 1
206
+ end
207
+
208
+ it "finds all object when searching for ''" do
209
+ Story.fulltext_search('').size.should == Story.count
210
+ end
211
+
212
+ it "finds using long strings" do
213
+ Story.fulltext_search("i am so blue").size.should == 1
214
+ end
215
+
216
+ it "finds using attributes" do
217
+ #FIXME this fails if iSTRAND is used, why?
218
+ Story.fulltext_search('',:attributes=>{:body=>'るとこ'}).size.should == 1
219
+ end
220
+
221
+ it "finds using long strings in attributes" do
222
+ Story.fulltext_search('',:attributes=>{:body=>'testing makes'}).size.should == 1
223
+ end
224
+
225
+ it "finds using long strings using and in attributes" do
226
+ pending do
227
+ #FIXME this works if iSTRAND is used, but then "finds using attributes" fails
228
+ Story.fulltext_search('',:attributes=>{:body=>'testing makes happy'}).size.should == 1
229
+ end
230
+ end
231
+
232
+ it "finds using upper or lowercase attributes" do
233
+ Story.fulltext_search('',:attributes=>{:body=>'MakeS'}).size.should == 1
234
+ end
235
+
236
+ it "finds using non_column attributes" do
237
+ pending do
238
+ Story.fulltext_search('',:attributes=>{:non_column=>'non column value'}).size.should == 1
239
+ end
240
+ end
241
+
242
+ # asserts HE raw_match order
243
+ it "finds in correct order(descending)" do
244
+ Story.matched_ids('記憶', :order => "@mdate NUMD").should == [101, 102]
245
+ end
246
+
247
+ it "finds in correct order(ascending)" do
248
+ Story.matched_ids('記憶', :order => "@mdate NUMA").should == [102, 101]
249
+ end
250
+
251
+ it "preserves order of found objects" do
252
+ Story.fulltext_search('記憶', :order => "@mdate NUMA").map(&:id).should == [102, 101]
253
+ end
254
+
255
+ it "preservers order if scope is given" do
256
+ pending
257
+ end
258
+
259
+ it "has all objects in index" do
260
+ Story.search_backend.index.size.should == Story.count
261
+ end
262
+ end
263
+
264
+ describe "partial updating" do
265
+ fixtures :stories
266
+ before do
267
+ @story = Story.find(:first)
268
+ @story.stub!(:record_timestamps).and_return(false)
269
+ end
270
+
271
+ it "updates when changing indexed column" do
272
+ Story.search_backend.should_receive(:add_to_index).once
273
+ @story.title = "new title"
274
+ @story.save
275
+ end
276
+
277
+ it "does not updates when changing unindexed column" do
278
+ Story.search_backend.should_not_receive(:add_to_index)
279
+ @story.popularity += 10
280
+ @story.save
281
+ end
282
+ end
283
+
284
+ def fake_raw(num=nil)
285
+ mock("Raw",:snippet=>"snip#{num}")
286
+ end
287
+ end
288
+
289
+ describe "StoryWithoutAutoUpdate" do
290
+ before(:all) do
291
+ class StoryWithoutAutoUpdate < ActiveRecord::Base
292
+ set_table_name :stories
293
+ acts_as_searchable :searchable_fields=>[:title, :body], :auto_update=>false
294
+ end
295
+ end
296
+
297
+ it "should not have callback :update_index" do
298
+ StoryWithoutAutoUpdate.after_update.should_not include(:update_index)
299
+ end
300
+
301
+ it "should not have callback :add_to_index" do
302
+ StoryWithoutAutoUpdate.after_create.should_not include(:add_to_index)
303
+ end
304
+
305
+ it "should not call add_to_index" do
306
+ story = StoryWithoutAutoUpdate.new
307
+ StoryWithoutAutoUpdate.search_backend.should_not_receive(:add_to_index)
308
+ story.popularity = 20
309
+ story.save
310
+ end
311
+ end
312
+
313
+ describe SearchDo::Utils do
314
+ describe "tokenize_query" do
315
+ it "converts nil to empty string" do
316
+ SearchDo::Utils.cleanup_query(nil).should == ''
317
+ end
318
+
319
+ it "does not convert empty strings to nil" do
320
+ SearchDo::Utils.cleanup_query('').should == ''
321
+ end
322
+
323
+ it "does not alter search terms'" do
324
+ SearchDo::Utils.cleanup_query('ruby vim').should == 'ruby vim'
325
+ end
326
+
327
+ it "leaves in quotes" do
328
+ SearchDo::Utils.cleanup_query('"ruby on rails" vim').should == '"ruby on rails" vim'
329
+ end
330
+
331
+ it "converts long unicode spaces" do
332
+ SearchDo::Utils.cleanup_query('rails vim').should == 'rails vim'
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'active_record/fixtures'
4
+
5
+ #activate will_paginate for models
6
+ require 'will_paginate'
7
+ WillPaginate.enable_activerecord
8
+
9
+ #create model table
10
+ ActiveRecord::Schema.define(:version => 1) do
11
+ create_table "stories" do |t|
12
+ t.date "written_on"
13
+ t.datetime "read_at"
14
+ t.string "title"
15
+ t.text "body"
16
+ t.integer "popularity", :default =>0
17
+ t.timestamps
18
+ end
19
+ end
20
+
21
+ #create model
22
+ class Story < ActiveRecord::Base
23
+ attr_accessor :snippet
24
+ acts_as_searchable :searchable_fields=>[:title, :body],:attributes=>{:non_column=>nil,:body=>nil}
25
+
26
+ def non_column
27
+ @non_column
28
+ end
29
+
30
+ def non_column=(val)
31
+ @non_column=val
32
+ end
33
+ end
34
+
35
+ #create node
36
+ require File.expand_path("../lib/estraier_admin", File.dirname(__FILE__))
37
+ admin = EstraierAdmin.new(ActiveRecord::Base.configurations["test"][:estraier])
38
+ admin.create_node(Story.search_backend.node_name)
@@ -0,0 +1,52 @@
1
+ # ---- requirements
2
+ $KCODE = 'u' #activate regex unicode
3
+ require 'rubygems'
4
+ require 'spec'
5
+ $LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
6
+
7
+
8
+ # ---- bugfix
9
+ #`exit?': undefined method `run?' for Test::Unit:Module (NoMethodError)
10
+ #can be solved with require test/unit but this will result in extra test-output
11
+ unless defined? Test::Unit
12
+ module Test
13
+ module Unit
14
+ def self.run?
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+ # ---- load active record
23
+ #gem 'activerecord', '2.0.2'
24
+ if ENV["AR"]
25
+ gem 'activerecord', ENV["AR"]
26
+ $stderr.puts("Using ActiveRecord #{ENV["AR"]}")
27
+ end
28
+ require 'active_record'
29
+
30
+ require File.expand_path("../init", File.dirname(__FILE__))
31
+
32
+ RAILS_ENV = "test"
33
+ ActiveRecord::Base.configurations = {"test" => {
34
+ :adapter => "sqlite3",
35
+ :database => ":memory:",
36
+ :estraier => {:host=> "localhost", :node=>"aas_e_test", :user=>"admin", :password=>"admin"}
37
+ }.with_indifferent_access}
38
+
39
+ ActiveRecord::Base.logger = Logger.new(File.directory?("log") ? "log/#{RAILS_ENV}.log" : "/dev/null")
40
+ ActiveRecord::Base.establish_connection(:test)
41
+
42
+ load File.expand_path("setup_test_model.rb", File.dirname(__FILE__))
43
+
44
+
45
+ # ---- fixtures
46
+ Spec::Example::ExampleGroupMethods.module_eval do
47
+ def fixtures(*tables)
48
+ dir = File.expand_path("fixtures", File.dirname(__FILE__))
49
+ tables.each{|table| Fixtures.create_fixtures(dir, table.to_s) }
50
+ end
51
+ end
52
+
@@ -0,0 +1,70 @@
1
+ require File.expand_path("../lib/estraier_admin", File.dirname(__FILE__))
2
+
3
+ namespace :search do
4
+ desc "Clears HE Index"
5
+ task :clear => :environment do
6
+ raise "Pass a searchable model with MODEL=" unless ENV['MODEL']
7
+ ENV['MODEL'].constantize.clear_index!
8
+ end
9
+
10
+ desc "Reindexes all model attributes"
11
+ task :reindex => :environment do
12
+ raise "Pass a searchable model with MODEL=" unless ENV['MODEL']
13
+ model_class = ENV['MODEL'].constantize
14
+ reindex = lambda { model_class.reindex! }
15
+ if ENV['INCLUDE']
16
+ model_class.with_scope :find => { :include => ENV['INCLUDE'].split(',').collect { |i| i.strip.to_sym } } do
17
+ reindex.call
18
+ end
19
+ else
20
+ reindex.call
21
+ end
22
+ end
23
+
24
+ namespace :node do
25
+ desc "Create HE node"
26
+ task :create => :environment do
27
+ raise "Pass a searchable model with MODEL=" unless ENV['MODEL']
28
+ model_class = ENV['MODEL'].constantize
29
+ admin.create_node(model_class.search_backend.node_name)
30
+ end
31
+
32
+ desc "Delete HE node"
33
+ task :delete => :environment do
34
+ raise "Pass a searchable model with MODEL=" unless ENV['MODEL']
35
+ model_class = ENV['MODEL'].constantize
36
+ admin.delete_node(model_class.search_backend.node_name)
37
+ end
38
+
39
+ def admin
40
+ EstraierAdmin.new(ActiveRecord::Base.configurations[RAILS_ENV]["estraier"])
41
+ end
42
+ end
43
+
44
+ namespace :server do
45
+ desc "initialize HyperEstraier index directory on localhost"
46
+ task :init do
47
+ system(estmaster, 'init', index_dir) unless File.directory?(index_dir)
48
+ end
49
+
50
+ desc "start HyperEstraier server on loaclhost"
51
+ task :start => [:init] do
52
+ $stderr.puts "starting HyperEstraier server..."
53
+ system(estmaster, 'start', '-bg', index_dir)
54
+ end
55
+
56
+ desc "stop HyperEstraier server on localhost"
57
+ task :stop => [:init] do
58
+ $stderr.puts "stopping HyperEstraier server..."
59
+ system(estmaster, 'stop', index_dir)
60
+ end
61
+
62
+ def estmaster
63
+ ENV['COMMAND'] || 'estmaster'
64
+ end
65
+
66
+ def index_dir
67
+ ENV['SEARCH_INDEX_DIR'] || File.expand_path("tmp/fulltext_index", Dir.pwd)
68
+ end
69
+ end
70
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: search_do
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - MOROHASHI Kyosuke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-24 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: moronatural@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - CHANGELOG
26
+ - MIT-LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - TESTING
30
+ - VERSION
31
+ - examples/he_search.rb
32
+ - examples/person.rb
33
+ - init.rb
34
+ - lib/estraier_admin.rb
35
+ - lib/search_do.rb
36
+ - lib/search_do/backends.rb
37
+ - lib/search_do/backends/hyper_estraier.rb
38
+ - lib/search_do/backends/hyper_estraier/estraier_pure_extention.rb
39
+ - lib/search_do/dirty_tracking.rb
40
+ - lib/search_do/dirty_tracking/bridge.rb
41
+ - lib/search_do/dirty_tracking/self_made.rb
42
+ - lib/search_do/indexer.rb
43
+ - lib/search_do/utils.rb
44
+ - lib/vendor/estraierpure.rb
45
+ - lib/vendor/overview
46
+ - recipes/mode_maintenance.rb
47
+ - spec/backends/hyper_estraier_spec.rb
48
+ - spec/backends/result_document_spec.rb
49
+ - spec/dirty_tracking/bridge_spec.rb
50
+ - spec/estraier_admin_spec.rb
51
+ - spec/fixtures/stories.yml
52
+ - spec/indexer_spec.rb
53
+ - spec/search_do_spec.rb
54
+ - spec/setup_test_model.rb
55
+ - spec/spec_helper.rb
56
+ - tasks/acts_as_searchable_tasks.rake
57
+ has_rdoc: true
58
+ homepage: http://github.com/grosser/search_do
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options:
63
+ - --charset=UTF-8
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.5
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: "AR: Hyperestraier integration"
85
+ test_files:
86
+ - spec/spec_helper.rb
87
+ - spec/backends/result_document_spec.rb
88
+ - spec/backends/hyper_estraier_spec.rb
89
+ - spec/indexer_spec.rb
90
+ - spec/setup_test_model.rb
91
+ - spec/estraier_admin_spec.rb
92
+ - spec/search_do_spec.rb
93
+ - spec/dirty_tracking/bridge_spec.rb
94
+ - examples/person.rb
95
+ - examples/he_search.rb