pose 0.3 → 1.0.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.
- 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
|