wvanbergen-scoped_search 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -3,12 +3,18 @@ TODO items for named_scope
3
3
  Contact willem AT vanbergen DOT org if you want to help out
4
4
 
5
5
 
6
- New functionality:
7
- - Search model associations as well
8
- - Allow OR-queries
6
+ New features:
7
+ - Search fields of associations as well
8
+ - Allow other search operators than %LIKE%
9
9
 
10
10
  Refactoring:
11
- - Better separation of query building and query string parser
11
+ - Create a separate class to build the actual SQL queries.
12
+ - For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
13
+ :only and :except can be values. That way it is possible for all fields to be loaded except
14
+ 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.
12
17
 
13
- Testing:
14
- - More tests for the query string parser
18
+ Documentation & testing:
19
+ - Put something useful in the wiki
20
+ - Add rdoc en comments to code
@@ -18,63 +18,53 @@ module ScopedSearch
18
18
  negate = false
19
19
  tokens.each do |item|
20
20
  case item
21
- when :not
22
- negate = true
23
- else
24
- conditions_tree << (negate ? [item, :not] : [item, :like])
25
- negate = false
21
+ when :not
22
+ negate = true
23
+ else
24
+ if /^.+[ ]OR[ ].+$/ =~ item
25
+ conditions_tree << [item, :or]
26
+ else
27
+ conditions_tree << (negate ? [item, :not] : [item, :like])
28
+ negate = false
29
+ end
26
30
  end
27
31
  end
28
32
  return conditions_tree
29
33
  end
30
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.
31
51
  def tokenize(query)
32
- tokens = []
33
- current_token = ""
34
- quoted_string_openend = false
35
-
36
- query.each_char do |char|
37
-
38
- case char
39
- when /\s/
40
- if quoted_string_openend
41
- current_token << char
42
- elsif current_token.length > 0
43
- tokens << current_token
44
- current_token = ""
45
- end
46
-
47
- when '-'
48
- if quoted_string_openend || current_token.length > 0
49
- current_token << char
50
- else
51
- tokens << :not
52
- end
53
-
54
- when '"'
55
- if quoted_string_openend
56
- if current_token.length > 0
57
- if current_token[-1,1] == "\\"
58
- current_token[-1] = char
59
- else
60
- tokens << current_token
61
- current_token = ""
62
- quoted_string_openend = false
63
- end
64
- else
65
- quoted_string_openend = false
66
- end
67
-
68
- else
69
- quoted_string_openend = true
70
- end
71
-
72
- else
73
- current_token << char
74
- end
52
+ pattern = ['([\w]+[ ]OR[ ][\w]+)',
53
+ '([\w]+[ ]OR[ ]["][\w ]+["])',
54
+ '(["][\w ]+["][ ]OR[ ][\w]+)',
55
+ '(["][\w ]+["][ ]OR[ ]["][\w ]+["])',
56
+ '([-]?[\w]+)',
57
+ '([-]?["][\w ]+["])']
58
+ pattern = Regexp.new(pattern.join('|'))
75
59
 
76
- end
77
- tokens << current_token if current_token.length > 0
60
+ tokens = []
61
+ matches = query.scan(pattern).flatten.compact
62
+ matches.each { |match|
63
+ tokens << :not unless match.index('-').nil?
64
+ # Remove any escaped quotes and any dashes - the dash usually the first character.
65
+ # Remove any additional spaces - more that one.
66
+ tokens << match.gsub(/[-"]/,'').gsub(/[ ]{2,}/, ' ')
67
+ }
78
68
  return tokens
79
69
  end
80
70
  end
data/lib/scoped_search.rb CHANGED
@@ -34,17 +34,31 @@ module ScopedSearch
34
34
  "(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
35
35
  end
36
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 ')})"
37
51
  else
38
52
  keyword_conditions = self.scoped_search_fields.map do |field|
39
53
  field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
40
54
  "#{field_name} LIKE :#{keyword_name.to_s}"
41
55
  end
42
56
  conditions << "(#{keyword_conditions.join(' OR ')})"
43
- end
57
+ end
44
58
  end
45
59
 
46
60
  # all keywords must be matched, so join the conditions with AND
47
- return { :conditions => [conditions.join(' AND '), query_params] }
61
+ return { :conditions => [conditions.join(' AND '), query_params] }
48
62
  end
49
63
  end
50
64
  end
@@ -19,7 +19,7 @@ class QueryLanguageTest < Test::Unit::TestCase
19
19
  parsed = parse_query('hallo')
20
20
  assert_equal 1, parsed.length
21
21
  assert_equal 'hallo', parsed.first.first
22
-
22
+
23
23
  parsed = parse_query(' hallo ')
24
24
  assert_equal 1, parsed.length
25
25
  assert_equal 'hallo', parsed.first.first
@@ -45,40 +45,96 @@ class QueryLanguageTest < Test::Unit::TestCase
45
45
 
46
46
  parsed = parse_query(' "hallo willem"')
47
47
  assert_equal 1, parsed.length
48
- assert_equal 'hallo willem', parsed.first.first
49
-
48
+ assert_equal 'hallo willem', parsed.first.first
49
+
50
50
  parsed = parse_query(' "hallo willem')
51
- assert_equal 1, parsed.length
52
- assert_equal 'hallo willem', parsed.first.first
53
-
51
+ assert_equal 2, parsed.length
52
+ assert_equal 'hallo', parsed[0].first
53
+ assert_equal 'willem', parsed[1].first
54
+
54
55
  parsed = parse_query(' "hallo wi"llem"')
55
56
  assert_equal 2, parsed.length
56
- assert_equal 'hallo wi', parsed.first.first
57
- assert_equal 'llem', parsed.last.first
57
+ assert_equal 'hallo wi', parsed[0].first
58
+ assert_equal 'llem', parsed[1].first
58
59
  end
59
60
 
60
61
  def test_quote_escaping
61
62
  parsed = parse_query(' "hallo wi\\"llem"')
62
- assert_equal 1, parsed.length
63
- assert_equal 'hallo wi"llem', parsed.first.first
63
+ assert_equal 3, parsed.length
64
+ assert_equal 'hallo', parsed[0].first
65
+ assert_equal 'wi', parsed[1].first
66
+ assert_equal 'llem', parsed[2].first
64
67
 
65
68
  parsed = parse_query('"\\"hallo willem\\""')
66
- assert_equal 1, parsed.length
67
- assert_equal '"hallo willem"', parsed.first.first
69
+ assert_equal 2, parsed.length
70
+ assert_equal 'hallo', parsed[0].first
71
+ assert_equal 'willem', parsed[1].first
68
72
  end
69
-
73
+
70
74
  def test_negation
71
75
  parsed = parse_query('-willem')
72
76
  assert_equal 1, parsed.length
73
- assert_equal 'willem', parsed.first.first
74
- assert_equal :not, parsed.first.last
77
+ assert_equal 'willem', parsed[0].first
78
+ assert_equal :not, parsed[0].last
75
79
 
76
80
  parsed = parse_query('123 -"456 789"')
77
81
  assert_equal 2, parsed.length
78
- assert_equal '123', parsed.first.first
79
- assert_equal :like, parsed.first.last
82
+ assert_equal '123', parsed[0].first
83
+ assert_equal :like, parsed[0].last
84
+
85
+ assert_equal '456 789', parsed[1].first
86
+ assert_equal :not, parsed[1].last
87
+ end
88
+
89
+ def test_or
90
+ parsed = parse_query('Wes OR Hays')
91
+ assert_equal 1, parsed.length
92
+ assert_equal 'Wes OR Hays', parsed[0][0]
93
+ assert_equal :or, parsed[0][1]
94
+
95
+ parsed = parse_query('"Man made" OR Dogs')
96
+ assert_equal 1, parsed.length
97
+ assert_equal 'Man made OR Dogs', parsed[0][0]
98
+ assert_equal :or, parsed[0][1]
99
+
100
+ parsed = parse_query('Cows OR "Frog Toys"')
101
+ assert_equal 1, parsed.length
102
+ assert_equal 'Cows OR Frog Toys', parsed[0][0]
103
+ assert_equal :or, parsed[0][1]
104
+
105
+ parsed = parse_query('"Happy cow" OR "Sad Frog"')
106
+ assert_equal 1, parsed.length
107
+ assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
108
+ assert_equal :or, parsed[0][1]
109
+ end
110
+
111
+ def test_long_string
112
+ str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
113
+ parsed = parse_query(str)
114
+ assert_equal 8, parsed.length
115
+
116
+ assert_equal 'Wes', parsed[0].first
117
+ assert_equal :like, parsed[0].last
118
+
119
+ assert_equal 'Hays', parsed[1].first
120
+ assert_equal :not, parsed[1].last
121
+
122
+ assert_equal 'Hello World', parsed[2].first
123
+ assert_equal :like, parsed[2].last
124
+
125
+ assert_equal 'Goodnight Moon', parsed[3].first
126
+ assert_equal :not, parsed[3].last
127
+
128
+ assert_equal 'Bob OR Wes', parsed[4].first
129
+ assert_equal :or, parsed[4].last
130
+
131
+ assert_equal 'Happy cow OR Sad Frog', parsed[5].first
132
+ assert_equal :or, parsed[5].last
133
+
134
+ assert_equal 'Man made OR Dogs', parsed[6].first
135
+ assert_equal :or, parsed[6].last
80
136
 
81
- assert_equal '456 789', parsed.last.first
82
- assert_equal :not, parsed.last.last
137
+ assert_equal 'Cows OR Frog Toys', parsed[7].first
138
+ assert_equal :or, parsed[7].last
83
139
  end
84
140
  end
@@ -23,16 +23,19 @@ class ScopedSearchTest < Test::Unit::TestCase
23
23
  def test_search
24
24
  SearchTestModel.searchable_on :string_field, :text_field
25
25
 
26
- assert_equal 3, SearchTestModel.search_for('123').count
27
- assert_equal 3, SearchTestModel.search_for('haLL').count
28
- assert_equal 1, SearchTestModel.search_for('456').count
29
- assert_equal 2, SearchTestModel.search_for('ha 23').count
30
- assert_equal 0, SearchTestModel.search_for('wi').count
31
-
32
- assert_equal 1, SearchTestModel.search_for('-hallo').count
33
- assert_equal 4, SearchTestModel.search_for('-wi').count
34
- assert_equal 3, SearchTestModel.search_for('-789').count
35
- assert_equal 2, SearchTestModel.search_for('123 -456').count
26
+ assert_equal 15, SearchTestModel.search_for('').count
27
+ assert_equal 0, SearchTestModel.search_for('456').count
28
+ assert_equal 2, SearchTestModel.search_for('hays').count
29
+ assert_equal 1, SearchTestModel.search_for('hay ob').count
30
+ assert_equal 13, SearchTestModel.search_for('o').count
31
+ assert_equal 2, SearchTestModel.search_for('-o').count
32
+ assert_equal 13, SearchTestModel.search_for('-Jim').count
33
+ assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
34
+ assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
35
+ assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
36
+ assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
37
+ assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
38
+ assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
36
39
  end
37
40
 
38
41
  end
data/test/test_helper.rb CHANGED
@@ -22,9 +22,20 @@ end
22
22
 
23
23
  class SearchTestModel < ActiveRecord::Base
24
24
  def self.create_corpus!
25
- create!(:string_field => "123", :text_field => "Hallo", :ignored_field => "123 willem")
26
- create!(:string_field => "456", :text_field => "Hallo 123", :ignored_field => "123")
27
- create!(:string_field => "789", :text_field => "HALLO", :ignored_field => "123456");
28
- create!(:string_field => "123", :text_field => nil, :ignored_field => "123456");
25
+ create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456")
26
+ create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a")
27
+ create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b")
28
+ create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c")
29
+ create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d")
30
+ create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e")
31
+ create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f")
32
+ create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g")
33
+ create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h")
34
+ create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i")
35
+ create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j")
36
+ create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k")
37
+ create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l")
38
+ create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m")
39
+ create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n")
29
40
  end
30
41
  end
metadata CHANGED
@@ -1,20 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wvanbergen-scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
8
+ - Wes Hays
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
12
 
12
- date: 2008-09-06 00:00:00 -07:00
13
+ date: 2008-09-13 00:00:00 -07:00
13
14
  default_executable:
14
15
  dependencies: []
15
16
 
16
17
  description: Scoped search makes it easy to search your ActiveRecord-based models. It will create a named scope according to a provided query string. The named_scope can be used like any other named_scope, so it can be cchained or combined with will_paginate.
17
- email: willem@vanbergen.org
18
+ email:
19
+ - willem@vanbergen.org
20
+ - weshays@gbdev.com
18
21
  executables: []
19
22
 
20
23
  extensions: []