cql-ruby 0.7.1 → 0.8.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.
@@ -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