ohm-contrib 0.0.42 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  module Ohm
2
2
  module Contrib
3
- VERSION = "0.0.42"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
 
6
6
  autoload :ActiveModelExtension, "ohm/contrib/active_model_extension"
@@ -14,8 +14,8 @@ module Ohm
14
14
  autoload :Typecast, "ohm/contrib/typecast"
15
15
  autoload :Locking, "ohm/contrib/locking"
16
16
  autoload :Callbacks, "ohm/contrib/callbacks"
17
- autoload :LunarMacros, "ohm/contrib/lunar_macros"
18
17
  autoload :Slug, "ohm/contrib/slug"
19
18
  autoload :Scope, "ohm/contrib/scope"
20
19
  autoload :SoftDelete, "ohm/contrib/soft_delete"
21
- end
20
+ autoload :FulltextSearching, "ohm/contrib/fulltext_searching"
21
+ end
@@ -0,0 +1,80 @@
1
+ begin
2
+ require "text"
3
+ rescue LoadError
4
+ raise LoadError,
5
+ "Type `[sudo] gem install text` to use Ohm::FulltextSearching."
6
+ end
7
+
8
+ module Ohm
9
+ module FulltextSearching
10
+ def self.included(model)
11
+ model.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ def search(hash)
16
+ find(hash_to_metaphones(hash))
17
+ end
18
+
19
+ def hash_to_metaphones(hash)
20
+ ret = Hash.new { |h, k| h[k] = [] }
21
+
22
+ hash.each do |att, string|
23
+ metaphones(string).each { |m| ret[:"fulltext_#{att}"] << m }
24
+ end
25
+
26
+ return ret
27
+ end
28
+
29
+ def double_metaphone(str)
30
+ return [] if STOPWORDS.include?(str.to_s.downcase)
31
+
32
+ Text::Metaphone.double_metaphone(str).compact
33
+ end
34
+
35
+ def metaphones(str)
36
+ str.to_s.strip.split(/\s+/).map { |s| double_metaphone(s) }.flatten
37
+ end
38
+
39
+ def fulltext(att)
40
+ field = :"fulltext_#{att}"
41
+
42
+ define_method(field) { self.class.metaphones(send(att)) }
43
+ index(field)
44
+ end
45
+ end
46
+
47
+ STOPWORDS = %w{a about above according across actually adj after
48
+ afterwards again against all almost alone along already also although
49
+ always among amongst an and another any anyhow anyone anything anywhere
50
+ are aren't around as at b be became because become becomes becoming
51
+ been before beforehand begin behind being below beside besides between
52
+ beyond both but by c can can't cannot caption co co. could couldn't d
53
+ did didn't do does doesn't don't down during e each eg eight eighty
54
+ either else elsewhere end ending enough etc even ever every everyone
55
+ everything everywhere except f few first for found from further g h
56
+ had has hasn't have haven't he he'd he'll he's hence her here here's
57
+ hereafter hereby herein hereupon hers herself him himself his how
58
+ however hundred i i'd i'll i'm i've ie if in inc. indeed instead into is
59
+ isn't it it's its itself j k l last later latter latterly least less let
60
+ let's like likely ltd m made make makes many maybe me meantime meanwhile
61
+ might miss more moreover most mostly mr mrs much must my myself n namely
62
+ neither never nevertheless next nine ninety no nobody none nonetheless
63
+ noone nor not nothing now nowhere o of off often on once one one's only
64
+ onto or other others otherwise our ours ourselves out over overall own
65
+ p per perhaps q r rather recent recently s same seem seemed seeming
66
+ seems seven several she she'd she'll she's should shouldn't since so
67
+ some somehow someone something sometime sometimes somewhere still such
68
+ t taking than that that'll that's that've the their them themselves then
69
+ thence there there'd there'll there're there's there've thereafter thereby
70
+ therefore therein thereupon these they they'd they'll they're they've
71
+ thirty this those though three through throughout thru thus to together
72
+ too toward towards u under unless unlike unlikely until up upon us used
73
+ using v very via w was wasn't we we'd we'll we're we've well were weren't
74
+ what what'll what's what've whatever when whence whenever where where's
75
+ whereafter whereas whereby wherein whereupon wherever whether which while
76
+ whither who who'd who'll who's whoever whole whom whomever whose why will
77
+ with within without won't would wouldn't x y yes yet you you'd you'll
78
+ you're you've your yours yourself yourselves z}
79
+ end
80
+ end
@@ -1,9 +1,20 @@
1
1
  require 'bigdecimal'
2
2
  require 'time'
3
3
  require 'date'
4
- require 'json'
5
4
  require 'forwardable'
6
5
 
6
+ begin
7
+ require "yajl/json_gem"
8
+ rescue LoadError
9
+ $stderr.puts(
10
+ "WARNING: Currently using the `json` gem. You might encounter encoding " +
11
+ "problems with `Ohm::Types::Hash` and `Ohm::Types::Array`. Best to use " +
12
+ "the `yajl-ruby` gem to avoid problems and also for performance reasons."
13
+ )
14
+
15
+ require "json"
16
+ end
17
+
7
18
  module Ohm
8
19
  # Provides all the primitive types. The following are included:
9
20
  #
@@ -342,5 +353,4 @@ module Ohm
342
353
  end
343
354
  end
344
355
  end
345
- end
346
-
356
+ end
@@ -4,6 +4,7 @@ require File.expand_path("./helper", File.dirname(__FILE__))
4
4
 
5
5
  test "autoloading of all libraries" do
6
6
  assert_nothing_raised NameError, LoadError do
7
+ Ohm::ActiveModelExtension
7
8
  Ohm::Boundaries
8
9
  Ohm::Timestamping
9
10
  Ohm::WebValidations
@@ -12,8 +13,7 @@ test "autoloading of all libraries" do
12
13
  Ohm::Locking
13
14
  Ohm::ExtraValidations
14
15
  Ohm::DateValidations
15
- Ohm::LunarMacros
16
+ Ohm::FulltextSearching
16
17
  Ohm::Slug
17
18
  end
18
- end
19
-
19
+ end
@@ -0,0 +1 @@
1
+ café
@@ -0,0 +1,63 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ class Video < Ohm::Model
6
+ include Ohm::FulltextSearching
7
+
8
+ attribute :title
9
+ attribute :description
10
+
11
+ fulltext :title
12
+ fulltext :description
13
+ end
14
+
15
+ test "finding basic words using metaphones" do
16
+ v = Video.create(:title => "The quick brown fox jumps over the lazy dog")
17
+
18
+ assert Video.find(:fulltext_title => "KK").include?(v)
19
+ assert Video.find(:fulltext_title => "PRN").include?(v)
20
+ assert Video.find(:fulltext_title => "FKS").include?(v)
21
+ assert Video.find(:fulltext_title => "JMPS").include?(v)
22
+ assert Video.find(:fulltext_title => "AMPS").include?(v)
23
+ assert Video.find(:fulltext_title => ["JMPS", "AMPS"]).include?(v)
24
+ assert Video.find(:fulltext_title => "LS").include?(v)
25
+ assert Video.find(:fulltext_title => "TK").include?(v)
26
+ end
27
+
28
+ test "finding using the actual words" do
29
+ v = Video.create(:title => "The quick brown fox jumps over the lazy dog")
30
+
31
+ assert Video.search(:title => "quick").include?(v)
32
+ assert Video.search(:title => "brown").include?(v)
33
+ assert Video.search(:title => "fox").include?(v)
34
+ assert Video.search(:title => "jumps").include?(v)
35
+ assert Video.search(:title => "lazy").include?(v)
36
+ assert Video.search(:title => "dog").include?(v)
37
+ end
38
+
39
+ test "finding using slightly mispelled words" do
40
+ v = Video.create(:title => "The quick brown fox jumps over the lazy dog")
41
+
42
+ assert Video.search(:title => "quik").include?(v)
43
+ assert Video.search(:title => "brwn").include?(v)
44
+ assert Video.search(:title => "fx").include?(v)
45
+ assert Video.search(:title => "fax").include?(v)
46
+ assert Video.search(:title => "jumps").include?(v)
47
+ assert Video.search(:title => "lazi").include?(v)
48
+ assert Video.search(:title => "dag").include?(v)
49
+ end
50
+
51
+ test "stopword stripping" do
52
+ # This is the actual code that strips out stopwords.
53
+
54
+ assert_equal [], Video.double_metaphone("about")
55
+
56
+ # Here we just verify that actually on a long string level,
57
+ # stop words are in fact stripped.
58
+ assert Video.metaphones(Video::STOPWORDS.join(" ")).empty?
59
+
60
+ # Finally we need to make sure that finding stop words
61
+ # should not even proceed.
62
+ assert Video.hash_to_metaphones(:title => "about").empty?
63
+ end
@@ -139,22 +139,8 @@ test "raises when trying to assign a non-array" do
139
139
  end
140
140
  end
141
141
 
142
- test "inspecting" do
143
- post = Post.create(:addresses => [{ "address1" => "#456",
144
- "city" => "Singapore",
145
- "country" => "SG" }])
146
-
147
- expected = %q{[{"address1":"#456","city":"Singapore","country":"SG"}]}
148
-
149
- assert expected == post.addresses.inspect
150
-
151
- post.addresses = 'FooBar'
152
- assert %{"\\\"FooBar\\\""} == post.addresses.inspect
153
- end
154
-
155
142
  test "type is array" do
156
143
  post = Post.create(:addresses => ["address1"])
157
144
 
158
145
  assert post.addresses.type == Array
159
- end
160
-
146
+ end
@@ -103,20 +103,18 @@ test "raises when trying to assign a non-hash" do
103
103
  end
104
104
  end
105
105
 
106
- test "inspecting" do
107
- post = Post.create(:address => { "address1" => "#456",
108
- "city" => "Singapore",
109
- "country" => "SG" })
110
-
111
- expected = %q{{"address1":"#456","city":"Singapore","country":"SG"}}
112
- assert expected == post.address.inspect
113
-
114
- post.address = 'FooBar'
115
- assert %{"\\\"FooBar\\\""} == post.address.inspect
116
- end
117
-
118
106
  test "type is Hash" do
119
107
  post = Post.new(:address => { "address1" => "#456" })
120
108
  assert Hash == post.address.type
121
109
  end
122
110
 
111
+ test "ascii 8bit encoding" do
112
+ txt = File.expand_path("fixtures/ascii8bit.txt", File.dirname(__FILE__))
113
+
114
+ data = File.read(txt, :encoding => "ascii-8bit")
115
+
116
+ post = Post.create(:address => { "address1" => data })
117
+ post = Post[post.id]
118
+
119
+ assert_equal post.address["address1"], data.force_encoding("UTF-8")
120
+ end if defined?(Encoding)
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 1
7
8
  - 0
8
- - 42
9
- version: 0.0.42
9
+ version: 0.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Cyril David
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-11-23 00:00:00 +08:00
17
+ date: 2011-01-08 00:00:00 +08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -96,9 +96,9 @@ files:
96
96
  - lib/ohm/contrib/callbacks.rb
97
97
  - lib/ohm/contrib/date_validations.rb
98
98
  - lib/ohm/contrib/extra_validations.rb
99
+ - lib/ohm/contrib/fulltext_searching.rb
99
100
  - lib/ohm/contrib/length_validations.rb
100
101
  - lib/ohm/contrib/locking.rb
101
- - lib/ohm/contrib/lunar_macros.rb
102
102
  - lib/ohm/contrib/number_validations.rb
103
103
  - lib/ohm/contrib/scope.rb
104
104
  - lib/ohm/contrib/slug.rb
@@ -115,10 +115,11 @@ files:
115
115
  - test/boundaries_test.rb
116
116
  - test/callbacks_lint.rb
117
117
  - test/date_validations_test.rb
118
+ - test/fixtures/ascii8bit.txt
119
+ - test/fulltext_searching_test.rb
118
120
  - test/helper.rb
119
121
  - test/instance_callbacks_test.rb
120
122
  - test/length_validations_test.rb
121
- - test/lunar_macros_test.rb
122
123
  - test/macro_callbacks_test.rb
123
124
  - test/membership_validation_test.rb
124
125
  - test/number_validations_test.rb
@@ -1,58 +0,0 @@
1
- begin
2
- require 'lunar'
3
- rescue LoadError
4
- raise "You have to install Lunar in order to use Ohm::LunarMacros."
5
- end
6
-
7
- module Ohm
8
- module LunarMacros
9
- def self.included(base)
10
- base.send :include, Ohm::Callbacks
11
- base.after :save, :update_lunar_index
12
- base.after :delete, :delete_lunar_index
13
-
14
- base.extend ClassMethods
15
- end
16
-
17
- module ClassMethods
18
- def fuzzy(*atts) lunar_fields(:fuzzy, *atts) end
19
- def text(*atts) lunar_fields(:text, *atts) end
20
- def number(*atts) lunar_fields(:number, *atts) end
21
- def sortable(*atts) lunar_fields(:sortable, *atts) end
22
-
23
- def lunar_fields(type, *atts)
24
- @lunar_fields ||= Hash.new { |h, k| h[k] = [] }
25
-
26
- atts.each { |att|
27
- @lunar_fields[type] << att unless @lunar_fields[type].include?(att)
28
- }
29
-
30
- @lunar_fields[type]
31
- end
32
- end
33
-
34
- def update_lunar_index
35
- Lunar.index self.class do |i|
36
- i.id id
37
-
38
- [:fuzzy, :text, :number, :sortable].each do |type|
39
- self.class.lunar_fields(type).each do |field|
40
- value = send(field)
41
-
42
- if type == :text and value.kind_of?(Enumerable)
43
- i.text field, value.join(' ') unless value.empty?
44
- else
45
- i.send type, field, value unless value.to_s.empty?
46
- end
47
- end
48
- end
49
- end
50
- end
51
-
52
- protected
53
- def delete_lunar_index
54
- Lunar.delete self.class, id
55
- end
56
- end
57
- end
58
-
@@ -1,146 +0,0 @@
1
- # encoding: UTF-8
2
-
3
- require File.expand_path("./helper", File.dirname(__FILE__))
4
-
5
- class Post < Ohm::Model
6
- include Ohm::LunarMacros
7
- end
8
-
9
- test "fuzzy / text / number / sortable" do
10
- Post.fuzzy :name
11
- assert [:name] == Post.fuzzy
12
-
13
- Post.text :name
14
- assert [:name] == Post.text
15
-
16
- Post.number :name
17
- assert [:name] == Post.number
18
-
19
- Post.sortable :name
20
- assert [:name] == Post.sortable
21
- end
22
-
23
- test "fuzzy / text / number / sortable done twice" do
24
- Post.fuzzy :name
25
- Post.fuzzy :name
26
- assert [:name] == Post.fuzzy
27
-
28
- Post.text :name
29
- Post.text :name
30
- assert [:name] == Post.text
31
-
32
- Post.number :name
33
- Post.number :name
34
- assert [:name] == Post.number
35
-
36
- Post.sortable :name
37
- Post.sortable :name
38
- assert [:name] == Post.sortable
39
- end
40
-
41
- class Document < Ohm::Model
42
- include Ohm::LunarMacros
43
-
44
- fuzzy :filename
45
- text :content
46
- number :author_id
47
- sortable :views
48
-
49
- attribute :filename
50
- attribute :content
51
- attribute :author_id
52
- attribute :views
53
- end
54
-
55
- test "fuzzy indexing" do
56
- doc = Document.create(:filename => "amazing.mp3")
57
-
58
- strs = %w{a am ama amaz amazi amazin amazing amazing. amazing.m
59
- amazing.mp amazing.mp3}
60
-
61
- strs.each do |str|
62
- r = Lunar.search(Document, :fuzzy => { :filename => str })
63
- assert r.include?(doc)
64
- end
65
-
66
- doc.update(:filename => "crazy.mp3")
67
-
68
- strs = %w{c cr cra craz crazy crazy. crazy.m crazy.mp crazy.mp3}
69
-
70
- strs.each do |str|
71
- r = Lunar.search(Document, :fuzzy => { :filename => str })
72
- assert r.include?(doc)
73
- end
74
- end
75
-
76
- test "text indexing" do
77
- doc = Document.create(:content => "The quick brown fox")
78
-
79
- queries = ['quick', 'brown', 'fox', 'quick brown' 'quick fox', 'fox brown',
80
- 'the quick brown fox']
81
-
82
- queries.each do |q|
83
- r = Lunar.search(Document, :q => q)
84
- assert r.include?(doc)
85
- end
86
-
87
- doc.update(:content => "lazy dog jumps over")
88
-
89
- queries = ['lazy', 'dog', 'jumps', 'over', 'lazy dog', 'jumps over']
90
-
91
- queries.each do |q|
92
- r = Lunar.search(Document, :q => q)
93
- assert r.include?(doc)
94
- end
95
- end
96
-
97
- test "number indexing" do
98
- doc = Document.create(:author_id => 99)
99
-
100
- [99..100, 98..99, 98..100].each do |range|
101
- r = Lunar.search(Document, :author_id => range)
102
- assert r.include?(doc)
103
- end
104
-
105
- [97..98, 100..101].each do |range|
106
- r = Lunar.search(Document, :author_id => range)
107
- assert ! r.include?(doc)
108
- end
109
-
110
- doc.update(:author_id => 49)
111
-
112
- [49..50, 48..49, 48..50].each do |range|
113
- r = Lunar.search(Document, :author_id => range)
114
- assert r.include?(doc)
115
- end
116
-
117
- [47..48, 50..51].each do |range|
118
- r = Lunar.search(Document, :author_id => range)
119
- assert ! r.include?(doc)
120
- end
121
- end
122
-
123
- test "sortable indexing" do
124
- osx = Document.create(:content => "apple mac osx", :views => 500)
125
- iphone = Document.create(:content => "apple iphone", :views => 10_000)
126
-
127
- assert [iphone, osx] == Lunar.search(Document, :q => "apple").sort_by(:views, :order => "DESC")
128
- assert [osx, iphone] == Lunar.search(Document, :q => "apple").sort_by(:views, :order => "ASC")
129
-
130
- osx.update(:content => "ios mac osx", :views => 1000)
131
- iphone.update(:content => "ios iphone", :views => 500)
132
-
133
- assert [osx, iphone] == Lunar.search(Document, :q => "ios").sort_by(:views, :order => "DESC")
134
- assert [iphone, osx] == Lunar.search(Document, :q => "ios").sort_by(:views, :order => "ASC")
135
- end
136
-
137
- test "on delete" do
138
- doc = Document.create(:filename => "amazing.mp3", :content => "apple mac osx",
139
- :author_id => 99, :views => 500)
140
-
141
- doc.delete
142
-
143
- assert Lunar.search(Document, :q => "apple").size.zero?
144
- assert Lunar.search(Document, :fuzzy => { :filename => "amazing" }).size.zero?
145
- end
146
-