cql-ruby 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,57 +1,160 @@
1
+ # This file adds #to_solr method to CqlRuby::CqlNodes, to
2
+ # convert parsed CQL to a query in the lucene-solr syntax.
3
+ # http://wiki.apache.org/solr/SolrQuerySyntax
4
+ #
5
+ # CQL version 1.2 spec was used to understand CQL semantics, although this
6
+ # will likely work for other CQL versions as well.
7
+ #
8
+ # All indexes specified in CQL are mapped to Solr fields, assumed to exist,
9
+ # with the same name as CQL index. Any 'context set' namespace prefixes
10
+ # on indexes are ignored, just the base name is mapped to solr field.
11
+
12
+ # The server-choice index specifications cql.anyindexes, cql.serverchoice,
13
+ # cql.keywords all map by default to no specified index (let
14
+ # solr use default from perhaps a solr 'df' param), but this can be changed
15
+ # with CqlRuby.to_solr_defaults[:default_index]. cql.allindexes maps
16
+ # by default to 'text', which can be changed with
17
+ # CqlRuby.to_solr_defaults[:all_index]
18
+ #
19
+ # Not all CQL can currently be converted to a solr query. If the CQL includes
20
+ # nodes that can not be converted, an exception will be raised.
21
+ #
22
+ # == CQL expressions that can be converted: ==
23
+ # * expressions using the following relations, which can be specified with "cql" prefix or without.
24
+ # ** adj
25
+ # ** all
26
+ # ** any
27
+ # ** == (note, for typical tokenized solr fields, this will be the same as adj, which is not quite proper CQL semantics, but best we can do on an arbitrary solr field).
28
+ # ** <> (note, for typical tokenized solr fields, won't have quite the right CQL semantics, instead of "not exactly equal to", it will be "does not contain the phrase").
29
+ # ** >, <, <=, >=, within (note, solr range/comparison queries may or may not actually produce anything sensical depending on solr field definition, but CQL to_solr will translate into solr range syntax anyway and hope for the best. )
30
+ # ** =, the server's choice relation, defaults to 'adj', but can be specified in CqlRuby.to_solr_defaults.
31
+ # * CQL Boolean Operators AND, OR, and NOT.
32
+ #
33
+ # == CQL expressions that can NOT be converted (at least in present version) ==
34
+ # And will raise exceptions if you try to call #to_solr on a CQL node which
35
+ # includes or has children that include the following:
36
+ # * PROX (boolean) operator.
37
+ # * cql.encloses relation
38
+ # * Any relation modifiers.
39
+ # * Any boolean (operator) modifiers.
40
+ # * sortBy
41
+ # * inline prefix map/prefix assignment specification.
42
+ # * cql.resultsetid
43
+
44
+ # == TODO==
45
+ # * support modifiers on adj relation to map to solr '~' slop param?
46
+ # * change all tests to rspec instead of Test::Unit
47
+ # * implemented for acts_as_solr, either more flavors or more general (from chick, jrochkind isn't sure what this means)
48
+
49
+
1
50
  module CqlRuby
51
+ def self.to_solr_defaults
52
+ @to_solr_params ||= {
53
+ #What's our default relation for "=" server's choice relation? how about
54
+ # adj.
55
+ :default_relation => "cql.adj",
56
+ # What's our default index for various server's choice index choices?
57
+ # nil means don't specify an index, let solr take care of it.
58
+ # Or you can specify one.
59
+ :default_index => nil,
60
+ # What index should we use for cql.allIndexes? Again can be nil
61
+ # meaning let the solr server use it's default.
62
+ :all_index => "text"
63
+ }
64
+ end
65
+
66
+
2
67
 
3
- # This set of of overrides to the CqlRuby::CqlNodes provides to_solr methods, where not
4
- # specified for a given node the CqlNode.to_solr method will be used
5
- # TODO: SOLR can cover much more functionality of CQL than is captured here
6
- # TODO: implemented for acts_as_solr, either more flavors or more general
7
68
 
8
69
  class CqlNode
9
- def to_solr
10
- quoted_index = maybe_quote( @index )
11
- quoted_term = maybe_quote( @term )
12
- relation_prefix = @relation.to_solr
13
- case quoted_index
14
- when 'cql.resultSetId': raise CqlException, "resultSet not supported"
15
- when 'cql.allRecords': "[* TO *]"
16
- when 'cql.allIndexes': "#{relation_prefix}text:#{quoted_term}"
17
- when 'cql.anyIndexes': "#{relation_prefix}text:#{quoted_term}"
18
- when 'cql.serverChoice': "#{relation_prefix}#{quoted_term}"
19
- else
20
- quoted_index.gsub!( /(dc|bath)\./, "" )
21
- "#{relation_prefix}#{quoted_index}:#{quoted_term}"
22
- end
23
-
70
+ # Default, raise not supported, will be implemented by specific
71
+ # classes where supported.
72
+ def to_solr
73
+ raise CqlException.new("#to_solr not supported for #{self.class}: #{self.to_cql}")
24
74
  end
25
75
  end
26
76
 
27
- # looks like we are just handling not equal now
28
- class CqlRelation
29
- def to_solr
30
- ms = @modifier_set.to_solr
31
- if ms == " <> "
32
- return "-"
33
- end
34
- ""
35
- end
36
- end
77
+
37
78
 
38
79
  class CqlTermNode
39
- # def to_solr
40
- # "arghh"
41
- # end
80
+ def to_solr
81
+ relation = @relation.modifier_set.base
82
+
83
+ relation = CqlRuby.to_solr_defaults[:default_relation] if relation == "="
84
+ # If no prefix to relation, normalize to "cql"
85
+ relation = "cql.#{relation}" unless relation.index(".") || ["<>", "<=", ">=", "<", ">", "=", "=="].include?(relation)
86
+
87
+
88
+ # What's our default index for server choice indexes? Let's call it
89
+ # "text".
90
+ # Otherwise, remove the namespace/"context set" prefix.
91
+ solr_field = case @index.downcase
92
+ when "cql.anyindexes", "cql.serverchoice", "cql.keywords"
93
+ CqlRuby.to_solr_defaults[:default_index]
94
+ when "cql.allindexes"
95
+ CqlRuby.to_solr_defaults[:all_index]
96
+ else
97
+ @index.gsub(/^[^.]*\./, "")
98
+ end
99
+
100
+ raise CqlException.new("resultSet not supported") if @index.downcase == "cql.resultsetid"
101
+ raise CqlException.new("relation modifiers not supported: #{@relation.modifier_set.to_cql}") if @relation.modifier_set.modifiers.length > 0
102
+
103
+ if index.downcase == "cql.allrecords"
104
+ #WARNING: Not sure if this will actually always work as intended, its
105
+ # a bit odd.
106
+ return "[* TO *]"
107
+ end
108
+
109
+
110
+ negate = false
111
+
112
+ value =
113
+ case relation
114
+ # WARNING: Depending on how you've tokenized, <> and == semantics
115
+ # may not be fully respected. For typical solr fields, will
116
+ # match/exclude on partial matches too, not only complete matches.
117
+ when "<>"
118
+ negate = true
119
+ maybe_quote(@term)
120
+ when "cql.adj", "==": maybe_quote(@term)
121
+ when "cql.all": '(' + @term.split(/\s/).collect{|a| '+'+a}.join(" ") + ')'
122
+ when "cql.any": '(' + @term.split(/\s/).join(" OR ") + ')'
123
+ when ">=": "[" + maybe_quote(@term) + " TO *]"
124
+ when ">": "{" + maybe_quote(@term) + " TO *}"
125
+ when "<=": "[* TO " + maybe_quote(@term) + "]"
126
+ when "<": "{* TO " + maybe_quote(@term) + "}"
127
+ when "cql.within"
128
+ bounds = @term.gsub('"', "").split(/\s/)
129
+ raise CqlException.new("can not extract two bounding values from within relation term: #{@term}") unless bounds.length == 2
130
+
131
+ "[" + maybe_quote(bounds[0]) + " TO " + maybe_quote(bounds[1]) + "]"
132
+ else
133
+ raise CqlException.new("relation not supported: #{relation}")
134
+ end
135
+
136
+ ret = ""
137
+ ret += "-" if negate
138
+ ret += "#{solr_field}:" if solr_field
139
+ ret += value
140
+
141
+ return ret
142
+ end
42
143
  end
43
144
 
44
145
  class CqlBooleanNode
45
146
  def to_solr
46
- "(#{@left_node.to_solr})#{@modifier_set.to_solr}(#{@right_node.to_solr})"
147
+ "(#{@left_node.to_solr} #{@modifier_set.to_solr} #{@right_node.to_solr})"
47
148
  end
48
149
  end
49
150
 
50
151
  class ModifierSet
51
152
  def to_solr
52
- raise CqlException, "PROX not supported" if @base.upcase == "prox"
53
- " #{@base.upcase} "
153
+ raise CqlException.new("#to_solr not supported for PROX operator") if @base.upcase == "PROX"
154
+ raise CqlException.new("#to_solr does not support boolean modifiers: #{to_cql}") if @modifiers.length != 0
155
+
156
+ "#{@base.upcase}"
54
157
  end
55
158
  end
56
159
 
57
- end
160
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'cql_ruby'
7
+
8
+ class Test::Unit::TestCase
9
+ end
@@ -14,8 +14,8 @@ class TestCqlGenerator < Test::Unit::TestCase
14
14
  puts tree.to_cql
15
15
  new_tree = parser.parse( tree.to_cql )
16
16
  assert( new_tree )
17
- assert( new_tree.to_solr )
18
- puts new_tree.to_solr
17
+ #assert( new_tree.to_solr )
18
+ #puts new_tree.to_solr
19
19
  # puts tree.to_xcql
20
20
  end
21
21
  puts "done"
@@ -2,20 +2,109 @@ require 'test/unit'
2
2
  require File.dirname(__FILE__) + '/../lib/cql_ruby'
3
3
 
4
4
  class CqlToSolrTest < Test::Unit::TestCase
5
- def test_cql_to_solr
6
- parser = CqlRuby::CqlParser.new
7
- tree = parser.parse( "dc.title = dog" )
8
- assert_equal( "title:dog", tree.to_solr )
9
- tree = parser.parse( "cql.resultSetId = dog" )
10
- assert_raises( CqlRuby::CqlException ) { tree.to_solr }
11
- tree = parser.parse( "cql.allIndexes = dog" )
12
- assert_equal( "text:dog", tree.to_solr )
13
- tree = parser.parse( "dog AND cat" )
14
- assert_equal( "(dog) AND (cat)", tree.to_solr )
15
- tree = parser.parse( "dog and cat" )
16
- assert_equal( "(dog) AND (cat)", tree.to_solr )
17
- tree = parser.parse( "dc.title <> dog" )
18
- assert_equal( "-title:dog", tree.to_solr )
19
-
5
+ @@parser = CqlRuby::CqlParser.new
6
+
7
+ def test_boolean
8
+ assert_to_solr_eq("dog or cat and mammal", '((dog OR cat) AND mammal)')
9
+ assert_to_solr_eq("dog or (cat and mammal)", '(dog OR (cat AND mammal))')
10
+ assert_to_solr_eq('dog not cat', "(dog NOT cat)")
11
+ end
12
+
13
+ def test_unsupported_cql
14
+ assert_can_not_to_solr("cql.resultSetId = dog")
15
+ assert_can_not_to_solr("field = value PROX field2 = value2")
16
+ assert_can_not_to_solr("something cql.encloses 2000")
17
+ assert_can_not_to_solr("field = dog sortBy someField")
18
+ assert_can_not_to_solr("field unknownrelation value")
19
+
20
+ assert_can_not_to_solr("cat or/rel.combine=sum dog")
21
+ assert_can_not_to_solr("title any/relevant fish")
22
+
23
+ assert_can_not_to_solr('> dc = "http://deepcustard.org/" dc.custardDepth > 10')
24
+ end
25
+
26
+ def test_rel_adj
27
+ assert_to_solr_eq('column cql.adj "one two three"', 'column:"one two three"')
28
+ end
29
+
30
+ def test_rel_eq
31
+ # '==' is same as 'adj', best we can do
32
+ assert_to_solr_eq('column == "one two three"', @@parser.parse('column adj "one two three"').to_solr)
33
+ end
34
+
35
+ def test_rel_any
36
+ assert_to_solr_eq('column cql.any "one two three"', 'column:(one OR two OR three)')
37
+ end
38
+
39
+ def test_rel_all
40
+ assert_to_solr_eq('column cql.all "one two three"', 'column:(+one +two +three)')
41
+ end
42
+
43
+ def test_rel_not
44
+ # Depending on solr schema, this will really map to "does not include phrase", not "does not exactly equal", best we can do.
45
+ assert_to_solr_eq('column <> "one two three"', '-column:"one two three"')
46
+ end
47
+
48
+ def test_rel_default
49
+ # '=' defaults to adj
50
+ assert_to_solr_eq('column = value', @@parser.parse("column adj value").to_solr)
51
+
52
+ # unless we set it otherwise
53
+ with_cql_default(:default_relation, "any") do
54
+ assert_to_solr_eq('column = value', @@parser.parse("column any value").to_solr)
55
+ end
56
+ end
57
+
58
+ def test_range
59
+ assert_to_solr_eq('column > 100', 'column:{100 TO *}')
60
+ assert_to_solr_eq('column < 100', 'column:{* TO 100}')
61
+ assert_to_solr_eq('column >= 100', 'column:[100 TO *]')
62
+ assert_to_solr_eq('column <= 100', 'column:[* TO 100]')
63
+ assert_to_solr_eq('column cql.within "100 200"', 'column:[100 TO 200]')
64
+ end
65
+
66
+ def test_drop_index_prefix
67
+ assert_to_solr_eq("dc.title = frog", @@parser.parse("title = frog").to_solr)
68
+ end
69
+
70
+ def test_specified_default_index
71
+ with_cql_default(:default_index, "default_index") do
72
+ ["cql.anyindexes", "cql.serverchoice", "cql.keywords"].each do |index|
73
+ assert_to_solr_eq("#{index} = val", @@parser.parse("default_index = val").to_solr)
74
+ end
75
+ end
76
+ end
77
+
78
+ def test_all_index
79
+ assert_to_solr_eq("cql.allindexes = val", @@parser.parse("text = val").to_solr)
80
+
81
+ with_cql_default(:all_index, "my_all_index") do
82
+ assert_to_solr_eq("cql.allindexes = val", @@parser.parse("my_all_index = val").to_solr)
83
+ end
84
+ end
85
+
86
+ #############
87
+ # Helpers
88
+ ##############
89
+
90
+ def assert_to_solr_eq(cql, should_solr)
91
+ solr = @@parser.parse(cql).to_solr
92
+ assert_equal(should_solr, solr)
93
+ end
94
+
95
+ def assert_can_not_to_solr(string)
96
+ assert_raises(CqlRuby::CqlException) do
97
+ CqlRuby::CqlParser.new.parse(string).to_solr
98
+ end
99
+ end
100
+
101
+ def with_cql_default(key, value)
102
+ old_value = CqlRuby.to_solr_defaults[key]
103
+ CqlRuby.to_solr_defaults[key] = value
104
+ begin
105
+ yield
106
+ ensure
107
+ CqlRuby.to_solr_defaults[key] = old_value
108
+ end
20
109
  end
21
110
  end
metadata CHANGED
@@ -1,58 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cql-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ hash: 63
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 8
9
+ - 0
10
+ version: 0.8.0
5
11
  platform: ruby
6
12
  authors:
13
+ - Jonathan Rochkind
7
14
  - Chick Markley
8
15
  autorequire:
9
16
  bindir: bin
10
17
  cert_chain: []
11
18
 
12
- date: 2008-04-10 00:00:00 -07:00
19
+ date: 2010-06-14 00:00:00 -07:00
13
20
  default_executable:
14
21
  dependencies: []
15
22
 
16
- description: CQL parser for Ruby
17
- email:
18
- - chick@qrhino.com
23
+ description: " CQL Parser, with serialization from cql node tree to cql, xcql, and solr query"
24
+ email: cql_ruby@googlegroups.com
19
25
  executables: []
20
26
 
21
27
  extensions: []
22
28
 
23
29
  extra_rdoc_files:
24
- - History.txt
25
- - License.txt
26
- - Manifest.txt
27
30
  - README.txt
28
- - test/fixtures/sample_queries.txt
29
- - website/index.txt
30
31
  files:
31
- - History.txt
32
- - License.txt
33
- - Manifest.txt
34
- - README.txt
35
- - Rakefile
36
- - config/hoe.rb
37
- - config/requirements.rb
38
32
  - lib/cql_ruby.rb
39
- - lib/cql_ruby/version.rb
40
33
  - lib/cql_ruby/cql_generator.rb
41
34
  - lib/cql_ruby/cql_lexer.rb
42
35
  - lib/cql_ruby/cql_nodes.rb
43
36
  - lib/cql_ruby/cql_parser.rb
44
37
  - lib/cql_ruby/cql_to_solr.rb
45
- - log/debug.log
46
- - script/console
47
- - script/destroy
48
- - script/generate
49
- - script/txt2html
50
- - setup.rb
51
- - tasks/deployment.rake
52
- - tasks/environment.rake
53
- - tasks/website.rake
54
- - test/fixtures
55
- - test/fixtures/sample_queries.txt
38
+ - lib/cql_ruby/version.rb
39
+ - README.txt
40
+ - test/helper.rb
56
41
  - test/test_cql_generator.rb
57
42
  - test/test_cql_lexer.rb
58
43
  - test/test_cql_nodes.rb
@@ -60,39 +45,42 @@ files:
60
45
  - test/test_cql_ruby.rb
61
46
  - test/test_cql_to_solr.rb
62
47
  - test/test_helper.rb
63
- - website/index.html
64
- - website/index.txt
65
- - website/javascripts/rounded_corners_lite.inc.js
66
- - website/stylesheets/screen.css
67
- - website/template.html.erb
68
48
  has_rdoc: true
69
- homepage: http://cql-ruby.rubyforge.org
49
+ homepage: http://cql-ruby.rubyforge.org/
50
+ licenses: []
51
+
70
52
  post_install_message:
71
53
  rdoc_options:
72
- - --main
73
- - README.txt
54
+ - --charset=UTF-8
74
55
  require_paths:
75
56
  - lib
76
57
  required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
77
59
  requirements:
78
60
  - - ">="
79
61
  - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
80
65
  version: "0"
81
- version:
82
66
  required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
83
68
  requirements:
84
69
  - - ">="
85
70
  - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
86
74
  version: "0"
87
- version:
88
75
  requirements: []
89
76
 
90
77
  rubyforge_project: cql-ruby
91
- rubygems_version: 1.0.1
78
+ rubygems_version: 1.3.7
92
79
  signing_key:
93
- specification_version: 2
94
- summary: CQL parser for Ruby
80
+ specification_version: 3
81
+ summary: CQL Parser
95
82
  test_files:
83
+ - test/helper.rb
96
84
  - test/test_cql_generator.rb
97
85
  - test/test_cql_lexer.rb
98
86
  - test/test_cql_nodes.rb