db_text_search 0.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 +7 -0
- data/CHANGES.md +3 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/db_text_search.gemspec +33 -0
- data/lib/db_text_search/case_insensitive_eq/abstract_adapter.rb +35 -0
- data/lib/db_text_search/case_insensitive_eq/collate_nocase_adapter.rb +26 -0
- data/lib/db_text_search/case_insensitive_eq/insensitive_column_adapter.rb +16 -0
- data/lib/db_text_search/case_insensitive_eq/lower_adapter.rb +37 -0
- data/lib/db_text_search/case_insensitive_eq.rb +88 -0
- data/lib/db_text_search/full_text_search/abstract_adapter.rb +50 -0
- data/lib/db_text_search/full_text_search/mysql_adapter.rb +19 -0
- data/lib/db_text_search/full_text_search/postgres_adapter.rb +25 -0
- data/lib/db_text_search/full_text_search/sqlite_adapter.rb +21 -0
- data/lib/db_text_search/full_text_search.rb +44 -0
- data/lib/db_text_search/query_building.rb +22 -0
- data/lib/db_text_search/version.rb +3 -0
- data/lib/db_text_search.rb +9 -0
- data/shared.gemfile +10 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 874e1b8761af33f55c19c2a67c12dea616a80bc4
|
4
|
+
data.tar.gz: a874ec1e3e6160ef2e5ad01f14020966b43f7cf9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 81aae784b663e11355935fe800aefb4a377bbfa2c9adca47114ecc3ff7040cfd53361b2b809655b328b76bd085f04d49535da9b1143b4777789ea4a8394b6cdc
|
7
|
+
data.tar.gz: f6496edfa09ee256d0ef65f95ce87a3a365c3e5095b4f4d62c544bdf7f93c95698bacefe3a4179081ee77dad8f992e6e6f14ede94a11160647d64ae25acd511d
|
data/CHANGES.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at glex.spb@gmail.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Gleb Mazovetskiy
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# DbTextSearch [](https://travis-ci.org/thredded/db_text_search) [](https://codeclimate.com/github/thredded/db_text_search) [](https://codeclimate.com/github/thredded/db_text_search/coverage)
|
2
|
+
|
3
|
+
Different relational databases treat text search very differently.
|
4
|
+
DbTextSearch provides a unified interface on top of ActiveRecord for SQLite, MySQL, and PostgreSQL to do:
|
5
|
+
|
6
|
+
* Case-insensitive string-in-set querying, and CI index creation.
|
7
|
+
* Basic full-text search for a list of terms, and FTS index creation.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'db_text_search', '~> 0.1.1'
|
15
|
+
```
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Case-insensitive string matching
|
20
|
+
|
21
|
+
Add an index in a migration to an existing CI or CS column:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
DbTextSearch::CaseInsensitiveEq.add_index connection, :users, :username
|
25
|
+
# Options: name, unique
|
26
|
+
```
|
27
|
+
|
28
|
+
Or, create a new CI column:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
DbTextSearch::CaseInsensitiveEq.add_ci_text_column connection, :users, :username
|
32
|
+
```
|
33
|
+
|
34
|
+
Perform a search for records with column that case-insensitively equals to one of the strings in a given set:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
DbTextSearch::CaseInsensitiveEq.new(User.confirmed, :username).find(%w(Alice Bob))
|
38
|
+
#=> ActiveRecord::Relation
|
39
|
+
```
|
40
|
+
|
41
|
+
See also: [API documentation][api-docs].
|
42
|
+
|
43
|
+
### Full text search
|
44
|
+
|
45
|
+
Add an index:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
DbTextSearch::FullTextSearch.add_index connection, :posts, :content
|
49
|
+
# Options: name
|
50
|
+
```
|
51
|
+
|
52
|
+
Perform a full-text search:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
DbTextSearch::FullTextSearch.new(Post.published, :content).find('peace')
|
56
|
+
DbTextSearch::FullTextSearch.new(Post.published, :content).find(%w(love kaori))
|
57
|
+
```
|
58
|
+
|
59
|
+
## Under the hood
|
60
|
+
|
61
|
+
<table>
|
62
|
+
<caption>Case-insensitive string matching methods</caption>
|
63
|
+
<thead>
|
64
|
+
<tr><th rowspan="2">Column type</th><th colspan="2">SQLite</th><th colspan="2">MySQL</th><th colspan="2">PostgreSQL</th>
|
65
|
+
<tr><th>Detected types</th><th>Search / index</th><th>Detected types</th><th>Search / index</th><th>Detected types</th><th>Search / index</th></tr>
|
66
|
+
</thead>
|
67
|
+
<tbody style="text-align: center">
|
68
|
+
<tr><th>CI</th>
|
69
|
+
<td rowspan="2">always treated as CS</td> <td rowspan="2"><code>COLLATE NOCASE</code></td>
|
70
|
+
<td><i>default</i></td> <td><i>default</i></td>
|
71
|
+
<td><code>CITEXT</code></td> <td><i>default</i></td>
|
72
|
+
<tr><th>CS</th>
|
73
|
+
<td>non-<code>ci</code> collations</td> <td><code>LOWER</code><br><b>no index</b></td>
|
74
|
+
<td><i>default</i></td> <td><code>LOWER</code></td>
|
75
|
+
</tr>
|
76
|
+
</tr>
|
77
|
+
</tbody>
|
78
|
+
</table>
|
79
|
+
|
80
|
+
### Full-text search
|
81
|
+
|
82
|
+
#### MySQL
|
83
|
+
|
84
|
+
A `FULLTEXT` index, and a `MATCH AGAINST` query.
|
85
|
+
|
86
|
+
#### PostgreSQL
|
87
|
+
|
88
|
+
A `gist(to_tsvector(...))` index, and a `@@ plainto_tsquery` query.
|
89
|
+
Methods also accept an optional `pg_ts_config` argument (default: `"'english'"`) that is ignored for other databases.
|
90
|
+
|
91
|
+
#### SQLite
|
92
|
+
|
93
|
+
**No index**, a `LIKE %term%` query for each term joined with `AND`.
|
94
|
+
|
95
|
+
## Development
|
96
|
+
|
97
|
+
Make sure you have a working installation of SQLite, MySQL, and PostgreSQL.
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
99
|
+
Then, run `rake test_all` to run the tests with all databases and gemfiles.
|
100
|
+
|
101
|
+
See the Rakefile for other available test tasks.
|
102
|
+
|
103
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
104
|
+
|
105
|
+
## Contributing
|
106
|
+
|
107
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/thredded/db_text_search. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
108
|
+
|
109
|
+
## License
|
110
|
+
|
111
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
112
|
+
|
113
|
+
[api-docs]: http://www.rubydoc.info/gems/db_text_search
|
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'db_text_search/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'db_text_search'
|
7
|
+
s.version = DbTextSearch::VERSION
|
8
|
+
s.authors = ['Gleb Mazovetskiy']
|
9
|
+
s.email = ['glex.spb@gmail.com']
|
10
|
+
|
11
|
+
s.summary = %q{A unified interface on top of ActiveRecord for SQLite, MySQL, and PostgreSQL for case-insensitive string search and basic full-text search.}
|
12
|
+
s.description = %q{Different relational databases treat text search very differently.
|
13
|
+
DbTextSearch provides a unified interface on top of ActiveRecord for SQLite, MySQL, and PostgreSQL to do:
|
14
|
+
* Case-insensitive string-in-set querying, and CI index creation.
|
15
|
+
* Basic full-text search for a list of terms, and FTS index creation.
|
16
|
+
}
|
17
|
+
s.homepage = 'https://github.com/thredded/db_text_search'
|
18
|
+
s.license = 'MIT'
|
19
|
+
|
20
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|bin|script)/|^\.|Rakefile|Gemfile}) }
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
s.required_ruby_version = '~> 2.1'
|
23
|
+
|
24
|
+
s.add_dependency 'activerecord', '>= 4.1.15', '< 6.0'
|
25
|
+
|
26
|
+
s.add_development_dependency 'mysql2', '>= 0.3.20'
|
27
|
+
s.add_development_dependency 'pg', '>= 0.18.4'
|
28
|
+
s.add_development_dependency 'sqlite3', '>= 1.3.11'
|
29
|
+
|
30
|
+
s.add_development_dependency 'bundler', '~> 1.11'
|
31
|
+
s.add_development_dependency 'rake', '~> 11.0'
|
32
|
+
s.add_development_dependency 'rspec', '~> 3.4'
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'db_text_search/query_building'
|
2
|
+
module DbTextSearch
|
3
|
+
class CaseInsensitiveEq
|
4
|
+
# A base class for CaseInsensitiveStringFinder adapters.
|
5
|
+
class AbstractAdapter
|
6
|
+
include ::DbTextSearch::QueryBuilding
|
7
|
+
|
8
|
+
# @param scope [ActiveRecord::Relation, Class<ActiveRecord::Base>]
|
9
|
+
# @param column [Symbol] name
|
10
|
+
def initialize(scope, column)
|
11
|
+
@scope = scope
|
12
|
+
@column = column
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param values [Array<String>]
|
16
|
+
# @return [ActiveRecord::Relation]
|
17
|
+
# @abstract
|
18
|
+
def find(values)
|
19
|
+
fail 'abstract'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add an index for case-insensitive string search.
|
23
|
+
#
|
24
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
25
|
+
# @param table_name [String, Symbol]
|
26
|
+
# @param column_name [String, Symbol]
|
27
|
+
# @param options [Hash] passed down to ActiveRecord::ConnectionAdapters::SchemaStatements#add_index.
|
28
|
+
# @return (see ActiveRecord::ConnectionAdapters::SchemaStatements#add_index)
|
29
|
+
# @abstract
|
30
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
31
|
+
fail 'abstract'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'db_text_search/case_insensitive_eq/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class CaseInsensitiveEq
|
4
|
+
class CollateNocaseAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter#find)
|
6
|
+
def find(values)
|
7
|
+
conn = @scope.connection
|
8
|
+
@scope.where <<-SQL.strip
|
9
|
+
#{quoted_scope_column} COLLATE NOCASE IN (#{values.map { |v| conn.quote(v.to_s) }.join(', ')})
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
|
13
|
+
# (see AbstractAdapter.add_index)
|
14
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
15
|
+
# TODO: Switch to the native Rails solution once it's landed, as the current one requires SQL dump format.
|
16
|
+
# https://github.com/rails/rails/pull/18499
|
17
|
+
options.assert_valid_keys(:name, :unique)
|
18
|
+
index_name = options[:name] || options[:name] || "#{column_name}_nocase"
|
19
|
+
connection.exec_query <<-SQL.strip
|
20
|
+
CREATE #{'UNIQUE ' if options[:unique]}INDEX #{index_name} ON #{connection.quote_table_name(table_name)}
|
21
|
+
(#{connection.quote_column_name(column_name)} COLLATE NOCASE);
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'db_text_search/case_insensitive_eq/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class CaseInsensitiveEq
|
4
|
+
class InsensitiveColumnAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter#find)
|
6
|
+
def find(values)
|
7
|
+
@scope.where(@column => values)
|
8
|
+
end
|
9
|
+
|
10
|
+
# (see AbstractAdapter.add_index)
|
11
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
12
|
+
connection.add_index table_name, column_name, options
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'db_text_search/case_insensitive_eq/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class CaseInsensitiveEq
|
4
|
+
class LowerAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter#find)
|
6
|
+
def find(values)
|
7
|
+
conn = @scope.connection
|
8
|
+
@scope.where <<-SQL.strip
|
9
|
+
LOWER(#{quoted_scope_column}) IN (#{values.map { |v| "LOWER(#{conn.quote(v.to_s)})" }.join(', ')})
|
10
|
+
SQL
|
11
|
+
end
|
12
|
+
|
13
|
+
# (see AbstractAdapter.add_index)
|
14
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
15
|
+
if connection.adapter_name =~ /postgres/i
|
16
|
+
# TODO: Switch to native Rails support once it lands.
|
17
|
+
# https://github.com/rails/rails/pull/18499
|
18
|
+
index_name = options[:name] || "#{table_name}_#{column_name}_lower"
|
19
|
+
if defined?(SchemaPlus)
|
20
|
+
connection.add_index(table_name, column_name, options.merge(
|
21
|
+
name: index_name, expression: "LOWER(#{connection.quote_column_name(column_name)})"))
|
22
|
+
else
|
23
|
+
options.assert_valid_keys(:name, :unique)
|
24
|
+
connection.exec_query <<-SQL.strip
|
25
|
+
CREATE #{'UNIQUE ' if options[:unique]}INDEX #{index_name} ON #{connection.quote_table_name(table_name)}
|
26
|
+
(LOWER(#{connection.quote_column_name(column_name)}));
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
elsif connection.adapter_name =~ /mysql/i
|
30
|
+
fail 'MySQL case-insensitive index creation for case-sensitive columns is not supported.'
|
31
|
+
else
|
32
|
+
fail "Cannot create a case-insensitive index for case-sensitive column on #{connection.adapter_name}."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'db_text_search/case_insensitive_eq/insensitive_column_adapter'
|
2
|
+
require 'db_text_search/case_insensitive_eq/lower_adapter'
|
3
|
+
require 'db_text_search/case_insensitive_eq/collate_nocase_adapter'
|
4
|
+
|
5
|
+
module DbTextSearch
|
6
|
+
# Provides case-insensitive string-in-set querying, and CI index creation.
|
7
|
+
class CaseInsensitiveEq
|
8
|
+
# (see AbstractAdapter)
|
9
|
+
def initialize(scope, column)
|
10
|
+
@adapter = self.class.adapter_class(scope.connection, scope.table_name, column).new(scope, column)
|
11
|
+
@scope = scope
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param value_or_values [String, Array<String>]
|
15
|
+
# @return (see AbstractAdapter#find)
|
16
|
+
def find(value_or_values)
|
17
|
+
values = Array(value_or_values)
|
18
|
+
return @scope.none if values.empty?
|
19
|
+
@adapter.find(values)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds a case-insensitive column to the given table.
|
23
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
24
|
+
# @param table_name [String, Symbol]
|
25
|
+
# @param column_name [String, Symbol]
|
26
|
+
# @param options [Hash]
|
27
|
+
def self.add_ci_text_column(connection, table_name, column_name, options = {})
|
28
|
+
case connection.adapter_name.downcase
|
29
|
+
when /mysql/
|
30
|
+
connection.add_column(table_name, column_name, :text, options)
|
31
|
+
when /postgres/
|
32
|
+
connection.enable_extension 'citext'
|
33
|
+
if ActiveRecord::VERSION::MAJOR >= 4 && ActiveRecord::VERSION::MINOR >= 2
|
34
|
+
connection.add_column(table_name, column_name, :citext, options)
|
35
|
+
else
|
36
|
+
connection.add_column(table_name, column_name, 'CITEXT', options)
|
37
|
+
end
|
38
|
+
when /sqlite/
|
39
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
40
|
+
connection.add_column(table_name, column_name, :text, options.merge(collation: 'NOCASE'))
|
41
|
+
else
|
42
|
+
connection.add_column(table_name, column_name, 'TEXT COLLATE NOCASE', options)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
fail "Unsupported adapter #{connection.adapter_name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# (see AbstractAdapter.add_index)
|
50
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
51
|
+
adapter_class(connection, table_name, column_name).add_index(connection, table_name, column_name, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
55
|
+
# @param table_name [String, Symbol]
|
56
|
+
# @param column_name [String, Symbol]
|
57
|
+
# @return [Class<AbstractAdapter>]
|
58
|
+
def self.adapter_class(connection, table_name, column_name)
|
59
|
+
if connection.adapter_name.downcase =~ /sqlite/
|
60
|
+
# Always use COLLATE NOCASE for SQLite, as we can't check if the column is case-sensitive.
|
61
|
+
# It has no performance impact apart from slightly longer query strings for case-insensitive columns.
|
62
|
+
CollateNocaseAdapter
|
63
|
+
elsif column_case_sensitive?(connection, table_name, column_name)
|
64
|
+
LowerAdapter
|
65
|
+
else
|
66
|
+
InsensitiveColumnAdapter
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
71
|
+
# @param table_name [String, Symbol]
|
72
|
+
# @param column_name [String, Symbol]
|
73
|
+
# @return [Boolean]
|
74
|
+
# @note sqlite not supported.
|
75
|
+
# @api private
|
76
|
+
def self.column_case_sensitive?(connection, table_name, column_name)
|
77
|
+
column = connection.schema_cache.columns(table_name).detect { |c| c.name == column_name.to_s }
|
78
|
+
case connection.adapter_name.downcase
|
79
|
+
when /mysql/
|
80
|
+
column.case_sensitive?
|
81
|
+
when /postgres/
|
82
|
+
column.sql_type !~ /citext/i
|
83
|
+
else
|
84
|
+
fail "Unsupported adapter #{connection.adapter_name}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module DbTextSearch
|
2
|
+
class FullTextSearch
|
3
|
+
# A base class for FullTextSearch adapters.
|
4
|
+
class AbstractAdapter
|
5
|
+
include ::DbTextSearch::QueryBuilding
|
6
|
+
DEFAULT_PG_TS_CONFIG = %q('english')
|
7
|
+
|
8
|
+
# @param scope [ActiveRecord::Relation, Class<ActiveRecord::Base>]
|
9
|
+
# @param column [Symbol] name
|
10
|
+
def initialize(scope, column)
|
11
|
+
@scope = scope
|
12
|
+
@column = column
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param terms [Array<String>]
|
16
|
+
# @return [ActiveRecord::Relation]
|
17
|
+
def find(terms)
|
18
|
+
@scope.where(*where_args(terms))
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param terms [Array<String>]
|
22
|
+
# @param options [Hash]
|
23
|
+
# @option options pg_ts_config [String] a pg text search config. Default: 'english'
|
24
|
+
# @return [query fragment, binds]
|
25
|
+
# @abstract
|
26
|
+
def where_args(terms, options = {})
|
27
|
+
fail 'abstract'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add an index for full text search.
|
31
|
+
#
|
32
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
33
|
+
# @param table_name [String, Symbol]
|
34
|
+
# @param column_name [String, Symbol]
|
35
|
+
# @param options [Hash] passed down to ActiveRecord::ConnectionAdapters::SchemaStatements#add_index.
|
36
|
+
# @return (see ActiveRecord::ConnectionAdapters::SchemaStatements#add_index)
|
37
|
+
# @abstract
|
38
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
39
|
+
fail 'abstract'
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def parse_search_options(options = {})
|
45
|
+
options.assert_valid_keys(:pg_ts_config)
|
46
|
+
options.reverse_merge(pg_ts_config: DEFAULT_PG_TS_CONFIG)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'db_text_search/full_text_search/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class FullTextSearch
|
4
|
+
class MysqlAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter#where_args)
|
6
|
+
def where_args(terms, options = {})
|
7
|
+
parse_search_options(options)
|
8
|
+
conn = @scope.connection
|
9
|
+
["MATCH (#{conn.quote_table_name(@scope.table_name)}.#{conn.quote_column_name(@column)}) AGAINST (?)",
|
10
|
+
terms.uniq.join(' ')]
|
11
|
+
end
|
12
|
+
|
13
|
+
# (see AbstractAdapter.add_index)
|
14
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
15
|
+
connection.add_index table_name, column_name, options.merge(type: :fulltext)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'db_text_search/full_text_search/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class FullTextSearch
|
4
|
+
class PostgresAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter#where_args)
|
6
|
+
def where_args(terms, options = {})
|
7
|
+
options = parse_search_options(options)
|
8
|
+
pg_ts_config = options[:pg_ts_config]
|
9
|
+
["to_tsvector(#{pg_ts_config}, #{quoted_scope_column}::text) @@ plainto_tsquery(#{pg_ts_config}, ?)",
|
10
|
+
terms.uniq.join(' ')]
|
11
|
+
end
|
12
|
+
|
13
|
+
# (see AbstractAdapter.add_index)
|
14
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
15
|
+
options.assert_valid_keys(:name, :pg_ts_config)
|
16
|
+
pg_ts_config = options[:pg_ts_config] || DEFAULT_PG_TS_CONFIG
|
17
|
+
index_name = options[:name] || "#{table_name}_#{column_name}_fts"
|
18
|
+
connection.exec_query <<-SQL.strip
|
19
|
+
CREATE INDEX #{index_name} ON #{connection.quote_table_name(table_name)}
|
20
|
+
USING gist(to_tsvector(#{pg_ts_config}, #{connection.quote_column_name column_name}))
|
21
|
+
SQL
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'db_text_search/full_text_search/abstract_adapter'
|
2
|
+
module DbTextSearch
|
3
|
+
class FullTextSearch
|
4
|
+
class SqliteAdapter < AbstractAdapter
|
5
|
+
# (see AbstractAdapter.where_args)
|
6
|
+
def where_args(terms, options = {})
|
7
|
+
parse_search_options(options)
|
8
|
+
quoted_col = quoted_scope_column
|
9
|
+
term_args = terms.map(&:downcase).uniq.map do |term|
|
10
|
+
["#{quoted_col} COLLATE NOCASE LIKE ?", "%#{sanitize_sql_like term}%"]
|
11
|
+
end
|
12
|
+
[term_args.map(&:first).join(' AND '), *term_args.map(&:second)]
|
13
|
+
end
|
14
|
+
|
15
|
+
# (see AbstractAdapter.add_index)
|
16
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
17
|
+
# A no-op, as we just use LIKE for sqlite.
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'db_text_search/full_text_search/postgres_adapter'
|
2
|
+
require 'db_text_search/full_text_search/mysql_adapter'
|
3
|
+
require 'db_text_search/full_text_search/sqlite_adapter'
|
4
|
+
|
5
|
+
module DbTextSearch
|
6
|
+
# Provides case-insensitive string-in-set querying, and CI index creation.
|
7
|
+
class FullTextSearch
|
8
|
+
# (see AbstractAdapter)
|
9
|
+
def initialize(scope, column)
|
10
|
+
@adapter = self.class.adapter_class(scope.connection, scope.table_name, column).new(scope, column)
|
11
|
+
@scope = scope
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param term_or_terms [String, Array<String>]
|
15
|
+
# @return (see AbstractAdapter#find)
|
16
|
+
def find(term_or_terms)
|
17
|
+
values = Array(term_or_terms)
|
18
|
+
return @scope.none if values.empty?
|
19
|
+
@adapter.find(values)
|
20
|
+
end
|
21
|
+
|
22
|
+
# (see AbstractAdapter.add_index)
|
23
|
+
def self.add_index(connection, table_name, column_name, options = {})
|
24
|
+
adapter_class(connection, table_name, column_name).add_index(connection, table_name, column_name, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
28
|
+
# @param _table_name [String, Symbol]
|
29
|
+
# @param _column_name [String, Symbol]
|
30
|
+
# @return [Class<AbstractAdapter>]
|
31
|
+
def self.adapter_class(connection, _table_name, _column_name)
|
32
|
+
case connection.adapter_name
|
33
|
+
when /mysql/i
|
34
|
+
MysqlAdapter
|
35
|
+
when /postgres/i
|
36
|
+
PostgresAdapter
|
37
|
+
when /sqlite/i
|
38
|
+
SqliteAdapter
|
39
|
+
else
|
40
|
+
fail "unknown adapter #{connection.adapter_name}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module DbTextSearch
|
2
|
+
module QueryBuilding
|
3
|
+
protected
|
4
|
+
|
5
|
+
def quoted_scope_table
|
6
|
+
@scope.connection.quote_table_name(@scope.table_name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def quoted_column
|
10
|
+
@scope.connection.quote_column_name(@column)
|
11
|
+
end
|
12
|
+
|
13
|
+
def quoted_scope_column
|
14
|
+
"#{quoted_scope_table}.#{quoted_column}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def sanitize_sql_like(string, escape_character = "\\")
|
18
|
+
pattern = Regexp.union(escape_character, '%', '_')
|
19
|
+
string.gsub(pattern) { |x| [escape_character, x].join }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/shared.gemfile
ADDED
metadata
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: db_text_search
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gleb Mazovetskiy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.1.15
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 4.1.15
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: mysql2
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.3.20
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.3.20
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: pg
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.18.4
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.18.4
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: sqlite3
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 1.3.11
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.3.11
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: bundler
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.11'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.11'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rake
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '11.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '11.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.4'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '3.4'
|
117
|
+
description: |
|
118
|
+
Different relational databases treat text search very differently.
|
119
|
+
DbTextSearch provides a unified interface on top of ActiveRecord for SQLite, MySQL, and PostgreSQL to do:
|
120
|
+
* Case-insensitive string-in-set querying, and CI index creation.
|
121
|
+
* Basic full-text search for a list of terms, and FTS index creation.
|
122
|
+
email:
|
123
|
+
- glex.spb@gmail.com
|
124
|
+
executables: []
|
125
|
+
extensions: []
|
126
|
+
extra_rdoc_files: []
|
127
|
+
files:
|
128
|
+
- CHANGES.md
|
129
|
+
- CODE_OF_CONDUCT.md
|
130
|
+
- LICENSE.txt
|
131
|
+
- README.md
|
132
|
+
- db_text_search.gemspec
|
133
|
+
- lib/db_text_search.rb
|
134
|
+
- lib/db_text_search/case_insensitive_eq.rb
|
135
|
+
- lib/db_text_search/case_insensitive_eq/abstract_adapter.rb
|
136
|
+
- lib/db_text_search/case_insensitive_eq/collate_nocase_adapter.rb
|
137
|
+
- lib/db_text_search/case_insensitive_eq/insensitive_column_adapter.rb
|
138
|
+
- lib/db_text_search/case_insensitive_eq/lower_adapter.rb
|
139
|
+
- lib/db_text_search/full_text_search.rb
|
140
|
+
- lib/db_text_search/full_text_search/abstract_adapter.rb
|
141
|
+
- lib/db_text_search/full_text_search/mysql_adapter.rb
|
142
|
+
- lib/db_text_search/full_text_search/postgres_adapter.rb
|
143
|
+
- lib/db_text_search/full_text_search/sqlite_adapter.rb
|
144
|
+
- lib/db_text_search/query_building.rb
|
145
|
+
- lib/db_text_search/version.rb
|
146
|
+
- shared.gemfile
|
147
|
+
homepage: https://github.com/thredded/db_text_search
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '2.1'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 2.5.1
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: A unified interface on top of ActiveRecord for SQLite, MySQL, and PostgreSQL
|
171
|
+
for case-insensitive string search and basic full-text search.
|
172
|
+
test_files: []
|