gbdev-scoped_search 0.2.0 → 0.3.0
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.
- data/TODO +1 -4
- data/lib/scoped_search.rb +13 -41
- data/lib/scoped_search/query_language_parser.rb +50 -23
- data/test/query_language_test.rb +23 -1
- data/test/search_for_test.rb +37 -13
- data/test/test_helper.rb +18 -15
- metadata +2 -2
data/TODO
CHANGED
@@ -5,15 +5,12 @@ Contact willem AT vanbergen DOT org if you want to help out
|
|
5
5
|
|
6
6
|
New features:
|
7
7
|
- Search fields of associations as well
|
8
|
-
- Allow
|
8
|
+
- Allow smart searches. Example: "90 days ago", "Yesterday", "10 days from now"
|
9
9
|
|
10
10
|
Refactoring:
|
11
|
-
- Create a separate class to build the actual SQL queries.
|
12
11
|
- For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
|
13
12
|
:only and :except can be values. That way it is possible for all fields to be loaded except
|
14
13
|
the ones specified with :except.
|
15
|
-
- Add checks for field types because the latest version of PostgreSQL (version 8.3.3) is more
|
16
|
-
strict about searching for strings in columns that are not string types.
|
17
14
|
|
18
15
|
Documentation & testing:
|
19
16
|
- Put something useful in the wiki
|
data/lib/scoped_search.rb
CHANGED
@@ -3,7 +3,9 @@ module ScopedSearch
|
|
3
3
|
module ClassMethods
|
4
4
|
|
5
5
|
def self.extended(base)
|
6
|
+
require 'scoped_search/reg_tokens'
|
6
7
|
require 'scoped_search/query_language_parser'
|
8
|
+
require 'scoped_search/query_conditions_builder'
|
7
9
|
end
|
8
10
|
|
9
11
|
# Creates a named scope in the class it was called upon
|
@@ -16,49 +18,19 @@ module ScopedSearch
|
|
16
18
|
# Build a hash that is used for the named_scope search_for.
|
17
19
|
# This function will split the search_string into keywords, and search for all the keywords
|
18
20
|
# in the fields that were provided to searchable_on
|
19
|
-
def build_scoped_search_conditions(search_string)
|
21
|
+
def build_scoped_search_conditions(search_string)
|
20
22
|
if search_string.nil? || search_string.strip.blank?
|
21
|
-
return {
|
22
|
-
else
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
keyword_name = "keyword_#{index}".to_sym
|
28
|
-
query_params[keyword_name] = "%#{search_condition.first}%"
|
29
|
-
|
30
|
-
# a keyword may be found in any of the provided fields, so join the conitions with OR
|
31
|
-
if search_condition.length == 2 && search_condition.last == :not
|
32
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
33
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
34
|
-
"(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
|
35
|
-
end
|
36
|
-
conditions << "(#{keyword_conditions.join(' AND ')})"
|
37
|
-
elsif search_condition.length == 2 && search_condition.last == :or
|
38
|
-
word1, word2 = query_params[keyword_name].split(' OR ')
|
39
|
-
|
40
|
-
query_params.delete(keyword_name)
|
41
|
-
keyword_name_a = "#{keyword_name.to_s}a".to_sym
|
42
|
-
keyword_name_b = "#{keyword_name.to_s}b".to_sym
|
43
|
-
query_params[keyword_name_a] = word1
|
44
|
-
query_params[keyword_name_b] = word2
|
45
|
-
|
46
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
47
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
48
|
-
"(#{field_name} LIKE :#{keyword_name_a.to_s} OR #{field_name} LIKE :#{keyword_name_b.to_s})"
|
49
|
-
end
|
50
|
-
conditions << "(#{keyword_conditions.join(' OR ')})"
|
51
|
-
else
|
52
|
-
keyword_conditions = self.scoped_search_fields.map do |field|
|
53
|
-
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
54
|
-
"#{field_name} LIKE :#{keyword_name.to_s}"
|
55
|
-
end
|
56
|
-
conditions << "(#{keyword_conditions.join(' OR ')})"
|
57
|
-
end
|
23
|
+
return {:conditions => nil}
|
24
|
+
else
|
25
|
+
query_fields = {}
|
26
|
+
self.scoped_search_fields.each do |field|
|
27
|
+
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
28
|
+
query_fields[field_name] = self.columns_hash[field.to_s].type
|
58
29
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
30
|
+
|
31
|
+
search_conditions = QueryLanguageParser.parse(search_string)
|
32
|
+
conditions = QueryConditionsBuilder.build_query(search_conditions, query_fields)
|
33
|
+
return {:conditions => conditions}
|
62
34
|
end
|
63
35
|
end
|
64
36
|
end
|
@@ -23,38 +23,65 @@ module ScopedSearch
|
|
23
23
|
else
|
24
24
|
if /^.+[ ]OR[ ].+$/ =~ item
|
25
25
|
conditions_tree << [item, :or]
|
26
|
+
elsif /^#{RegTokens::BetweenDateFormatMMDDYYYY}$/ =~ item or
|
27
|
+
/^#{RegTokens::BetweenDateFormatYYYYMMDD}$/ =~ item or
|
28
|
+
/^#{RegTokens::BetweenDatabaseFormat}$/ =~ item
|
29
|
+
conditions_tree << [item, :between_dates]
|
30
|
+
elsif /^#{RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
|
31
|
+
/^#{RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
|
32
|
+
/^#{RegTokens::GreaterThanOrEqualToDatabaseFormat}$/ =~ item
|
33
|
+
conditions_tree << [item, :greater_than_or_equal_to_date]
|
34
|
+
elsif /^#{RegTokens::LessThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
|
35
|
+
/^#{RegTokens::LessThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
|
36
|
+
/^#{RegTokens::LessThanOrEqualToDatabaseFormat}$/ =~ item
|
37
|
+
conditions_tree << [item, :less_than_or_equal_to_date]
|
38
|
+
elsif /^#{RegTokens::GreaterThanDateFormatMMDDYYYY}$/ =~ item or
|
39
|
+
/^#{RegTokens::GreaterThanDateFormatYYYYMMDD}$/ =~ item or
|
40
|
+
/^#{RegTokens::GreaterThanDatabaseFormat}$/ =~ item
|
41
|
+
conditions_tree << [item, :greater_than_date]
|
42
|
+
elsif /^#{RegTokens::LessThanDateFormatMMDDYYYY}$/ =~ item or
|
43
|
+
/^#{RegTokens::LessThanDateFormatYYYYMMDD}$/ =~ item or
|
44
|
+
/^#{RegTokens::LessThanDatabaseFormat}$/ =~ item
|
45
|
+
conditions_tree << [item, :less_than_date]
|
46
|
+
elsif /^#{RegTokens::DateFormatMMDDYYYY}$/ =~ item or
|
47
|
+
/^#{RegTokens::DateFormatYYYYMMDD}$/ =~ item or
|
48
|
+
/^#{RegTokens::DatabaseFormat}$/ =~ item
|
49
|
+
conditions_tree << [item, :as_of_date]
|
26
50
|
else
|
27
51
|
conditions_tree << (negate ? [item, :not] : [item, :like])
|
28
52
|
negate = false
|
29
53
|
end
|
30
54
|
end
|
31
55
|
end
|
56
|
+
|
32
57
|
return conditions_tree
|
33
58
|
end
|
34
|
-
|
35
|
-
# **Patterns**
|
36
|
-
# Each pattern is sperated by a "|". With regular expressions the order of the expression does matter.
|
37
|
-
#
|
38
|
-
# ([\w]+[ ]OR[ ][\w]+)
|
39
|
-
# ([\w]+[ ]OR[ ]["][\w ]+["])
|
40
|
-
# (["][\w ]+["][ ]OR[ ][\w]+)
|
41
|
-
# (["][\w ]+["][ ]OR[ ]["][\w ]+["])
|
42
|
-
# Any two combinations of letters, numbers and underscores that are seperated by " OR " (a single space must
|
43
|
-
# be on each side of the "OR").
|
44
|
-
# THESE COULD BE COMBINED BUT BECAUSE OF THE WAY PARSING WORKS THIS IS NOT DONE ON PURPOSE!!
|
45
|
-
#
|
46
|
-
# ([-]?[\w]+)
|
47
|
-
# Any combination of letters, numbers and underscores that may or may not have a dash in front.
|
48
|
-
#
|
49
|
-
# ([-]?["][\w ]+["])
|
50
|
-
# Any combination of letters, numbers, underscores and spaces within double quotes that may or may not have a dash in front.
|
59
|
+
|
51
60
|
def tokenize(query)
|
52
|
-
pattern = [
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
pattern = [RegTokens::BetweenDateFormatMMDDYYYY,
|
62
|
+
RegTokens::BetweenDateFormatYYYYMMDD,
|
63
|
+
RegTokens::BetweenDatabaseFormat,
|
64
|
+
RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY,
|
65
|
+
RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD,
|
66
|
+
RegTokens::GreaterThanOrEqualToDatabaseFormat,
|
67
|
+
RegTokens::LessThanOrEqualToDateFormatMMDDYYYY,
|
68
|
+
RegTokens::LessThanOrEqualToDateFormatYYYYMMDD,
|
69
|
+
RegTokens::LessThanOrEqualToDatabaseFormat,
|
70
|
+
RegTokens::GreaterThanDateFormatMMDDYYYY,
|
71
|
+
RegTokens::GreaterThanDateFormatYYYYMMDD,
|
72
|
+
RegTokens::GreaterThanDatabaseFormat,
|
73
|
+
RegTokens::LessThanDateFormatMMDDYYYY,
|
74
|
+
RegTokens::LessThanDateFormatYYYYMMDD,
|
75
|
+
RegTokens::LessThanDatabaseFormat,
|
76
|
+
RegTokens::DateFormatMMDDYYYY,
|
77
|
+
RegTokens::DateFormatYYYYMMDD,
|
78
|
+
RegTokens::DatabaseFormat,
|
79
|
+
RegTokens::WordOrWord,
|
80
|
+
RegTokens::WordOrString,
|
81
|
+
RegTokens::StringOrWord,
|
82
|
+
RegTokens::StringOrString,
|
83
|
+
RegTokens::PossiblyNegatedWord,
|
84
|
+
RegTokens::PossiblyNegatedString]
|
58
85
|
pattern = Regexp.new(pattern.join('|'))
|
59
86
|
|
60
87
|
tokens = []
|
data/test/query_language_test.rb
CHANGED
@@ -51,7 +51,7 @@ class QueryLanguageTest < Test::Unit::TestCase
|
|
51
51
|
assert_equal 2, parsed.length
|
52
52
|
assert_equal 'hallo', parsed[0].first
|
53
53
|
assert_equal 'willem', parsed[1].first
|
54
|
-
|
54
|
+
|
55
55
|
parsed = parse_query(' "hallo wi"llem"')
|
56
56
|
assert_equal 2, parsed.length
|
57
57
|
assert_equal 'hallo wi', parsed[0].first
|
@@ -108,6 +108,28 @@ class QueryLanguageTest < Test::Unit::TestCase
|
|
108
108
|
assert_equal :or, parsed[0][1]
|
109
109
|
end
|
110
110
|
|
111
|
+
def test_as_of_date
|
112
|
+
# parsed = parse_query('9/27/1980')
|
113
|
+
# assert_equal 1, parsed.length
|
114
|
+
# assert_equal '9/27/1980', parsed[0][0]
|
115
|
+
# assert_equal :as_of, parsed[0][1]
|
116
|
+
|
117
|
+
# parsed = parse_query('"Man made" OR Dogs')
|
118
|
+
# assert_equal 1, parsed.length
|
119
|
+
# assert_equal 'Man made OR Dogs', parsed[0][0]
|
120
|
+
# assert_equal :or, parsed[0][1]
|
121
|
+
#
|
122
|
+
# parsed = parse_query('Cows OR "Frog Toys"')
|
123
|
+
# assert_equal 1, parsed.length
|
124
|
+
# assert_equal 'Cows OR Frog Toys', parsed[0][0]
|
125
|
+
# assert_equal :or, parsed[0][1]
|
126
|
+
#
|
127
|
+
# parsed = parse_query('"Happy cow" OR "Sad Frog"')
|
128
|
+
# assert_equal 1, parsed.length
|
129
|
+
# assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
|
130
|
+
# assert_equal :or, parsed[0][1]
|
131
|
+
end
|
132
|
+
|
111
133
|
def test_long_string
|
112
134
|
str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
|
113
135
|
parsed = parse_query(str)
|
data/test/search_for_test.rb
CHANGED
@@ -11,31 +11,55 @@ class ScopedSearchTest < Test::Unit::TestCase
|
|
11
11
|
teardown_db
|
12
12
|
end
|
13
13
|
|
14
|
-
def test_enabling
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
14
|
+
# def test_enabling
|
15
|
+
# assert !SearchTestModel.respond_to?(:search_for)
|
16
|
+
# SearchTestModel.searchable_on :string_field, :text_field, :date_field
|
17
|
+
# assert SearchTestModel.respond_to?(:search_for)
|
18
|
+
#
|
19
|
+
# assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
|
20
|
+
# end
|
21
|
+
#
|
23
22
|
def test_search
|
24
|
-
SearchTestModel.searchable_on :string_field, :text_field
|
23
|
+
SearchTestModel.searchable_on :string_field, :text_field, :date_field
|
25
24
|
|
26
|
-
assert_equal
|
25
|
+
assert_equal 16, SearchTestModel.search_for('').count
|
27
26
|
assert_equal 0, SearchTestModel.search_for('456').count
|
28
27
|
assert_equal 2, SearchTestModel.search_for('hays').count
|
29
28
|
assert_equal 1, SearchTestModel.search_for('hay ob').count
|
30
|
-
assert_equal
|
29
|
+
assert_equal 14, SearchTestModel.search_for('o').count
|
31
30
|
assert_equal 2, SearchTestModel.search_for('-o').count
|
32
|
-
assert_equal
|
31
|
+
assert_equal 14, SearchTestModel.search_for('-Jim').count
|
33
32
|
assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
|
34
33
|
assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
|
35
34
|
assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
|
36
35
|
assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
|
37
36
|
assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
|
38
37
|
assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
|
38
|
+
|
39
|
+
# ** DATES **
|
40
|
+
#
|
41
|
+
# The next two dates are invalid therefore it will be ignored.
|
42
|
+
# Thus it would be the same as searching for an empty string
|
43
|
+
assert_equal 16, SearchTestModel.search_for('2/30/1980').count
|
44
|
+
assert_equal 16, SearchTestModel.search_for('99/99/9999').count
|
45
|
+
|
46
|
+
assert_equal 1, SearchTestModel.search_for('9/27/1980').count
|
47
|
+
assert_equal 1, SearchTestModel.search_for('hays 9/27/1980').count
|
48
|
+
assert_equal 2, SearchTestModel.search_for('hays 2/30/1980').count
|
49
|
+
assert_equal 1, SearchTestModel.search_for('2006/07/15').count
|
50
|
+
|
51
|
+
assert_equal 1, SearchTestModel.search_for('< 12/01/1980').count
|
52
|
+
assert_equal 5, SearchTestModel.search_for('> 1/1/2006').count
|
53
|
+
|
54
|
+
assert_equal 5, SearchTestModel.search_for('< 12/26/2002').count
|
55
|
+
assert_equal 6, SearchTestModel.search_for('<= 12/26/2002').count
|
56
|
+
|
57
|
+
assert_equal 5, SearchTestModel.search_for('> 2/5/2005').count
|
58
|
+
assert_equal 6, SearchTestModel.search_for('>= 2/5/2005').count
|
59
|
+
|
60
|
+
assert_equal 3, SearchTestModel.search_for('1/1/2005 TO 1/1/2007').count
|
61
|
+
|
62
|
+
assert_equal 2, SearchTestModel.search_for('Happy 1/1/2005 TO 1/1/2007').count
|
39
63
|
end
|
40
64
|
|
41
65
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'rubygems'
|
3
3
|
require 'active_record'
|
4
|
+
require 'ruby-debug'
|
4
5
|
|
5
6
|
require "#{File.dirname(__FILE__)}/../lib/scoped_search"
|
6
7
|
|
@@ -11,6 +12,7 @@ def setup_db
|
|
11
12
|
t.string :string_field
|
12
13
|
t.text :text_field
|
13
14
|
t.string :ignored_field
|
15
|
+
t.date :date_field
|
14
16
|
t.timestamps
|
15
17
|
end
|
16
18
|
end
|
@@ -22,20 +24,21 @@ end
|
|
22
24
|
|
23
25
|
class SearchTestModel < ActiveRecord::Base
|
24
26
|
def self.create_corpus!
|
25
|
-
create!(:string_field => "Programmer 123", :text_field => nil,
|
26
|
-
create!(:string_field => "Jim", :text_field => "Henson",
|
27
|
-
create!(:string_field => "Jim", :text_field => "Bush",
|
28
|
-
create!(:string_field => "Wes", :text_field => "Hays",
|
29
|
-
create!(:string_field => "Bob", :text_field => "Hays",
|
30
|
-
create!(:string_field => "Dogs", :text_field => "Pit Bull",
|
31
|
-
create!(:string_field => "Dogs", :text_field => "Eskimo",
|
32
|
-
create!(:string_field => "Cows", :text_field => "Farms",
|
33
|
-
create!(:string_field => "Hello World", :text_field => "Hello Moon",
|
34
|
-
create!(:string_field => "Hello World", :text_field => "Goodnight Moon",
|
35
|
-
create!(:string_field => "Happy Cow", :text_field => "Sad Cow",
|
36
|
-
create!(:string_field => "Happy Frog", :text_field => "Sad Frog",
|
37
|
-
create!(:string_field => "Excited Frog", :text_field => "Sad Frog",
|
38
|
-
create!(:string_field => "Man made", :text_field => "Woman made",
|
39
|
-
create!(:string_field => "Cat Toys", :text_field => "Frog Toys",
|
27
|
+
create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456", :date_field => '2000-01-01')
|
28
|
+
create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a", :date_field => '2001-04-15')
|
29
|
+
create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b", :date_field => '2001-04-17')
|
30
|
+
create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c", :date_field => '1980-09-27')
|
31
|
+
create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d", :date_field => '2002-11-09')
|
32
|
+
create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e", :date_field => '2002-12-26')
|
33
|
+
create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f", :date_field => '2003-03-19')
|
34
|
+
create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g", :date_field => '2004-05-01')
|
35
|
+
create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h", :date_field => '2004-07-11')
|
36
|
+
create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i", :date_field => '2004-09-12')
|
37
|
+
create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j", :date_field => '2005-02-05')
|
38
|
+
create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k", :date_field => '2006-03-09')
|
39
|
+
create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l", :date_field => '2006-07-15')
|
40
|
+
create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m", :date_field => '2007-06-13')
|
41
|
+
create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n", :date_field => '2008-03-04')
|
42
|
+
create!(:string_field => "Happy Toys", :text_field => "Sad Toys", :ignored_field => "123456n", :date_field => '2008-05-12')
|
40
43
|
end
|
41
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gbdev-scoped_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2008-09-
|
13
|
+
date: 2008-09-21 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|