search_do 0.2.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.
@@ -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