pose 3.0.0 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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> [![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
|
-
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
|