pose 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +42 -19
- data/Rakefile +37 -5
- data/lib/pose/activerecord_base_additions.rb +7 -6
- data/{app/models → lib}/pose/assignment.rb +6 -1
- data/lib/pose/jobs/install.rb +27 -0
- data/lib/pose/jobs/reindex_all.rb +34 -0
- data/lib/pose/jobs/remove.rb +28 -0
- data/lib/pose/jobs/uninstall.rb +19 -0
- data/lib/pose/jobs/vacuum.rb +23 -0
- data/lib/pose/model_class_additions.rb +32 -14
- data/lib/pose/query.rb +49 -9
- data/lib/pose/search.rb +5 -5
- data/lib/pose/static_api.rb +21 -3
- data/lib/pose/version.rb +1 -1
- data/lib/pose/word.rb +33 -0
- data/lib/pose.rb +11 -5
- data/lib/tasks/pose_tasks.rake +16 -27
- data/spec/{dummy/db/development.sqlite3 → db/pose.sqlite3} +0 -0
- data/spec/factories/posable_one.rb +2 -0
- data/spec/factories/posable_three.rb +8 -0
- data/spec/factories/posable_two.rb +2 -0
- data/spec/factories/user.rb +2 -0
- data/spec/lib/pose/activerecord_base_additions_spec.rb +11 -0
- data/spec/{models → lib/pose}/assignment_spec.rb +5 -5
- data/spec/lib/pose/jobs/reindex_all_spec.rb +31 -0
- data/spec/lib/pose/jobs/remove_spec.rb +20 -0
- data/spec/lib/pose/model_class_additions_spec.rb +150 -0
- data/spec/lib/pose/query_spec.rb +106 -8
- data/spec/lib/pose/search_spec.rb +2 -22
- data/spec/lib/pose/word_spec.rb +68 -0
- data/spec/pose_api_spec.rb +33 -13
- data/spec/spec_helper.rb +4 -32
- data/spec/support/config/database.yml +51 -0
- data/spec/support/config/database.yml.example +16 -0
- data/spec/{dummy/db/migrate → support/migrations}/20130308054001_create_posable_one.rb +0 -0
- data/spec/{dummy/db/migrate → support/migrations}/20130308054142_create_posable_two.rb +0 -0
- data/spec/support/migrations/20130308054143_create_posable_three.rb +9 -0
- data/spec/{dummy/db/migrate → support/migrations}/20130708084009_create_users.rb +0 -0
- data/spec/support/migrations/20130808084009_setup_pose_specs.rb +5 -0
- data/spec/{dummy/app → support}/models/posable_one.rb +1 -1
- data/spec/support/models/posable_three.rb +10 -0
- data/spec/{dummy/app → support}/models/posable_two.rb +1 -1
- data/spec/{dummy/app → support}/models/user.rb +1 -1
- data/spec/support/spec_manager.rb +105 -0
- metadata +70 -101
- data/app/models/pose/word.rb +0 -22
- data/db/migrate/20130308144915_pose_install.rb +0 -18
- data/lib/generators/pose/install/install_generator.rb +0 -56
- data/lib/generators/pose/install/templates/install_migration.rb +0 -24
- data/lib/generators/pose/remove/remove_generator.rb +0 -56
- data/lib/generators/pose/remove/templates/remove_migration.rb +0 -23
- data/lib/generators/pose/upgrade/templates/upgrade_migration.rb +0 -7
- data/lib/generators/pose/upgrade/upgrade_generator.rb +0 -35
- data/lib/pose/engine.rb +0 -5
- data/lib/pose/helpers.rb +0 -89
- data/lib/pose/railtie.rb +0 -19
- data/spec/dummy/Rakefile +0 -7
- data/spec/dummy/config/application.rb +0 -23
- data/spec/dummy/config/boot.rb +0 -10
- data/spec/dummy/config/database.yml +0 -25
- data/spec/dummy/config/environment.rb +0 -5
- data/spec/dummy/config/environments/development.rb +0 -30
- data/spec/dummy/config/environments/production.rb +0 -81
- data/spec/dummy/config/environments/test.rb +0 -37
- data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/dummy/config/initializers/inflections.rb +0 -15
- data/spec/dummy/config/initializers/mime_types.rb +0 -5
- data/spec/dummy/config/initializers/secret_token.rb +0 -7
- data/spec/dummy/config/initializers/session_store.rb +0 -8
- data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/spec/dummy/config/locales/en.yml +0 -5
- data/spec/dummy/config/routes.rb +0 -2
- data/spec/dummy/config.ru +0 -4
- data/spec/dummy/db/schema.rb +0 -47
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -309
- data/spec/dummy/log/test.log +0 -88518
- data/spec/dummy/script/rails +0 -6
- data/spec/lib/pose/helpers_spec.rb +0 -147
- data/spec/models/word_spec.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1059e7c72d577be6f7d5f3c0fc4fdc30b429bd4b
|
4
|
+
data.tar.gz: d0ed2c994af2326ec73a779b0fb61a44dca2602b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9eb63377caee2f07d3e7c9118ee9715d9e386371ca766aeb35b98eabe8d76f2d2128a27188ce71dd5f05c61e8bc46dc9726a351278392fc478dcb38aeeb44f6
|
7
|
+
data.tar.gz: 28e599e762c76d35070e5d81c71c9b7069270dbd3f1da463db8eb1f6f546830062da027e6eb47a794d85efa475680f66052e7c8373462bbcbdb9b9b0bbd2d37f
|
data/README.md
CHANGED
@@ -1,19 +1,28 @@
|
|
1
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> [](https://codeclimate.com/github/kevgo/pose) [](https://coveralls.io/r/kevgo/pose) [](https://gemnasium.com/kevgo/pose)
|
2
2
|
|
3
|
-
A database agnostic fulltext search engine for ActiveRecord objects
|
3
|
+
A database agnostic fulltext search engine for ActiveRecord objects. See also [Pose Rails Adapter](http://github.com/einzige/pose-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
|
7
|
+
* Uses application database - no separate servers, databases, or search engines required.
|
8
8
|
* Does not pollute the searchable classes nor their database tables.
|
9
9
|
* Very fast search, doing only simple queries over fully indexed columns.
|
10
10
|
* Allows to augment the fulltext search query with your own joins and where clauses.
|
11
11
|
|
12
|
-
Version 3.x is for Rails 4 compatibilty. For Rails 3.x, please use versions 2.x.
|
13
|
-
|
14
12
|
|
15
13
|
## Installation
|
16
14
|
|
15
|
+
### Versions
|
16
|
+
|
17
|
+
* __2.x__ for _Rails 3.x_ compatibilty
|
18
|
+
* __3.0__ for _Rails 4_ compatibilty
|
19
|
+
* __3.1__ introduces a new setup. The _pose_ gem is now a generic Ruby gem that works
|
20
|
+
with any Ruby web server that uses ActiveRecord, like [Sinatra](http://www.sinatrarb.com),
|
21
|
+
[Padrino](http://www.padrinorb.com), or [Rails](http://rubyonrails.org).
|
22
|
+
Generators for installation and uninstallation are extracted into the
|
23
|
+
[pose_rails](https://github.com/po-se/pose-rails) gem.
|
24
|
+
|
25
|
+
|
17
26
|
### Set up the gem.
|
18
27
|
|
19
28
|
Add the gem to your Gemfile and run `bundle install`
|
@@ -25,8 +34,7 @@ gem 'pose'
|
|
25
34
|
### Create the database tables for pose.
|
26
35
|
|
27
36
|
```bash
|
28
|
-
$
|
29
|
-
$ rake db:migrate
|
37
|
+
$ rake pose:install
|
30
38
|
```
|
31
39
|
|
32
40
|
Pose creates two tables in your database. These tables are automatically populated and kept up to date.
|
@@ -37,11 +45,32 @@ Pose creates two tables in your database. These tables are automatically populat
|
|
37
45
|
|
38
46
|
### Make your ActiveRecord models searchable
|
39
47
|
|
48
|
+
Each model defines the searchable content through the `posify` method.
|
49
|
+
Valid parameters are
|
50
|
+
* names of __fields__
|
51
|
+
* names of __methods__
|
52
|
+
* a __block__
|
53
|
+
|
54
|
+
The value/result of each parameter is added to the search index for this instance.
|
55
|
+
Here are a few examples:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class User < ActiveRecord::Base
|
59
|
+
# first_name and last_name are attributes
|
60
|
+
|
61
|
+
# This line makes the class searchable.
|
62
|
+
posify :first_name, :last_name, :address
|
63
|
+
|
64
|
+
def address
|
65
|
+
"#{street} #{city} #{state}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
40
70
|
```ruby
|
41
71
|
class MyClass < ActiveRecord::Base
|
42
72
|
|
43
|
-
# This line makes
|
44
|
-
# The given block must return the searchable content as a string.
|
73
|
+
# This line makes the class searchable.
|
45
74
|
posify do
|
46
75
|
|
47
76
|
# Only active instances should show up in search results.
|
@@ -55,6 +84,7 @@ class MyClass < ActiveRecord::Base
|
|
55
84
|
end
|
56
85
|
```
|
57
86
|
|
87
|
+
You can mix and match all parameter types for `posify` at will.
|
58
88
|
Note that you can return whatever content you want in the `posify` block,
|
59
89
|
not only data from this object, but also data from related objects, class names, etc.
|
60
90
|
|
@@ -241,23 +271,16 @@ Or, clone the repository, make your changes, and submit a unit-tested pull reque
|
|
241
271
|
|
242
272
|
### Run the unit tests for the Pose Gem
|
243
273
|
|
244
|
-
Pose
|
245
|
-
To run tests,
|
246
|
-
|
247
|
-
```bash
|
248
|
-
bundle
|
249
|
-
rake app:db:create
|
250
|
-
rake app:db:migrate
|
251
|
-
rake app:db:test:prepare
|
252
|
-
```
|
274
|
+
Pose can work with Sqlite3, Postgesql and MySQL by default.
|
275
|
+
To run tests, please create database configuration file `spec/support/config/database.yml`, please refer to the template:
|
276
|
+
[spec/support/config/database.yml.example](spec/support/config/database.yml.example)
|
253
277
|
|
254
278
|
Then run the tests.
|
255
279
|
|
256
280
|
```bash
|
257
|
-
rake
|
281
|
+
bundle exec rake test
|
258
282
|
```
|
259
283
|
|
260
|
-
|
261
284
|
### Road Map
|
262
285
|
|
263
286
|
* pagination of search results
|
data/Rakefile
CHANGED
@@ -1,16 +1,48 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
|
+
|
2
3
|
begin
|
3
4
|
require 'bundler/setup'
|
4
5
|
rescue LoadError
|
5
6
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
7
|
end
|
7
8
|
|
8
|
-
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
9
|
-
load 'rails/tasks/engine.rake'
|
10
|
-
|
11
9
|
Bundler::GemHelper.install_tasks
|
12
10
|
|
13
11
|
# RSpec tasks.
|
14
12
|
require 'rspec/core/rake_task'
|
15
|
-
|
16
|
-
|
13
|
+
|
14
|
+
RSpec::Core::RakeTask.new :spec_mysql do
|
15
|
+
ENV['POSE_ENV'] = 'mysql'
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec::Core::RakeTask.new :spec_mysql2 do
|
19
|
+
ENV['POSE_ENV'] = 'mysql2'
|
20
|
+
end
|
21
|
+
|
22
|
+
RSpec::Core::RakeTask.new :spec_sqlite do
|
23
|
+
ENV['POSE_ENV'] = 'sqlite'
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec::Core::RakeTask.new :spec_postgres do
|
27
|
+
ENV['POSE_ENV'] = 'postgres'
|
28
|
+
end
|
29
|
+
|
30
|
+
RSpec::Core::RakeTask.new :spec_mysql_ci do
|
31
|
+
ENV['POSE_ENV'] = 'mysql_ci'
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new :spec_mysql2_ci do
|
35
|
+
ENV['POSE_ENV'] = 'mysql2_ci'
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec::Core::RakeTask.new :spec_sqlite_ci do
|
39
|
+
ENV['POSE_ENV'] = 'sqlite_ci'
|
40
|
+
end
|
41
|
+
|
42
|
+
RSpec::Core::RakeTask.new :spec_postgres_ci do
|
43
|
+
ENV['POSE_ENV'] = 'postgres_ci'
|
44
|
+
end
|
45
|
+
|
46
|
+
task :test_ci => [:spec_sqlite_ci, :spec_postgres_ci, :spec_mysql2_ci]
|
47
|
+
task :test => [:spec_sqlite, :spec_postgres, :spec_mysql2]
|
48
|
+
task :default => :test_ci
|
@@ -3,13 +3,14 @@ module Pose
|
|
3
3
|
module ActiveRecordBaseAdditions
|
4
4
|
|
5
5
|
# Defines the searchable content in ActiveRecord objects.
|
6
|
-
def posify &block
|
7
|
-
raise "Error while posifying class '#{name}': " \
|
8
|
-
"You must provide a block that returns the searchable content to 'posify'." unless block_given?
|
9
|
-
|
6
|
+
def posify *source_methods, &block
|
10
7
|
include ModelClassAdditions
|
11
|
-
self.pose_content = block
|
12
|
-
end
|
13
8
|
|
9
|
+
self.pose_content = proc do
|
10
|
+
text_chunks = source_methods.map { |source| send(source) }
|
11
|
+
text_chunks << instance_eval(&block) if block
|
12
|
+
text_chunks.reject(&:blank?).join(' ')
|
13
|
+
end
|
14
|
+
end
|
14
15
|
end
|
15
16
|
end
|
@@ -1,17 +1,22 @@
|
|
1
1
|
# Assigns searchable objects to words in the search index.
|
2
2
|
module Pose
|
3
3
|
class Assignment < ActiveRecord::Base
|
4
|
+
self.table_name_prefix = 'pose_'
|
5
|
+
|
4
6
|
belongs_to :word, class_name: 'Pose::Word'
|
5
7
|
belongs_to :posable, polymorphic: true
|
6
8
|
|
7
9
|
# Removes all Assignments for the given class.
|
10
|
+
# Returns a number for removed records.
|
11
|
+
# @param [Class] clazz
|
12
|
+
# @return [Integer]
|
8
13
|
def self.delete_class_index clazz
|
9
14
|
Assignment.delete_all(posable_type: clazz.name)
|
10
15
|
end
|
11
16
|
|
12
17
|
# Removes all Assignments that aren't used anymore.
|
13
18
|
def self.cleanup_orphaned_pose_assignments progress_bar = nil
|
14
|
-
Assignment.
|
19
|
+
Assignment.includes([:posable, :word]).find_each(batch_size: 5000) do |assignment|
|
15
20
|
progress_bar.increment if progress_bar
|
16
21
|
|
17
22
|
# Delete the assignment if the posable object no longer exists.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pose
|
2
|
+
module Jobs
|
3
|
+
class InitialMigration < ActiveRecord::Migration
|
4
|
+
def change
|
5
|
+
create_table "pose_assignments" do |t|
|
6
|
+
t.integer "word_id", null: false
|
7
|
+
t.integer "posable_id", null: false
|
8
|
+
t.string "posable_type", limit: 60, null: false
|
9
|
+
end
|
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
|
+
add_index "pose_words", :text
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates the Pose tables in the database.
|
21
|
+
class Install
|
22
|
+
def perform
|
23
|
+
InitialMigration.new.change
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'ruby-progressbar'
|
2
|
+
|
3
|
+
module Pose
|
4
|
+
module Jobs
|
5
|
+
|
6
|
+
# Recreates the Pose search index for the given class from scratch.
|
7
|
+
class ReindexAll
|
8
|
+
attr_reader :klass
|
9
|
+
|
10
|
+
# @param [String, Class] clazz
|
11
|
+
def initialize(clazz)
|
12
|
+
@klass = case clazz
|
13
|
+
when String
|
14
|
+
clazz.constantize
|
15
|
+
when Class
|
16
|
+
clazz
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Class or String expected, #{clazz.class} given"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
progress_bar = ProgressBar.create title: " reindexing", total: klass.count
|
24
|
+
|
25
|
+
klass.find_each do |instance|
|
26
|
+
instance.update_pose_words
|
27
|
+
progress_bar.increment
|
28
|
+
end
|
29
|
+
|
30
|
+
progress_bar.finish
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'ruby-progressbar'
|
2
|
+
|
3
|
+
module Pose
|
4
|
+
module Jobs
|
5
|
+
|
6
|
+
# Removes the Pose search index for the given class.
|
7
|
+
class Remove
|
8
|
+
attr_reader :klass
|
9
|
+
|
10
|
+
# @param [String, Class] clazz
|
11
|
+
def initialize(clazz)
|
12
|
+
@klass = case clazz
|
13
|
+
when String
|
14
|
+
clazz.constantize
|
15
|
+
when Class
|
16
|
+
clazz
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Class or String expected, #{clazz.class} given"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform
|
23
|
+
Pose::Assignment.delete_class_index(klass)
|
24
|
+
puts "Search index for class #{klass.name} deleted.\n\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pose/jobs/install'
|
2
|
+
|
3
|
+
module Pose
|
4
|
+
module Jobs
|
5
|
+
|
6
|
+
# Removes the Pose database tables.
|
7
|
+
class KillMigration < ActiveRecord::Migration
|
8
|
+
def change
|
9
|
+
revert InitialMigration
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Uninstall
|
14
|
+
def perform
|
15
|
+
KillMigration.new.change
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'ruby-progressbar'
|
2
|
+
|
3
|
+
module Pose
|
4
|
+
module Jobs
|
5
|
+
|
6
|
+
# Cleans up unused data in the search index.
|
7
|
+
class Vacuum
|
8
|
+
def perform
|
9
|
+
puts "Cleaning Pose search index...\n\n"
|
10
|
+
|
11
|
+
progress_bar = ProgressBar.create title: ' assignments', total: Pose::Assignment.count
|
12
|
+
Pose::Assignment.cleanup_orphaned_pose_assignments progress_bar
|
13
|
+
progress_bar.finish
|
14
|
+
|
15
|
+
progress_bar = ProgressBar.create title: ' words', total: Pose::Word.count
|
16
|
+
Pose::Word.remove_unused_words progress_bar
|
17
|
+
progress_bar.finish
|
18
|
+
|
19
|
+
puts "\nPose search index cleanup complete.\n\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -13,6 +13,36 @@ module Pose
|
|
13
13
|
cattr_accessor :pose_content
|
14
14
|
end
|
15
15
|
|
16
|
+
# Returns all words in the search index for this instance.
|
17
|
+
# @return [Array<String>]
|
18
|
+
def pose_current_words
|
19
|
+
pose_words.map(&:text)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the searchable text snippet for this instance.
|
23
|
+
# This data is not stored in the search engine.
|
24
|
+
# It is recomputes this from data in the database here.
|
25
|
+
# @return [String]
|
26
|
+
def pose_fetch_content
|
27
|
+
instance_eval(&(pose_content)).to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array<String>]
|
31
|
+
def pose_fresh_words reload = false
|
32
|
+
@pose_fresh_words = nil if reload
|
33
|
+
@pose_fresh_words ||= Query.query_words pose_fetch_content
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Array<String>]
|
37
|
+
def pose_stale_words reload = false
|
38
|
+
pose_current_words - pose_fresh_words(reload)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array<String>]
|
42
|
+
def pose_words_to_add reload = false
|
43
|
+
pose_fresh_words(reload) - pose_current_words
|
44
|
+
end
|
45
|
+
|
16
46
|
# Updates the associated words for this object in the database.
|
17
47
|
def update_pose_index
|
18
48
|
update_pose_words if Pose.perform_search?
|
@@ -26,20 +56,8 @@ module Pose
|
|
26
56
|
# Helper method.
|
27
57
|
# Updates the search words with the text returned by search_strings.
|
28
58
|
def update_pose_words
|
29
|
-
|
30
|
-
|
31
|
-
search_text = instance_eval &(self.class.pose_content)
|
32
|
-
new_words = Query.new([], search_text.to_s).query_words
|
33
|
-
|
34
|
-
# Step 2: Add new words to the search index.
|
35
|
-
Helpers.get_words_to_add(self.pose_words, new_words).each do |word_to_add|
|
36
|
-
self.pose_words << Word.find_or_create_by(text: word_to_add)
|
37
|
-
end
|
38
|
-
|
39
|
-
# Step 3: Remove now obsolete words from search index.
|
40
|
-
Helpers.get_words_to_remove(self.pose_words, new_words).each do |word_to_remove|
|
41
|
-
self.pose_words.delete word_to_remove
|
42
|
-
end
|
59
|
+
self.pose_words.delete(Word.factory(pose_stale_words true))
|
60
|
+
self.pose_words << Word.factory(pose_words_to_add)
|
43
61
|
end
|
44
62
|
end
|
45
63
|
end
|
data/lib/pose/query.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module Pose
|
2
|
+
|
2
3
|
# Represents a search query.
|
3
4
|
#
|
4
5
|
# Provides convenient access to all elements of the search query:
|
@@ -12,7 +13,7 @@ module Pose
|
|
12
13
|
|
13
14
|
|
14
15
|
def initialize classes, text, options = {}
|
15
|
-
@classes =
|
16
|
+
@classes = Array(classes).flatten
|
16
17
|
@text = text
|
17
18
|
@options = options
|
18
19
|
end
|
@@ -27,19 +28,19 @@ module Pose
|
|
27
28
|
|
28
29
|
# Returns whether this query contains custom JOIN expressions.
|
29
30
|
def has_joins?
|
30
|
-
|
31
|
+
@options[:joins].present?
|
31
32
|
end
|
32
33
|
|
33
34
|
|
34
35
|
# Returns whether the query defines a limit on the number of results.
|
35
36
|
def has_limit?
|
36
|
-
|
37
|
+
@options[:limit].present?
|
37
38
|
end
|
38
39
|
|
39
40
|
|
40
41
|
# Returns whether this query contains WHERE clauses.
|
41
42
|
def has_where?
|
42
|
-
|
43
|
+
@options[:where].present?
|
43
44
|
end
|
44
45
|
|
45
46
|
|
@@ -50,9 +51,23 @@ module Pose
|
|
50
51
|
end
|
51
52
|
|
52
53
|
|
54
|
+
# Returns whether the given string is a URL.
|
55
|
+
#
|
56
|
+
# @param [String] word The string to check.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def self.is_url? word
|
60
|
+
|
61
|
+
# Handle localhost separately.
|
62
|
+
return true if /^http:\/\/localhost(:\d+)?/ =~ word
|
63
|
+
|
64
|
+
/^https?:\/\/([\w\.])+\.([\w\.])+/ =~ word
|
65
|
+
end
|
66
|
+
|
67
|
+
|
53
68
|
# Returns the custom JOIN expressions of this query.
|
54
69
|
def joins
|
55
|
-
@joins ||=
|
70
|
+
@joins ||= Array(@options[:joins]).flatten.compact
|
56
71
|
end
|
57
72
|
|
58
73
|
|
@@ -69,16 +84,41 @@ module Pose
|
|
69
84
|
|
70
85
|
|
71
86
|
def self.query_words query_string
|
72
|
-
query_string.split(' ').map{|query_word|
|
87
|
+
query_string.split(' ').map{|query_word| Query.root_word query_word}.flatten.uniq
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Simplifies the given word to a generic search form.
|
92
|
+
#
|
93
|
+
# @param [String] raw_word The word to make searchable.
|
94
|
+
#
|
95
|
+
# @return [String] The stemmed version of the word.
|
96
|
+
def self.root_word raw_word
|
97
|
+
result = []
|
98
|
+
raw_word_copy = raw_word[0..-1]
|
99
|
+
raw_word_copy.gsub! '%20', ' '
|
100
|
+
raw_word_copy.gsub! /[()*<>'",;\?\-\=&%#]/, ' '
|
101
|
+
raw_word_copy.gsub! /\s+/, ' '
|
102
|
+
raw_word_copy.split(' ').each do |word|
|
103
|
+
if Query.is_url?(word)
|
104
|
+
result.concat word.split(/[\.\/\:]/).delete_if(&:blank?)
|
105
|
+
else
|
106
|
+
word.gsub! /[\-\/\._:]/, ' '
|
107
|
+
word.gsub! /\s+/, ' '
|
108
|
+
word.split(' ').each do |w|
|
109
|
+
stemmed_word = w.parameterize.singularize
|
110
|
+
result.concat stemmed_word.split ' '
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
result.uniq
|
73
115
|
end
|
74
116
|
|
75
117
|
|
76
118
|
# Returns the WHERE clause of this query.
|
77
119
|
def where
|
78
120
|
return [] unless has_where?
|
79
|
-
if @options[:where].size == 2
|
80
|
-
return [ @options[:where] ]
|
81
|
-
end
|
121
|
+
return [ @options[:where] ] if @options[:where].size == 2 && @options[:where][0].is_a?(String)
|
82
122
|
@options[:where]
|
83
123
|
end
|
84
124
|
end
|
data/lib/pose/search.rb
CHANGED
@@ -108,13 +108,13 @@ module Pose
|
|
108
108
|
# Finds all matching ids for a single word of the search query.
|
109
109
|
def search_word word
|
110
110
|
empty_result.tap do |result|
|
111
|
-
data =
|
112
|
-
|
113
|
-
|
114
|
-
|
111
|
+
data = 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
115
|
data = add_joins data
|
116
116
|
data = add_wheres data
|
117
|
-
|
117
|
+
Assignment.connection.select_all(data.to_sql).each do |pose_assignment|
|
118
118
|
result[pose_assignment['posable_type']] << pose_assignment['posable_id'].to_i
|
119
119
|
end
|
120
120
|
end
|
data/lib/pose/static_api.rb
CHANGED
@@ -17,7 +17,7 @@ module Pose
|
|
17
17
|
# @return [Array<String>]
|
18
18
|
def autocomplete_words query
|
19
19
|
return [] if query.blank?
|
20
|
-
Word.where('text LIKE ?', "#{
|
20
|
+
Word.where('text LIKE ?', "#{Query.root_word(query)[0]}%").map(&:text)
|
21
21
|
end
|
22
22
|
|
23
23
|
|
@@ -38,8 +38,26 @@ module Pose
|
|
38
38
|
#
|
39
39
|
# @return [Hash<Class, ActiveRecord::Relation>]
|
40
40
|
def search query_string, classes, options = {}
|
41
|
-
|
42
|
-
|
41
|
+
Search.new(classes, query_string, options).results
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Returns whether the currently used database is a relational one.
|
46
|
+
def has_sql_connection?
|
47
|
+
has_mysql_connection? || has_postgres_connection? || has_sqlite_connection?
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_mysql_connection?
|
51
|
+
['ActiveRecord::ConnectionAdapters::MysqlAdapter',
|
52
|
+
'ActiveRecord::ConnectionAdapters::Mysql2Adapter'].include? ActiveRecord::Base.connection.class.name
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_postgres_connection?
|
56
|
+
ActiveRecord::Base.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_sqlite_connection?
|
60
|
+
ActiveRecord::Base.connection.class.name == 'ActiveRecord::ConnectionAdapters::SQLite3Adapter'
|
43
61
|
end
|
44
62
|
end
|
45
63
|
end
|
data/lib/pose/version.rb
CHANGED
data/lib/pose/word.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# A single word in the search index.
|
2
|
+
module Pose
|
3
|
+
class Word < ActiveRecord::Base
|
4
|
+
self.table_name_prefix = 'pose_'
|
5
|
+
|
6
|
+
has_many :assignments, class_name: 'Pose::Assignment', dependent: :destroy
|
7
|
+
|
8
|
+
|
9
|
+
# Returns the Pose::Word instances with the given texts.
|
10
|
+
# @param [Array<String>]
|
11
|
+
def self.factory texts
|
12
|
+
texts.map {|text| Word.find_or_create_by text: text}
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def self.remove_unused_words progress_bar = nil
|
17
|
+
if Pose.has_sql_connection?
|
18
|
+
# SQL database --> use an optimized query.
|
19
|
+
Word.select("pose_words.id").
|
20
|
+
joins("LEFT OUTER JOIN pose_assignments ON pose_assignments.word_id = pose_words.id").
|
21
|
+
group("pose_words.id").
|
22
|
+
having("COUNT(pose_assignments.id) = 0").
|
23
|
+
delete_all
|
24
|
+
else
|
25
|
+
# Unknown database --> use metawhere.
|
26
|
+
Word.select(:id).includes(:assignments).find_each(batch_size: 100) do |word|
|
27
|
+
word.delete if word.assignments.size.zero?
|
28
|
+
progress_bar.try(:increment)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/pose.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
|
-
require
|
1
|
+
require 'active_record'
|
2
2
|
require 'pose/query'
|
3
3
|
require 'pose/search'
|
4
4
|
require 'pose/static_api'
|
5
|
-
require 'pose/helpers'
|
6
5
|
require 'pose/activerecord_base_additions'
|
7
6
|
require 'pose/model_class_additions'
|
8
|
-
require 'pose/
|
7
|
+
require 'pose/assignment'
|
8
|
+
require 'pose/word'
|
9
|
+
require 'pose/jobs/reindex_all'
|
10
|
+
require 'pose/jobs/remove'
|
11
|
+
require 'pose/jobs/vacuum'
|
12
|
+
require 'pose/jobs/install'
|
13
|
+
require 'pose/jobs/uninstall'
|
9
14
|
|
10
|
-
|
11
|
-
|
15
|
+
Dir[File.join(File.dirname(__FILE__), "tasks/**/*.rake")].each { |ext| load ext } if defined? Rake
|
16
|
+
|
17
|
+
ActiveRecord::Base.send :extend, Pose::ActiveRecordBaseAdditions
|