pose 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a28304ad3e5a60d843f3df073cc68a0842b3a157
4
+ data.tar.gz: 8e1bb1ea33bcc319b84c06e8cd13a2332453bbfb
5
+ SHA512:
6
+ metadata.gz: 0c8b59c95e6a5cc7d1abf04743ece0b64882080c23a8e1dc96966ab2550ca6e858899ac2ef3d05f6d997df01d324c98598ba158dadede884d7fbf1dd77d23ed0
7
+ data.tar.gz: a628c6936417db7d987fb88a84b60149364507be84c4b3ad4808366e28885013f7867c8c99bf5d81853bec730ea423c94cd2d00170c39d581b5fb8eb1306201a
data/README.md CHANGED
@@ -1,14 +1,13 @@
1
- # Pose <a href="http://travis-ci.org/#!/kevgo/pose" target="_blank"><img src="https://secure.travis-ci.org/kevgo/pose.png" alt="Build status"></a> <a href="https://codeclimate.com/github/kevgo/pose" target="_blank"><img src="https://codeclimate.com/badge.png" /></a>
1
+ # POlymorphic SEarch <a href="http://travis-ci.org/#!/kevgo/pose" target="_blank"><img src="https://secure.travis-ci.org/kevgo/pose.png" alt="Build status"></a> [![Code Climate](https://codeclimate.com/github/kevgo/pose.png)](https://codeclimate.com/github/kevgo/pose) [![Coverage Status](https://coveralls.io/repos/kevgo/pose/badge.png?branch=master)](https://coveralls.io/r/kevgo/pose) [![Dependency Status](https://gemnasium.com/kevgo/pose.png)](https://gemnasium.com/kevgo/pose)
2
2
 
3
- Pose ("Polymorphic Search") allows fulltext search for ActiveRecord objects in Ruby on Rails.
3
+ A database agnostic fulltext search engine for ActiveRecord objects in Ruby on Rails.
4
4
 
5
5
  * Searches over several classes at once.
6
6
  * The searchable content of each class and document can be freely customized.
7
- * Uses the main Rails database, no separate servers, databases, or search engines are necessary.
8
- * Does not pollute the searchable classes or their database tables with any attributes.
9
- * Allows to combine the fulltext search with any other custom database searches.
10
- * The algorithm is designed to work with any data store that allows for range queries, which covers pretty much every SQL or NoSQL database.
11
- * The search is very fast, doing only simple queries over fully indexed columns.
7
+ * Uses the main Rails database - no separate servers, databases, or search engines required.
8
+ * Does not pollute the searchable classes nor their database tables.
9
+ * Very fast search, doing only simple queries over fully indexed columns.
10
+ * Allows to augment the fulltext search query with your own joins and where clauses.
12
11
 
13
12
 
14
13
  ## Installation
@@ -40,7 +39,7 @@ Pose creates two tables in your database. These tables are automatically populat
40
39
  class MyClass < ActiveRecord::Base
41
40
 
42
41
  # This line makes your class searchable.
43
- # The given block must return the searchble content as a string.
42
+ # The given block must return the searchable content as a string.
44
43
  posify do
45
44
 
46
45
  # Only active instances should show up in search results.
@@ -65,12 +64,12 @@ Now that this class is posified, any `create`, `update`, or `delete` operation o
65
64
  Data that existed in your database before adding Pose isn't automatically included in the search index.
66
65
  You have to index those records manually once. Future updates will happen automatically.
67
66
 
68
- To index all entries of `MyClass`, run `rake pose:reindex_all[MyClass]` on the command line.
67
+ To index all entries of `MyClass`, run `rake 'pose:reindex_all[MyClass]'` on the command line.
69
68
 
70
69
  At this point, you are all set up. Let's perform a search!
71
70
 
72
71
 
73
- ## Upgrading from version 1.x
72
+ ### Upgrading from version 1.x
74
73
 
75
74
  Version 2 is a proper Rails engine, and comes with a slightly different database table schema.
76
75
  Upgrading is as simple as
@@ -108,7 +107,13 @@ search options.
108
107
 
109
108
  ### Configure the searched classes
110
109
 
111
- Pose accepts an array of classes to search over. When searching a single class, it can be provided directly, i.e. not as an array.
110
+ Pose accepts an array of classes to search over.
111
+
112
+ ```ruby
113
+ result = Pose.search 'search text', [MyClass, MyOtherClass]
114
+ ```
115
+
116
+ When searching a single class, it can be provided directly, i.e. not as an array.
112
117
 
113
118
  ```ruby
114
119
  result = Pose.search 'foo', MyClass
@@ -117,34 +122,42 @@ result = Pose.search 'foo', MyClass
117
122
 
118
123
  ### Configure the result data
119
124
 
120
- By default, search results are the instances of the objects matching the search query.
125
+ Search results are the instances of the objects matching the search query.
121
126
  If you want to just get the ids of the search results, and not the full instances, use the parameter `:result_type`.
122
127
 
123
128
  ```ruby
124
- result = Pose.search 'foo', MyClass, result_type: :ids # Returns ids instead of object instances.
129
+ # Returns ids instead of object instances.
130
+ result = Pose.search 'foo', MyClass, result_type: :ids
125
131
  ```
126
132
 
127
133
 
128
134
  ### Limit the amount of search results
129
135
 
130
- By default, Pose returns all matching items. Large result sets can become very slow and resource intensive to process.
136
+ By default, Pose returns all matching items.
131
137
  To limit the result set, use the `:limit` search parameter.
132
138
 
133
139
  ```ruby
134
- result = Pose.search 'foo', MyClass, limit: 20 # Returns only 20 search results.
140
+ # Returns only 20 search results.
141
+ result = Pose.search 'foo', MyClass, limit: 20
135
142
  ```
136
143
 
137
144
 
138
145
  ### Combine fulltext search with structured data search
139
146
 
140
- You can add your own ActiveRecord query clauses to a fulltext search operation.
147
+ You can add your own ActiveRecord query clauses (JOINs and WHEREs) to a fulltext search operation.
141
148
  For example, given a class `Note` that belongs to a `User` class and has a boolean attribute `public`,
142
149
  finding all public notes from other users containing "foo" is as easy as:
143
150
 
144
151
  ```ruby
145
- result = Pose.search 'foo', MyClass, where: [ public: true, ['user_id <> ?', @current_user.id] ]
152
+ result = Pose.search 'foo',
153
+ Note,
154
+ joins: Note,
155
+ where: [ ['notes.public = ?', true],
156
+ ['user_id <> ?', @current_user.id] ] ]
146
157
  ```
147
158
 
159
+ Combining ActiveRecord query clauses with fulltext search only works when searching over a single class.
160
+
148
161
 
149
162
  ## Maintenance
150
163
 
@@ -154,8 +167,8 @@ The search index is automatically updated when objects are created, updated, or
154
167
 
155
168
  ### Optimizing the search index
156
169
 
157
- For performance reasons, the search index keeps all the words that were ever used around, in order to try to reuse them as much as possible.
158
- After deleting or changing a large number of objects, you can shrink the memory consumption of Pose's search index by
170
+ The search index keeps all the words that were ever used around.
171
+ After deleting or changing a large number of objects, you can shrink the database storage consumption of Pose's search index by
159
172
  removing no longer used search terms from it.
160
173
 
161
174
  ```bash
@@ -164,7 +177,7 @@ $ rake pose:index:vacuum
164
177
 
165
178
 
166
179
  ### Recreating the search index from scratch
167
- To index existing data in your database, or after loading additional data outside of ActiveRecord into your database,
180
+ To index existing data in your database, or after buld-loading data outside of ActiveRecord into your database,
168
181
  you should recreate the search index from scratch.
169
182
 
170
183
  ```bash
@@ -180,14 +193,14 @@ To remove all traces of Pose from your database, run:
180
193
  rails generate pose:remove
181
194
  ```
182
195
 
183
- Also don't forget to remove the `posify` block from your models as well as the gem entry from your Gemfile.
196
+ Also don't forget to remove the `posify` block from your models as well as the _pose_ gem from your Gemfile.
184
197
 
185
198
 
186
199
  ## Use Pose in your tests
187
200
 
188
201
  Pose can slow down your tests, because it updates the search index on every `:create`, `:update`, and `:delete`
189
202
  operation in the database.
190
- If this becomes a problem, you can disable Pose in your `test` environments,
203
+ To avoid that in your not search-related tests, you can disable Pose in your `test` environments,
191
204
  and only enable it for the tests that actually need search functionality.
192
205
 
193
206
  To disable Pose for tests, add this line to `config/environments/test.rb`
@@ -196,7 +209,8 @@ To disable Pose for tests, add this line to `config/environments/test.rb`
196
209
  Pose::CONFIGURATION[:perform_search] = false
197
210
  ```
198
211
 
199
- Now, with search disabled in the test environment, enable Pose in some of your tests by setting the same value to `true` inside the tests:
212
+ Now, with search disabled in the test environment, enable Pose in some of your tests
213
+ by setting the same value to `true` inside the tests:
200
214
 
201
215
  ```ruby
202
216
 
@@ -221,27 +235,29 @@ end
221
235
 
222
236
  If you find a bug, have a question, or a better idea, please open an issue on the
223
237
  <a href="https://github.com/kevgo/pose/issues">Pose issue tracker</a>.
224
- Or, clone the repository, make your changes, and submit a pull request.
238
+ Or, clone the repository, make your changes, and submit a unit-tested pull request!
225
239
 
226
240
  ### Run the unit tests for the Pose Gem
227
241
 
228
- For now, Pose uses Postgresql for tests, since it is free, and one of the most strict databases.
242
+ Pose uses Sqlite3 for tests.
229
243
  To run tests, first create a test database.
230
244
 
231
245
  ```bash
232
- createdb pose_test
246
+ bundle
247
+ rake app:db:create
248
+ rake app:db:migrate
249
+ rake app:db:test:prepare
233
250
  ```
234
251
 
235
252
  Then run the tests.
236
253
 
237
254
  ```bash
238
- $ rake spec
255
+ rake
239
256
  ```
240
257
 
241
258
 
242
259
  ### Road Map
243
260
 
244
- * add `join` to search parameters
245
261
  * pagination of search results
246
262
  * ordering
247
263
  * weighting search results
@@ -0,0 +1,56 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Pose
5
+ module Generators
6
+
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+ source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
10
+
11
+ def create_migration_file
12
+ say ''
13
+ say ' Creating database migration for the Pose tables.'
14
+ say ''
15
+ migration_template 'install_migration.rb', 'db/migrate/install_pose.rb'
16
+ say ''
17
+ end
18
+
19
+ def installation_instructions
20
+ say ''
21
+ say ' All done! You need to do two things now:'
22
+ say ''
23
+ say ' 1. Run the database migration'
24
+ say ''
25
+ say ' rake db:migrate', Thor::Shell::Color::BOLD
26
+ say ''
27
+ say ''
28
+ say ' 2. Add a posify block to all your models.'
29
+ say ' Here is an example:'
30
+ say ''
31
+ say ' class MyClass < ActiveRecord::Base'
32
+ say ' ...'
33
+ say ''
34
+ say ' posify do', Thor::Shell::Color::BOLD
35
+ say ' # return searchable text as a string here', Thor::Shell::Color::BOLD
36
+ say ' end', Thor::Shell::Color::BOLD
37
+ say ''
38
+ say ' ...'
39
+ say ' end'
40
+ say ''
41
+ say ''
42
+ say ' Happy searching! :)'
43
+ say ''
44
+ end
45
+
46
+
47
+ private
48
+
49
+ # Helper method for creating the migration.
50
+ def self.next_migration_number(path)
51
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ class InstallPose < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table "pose_assignments" do |t|
5
+ t.integer "word_id", null: false
6
+ t.integer "posable_id", null: false
7
+ t.string "posable_type", limit: 40, null: false
8
+ end
9
+
10
+ add_index "pose_assignments", :word_id
11
+ add_index "pose_assignments", :posable_id
12
+
13
+ create_table "pose_words" do |t|
14
+ t.string "text", limit: 80, null: false
15
+ end
16
+
17
+ add_index "pose_words", :text
18
+ end
19
+
20
+ def self.down
21
+ drop_table 'pose_assignments'
22
+ drop_table 'pose_words'
23
+ end
24
+ end
data/lib/pose.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "pose/engine"
2
+ require 'pose/query'
3
+ require 'pose/search'
2
4
  require 'pose/static_api'
3
- require 'pose/internal_helpers'
5
+ require 'pose/helpers'
4
6
  require 'pose/activerecord_base_additions'
5
7
  require 'pose/model_class_additions'
6
8
  require 'pose/railtie' if defined? Rails
@@ -1,4 +1,5 @@
1
1
  # Internal helper methods for the Pose module.
2
+ # TODO: remove
2
3
  module Pose
3
4
  module Helpers
4
5
  class <<self
@@ -41,33 +42,13 @@ module Pose
41
42
  #
42
43
  # @return [Boolean]
43
44
  def is_url? word
44
- URI::parse(word).scheme == 'http'
45
- rescue URI::InvalidURIError
46
- false
45
+ /https?:\/\/(\w)+\.(\w+)/ =~ word
47
46
  end
48
47
 
49
48
 
50
- # Merges the given posable object ids for a single query word into the given search result.
51
- def merge_search_result_word_matches result, class_name, ids
52
- if result.has_key? class_name
53
- result[class_name] = result[class_name] & ids
54
- else
55
- result[class_name] = ids
56
- end
57
- end
58
-
59
-
60
- # Returns a hash mapping classes to ids for the a single given word.
61
- def search_classes_and_ids_for_word word, class_names
62
- result = {}.tap { |hash| class_names.each { |class_name| hash[class_name] = [] }}
63
- query = Pose::Assignment.joins(:word) \
64
- .select('pose_assignments.posable_id, pose_assignments.posable_type') \
65
- .where('pose_words.text LIKE ?', "#{word}%") \
66
- .where('posable_type IN (?)', class_names)
67
- Pose::Assignment.connection.select_all(query.to_sql).each do |pose_assignment|
68
- result[pose_assignment['posable_type']] << pose_assignment['posable_id'].to_i
69
- end
70
- result
49
+ # Makes the given input an array.
50
+ def make_array input
51
+ [input].flatten
71
52
  end
72
53
 
73
54
 
@@ -77,11 +58,6 @@ module Pose
77
58
  end
78
59
 
79
60
 
80
- # Returns the search terms that are contained in the given query.
81
- def query_terms query
82
- query.split(' ').map{|query_word| Helpers.root_word query_word}.flatten.uniq
83
- end
84
-
85
61
  # Simplifies the given word to a generic search form.
86
62
  #
87
63
  # @param [String] raw_word The word to make searchable.
@@ -29,7 +29,7 @@ module Pose
29
29
 
30
30
  # Step 1: get an array of all words for the current object.
31
31
  search_text = instance_eval &(self.class.pose_content)
32
- new_words = Helpers.query_terms search_text.to_s
32
+ new_words = Query.new([], search_text.to_s).query_words
33
33
 
34
34
  # Step 2: Add new words to the search index.
35
35
  Helpers.get_words_to_add(self.pose_words, new_words).each do |word_to_add|
data/lib/pose/query.rb ADDED
@@ -0,0 +1,85 @@
1
+ module Pose
2
+ # Represents a search query.
3
+ #
4
+ # Provides convenient access to all elements of the search query:
5
+ # * fulltext
6
+ # * classes to search in
7
+ # * additional JOINs
8
+ # * additional WHEREs
9
+ class Query
10
+
11
+ attr_reader :classes, :text, :options
12
+
13
+
14
+ def initialize classes, text, options = {}
15
+ @classes = [classes].flatten
16
+ @text = text
17
+ @options = options
18
+ end
19
+
20
+
21
+ # The names of the classes to search in.
22
+ # @return [Array<String>]
23
+ def class_names
24
+ classes.map &:name
25
+ end
26
+
27
+
28
+ # Returns whether this query contains custom JOIN expressions.
29
+ def has_joins?
30
+ !@options[:joins].blank?
31
+ end
32
+
33
+
34
+ # Returns whether the query defines a limit on the number of results.
35
+ def has_limit?
36
+ !@options[:limit].blank?
37
+ end
38
+
39
+
40
+ # Returns whether this query contains WHERE clauses.
41
+ def has_where?
42
+ !@options[:where].blank?
43
+ end
44
+
45
+
46
+ # Returns whether only result ids are requested,
47
+ # opposed to full objects.
48
+ def ids_requested?
49
+ @options[:result_type] == :ids
50
+ end
51
+
52
+
53
+ # Returns the custom JOIN expressions of this query.
54
+ def joins
55
+ @joins ||= [@options[:joins]].flatten.compact
56
+ end
57
+
58
+
59
+ # Returns the limitation on the number of results.
60
+ def limit
61
+ @options[:limit]
62
+ end
63
+
64
+
65
+ # Returns the search terms that are contained in the given query.
66
+ def query_words
67
+ @query_words ||= Query.query_words @text
68
+ end
69
+
70
+
71
+ def self.query_words query_string
72
+ query_string.split(' ').map{|query_word| Helpers.root_word query_word}.flatten.uniq
73
+ end
74
+
75
+
76
+ # Returns the WHERE clause of this query.
77
+ def where
78
+ return [] unless has_where?
79
+ if @options[:where].size == 2 and @options[:where][0].class == String
80
+ return [ @options[:where] ]
81
+ end
82
+ @options[:where]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,135 @@
1
+ module Pose
2
+
3
+ # A search operation.
4
+ #
5
+ # Is given a query and search options, and returns the search results.
6
+ class Search
7
+
8
+ # @param [Array<Class>] classes The classes to search over.
9
+ # @param [String] query_string The full-text part of the search query.
10
+ # @param options Additional search options:
11
+ # * where: additional where clauses
12
+ # * join: additional join clauses
13
+ def initialize classes, query_string, options = {}
14
+ @query = Query.new classes, query_string, options
15
+ end
16
+
17
+
18
+ # Adds the given join expression to the given arel query.
19
+ def add_join arel, join_expression
20
+ case join_expression.class.name
21
+ when 'Class'
22
+ table_name = join_expression.name.tableize
23
+ return arel.joins "INNER JOIN #{table_name} ON pose_assignments.posable_id=#{table_name}.id AND pose_assignments.posable_type='#{join_expression.name}'"
24
+ when 'String', 'Symbol'
25
+ return arel.joins join_expression
26
+ else
27
+ raise "Unknown join expression: #{join_expression}"
28
+ end
29
+ end
30
+
31
+
32
+ # Creates a JOIN to the given expression.
33
+ def add_joins arel
34
+ @query.joins.inject(arel) do |memo, join_data|
35
+ add_join memo, join_data
36
+ end
37
+ end
38
+
39
+
40
+ # Adds the WHERE clauses from the given query to the given arel construct.
41
+ def add_wheres arel
42
+ @query.where.inject(arel) { |memo, where| memo.where where }
43
+ end
44
+
45
+
46
+ # Returns an empty result structure.
47
+ def empty_result
48
+ {}.tap do |result|
49
+ @query.class_names.each do |class_name|
50
+ result[class_name] = []
51
+ end
52
+ end
53
+ end
54
+
55
+
56
+ # Truncates the result set based on the :limit parameter in the query.
57
+ def limit_ids result
58
+ return unless @query.has_limit?
59
+ result.each do |clazz, ids|
60
+ result[clazz] = ids.slice 0, @query.limit
61
+ end
62
+ end
63
+
64
+
65
+ # Converts the ids to classes, if the user wants classes.
66
+ def load_classes result
67
+ return if @query.ids_requested?
68
+ result.each do |clazz, ids|
69
+ if ids.size > 0
70
+ result[clazz] = clazz.where(id: ids)
71
+ end
72
+ end
73
+ end
74
+
75
+
76
+ # Merges the given posable object ids for a single query word into the given search result.
77
+ # Helper method for :search_words.
78
+ def merge_search_result_word_matches result, class_name, ids
79
+ if result.has_key? class_name
80
+ result[class_name] = result[class_name] & ids
81
+ else
82
+ result[class_name] = ids
83
+ end
84
+ end
85
+
86
+
87
+ # Returns the search results cached.
88
+ # Use this method to access the results of the search.
89
+ def results
90
+ @results ||= search
91
+ end
92
+
93
+
94
+ # Performs a complete search.
95
+ # Clients should use :results to perform a search,
96
+ # since it caches the results.
97
+ def search
98
+ {}.tap do |result|
99
+ search_words.each do |class_name, ids|
100
+ result[class_name.constantize] = ids
101
+ end
102
+ limit_ids result
103
+ load_classes result
104
+ end
105
+ end
106
+
107
+
108
+ # Finds all matching ids for a single word of the search query.
109
+ def search_word word
110
+ empty_result.tap do |result|
111
+ data = Pose::Assignment.joins(:word) \
112
+ .select('pose_assignments.posable_id, pose_assignments.posable_type') \
113
+ .where('pose_words.text LIKE ?', "#{word}%") \
114
+ .where('pose_assignments.posable_type IN (?)', @query.class_names)
115
+ data = add_joins data
116
+ data = add_wheres data
117
+ Pose::Assignment.connection.select_all(data.to_sql).each do |pose_assignment|
118
+ result[pose_assignment['posable_type']] << pose_assignment['posable_id'].to_i
119
+ end
120
+ end
121
+ end
122
+
123
+
124
+ # Returns all matching ids for all words of the search query.
125
+ def search_words
126
+ {}.tap do |result|
127
+ @query.query_words.each do |query_word|
128
+ search_word(query_word).each do |class_name, ids|
129
+ merge_search_result_word_matches result, class_name, ids
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end