hunt 0.1

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,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'bson_ext', '1.1.5', :require => false
4
+
5
+ gemspec
6
+
7
+ gem 'i18n'
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ hunt (0.1)
5
+ fast-stemmer (~> 1.0)
6
+ mongo_mapper (~> 0.8.6)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activesupport (3.0.3)
12
+ bson (1.1.5)
13
+ bson_ext (1.1.5)
14
+ diff-lcs (1.1.2)
15
+ fast-stemmer (1.0.0)
16
+ i18n (0.4.2)
17
+ jnunemaker-validatable (1.8.4)
18
+ activesupport (>= 2.3.4)
19
+ mongo (1.1.5)
20
+ bson (>= 1.1.5)
21
+ mongo_mapper (0.8.6)
22
+ activesupport (>= 2.3.4)
23
+ jnunemaker-validatable (~> 1.8.4)
24
+ plucky (~> 0.3.6)
25
+ plucky (0.3.6)
26
+ mongo (~> 1.1)
27
+ rspec (2.3.0)
28
+ rspec-core (~> 2.3.0)
29
+ rspec-expectations (~> 2.3.0)
30
+ rspec-mocks (~> 2.3.0)
31
+ rspec-core (2.3.1)
32
+ rspec-expectations (2.3.0)
33
+ diff-lcs (~> 1.1.2)
34
+ rspec-mocks (2.3.0)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ bson_ext (= 1.1.5)
41
+ fast-stemmer (~> 1.0)
42
+ hunt!
43
+ i18n
44
+ mongo_mapper (~> 0.8.6)
45
+ rspec (~> 2.3)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 John Nunemaker
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.
@@ -0,0 +1,57 @@
1
+ = Hunt
2
+
3
+ Insanely stupid and basic indexed search for MongoMapper. Probably pointless, but something I am playing around with on side projects.
4
+
5
+ == Usage
6
+
7
+ Declare the plugin.
8
+
9
+ class Note
10
+ include MongoMapper::Document
11
+ plugin Hunt
12
+
13
+ key :title, String
14
+ key :body, String
15
+ key :tags, Array
16
+ timestamps!
17
+
18
+ searches :title, :body, :tags
19
+ end
20
+
21
+ This creates a key named searches that is a Hash. Title, body, and tags get mashed together before save into a unique array of stemmed words and stored in searches.default.
22
+
23
+ You can index the terms individually or with any other combination of keys.
24
+
25
+ Note.ensure_index :'searches.default' # or ...
26
+ Note.ensure_index [[:user_id, Mongo::Ascending], [:'searches.default', Mongo::Ascending]]
27
+
28
+ You also get a search class method that returns a scope.
29
+
30
+ # Returns Plucky::Query (MM Scope), no query actually fired
31
+ Note.search('mongodb')
32
+
33
+ # Gets everything matching mongodb
34
+ Note.search('mongodb').all
35
+
36
+ # Gets first page of everything matching mongodb
37
+ Note.search('mongodb').paginate(:page => 1)
38
+
39
+ # Counts everything matching mongodb
40
+ Note.search('mongodb').count
41
+
42
+ # Matches everything with any of the terms
43
+ Note.search('mongodb is awesome')
44
+
45
+ == Note on Patches/Pull Requests
46
+
47
+ * Fork the project.
48
+ * Make your feature addition or bug fix.
49
+ * Add tests for it. This is important so I don't break it in a
50
+ future version unintentionally.
51
+ * Commit, do not mess with rakefile, version, or history.
52
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
53
+ * Send me a pull request. Bonus points for topic branches.
54
+
55
+ == Copyright
56
+
57
+ Copyright (c) 2010 John Nunemaker. See LICENSE for details.
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task :default => :spec
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hunt/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hunt"
7
+ s.version = Hunt::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["John Nunemaker"]
10
+ s.email = ["nunemaker@gmail.com"]
11
+ s.homepage = "http://github.com/jnunemaker/hunt"
12
+ s.summary = %q{Really basic search for MongoMapper models.}
13
+ s.description = %q{Really basic search for MongoMapper models.}
14
+
15
+ s.add_dependency 'fast-stemmer', '~> 1.0'
16
+ s.add_dependency 'mongo_mapper', '~> 0.8.6'
17
+
18
+ s.add_development_dependency 'rspec', '~> 2.3'
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,36 @@
1
+ require 'fast_stemmer'
2
+ require 'hunt/util'
3
+
4
+ module Hunt
5
+ def self.configure(model)
6
+ model.before_save(:index_search_terms)
7
+ end
8
+
9
+ module ClassMethods
10
+ def search_keys
11
+ @search_keys ||= []
12
+ end
13
+
14
+ def searches(*keys)
15
+ # Using a hash to support multiple indexes per document at some point
16
+ key(:searches, Hash)
17
+ @search_keys = keys
18
+ end
19
+
20
+ def search(value)
21
+ terms = Util.to_stemmed_words(value)
22
+ return [] if terms.blank?
23
+ where('searches.default' => terms)
24
+ end
25
+ end
26
+
27
+ module InstanceMethods
28
+ def concatted_search_values
29
+ self.class.search_keys.map { |key| send(key) }.flatten.join(' ')
30
+ end
31
+
32
+ def index_search_terms
33
+ self.searches['default'] = Util.to_stemmed_words(concatted_search_values)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ module Hunt
2
+ module Util
3
+ Separator = ' '
4
+ StripPunctuationRegex = /[^a-zA-Z0-9]/
5
+ PunctuationReplacement = ''
6
+ WordsToIgnore = [
7
+ "about", "above", "after", "again", "against", "all",
8
+ "and", "any", "are", "aren't", "because", "been",
9
+ "before", "being", "below", "between", "both", "but", "cannot",
10
+ "can't", "could", "couldn't", "did", "didn't", "does", "doesn't",
11
+ "doing", "don't", "down", "during", "each", "few", "for", "from",
12
+ "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having",
13
+ "he'd", "he'll", "her", "here", "here's", "hers", "herself", "he's",
14
+ "him", "himself", "his", "how", "how's", "i'd", "if", "i'll", "i'm",
15
+ "into", "isn't", "its", "it's", "itself", "i've", "let's",
16
+ "more", "most", "mustn't", "myself", "nor", "not",
17
+ "off", "once", "only", "other", "ought", "our", "ours", "ourselves",
18
+ "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's",
19
+ "should", "shouldn't", "some", "such", "than", "that", "that's", "the",
20
+ "their", "theirs", "them", "themselves", "then", "there", "there's", "these",
21
+ "they", "they'd", "they'll", "they're", "they've", "this", "those", "through",
22
+ "too", "under", "until", "up", "very", "was", "wasn't", "we'd",
23
+ "we'll", "were", "we're", "weren't", "we've", "what", "what's", "when",
24
+ "when's", "where", "where's", "which", "while", "who", "whom", "who's",
25
+ "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd",
26
+ "you'll", "your", "you're", "yours", "yourself", "yourselves", "you've",
27
+ "the", "how"
28
+ ]
29
+
30
+ def strip_puncuation(value)
31
+ value.to_s.gsub(StripPunctuationRegex, PunctuationReplacement)
32
+ end
33
+
34
+ def stem(word)
35
+ Stemmer.stem_word(word)
36
+ end
37
+
38
+ def to_words(value)
39
+ value.
40
+ to_s.
41
+ squeeze(Separator).
42
+ split(Separator).
43
+ map { |word| word.downcase }.
44
+ reject { |word| word.size < 3 }.
45
+ reject { |word| WordsToIgnore.include?(word) }.
46
+ map { |word| strip_puncuation(word) }.
47
+ reject { |word| word.blank? }.
48
+ uniq
49
+ end
50
+
51
+ def to_stemmed_words(value)
52
+ to_words(value).map { |word| stem(word) }
53
+ end
54
+
55
+ extend self
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Hunt
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require(:default, :development)
7
+
8
+ require 'hunt'
9
+
10
+ MongoMapper.connection = Mongo::Connection.new
11
+ MongoMapper.database = 'hunt-test'
12
+
13
+ Rspec.configure do |c|
14
+ c.before(:each) do
15
+ MongoMapper.database.collections.each(&:remove)
16
+ end
17
+ end
@@ -0,0 +1,62 @@
1
+ require 'helper'
2
+
3
+ describe Hunt::Util do
4
+ describe ".strip_puncuation" do
5
+ it "removes punctuation" do
6
+ Hunt::Util.strip_puncuation('woot!').should == 'woot'
7
+ end
8
+ end
9
+
10
+ describe ".stem" do
11
+ it "stems word" do
12
+ Hunt::Util.stem('kissing').should == 'kiss'
13
+ Hunt::Util.stem('hello').should == 'hello'
14
+ Hunt::Util.stem('barfing').should == 'barf'
15
+ end
16
+ end
17
+
18
+ describe ".to_words" do
19
+ it "does not fail with nil" do
20
+ Hunt::Util.to_words(nil).should == []
21
+ end
22
+
23
+ it "converts string to array of words" do
24
+ Hunt::Util.to_words('first sentence').should == %w(first sentence)
25
+ end
26
+
27
+ it "squeezes multiple spaces" do
28
+ Hunt::Util.to_words('first sentence').should == %w(first sentence)
29
+ end
30
+
31
+ it "removes punctuation" do
32
+ Hunt::Util.to_words('woot!').should == %w(woot)
33
+ end
34
+
35
+ it "removes blanks from removed punctuation" do
36
+ Hunt::Util.to_words('first sentence & second').should == %w(first sentence second)
37
+ end
38
+
39
+ it "lowercases each word" do
40
+ Hunt::Util.to_words('Sweet First Sentence').should == %w(sweet first sentence)
41
+ end
42
+
43
+ it "removes any words under 3 characters" do
44
+ Hunt::Util.to_words('my first sentence').should == %w(first sentence)
45
+ end
46
+
47
+ it "removes words that should be ignored" do
48
+ Hunt::Util.to_words('how was your day').should == %w(day)
49
+ Hunt::Util.to_words("didn't you see that").should == %w(see)
50
+ end
51
+
52
+ it "removes duplicates" do
53
+ Hunt::Util.to_words('boom boom').should == %w(boom)
54
+ end
55
+ end
56
+
57
+ describe ".to_stemmed_words" do
58
+ it "converts value to array of stemmed words" do
59
+ Hunt::Util.to_stemmed_words('I just Caught you kissing.').should == %w(just caught kiss)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,150 @@
1
+ require 'helper'
2
+
3
+ class Note
4
+ include MongoMapper::Document
5
+
6
+ plugin Hunt
7
+
8
+ key :title, String
9
+ key :body, String
10
+ key :tags, Array
11
+ end
12
+
13
+ describe Hunt do
14
+ it "adds searches key to model to store search terms" do
15
+ Note.searches(:title)
16
+ Note.new.should respond_to(:searches)
17
+ Note.new.should respond_to(:searches=)
18
+ end
19
+
20
+ describe ".search" do
21
+ before(:each) do
22
+ Note.searches(:title)
23
+ end
24
+
25
+ it "returns empty array if nil" do
26
+ Note.search(nil).should == []
27
+ end
28
+
29
+ it "returns empty array if blank" do
30
+ Note.search('').should == []
31
+ end
32
+
33
+ context "with one search term" do
34
+ before(:each) do
35
+ @note = Note.create(:title => 'MongoDB is awesome!')
36
+ @result = Note.search('mongodb')
37
+ end
38
+
39
+ let(:note) { @note }
40
+ let(:result) { @result }
41
+
42
+ it "returns plucky query" do
43
+ result.should be_instance_of(Plucky::Query)
44
+ end
45
+
46
+ it "scopes query to searches.default in stemmed terms" do
47
+ result['searches.default'].should == {'$in' => %w(mongodb)}
48
+ end
49
+
50
+ it "does return matched documents" do
51
+ result.all.should include(note)
52
+ end
53
+
54
+ it "does not query unmatched documents" do
55
+ not_found = Note.create(:title => 'Something different')
56
+ result.all.should_not include(not_found)
57
+ end
58
+ end
59
+
60
+ context "with multiple search terms" do
61
+ before(:each) do
62
+ @note = Note.create(:title => 'MongoDB is awesome!')
63
+ @result = Note.search('mongodb is awesome')
64
+ end
65
+
66
+ let(:note) { @note }
67
+ let(:result) { @result }
68
+
69
+ it "returns plucky query" do
70
+ result.should be_instance_of(Plucky::Query)
71
+ end
72
+
73
+ it "scopes query to searches.default in stemmed terms" do
74
+ result['searches.default'].should == {'$in' => Hunt::Util.to_stemmed_words(note.concatted_search_values)}
75
+ end
76
+
77
+ it "returns documents that match both terms" do
78
+ result.all.should include(note)
79
+ end
80
+
81
+ it "returns documents that match any of the terms" do
82
+ awesome = Note.create(:title => 'Something awesome')
83
+ mongodb = Note.create(:title => 'Something MongoDB')
84
+ result.all.should include(awesome)
85
+ result.all.should include(mongodb)
86
+ end
87
+
88
+ it "does not query unmatched documents" do
89
+ not_found = Note.create(:title => 'Something different')
90
+ result.all.should_not include(not_found)
91
+ end
92
+ end
93
+ end
94
+
95
+ context "Search indexing" do
96
+ context "on one field" do
97
+ before(:each) do
98
+ Note.searches(:title)
99
+ @note = Note.create(:title => 'Woot for MongoDB!')
100
+ end
101
+
102
+ let(:note) { @note }
103
+
104
+ it "indexes terms on create" do
105
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
106
+ end
107
+
108
+ it "indexes terms on update" do
109
+ note.update_attributes(:title => 'Another woot')
110
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
111
+ end
112
+ end
113
+
114
+ context "on multiple fields" do
115
+ before(:each) do
116
+ Note.searches(:title, :body)
117
+ @note = Note.create(:title => 'Woot for MongoDB!', :body => 'This is my body.')
118
+ end
119
+
120
+ let(:note) { @note }
121
+
122
+ it "indexes merged terms on create" do
123
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
124
+ end
125
+
126
+ it "indexes merged terms on update" do
127
+ note.update_attributes(:title => 'Another woot', :body => 'An updated body.')
128
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
129
+ end
130
+ end
131
+
132
+ context "on multiple fields one of which is array key" do
133
+ before(:each) do
134
+ Note.searches(:title, :tags)
135
+ @note = Note.create(:title => 'Woot for MongoDB!', :tags => %w(mongo nosql))
136
+ end
137
+
138
+ let(:note) { @note }
139
+
140
+ it "indexes merged terms on create" do
141
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
142
+ end
143
+
144
+ it "indexes merged terms on update" do
145
+ note.update_attributes(:title => 'Another woot', :tags => %w(mongo))
146
+ note.searches['default'].should == Hunt::Util.to_stemmed_words(note.concatted_search_values)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,45 @@
1
+ def growl(title, msg, img)
2
+ %x{growlnotify -m #{ msg.inspect} -t #{title.inspect} --image ~/.watchr/#{img}.png}
3
+ end
4
+
5
+ def form_growl_message(str)
6
+ msg = str.split("\n").last
7
+ if msg =~ /(\d)\sfailure/
8
+ img = $1.to_i > 0 ? 'fail' : 'pass'
9
+ end
10
+ growl 'Results', msg, img
11
+ end
12
+
13
+ def run(cmd)
14
+ puts cmd
15
+ output = ""
16
+ IO.popen(cmd) do |com|
17
+ com.each_char do |c|
18
+ print c
19
+ output << c
20
+ $stdout.flush
21
+ end
22
+ end
23
+ form_growl_message output
24
+ end
25
+
26
+ def run_spec(path)
27
+ path.gsub!('lib/', 'spec/')
28
+ path.gsub!('_spec', '')
29
+ file_name = File.basename(path, '.rb')
30
+ path.gsub!(file_name, file_name + "_spec")
31
+ run %Q(bundle exec rspec #{path})
32
+ end
33
+
34
+ watch('spec/helper\.rb') { system('clear'); run('rake') }
35
+ watch('lib/.*\.rb') { |m| system('clear'); run_spec(m[0]) }
36
+ watch('spec/.*_spec\.rb') { |m| system('clear'); run_spec(m[0]) }
37
+
38
+ # Ctrl-\
39
+ Signal.trap('QUIT') do
40
+ puts " --- Running all tests ---\n\n"
41
+ run('rake')
42
+ end
43
+
44
+ # Ctrl-C
45
+ Signal.trap('INT') { abort("\n") }
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hunt
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - John Nunemaker
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-19 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: fast-stemmer
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 15
29
+ segments:
30
+ - 1
31
+ - 0
32
+ version: "1.0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: mongo_mapper
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 51
44
+ segments:
45
+ - 0
46
+ - 8
47
+ - 6
48
+ version: 0.8.6
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: rspec
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 5
60
+ segments:
61
+ - 2
62
+ - 3
63
+ version: "2.3"
64
+ type: :development
65
+ version_requirements: *id003
66
+ description: Really basic search for MongoMapper models.
67
+ email:
68
+ - nunemaker@gmail.com
69
+ executables: []
70
+
71
+ extensions: []
72
+
73
+ extra_rdoc_files: []
74
+
75
+ files:
76
+ - .gitignore
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - LICENSE
80
+ - README.rdoc
81
+ - Rakefile
82
+ - hunt.gemspec
83
+ - lib/hunt.rb
84
+ - lib/hunt/util.rb
85
+ - lib/hunt/version.rb
86
+ - spec/helper.rb
87
+ - spec/hunt/util_spec.rb
88
+ - spec/hunt_spec.rb
89
+ - specs.watchr
90
+ has_rdoc: true
91
+ homepage: http://github.com/jnunemaker/hunt
92
+ licenses: []
93
+
94
+ post_install_message:
95
+ rdoc_options: []
96
+
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ requirements: []
118
+
119
+ rubyforge_project:
120
+ rubygems_version: 1.3.7
121
+ signing_key:
122
+ specification_version: 3
123
+ summary: Really basic search for MongoMapper models.
124
+ test_files:
125
+ - spec/helper.rb
126
+ - spec/hunt/util_spec.rb
127
+ - spec/hunt_spec.rb