db_text_search 0.1.1

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: 874e1b8761af33f55c19c2a67c12dea616a80bc4
4
+ data.tar.gz: a874ec1e3e6160ef2e5ad01f14020966b43f7cf9
5
+ SHA512:
6
+ metadata.gz: 81aae784b663e11355935fe800aefb4a377bbfa2c9adca47114ecc3ff7040cfd53361b2b809655b328b76bd085f04d49535da9b1143b4777789ea4a8394b6cdc
7
+ data.tar.gz: f6496edfa09ee256d0ef65f95ce87a3a365c3e5095b4f4d62c544bdf7f93c95698bacefe3a4179081ee77dad8f992e6e6f14ede94a11160647d64ae25acd511d
data/CHANGES.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.1.1
2
+
3
+ Initial release.
@@ -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 [![Build Status](https://travis-ci.org/thredded/db_text_search.svg?branch=master)](https://travis-ci.org/thredded/db_text_search) [![Code Climate](https://codeclimate.com/github/thredded/db_text_search/badges/gpa.svg)](https://codeclimate.com/github/thredded/db_text_search) [![Test Coverage](https://codeclimate.com/github/thredded/db_text_search/badges/coverage.svg)](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&nbsp;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
@@ -0,0 +1,3 @@
1
+ module DbTextSearch
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,9 @@
1
+ require 'active_record'
2
+ require 'active_support/core_ext/hash/keys'
3
+
4
+ require 'db_text_search/version'
5
+ require 'db_text_search/case_insensitive_eq'
6
+ require 'db_text_search/full_text_search'
7
+
8
+ module DbTextSearch
9
+ end
data/shared.gemfile ADDED
@@ -0,0 +1,10 @@
1
+ if ENV['TRAVIS']
2
+ group :test do
3
+ gem 'codeclimate-test-reporter', require: false
4
+ gem 'codeclimate_batch', require: false
5
+ end
6
+ else
7
+ group :test, :development do
8
+ gem 'byebug', platform: :mri, require: false
9
+ end
10
+ end
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: []