hunt 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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