pose 0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{README.markdown → README.md} +83 -40
- data/Rakefile +3 -15
- data/lib/pose/base_additions.rb +10 -0
- data/lib/pose/model_additions.rb +45 -0
- data/lib/pose/railtie.rb +9 -0
- data/lib/pose/static_helpers.rb +139 -0
- data/lib/pose/version.rb +1 -1
- data/lib/pose.rb +4 -186
- data/spec/pose_spec.rb +8 -0
- data/spec/spec_helper.rb +17 -8
- metadata +22 -20
- data/lib/pose/posifier.rb +0 -13
@@ -5,7 +5,7 @@ Pose ("Polymorphic Search") allows fulltext search for ActiveRecord objects.
|
|
5
5
|
* Searches over several ActiveRecord classes at once.
|
6
6
|
* The searchable fulltext content per document can be freely customized.
|
7
7
|
* Uses the Rails database, no sparate search engines are necessary.
|
8
|
-
* The algorithm is designed to work with any data store that allows for range queries: SQL and NoSQL.
|
8
|
+
* The algorithm is designed to work with any data store that allows for range queries: SQL and NoSQL.
|
9
9
|
* The search runs very fast, doing simple queries over fully indexed columns.
|
10
10
|
* The search index provides data for autocomplete search fields.
|
11
11
|
|
@@ -14,17 +14,23 @@ Pose ("Polymorphic Search") allows fulltext search for ActiveRecord objects.
|
|
14
14
|
|
15
15
|
1. Add the gem to your Gemfile.
|
16
16
|
|
17
|
-
|
17
|
+
```ruby
|
18
|
+
gem 'pose'
|
19
|
+
```
|
18
20
|
|
19
21
|
2. Update your gem bundle.
|
20
22
|
|
21
|
-
|
23
|
+
```bash
|
24
|
+
$ bundle install
|
25
|
+
```
|
22
26
|
|
23
27
|
3. Create the database tables for pose.
|
24
28
|
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
```bash
|
30
|
+
$ rails generate pose
|
31
|
+
$ rake db:migrate
|
32
|
+
```
|
33
|
+
|
28
34
|
Pose creates two tables in your database:
|
29
35
|
|
30
36
|
* _pose_words_: index of all the words that occur in the searchable content.
|
@@ -33,25 +39,27 @@ Pose ("Polymorphic Search") allows fulltext search for ActiveRecord objects.
|
|
33
39
|
|
34
40
|
# Make your ActiveRecord models searchable
|
35
41
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
42
|
+
```ruby
|
43
|
+
class MyClass < ActiveRecord::Base
|
44
|
+
|
45
|
+
# This line tells Pose that your class should be searchable.
|
46
|
+
# Once Pose knows that, it will update the search index every time an instance is saved or deleted.
|
47
|
+
#
|
48
|
+
# The given block must return the searchble content as a string.
|
49
|
+
# Note that you can return whatever content you want here,
|
50
|
+
# not only data from this object but also data from related objects, class names, etc.
|
51
|
+
posify do
|
52
|
+
|
53
|
+
# Only active instances should show up in search results.
|
54
|
+
return unless status == :active
|
55
|
+
|
56
|
+
# Return the fulltext content.
|
57
|
+
[ self.foo,
|
58
|
+
self.parent.bar,
|
59
|
+
self.children.map &:name ].join ' '
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
55
63
|
|
56
64
|
|
57
65
|
# Maintain the search index
|
@@ -60,53 +68,88 @@ The search index is automatically updated when Objects are saved or deleted.
|
|
60
68
|
|
61
69
|
## Indexing existing objects in the database
|
62
70
|
If you had existing data in your database before adding Pose, it isn't automatically included in the search index.
|
63
|
-
They will be added on the next save/update operation on them.
|
71
|
+
They will be added on the next save/update operation on them.
|
64
72
|
You can also manually add existing objects to the search index.
|
65
73
|
|
66
|
-
|
74
|
+
```bash
|
75
|
+
$ rake pose:reindex_all[MyClass]
|
76
|
+
```
|
67
77
|
|
68
78
|
## Optimizing the search index
|
69
79
|
The search index keeps all the words that were ever used around, in order to try to reuse them in the future.
|
70
80
|
If you deleted a lot of objects, you can shrink the memory consumption of the search index by removing unused words.
|
71
81
|
|
72
|
-
|
82
|
+
```bash
|
83
|
+
$ rake pose:cleanup_index
|
84
|
+
```
|
73
85
|
|
74
86
|
## Removing the search index
|
75
87
|
For development purposes, or if something went wrong, you can remove the search index for a class
|
76
88
|
(let's call it "MyClass") completely.
|
77
89
|
|
78
|
-
|
90
|
+
```bash
|
91
|
+
rake pose:delete_index[MyClass]
|
92
|
+
```
|
79
93
|
|
80
94
|
|
81
95
|
# Perform a search
|
82
96
|
|
83
|
-
|
97
|
+
```ruby
|
98
|
+
result = Pose.search 'foo', [MyClass, MyOtherClass]
|
99
|
+
```
|
84
100
|
|
85
101
|
This searches for all instances of MyClass and MyOtherClass that contain the word 'foo'.
|
86
102
|
The method returns a hash that looks like this:
|
87
103
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
104
|
+
```ruby
|
105
|
+
{
|
106
|
+
MyClass => [ <myclass instance 1>, <myclass instance 2> ],
|
107
|
+
MyOtherClass => [ ],
|
108
|
+
}
|
109
|
+
```
|
110
|
+
|
93
111
|
In this example, it found two results of type _MyClass_ and no results of type _MyOtherClass_.
|
94
112
|
|
95
113
|
Happy searching! :)
|
96
114
|
|
97
115
|
|
116
|
+
# Autocomplete support
|
117
|
+
|
118
|
+
Because the search index contains a list of all the words known to the search engine,
|
119
|
+
it can provide data for autocompletion functionality through the following convenience method:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# Returns an array of strings that start with 'cat'.
|
123
|
+
autocomplete_words = Pose.autocomplete_words 'cat'
|
124
|
+
```
|
125
|
+
|
126
|
+
# Use Pose in your tests
|
127
|
+
|
128
|
+
By default, Pose doesn't run in Rails' test environment. This is to not slow down tests due to constant updating of the search index when objects are created.
|
129
|
+
If you want to test your models search functionality, you need to enable searching in tests:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
Pose::CONFIGURATION[:search_in_tests] = true
|
133
|
+
```
|
134
|
+
|
135
|
+
Please don't forget to set this value to `false` when you are done, or your remaining tests will be slow. A good place to enable/disable this flag is in before/after blocks of your test cases.
|
136
|
+
|
137
|
+
|
98
138
|
# Development
|
99
139
|
|
100
|
-
If you find a bug, have a question, or a better idea, please open an issue on the
|
101
|
-
<a href="https://github.com/kevgo/pose/issues">Pose issue tracker</a>.
|
140
|
+
If you find a bug, have a question, or a better idea, please open an issue on the
|
141
|
+
<a href="https://github.com/kevgo/pose/issues">Pose issue tracker</a>.
|
102
142
|
Or, clone the repository, make your changes, and submit a pull request.
|
103
143
|
|
104
|
-
##
|
144
|
+
## Run the unit tests for the Pose Gem
|
145
|
+
|
146
|
+
```bash
|
147
|
+
$ rake spec
|
148
|
+
```
|
105
149
|
|
106
|
-
rake spec
|
107
150
|
|
108
151
|
## Road Map
|
109
152
|
|
110
|
-
Pose's algorithm works with all sorts of storage technologies that support range queries, i.e. relational databases,
|
153
|
+
Pose's algorithm works with all sorts of storage technologies that support range queries, i.e. relational databases,
|
111
154
|
Google's DataStore, and other NoSQL stores. Right now, only relational databases are supported. NoSQL support is easy,
|
112
155
|
but not yet implemented.
|
data/Rakefile
CHANGED
@@ -9,18 +9,6 @@ require 'rspec/core/rake_task'
|
|
9
9
|
|
10
10
|
Bundler::GemHelper.install_tasks
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
namespace :spec do
|
18
|
-
|
19
|
-
desc "Run unit specs"
|
20
|
-
RSpec::Core::RakeTask.new('unit') do |t|
|
21
|
-
t.pattern = 'spec/{*_spec.rb}'
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
desc "Run the unit tests"
|
26
|
-
task :spec => ['spec:unit']
|
12
|
+
# RSpec tasks.
|
13
|
+
RSpec::Core::RakeTask.new :spec
|
14
|
+
task :default => :spec
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# This Module contains code that gets added to classes that are posified.
|
2
|
+
module Pose
|
3
|
+
module ModelAdditions
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_many :pose_assignments, :as => :posable, :dependent => :delete_all
|
8
|
+
has_many :pose_words, :through => :pose_assignments
|
9
|
+
|
10
|
+
after_save :update_pose_index
|
11
|
+
before_destroy :delete_pose_index
|
12
|
+
|
13
|
+
cattr_accessor :pose_content
|
14
|
+
end
|
15
|
+
|
16
|
+
# Updates the associated words for this object in the database.
|
17
|
+
def update_pose_index
|
18
|
+
update_pose_words if Pose.perform_search?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Removes this objects from the search index.
|
22
|
+
def delete_pose_index
|
23
|
+
self.pose_words.clear if Pose.perform_search?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper method.
|
27
|
+
# Updates the search words with the text returned by search_strings.
|
28
|
+
def update_pose_words
|
29
|
+
|
30
|
+
# Step 1: get an array of all words for the current object.
|
31
|
+
search_string = instance_eval &(self.class.pose_content)
|
32
|
+
new_words = search_string.to_s.split(' ').map { |word| Pose.root_word(word) }.flatten.uniq
|
33
|
+
|
34
|
+
# Step 2: Add new words to the search index.
|
35
|
+
Pose.get_words_to_add(self.pose_words, new_words).each do |word_to_add|
|
36
|
+
self.pose_words << PoseWord.find_or_create_by_text(word_to_add)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Step 3: Remove now obsolete words from search index.
|
40
|
+
Pose.get_words_to_remove(self.pose_words, new_words).each do |word_to_remove|
|
41
|
+
self.pose_words.delete word_to_remove
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/pose/railtie.rb
CHANGED
@@ -3,6 +3,15 @@ require "rails/railtie"
|
|
3
3
|
module Pose
|
4
4
|
|
5
5
|
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
# Add the Pose additions to ActiveRecord::Base once it is loaded.
|
8
|
+
initializer 'Pose.base_additions' do
|
9
|
+
ActiveSupport.on_load :active_record do
|
10
|
+
extend Pose::BaseAdditions
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Load the Pose rake tasks.
|
6
15
|
rake_tasks do
|
7
16
|
load "tasks/pose_tasks.rake"
|
8
17
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# Static helper methods of the Pose gem.
|
2
|
+
module Pose
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# By default, doesn't run in tests.
|
6
|
+
# Set this to true to test the search functionality.
|
7
|
+
CONFIGURATION = { :search_in_tests => false }
|
8
|
+
|
9
|
+
class <<self
|
10
|
+
|
11
|
+
# Asks if model should perform search.
|
12
|
+
#
|
13
|
+
# @return [false, true]
|
14
|
+
def perform_search?
|
15
|
+
!(Rails.env == 'test' and !CONFIGURATION[:search_in_tests])
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns all words that begin with the given query string.
|
19
|
+
# This can be used for autocompletion functionality.
|
20
|
+
#
|
21
|
+
# @param [String]
|
22
|
+
# @return [Array<String>]
|
23
|
+
def autocomplete_words query
|
24
|
+
return [] if query.blank?
|
25
|
+
PoseWord.where('text LIKE ?', "#{Pose.root_word(query)[0]}%").map(&:text)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns all strings that are in new_words, but not in existing_words.
|
29
|
+
# Helper method.
|
30
|
+
#
|
31
|
+
# @param [Array<String>] existing_words The words that are already associated with the object.
|
32
|
+
# @param [Array<String>] new_words The words thet the object should have from now on.
|
33
|
+
#
|
34
|
+
# @return [Array<String>] The words that need to be added to the existing_words array.
|
35
|
+
def get_words_to_add existing_words, new_words
|
36
|
+
new_words - existing_words.map(&:text)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Helper method.
|
40
|
+
# Returns the id of all word objects that are in existing_words, but not in new_words.
|
41
|
+
#
|
42
|
+
# @param [Array<String>] existing_words The words that are already associated with the object.
|
43
|
+
# @param [Array<String>] new_words The words thet the object should have from now on.
|
44
|
+
#
|
45
|
+
# @return [Array<String>] The words that need to be removed from the existing_words array.
|
46
|
+
def get_words_to_remove existing_words, new_words
|
47
|
+
existing_words.map do |existing_word|
|
48
|
+
existing_word unless new_words.include?(existing_word.text)
|
49
|
+
end.compact
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns whether the given string is a URL.
|
53
|
+
#
|
54
|
+
# @param [String] word The string to check.
|
55
|
+
#
|
56
|
+
# @return [Boolean]
|
57
|
+
def is_url? word
|
58
|
+
URI::parse(word).scheme == 'http'
|
59
|
+
rescue URI::InvalidURIError
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
# Simplifies the given word to a generic search form.
|
64
|
+
#
|
65
|
+
# @param [String] raw_word The word to make searchable.
|
66
|
+
#
|
67
|
+
# @return [String] The stemmed version of the word.
|
68
|
+
def root_word raw_word
|
69
|
+
result = []
|
70
|
+
raw_word_copy = raw_word[0..-1]
|
71
|
+
raw_word_copy.gsub! '%20', ' '
|
72
|
+
raw_word_copy.gsub! /[()*<>'",;\?\-\=&%#]/, ' '
|
73
|
+
raw_word_copy.gsub! /\s+/, ' '
|
74
|
+
raw_word_copy.split(' ').each do |word|
|
75
|
+
if Pose.is_url?(word)
|
76
|
+
result.concat word.split(/[\.\/\:]/).delete_if(&:blank?)
|
77
|
+
else
|
78
|
+
word.gsub! /[\-\/\._:]/, ' '
|
79
|
+
word.gsub! /\s+/, ' '
|
80
|
+
word.split(' ').each do |w|
|
81
|
+
stemmed_word = w.parameterize.singularize
|
82
|
+
result.concat stemmed_word.split ' '
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
result.uniq
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns all objects matching the given query.
|
90
|
+
#
|
91
|
+
# @param [String] query
|
92
|
+
# @param (Class|[Array<Class>]) classes
|
93
|
+
# @param [Number?] limit Optional limit.
|
94
|
+
#
|
95
|
+
# @return [Hash<Class, ActiveRecord::Relation>]
|
96
|
+
def search query, classes, limit = nil
|
97
|
+
|
98
|
+
# Turn 'classes' into an array.
|
99
|
+
classes = [classes].flatten
|
100
|
+
classes_names = classes.map &:name
|
101
|
+
classes_names = classes_names[0] if classes_names.size == 1
|
102
|
+
|
103
|
+
# Get the ids of the results.
|
104
|
+
result_classes_and_ids = {}
|
105
|
+
query.split(' ').each do |query_word|
|
106
|
+
current_word_classes_and_ids = {}
|
107
|
+
classes.each { |clazz| current_word_classes_and_ids[clazz.name] = [] }
|
108
|
+
query = PoseAssignment.joins(:pose_word) \
|
109
|
+
.where('pose_words.text LIKE ?', "#{query_word}%") \
|
110
|
+
.where('posable_type IN (?)', classes_names)
|
111
|
+
query.each do |pose_assignment|
|
112
|
+
current_word_classes_and_ids[pose_assignment.posable_type] << pose_assignment.posable_id
|
113
|
+
end
|
114
|
+
|
115
|
+
current_word_classes_and_ids.each do |class_name, ids|
|
116
|
+
if result_classes_and_ids.has_key? class_name
|
117
|
+
result_classes_and_ids[class_name] = result_classes_and_ids[class_name] & ids
|
118
|
+
else
|
119
|
+
result_classes_and_ids[class_name] = ids
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Load the results by id.
|
125
|
+
{}.tap do |result|
|
126
|
+
result_classes_and_ids.each do |class_name, ids|
|
127
|
+
result_class = Kernel.const_get class_name
|
128
|
+
|
129
|
+
if ids.any? && classes.include?(result_class)
|
130
|
+
ids = ids.slice(0, limit) if limit
|
131
|
+
result[result_class] = result_class.where :id => ids
|
132
|
+
else
|
133
|
+
result[result_class] = []
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/pose/version.rb
CHANGED
data/lib/pose.rb
CHANGED
@@ -2,191 +2,9 @@
|
|
2
2
|
require 'rake'
|
3
3
|
include Rake::DSL if defined? Rake::DSL
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# By default, doesn't run in tests.
|
11
|
-
# Set this to true to test the search functionality.
|
12
|
-
CONFIGURATION = { :search_in_tests => false }
|
13
|
-
|
14
|
-
included do
|
15
|
-
has_many :pose_assignments, :as => :posable, :dependent => :delete_all
|
16
|
-
has_many :pose_words, :through => :pose_assignments
|
17
|
-
|
18
|
-
after_save :update_pose_index
|
19
|
-
before_destroy :delete_pose_index
|
20
|
-
|
21
|
-
cattr_accessor :pose_content
|
22
|
-
end
|
23
|
-
|
24
|
-
# Asks if model should perform search.
|
25
|
-
#
|
26
|
-
# @return [false, true]
|
27
|
-
def perform_search?
|
28
|
-
!(Rails.env == 'test' and !CONFIGURATION[:search_in_tests])
|
29
|
-
end
|
30
|
-
|
31
|
-
module InstanceMethods
|
32
|
-
|
33
|
-
# Updates the associated words for this object in the database.
|
34
|
-
def update_pose_index
|
35
|
-
update_pose_words if perform_search?
|
36
|
-
end
|
37
|
-
|
38
|
-
# Removes this objects from the search index.
|
39
|
-
def delete_pose_index
|
40
|
-
self.pose_words.clear if perform_search?
|
41
|
-
end
|
42
|
-
|
43
|
-
# Helper method.
|
44
|
-
# Updates the search words with the text returned by search_strings.
|
45
|
-
def update_pose_words
|
46
|
-
|
47
|
-
# Step 1: get an array of all words for the current object.
|
48
|
-
search_string = instance_eval &(self.class.pose_content)
|
49
|
-
new_words = search_string.to_s.split(' ').map { |word| Pose.root_word(word) }.flatten.uniq
|
50
|
-
|
51
|
-
# Step 2: Add new words to the search index.
|
52
|
-
Pose.get_words_to_add(self.pose_words, new_words).each do |word_to_add|
|
53
|
-
self.pose_words << PoseWord.find_or_create_by_text(word_to_add)
|
54
|
-
end
|
55
|
-
|
56
|
-
# Step 3: Remove now obsolete words from search index.
|
57
|
-
Pose.get_words_to_remove(self.pose_words, new_words).each do |word_to_remove|
|
58
|
-
self.pose_words.delete word_to_remove
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
class <<self
|
64
|
-
|
65
|
-
# Returns all words that begin with the given query string.
|
66
|
-
# This can be used for autocompletion functionality.
|
67
|
-
#
|
68
|
-
# @param [String]
|
69
|
-
# @return [Array<String>]
|
70
|
-
def autocomplete_words query
|
71
|
-
return [] if query.blank?
|
72
|
-
PoseWord.where('text LIKE ?', "#{Pose.root_word(query)[0]}%").map(&:text)
|
73
|
-
end
|
74
|
-
|
75
|
-
# Returns all strings that are in new_words, but not in existing_words.
|
76
|
-
# Helper method.
|
77
|
-
#
|
78
|
-
# @param [Array<String>] existing_words The words that are already associated with the object.
|
79
|
-
# @param [Array<String>] new_words The words thet the object should have from now on.
|
80
|
-
#
|
81
|
-
# @return [Array<String>] The words that need to be added to the existing_words array.
|
82
|
-
def get_words_to_add existing_words, new_words
|
83
|
-
new_words - existing_words.map(&:text)
|
84
|
-
end
|
85
|
-
|
86
|
-
# Helper method.
|
87
|
-
# Returns the id of all word objects that are in existing_words, but not in new_words.
|
88
|
-
#
|
89
|
-
# @param [Array<String>] existing_words The words that are already associated with the object.
|
90
|
-
# @param [Array<String>] new_words The words thet the object should have from now on.
|
91
|
-
#
|
92
|
-
# @return [Array<String>] The words that need to be removed from the existing_words array.
|
93
|
-
def get_words_to_remove existing_words, new_words
|
94
|
-
existing_words.map do |existing_word|
|
95
|
-
existing_word unless new_words.include?(existing_word.text)
|
96
|
-
end.compact
|
97
|
-
end
|
98
|
-
|
99
|
-
# Returns whether the given string is a URL.
|
100
|
-
#
|
101
|
-
# @param [String] word The string to check.
|
102
|
-
#
|
103
|
-
# @return [Boolean]
|
104
|
-
def is_url? word
|
105
|
-
URI::parse(word).scheme == 'http'
|
106
|
-
rescue URI::InvalidURIError
|
107
|
-
false
|
108
|
-
end
|
109
|
-
|
110
|
-
# Simplifies the given word to a generic search form.
|
111
|
-
#
|
112
|
-
# @param [String] raw_word The word to make searchable.
|
113
|
-
#
|
114
|
-
# @return [String] The stemmed version of the word.
|
115
|
-
def root_word raw_word
|
116
|
-
result = []
|
117
|
-
raw_word_copy = raw_word[0..-1]
|
118
|
-
raw_word_copy.gsub! '%20', ' '
|
119
|
-
raw_word_copy.gsub! /[()*<>'",;\?\-\=&%#]/, ' '
|
120
|
-
raw_word_copy.gsub! /\s+/, ' '
|
121
|
-
raw_word_copy.split(' ').each do |word|
|
122
|
-
if Pose.is_url?(word)
|
123
|
-
result.concat word.split(/[\.\/\:]/).delete_if(&:blank?)
|
124
|
-
else
|
125
|
-
word.gsub! /[\-\/\._:]/, ' '
|
126
|
-
word.gsub! /\s+/, ' '
|
127
|
-
word.split(' ').each do |w|
|
128
|
-
stemmed_word = w.parameterize.singularize
|
129
|
-
result.concat stemmed_word.split ' '
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
result.uniq
|
134
|
-
end
|
135
|
-
|
136
|
-
# Returns all objects matching the given query.
|
137
|
-
#
|
138
|
-
# @param [String] query
|
139
|
-
# @param (Class|[Array<Class>]) classes
|
140
|
-
# @param [Number?] limit Optional limit.
|
141
|
-
#
|
142
|
-
# @return [Hash<Class, ActiveRecord::Relation>]
|
143
|
-
def search query, classes, limit = nil
|
144
|
-
|
145
|
-
# Turn 'classes' into an array.
|
146
|
-
classes = [classes].flatten
|
147
|
-
classes_names = classes.map &:name
|
148
|
-
classes_names = classes_names[0] if classes_names.size == 1
|
149
|
-
|
150
|
-
# Get the ids of the results.
|
151
|
-
result_classes_and_ids = {}
|
152
|
-
query.split(' ').each do |query_word|
|
153
|
-
current_word_classes_and_ids = {}
|
154
|
-
classes.each { |clazz| current_word_classes_and_ids[clazz.name] = [] }
|
155
|
-
query = PoseAssignment.joins(:pose_word) \
|
156
|
-
.where('pose_words.text LIKE ?', "#{query_word}%") \
|
157
|
-
.where('posable_type IN (?)', classes_names)
|
158
|
-
query.each do |pose_assignment|
|
159
|
-
current_word_classes_and_ids[pose_assignment.posable_type] << pose_assignment.posable_id
|
160
|
-
end
|
161
|
-
|
162
|
-
current_word_classes_and_ids.each do |class_name, ids|
|
163
|
-
if result_classes_and_ids.has_key? class_name
|
164
|
-
result_classes_and_ids[class_name] = result_classes_and_ids[class_name] & ids
|
165
|
-
else
|
166
|
-
result_classes_and_ids[class_name] = ids
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
# Load the results by id.
|
172
|
-
{}.tap do |result|
|
173
|
-
result_classes_and_ids.each do |class_name, ids|
|
174
|
-
result_class = Kernel.const_get class_name
|
175
|
-
|
176
|
-
if ids.any? && classes.include?(result_class)
|
177
|
-
ids = ids.slice(0, limit) if limit
|
178
|
-
result[result_class] = result_class.where :id => ids
|
179
|
-
else
|
180
|
-
result[result_class] = []
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
|
189
|
-
require 'pose/posifier'
|
190
|
-
require "pose/railtie" if defined? Rails
|
5
|
+
require 'pose/static_helpers'
|
6
|
+
require 'pose/base_additions'
|
7
|
+
require 'pose/model_additions'
|
8
|
+
require 'pose/railtie' if defined? Rails
|
191
9
|
require 'pose_assignment'
|
192
10
|
require 'pose_word'
|
data/spec/pose_spec.rb
CHANGED
@@ -300,6 +300,14 @@ describe Pose do
|
|
300
300
|
result.should include('green')
|
301
301
|
end
|
302
302
|
|
303
|
+
it 'returns words that match the given phrase exactly' do
|
304
|
+
PoseWord.create :text => 'cat'
|
305
|
+
|
306
|
+
result = Pose.autocomplete_words 'cat'
|
307
|
+
|
308
|
+
result.should == ['cat']
|
309
|
+
end
|
310
|
+
|
303
311
|
it 'stems the search query' do
|
304
312
|
PosableOne.create :text => 'car'
|
305
313
|
|
data/spec/spec_helper.rb
CHANGED
@@ -5,11 +5,8 @@ require 'bundler/setup'
|
|
5
5
|
require 'hashie'
|
6
6
|
require 'active_record'
|
7
7
|
require 'active_support/core_ext/module/aliasing'
|
8
|
-
require "pose"
|
9
|
-
require 'pose_word.rb'
|
10
|
-
require 'pose_assignment.rb'
|
11
|
-
require "pose/posifier.rb"
|
12
8
|
require 'active_support/core_ext/string'
|
9
|
+
require 'pose'
|
13
10
|
require 'faker'
|
14
11
|
require 'factory_girl'
|
15
12
|
FactoryGirl.find_definitions
|
@@ -18,13 +15,11 @@ FactoryGirl.find_definitions
|
|
18
15
|
ENV["RAILS_ENV"] = "test"
|
19
16
|
Rails = Hashie::Mash.new({:env => 'test'})
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
# We have no Railtie in these tests --> load Pose manually.
|
19
|
+
ActiveRecord::Base.send :extend, Pose::BaseAdditions
|
24
20
|
|
25
21
|
# Load support files
|
26
22
|
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
27
|
-
#Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
|
28
23
|
|
29
24
|
|
30
25
|
RSpec.configure do |config|
|
@@ -52,6 +47,11 @@ RSpec.configure do |config|
|
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
50
|
+
|
51
|
+
#####################
|
52
|
+
# MATCHERS
|
53
|
+
#
|
54
|
+
|
55
55
|
# Verifies that a taggable object has the given tags.
|
56
56
|
RSpec::Matchers.define :have_pose_words do |expected|
|
57
57
|
match do |actual|
|
@@ -70,6 +70,10 @@ RSpec::Matchers.define :have_pose_words do |expected|
|
|
70
70
|
end
|
71
71
|
|
72
72
|
|
73
|
+
#####################
|
74
|
+
# TEST CLASSES
|
75
|
+
#
|
76
|
+
|
73
77
|
class PosableOne < ActiveRecord::Base
|
74
78
|
posify { text }
|
75
79
|
end
|
@@ -78,6 +82,11 @@ class PosableTwo < ActiveRecord::Base
|
|
78
82
|
posify { text }
|
79
83
|
end
|
80
84
|
|
85
|
+
|
86
|
+
#####################
|
87
|
+
# DATABASE SETUP
|
88
|
+
#
|
89
|
+
|
81
90
|
def setup_db
|
82
91
|
ActiveRecord::Schema.define(:version => 1) do
|
83
92
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pose
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02
|
12
|
+
date: 2012-03-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement: &
|
16
|
+
requirement: &70121989270460 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.0.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70121989270460
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
requirement: &
|
27
|
+
requirement: &70121989278420 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70121989278420
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: ruby-progressbar
|
38
|
-
requirement: &
|
38
|
+
requirement: &70121993362680 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70121993362680
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: factory_girl
|
49
|
-
requirement: &
|
49
|
+
requirement: &70121993370180 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70121993370180
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: faker
|
60
|
-
requirement: &
|
60
|
+
requirement: &70121993367740 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70121993367740
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: hashie
|
71
|
-
requirement: &
|
71
|
+
requirement: &70121993380460 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70121993380460
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: rspec
|
82
|
-
requirement: &
|
82
|
+
requirement: &70121993397120 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70121993397120
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: sqlite3
|
93
|
-
requirement: &
|
93
|
+
requirement: &70121993395560 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,7 +98,7 @@ dependencies:
|
|
98
98
|
version: '0'
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70121993395560
|
102
102
|
description: Pose ('Polymorphic Search') allows fulltext search for ActiveRecord objects.
|
103
103
|
email:
|
104
104
|
- kevin.goslar@gmail.com
|
@@ -127,8 +127,10 @@ files:
|
|
127
127
|
- doc/top-level-namespace.html
|
128
128
|
- lib/generators/pose_generator.rb
|
129
129
|
- lib/generators/templates/migration.rb
|
130
|
-
- lib/pose/
|
130
|
+
- lib/pose/base_additions.rb
|
131
|
+
- lib/pose/model_additions.rb
|
131
132
|
- lib/pose/railtie.rb
|
133
|
+
- lib/pose/static_helpers.rb
|
132
134
|
- lib/pose/version.rb
|
133
135
|
- lib/pose.rb
|
134
136
|
- lib/pose_assignment.rb
|
@@ -136,7 +138,7 @@ files:
|
|
136
138
|
- lib/tasks/pose_tasks.rake
|
137
139
|
- MIT-LICENSE
|
138
140
|
- Rakefile
|
139
|
-
- README.
|
141
|
+
- README.md
|
140
142
|
- spec/factories.rb
|
141
143
|
- spec/pose_assignment_spec.rb
|
142
144
|
- spec/pose_spec.rb
|
data/lib/pose/posifier.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
module Pose
|
2
|
-
module Posifier
|
3
|
-
extend ActiveSupport::Concern
|
4
|
-
|
5
|
-
module ClassMethods
|
6
|
-
def posify &block
|
7
|
-
include Pose
|
8
|
-
self.pose_content = block_given? ? block : :pose_content.to_proc
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
ActiveRecord::Base.send :include, Pose::Posifier
|