Skip to content

Commit

Permalink
Added right_wilcard option for fulltext attributes #23
Browse files Browse the repository at this point in the history
  • Loading branch information
mrkamel committed Oct 15, 2018
1 parent 1141355 commit 2eeca61
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 11 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,32 @@ end
For more details about PostgreSQL fulltext indices visit
[http://www.postgresql.org/docs/9.3/static/textsearch.html](http://www.postgresql.org/docs/9.3/static/textsearch.html)

## Fulltext wildcard

By default, SearchCop won't add wildcards for fulltext queries/fields. To auto-
append a wildcard for fulltext queries pass `right_wildcard: true` to the
`options` method

```ruby
search_scope :search do
attributes :title

options :title, type: :fulltext, right_wildcard: true
end
```

such that

```ruby
Product.search("title:movi")
```

becomes equivalent to

```ruby
Product.search("title:movi*")
```

## Other indices

In case you expose non-fulltext attributes to search queries (price, stock,
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ services:
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_ROOT_PASSWORD=
- MYSQL_DATABASE=search_cop
ports:
- 3306:3306
postgres:
image: postgres
environment:
- POSTGRES_USER=search_cop
- POSTGRES_PASSWORD=
- POSTGRES_DB=search_cop
ports:
- 5432:5432

16 changes: 14 additions & 2 deletions lib/search_cop/visitors/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@ module Visitors
module Postgres
class FulltextQuery < Visitor
def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(node)
"!'#{node.right.gsub(/[\s&|!:'"]+/, " ")}'"
text = node.right.gsub(/[\s&|!:'"]+/, " ")

if text =~ /(?<!\\)\*\z/
"!'#{text.gsub(/\*\z/, "")}':*"
else
"!'#{text}'"
end
end

def visit_SearchCopGrammar_Nodes_MatchesFulltext(node)
"'#{node.right.gsub(/[\s&|!:'"]+/, " ")}'"
text = node.right.gsub(/[\s&|!:'"]+/, " ")

if text =~ /(?<!\\)\*\z/
"'#{text.gsub(/\*\z/, "")}':*"
else
"'#{text}'"
end
end

def visit_SearchCopGrammar_Nodes_And_Fulltext(node)
Expand Down
12 changes: 8 additions & 4 deletions lib/search_cop_grammar/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def generator(generator, value)

def matches(value)
if fulltext?
SearchCopGrammar::Nodes::MatchesFulltext.new self, value.to_s
res = value.to_s
res = "#{res}*" if options[:right_wildcard] && res !~ /(?<!\\)\*\z/

SearchCopGrammar::Nodes::MatchesFulltext.new(self, res)
else
attributes.collect! { |attribute| attribute.matches value }.inject(:or)
end
Expand Down Expand Up @@ -156,9 +159,10 @@ def respond_to?(*args)

class String < Base
def matches_value(value)
return value.gsub(/\*/, "%") if (options[:left_wildcard] != false && value.strip =~ /^[^*]+\*$|^\*[^*]+$/) || value.strip =~ /^[^*]+\*$/

options[:left_wildcard] != false ? "%#{value}%" : "#{value}%"
res = value.gsub(/(?<!\\)\*/, "%")
res = "%#{res}" unless options[:left_wildcard] == false || res.starts_with?("%")
res = "#{res}%" unless res.ends_with?("%")
res
end

def matches(value)
Expand Down
2 changes: 1 addition & 1 deletion search_cop.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "activerecord", ">= 3.0.0"
spec.add_development_dependency "factory_girl"
spec.add_development_dependency "factory_bot"
spec.add_development_dependency "appraisal"
spec.add_development_dependency "minitest"
end
2 changes: 2 additions & 0 deletions test/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ sqlite:
mysql:
adapter: mysql2
database: search_cop
host: 127.0.0.1
username: root
password:
encoding: utf8

postgres:
Expand Down
20 changes: 20 additions & 0 deletions test/fulltext_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,25 @@ def test_mixed
assert_includes results, expected
refute_includes results, rejected
end

def test_wildcard
expected = create(:product, title: "Expected")
rejected = create(:product, title: "Rejected")

results = Product.search("title:expect*")

assert_includes results, expected
refute_includes results, rejected
end

def test_fulltext_with_wildcards
expected = create(:product, description: "Expected")
rejected = create(:product, description: "Rejected")

results = Product.search("description:expect")

assert_includes results, expected
refute_includes results, rejected
end
end

9 changes: 5 additions & 4 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class SearchCop::TestCase < MiniTest::Unit::TestCase; end

require "minitest/autorun"
require "active_record"
require "factory_girl"
require "factory_bot"
require "yaml"

DATABASE = ENV["DATABASE"] || "sqlite"
Expand Down Expand Up @@ -44,8 +44,9 @@ class Product < ActiveRecord::Base
aliases :users_products => :user

if DATABASE != "sqlite"
options :primary, type: :fulltext, coalesce: true
options :title, :type => :fulltext, coalesce: true
options :description, :type => :fulltext, coalesce: true
options :description, :type => :fulltext, coalesce: true, right_wildcard: true
options :comment, :type => :fulltext, coalesce: true
end

Expand Down Expand Up @@ -78,7 +79,7 @@ class Product < ActiveRecord::Base
belongs_to :user
end

FactoryGirl.define do
FactoryBot.define do
factory :product do
end

Expand Down Expand Up @@ -126,7 +127,7 @@ class Product < ActiveRecord::Base
end

class SearchCop::TestCase
include FactoryGirl::Syntax::Methods
include FactoryBot::Syntax::Methods

def teardown
Product.delete_all
Expand Down

0 comments on commit 2eeca61

Please sign in to comment.