line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Catmandu::Store::Solr::CQL; #TODO see Catmandu::Store::ElasticSearch::CQL |
2
|
|
|
|
|
|
|
|
3
|
3
|
|
|
3
|
|
13
|
use Catmandu::Sane; |
|
3
|
|
|
|
|
3
|
|
|
3
|
|
|
|
|
21
|
|
4
|
3
|
|
|
3
|
|
1849
|
use CQL::Parser; |
|
3
|
|
|
|
|
52027
|
|
|
3
|
|
|
|
|
97
|
|
5
|
3
|
|
|
3
|
|
38
|
use Carp qw(confess); |
|
3
|
|
|
|
|
4
|
|
|
3
|
|
|
|
|
138
|
|
6
|
3
|
|
|
3
|
|
12
|
use Catmandu::Util qw(:is require_package); |
|
3
|
|
|
|
|
20
|
|
|
3
|
|
|
|
|
907
|
|
7
|
3
|
|
|
3
|
|
15
|
use Moo; |
|
3
|
|
|
|
|
7
|
|
|
3
|
|
|
|
|
28
|
|
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
with 'Catmandu::Logger'; |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
our $VERSION = "0.0302"; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
has parser => (is => 'ro', lazy => 1, builder => '_build_parser'); |
14
|
|
|
|
|
|
|
has mapping => (is => 'ro'); |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
my $any_field = qr'^(srw|cql)\.(serverChoice|anywhere)$'i; |
17
|
|
|
|
|
|
|
my $match_all = qr'^(srw|cql)\.allRecords$'i; |
18
|
|
|
|
|
|
|
my $distance_modifier = qr'\s*\/\s*distance\s*<\s*(\d+)'i; |
19
|
|
|
|
|
|
|
my $reserved = qr'[\+\-\&\|\!\(\)\{\}\[\]\^\"\~\*\?\:\\]'; |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
sub _build_parser { |
22
|
0
|
|
|
0
|
|
|
CQL::Parser->new; |
23
|
|
|
|
|
|
|
} |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
sub parse { |
26
|
0
|
|
|
0
|
1
|
|
my ($self, $query) = @_; |
27
|
0
|
|
|
|
|
|
$self->log->debug("cql query: $query"); |
28
|
|
|
|
|
|
|
my $node = eval { |
29
|
0
|
|
|
|
|
|
$self->parser->parse($query) |
30
|
0
|
0
|
|
|
|
|
} or do { |
31
|
0
|
|
|
|
|
|
my $error = $@; |
32
|
0
|
|
|
|
|
|
$self->log->error("cql error: $error"); |
33
|
0
|
|
|
|
|
|
die "cql error: $error"; |
34
|
|
|
|
|
|
|
}; |
35
|
0
|
|
|
|
|
|
my $solr_query = $self->visit($node); |
36
|
0
|
|
|
|
|
|
$self->log->debug("solr query: $solr_query"); |
37
|
0
|
|
|
|
|
|
$solr_query; |
38
|
|
|
|
|
|
|
} |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
sub escape_term { |
41
|
0
|
|
|
0
|
0
|
|
my $term = $_[0]; |
42
|
0
|
|
|
|
|
|
$term =~ s/($reserved)/\\$1/g; |
43
|
0
|
|
|
|
|
|
$term; |
44
|
|
|
|
|
|
|
} |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
sub quote_term { |
47
|
0
|
|
|
0
|
0
|
|
my $term = $_[0]; |
48
|
0
|
0
|
|
|
|
|
$term = qq("$term") if $term =~ /\s/; |
49
|
0
|
|
|
|
|
|
$term; |
50
|
|
|
|
|
|
|
} |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
sub visit { |
53
|
0
|
|
|
0
|
1
|
|
my ($self, $node) = @_; |
54
|
|
|
|
|
|
|
|
55
|
0
|
|
|
|
|
|
my $mapping = $self->mapping; |
56
|
0
|
0
|
|
|
|
|
my $indexes = $mapping ? $mapping->{indexes} : undef; |
57
|
|
|
|
|
|
|
|
58
|
0
|
0
|
|
|
|
|
if ($node->isa('CQL::TermNode')) { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
59
|
|
|
|
|
|
|
|
60
|
0
|
|
|
|
|
|
my $term = escape_term($node->getTerm); |
61
|
|
|
|
|
|
|
|
62
|
0
|
0
|
|
|
|
|
if ($term =~ $match_all) { |
63
|
0
|
|
|
|
|
|
return "*:*"; |
64
|
|
|
|
|
|
|
} |
65
|
|
|
|
|
|
|
|
66
|
0
|
|
|
|
|
|
my $qualifier = $node->getQualifier; |
67
|
0
|
|
|
|
|
|
my $relation = $node->getRelation; |
68
|
0
|
|
|
|
|
|
my @modifiers = $relation->getModifiers; |
69
|
0
|
|
|
|
|
|
my $base = lc $relation->getBase; |
70
|
|
|
|
|
|
|
|
71
|
0
|
0
|
|
|
|
|
if ($base eq 'scr') { |
72
|
0
|
0
|
0
|
|
|
|
if ($mapping && $mapping->{default_relation}) { |
73
|
0
|
|
|
|
|
|
$base = $mapping->{default_relation}; |
74
|
|
|
|
|
|
|
} else { |
75
|
0
|
|
|
|
|
|
$base = '='; |
76
|
|
|
|
|
|
|
} |
77
|
|
|
|
|
|
|
} |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
#default field |
80
|
0
|
0
|
|
|
|
|
if ($qualifier =~ $any_field) { |
81
|
|
|
|
|
|
|
#set default field explicitely |
82
|
0
|
0
|
0
|
|
|
|
if ( $mapping && $mapping->{default_index} ) { |
83
|
0
|
|
|
|
|
|
$qualifier = $mapping->{default_index}; |
84
|
|
|
|
|
|
|
} |
85
|
|
|
|
|
|
|
#make solr decide what the default field should be |
86
|
|
|
|
|
|
|
else { |
87
|
0
|
|
|
|
|
|
$qualifier = ""; |
88
|
|
|
|
|
|
|
} |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
#field search: new way |
92
|
0
|
0
|
|
|
|
|
if( $indexes ) { |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
#empty qualifier: Solr should decide how to query |
95
|
0
|
0
|
|
|
|
|
if ( is_string( $qualifier ) ) { |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
#change qualifier |
98
|
0
|
|
|
|
|
|
$qualifier = lc $qualifier; |
99
|
0
|
|
|
|
|
|
my $old_qualifier = $qualifier; |
100
|
0
|
0
|
|
|
|
|
$qualifier =~ s/(?<=[^_])_(?=[^_])//g if $mapping->{strip_separating_underscores}; |
101
|
0
|
0
|
|
|
|
|
unless($qualifier eq $old_qualifier){ |
102
|
0
|
|
|
|
|
|
$self->log->debug("value of qualifier '$old_qualifier' reset to '$qualifier' because of setting 'strip_separating_underscores'"); |
103
|
|
|
|
|
|
|
} |
104
|
0
|
0
|
|
|
|
|
my $q_mapping = $indexes->{$qualifier} or confess "cql error: unknown index $qualifier"; |
105
|
0
|
0
|
|
|
|
|
$q_mapping->{op}->{$base} or confess "cql error: relation $base not allowed"; |
106
|
|
|
|
|
|
|
|
107
|
0
|
|
|
|
|
|
my $op = $q_mapping->{op}->{$base}; |
108
|
|
|
|
|
|
|
|
109
|
0
|
|
|
|
|
|
$old_qualifier = $qualifier; |
110
|
0
|
0
|
0
|
|
|
|
if (ref $op && $op->{field}) { |
|
|
0
|
|
|
|
|
|
111
|
|
|
|
|
|
|
|
112
|
0
|
|
|
|
|
|
$qualifier = $op->{field}; |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
} elsif ($q_mapping->{field}) { |
115
|
|
|
|
|
|
|
|
116
|
0
|
|
|
|
|
|
$qualifier = $q_mapping->{field}; |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
0
|
0
|
|
|
|
|
unless($qualifier eq $old_qualifier){ |
121
|
0
|
|
|
|
|
|
$self->log->debug("value of qualifier '$old_qualifier' reset to '$qualifier' because of field mapping"); |
122
|
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
#add solr ':' |
125
|
0
|
|
|
|
|
|
$qualifier = "$qualifier:"; |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
#change term using filters |
128
|
0
|
|
|
|
|
|
my $filters; |
129
|
0
|
0
|
0
|
|
|
|
if (ref $op && $op->{filter}) { |
|
|
0
|
|
|
|
|
|
130
|
|
|
|
|
|
|
|
131
|
0
|
|
|
|
|
|
$filters = $op->{filter}; |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
} elsif ($q_mapping->{filter}) { |
134
|
|
|
|
|
|
|
|
135
|
0
|
|
|
|
|
|
$filters = $q_mapping->{filter}; |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
} |
138
|
0
|
0
|
|
|
|
|
if ($filters) { |
139
|
0
|
|
|
|
|
|
for my $filter (@$filters) { |
140
|
0
|
0
|
|
|
|
|
if ($filter eq 'lowercase') { |
141
|
0
|
|
|
|
|
|
$self->log->debug("term '$term' filtered to lowercase"); |
142
|
0
|
|
|
|
|
|
$term = lc $term; |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
} |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
#change term using callbacks |
148
|
0
|
0
|
0
|
|
|
|
if (ref $op && $op->{cb}) { |
|
|
0
|
|
|
|
|
|
149
|
0
|
|
|
|
|
|
my ($pkg, $sub) = @{$op->{cb}}; |
|
0
|
|
|
|
|
|
|
150
|
0
|
|
|
|
|
|
$self->log->debug("term '$term' changed to ${pkg}->${sub}"); |
151
|
0
|
|
|
|
|
|
$term = require_package($pkg)->$sub($term); |
152
|
|
|
|
|
|
|
} elsif ($q_mapping->{cb}) { |
153
|
0
|
|
|
|
|
|
my ($pkg, $sub) = @{$q_mapping->{cb}}; |
|
0
|
|
|
|
|
|
|
154
|
0
|
|
|
|
|
|
$self->log->debug("term '$term' changed to ${pkg}->${sub}"); |
155
|
0
|
|
|
|
|
|
$term = require_package($pkg)->$sub($term); |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
} |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
#field search: old way |
162
|
|
|
|
|
|
|
else { |
163
|
|
|
|
|
|
|
#add solr ':' |
164
|
0
|
0
|
|
|
|
|
$qualifier = "$qualifier:" if is_string $qualifier; |
165
|
|
|
|
|
|
|
} |
166
|
|
|
|
|
|
|
|
167
|
0
|
0
|
0
|
|
|
|
if ($base eq '=' or $base eq 'scr') { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
168
|
0
|
|
|
|
|
|
$term = quote_term($term); |
169
|
0
|
|
|
|
|
|
for my $m (@modifiers) { |
170
|
0
|
0
|
|
|
|
|
if ($m->[1] eq 'fuzzy') { |
171
|
0
|
|
|
|
|
|
return "$qualifier$term~"; |
172
|
|
|
|
|
|
|
} |
173
|
|
|
|
|
|
|
} |
174
|
0
|
|
|
|
|
|
return $qualifier.$term; |
175
|
|
|
|
|
|
|
} elsif ($base eq '<') { |
176
|
0
|
|
|
|
|
|
$term = quote_term($term); |
177
|
0
|
|
|
|
|
|
return $qualifier."{* TO $term}"; |
178
|
|
|
|
|
|
|
} elsif ($base eq '>') { |
179
|
0
|
|
|
|
|
|
$term = quote_term($term); |
180
|
0
|
|
|
|
|
|
return $qualifier."{$term TO *}"; |
181
|
|
|
|
|
|
|
} elsif ($base eq '<=') { |
182
|
0
|
|
|
|
|
|
$term = quote_term($term); |
183
|
0
|
|
|
|
|
|
return $qualifier."[* TO $term]"; |
184
|
|
|
|
|
|
|
} elsif ($base eq '>=') { |
185
|
0
|
|
|
|
|
|
$term = quote_term($term); |
186
|
0
|
|
|
|
|
|
return $qualifier."[$term TO *]"; |
187
|
|
|
|
|
|
|
} elsif ($base eq '<>') { |
188
|
0
|
|
|
|
|
|
$term = quote_term($term); |
189
|
0
|
|
|
|
|
|
return "-$qualifier$term"; |
190
|
|
|
|
|
|
|
} elsif ($base eq 'exact') { |
191
|
0
|
|
|
|
|
|
return $qualifier.quote_term($term); |
192
|
|
|
|
|
|
|
} elsif ($base eq 'all') { |
193
|
0
|
|
|
|
|
|
my @terms = split /\s+/, $term; |
194
|
0
|
0
|
|
|
|
|
if (@terms == 1) { |
195
|
0
|
|
|
|
|
|
return $qualifier.$term; |
196
|
|
|
|
|
|
|
} |
197
|
0
|
|
|
|
|
|
$term = join ' ', map { "+$_" } @terms; |
|
0
|
|
|
|
|
|
|
198
|
0
|
0
|
|
|
|
|
if ($qualifier) { |
199
|
0
|
|
|
|
|
|
return "$qualifier($term)"; |
200
|
|
|
|
|
|
|
} |
201
|
0
|
|
|
|
|
|
return $term; |
202
|
|
|
|
|
|
|
} elsif ($base eq 'any') { |
203
|
0
|
|
|
|
|
|
$term = join ' OR ', map { $qualifier.$_ } split /\s+/, $term; |
|
0
|
|
|
|
|
|
|
204
|
0
|
|
|
|
|
|
return "( $term)"; |
205
|
|
|
|
|
|
|
} elsif ($base eq 'within') { |
206
|
0
|
|
|
|
|
|
my @range = split /\s+/, $term; |
207
|
0
|
0
|
|
|
|
|
if (@range == 1) { |
208
|
0
|
|
|
|
|
|
return $qualifier.$term; |
209
|
|
|
|
|
|
|
} else { |
210
|
0
|
|
|
|
|
|
return $qualifier."[$range[0] TO $range[1]]"; |
211
|
|
|
|
|
|
|
} |
212
|
|
|
|
|
|
|
} else { |
213
|
0
|
|
|
|
|
|
return $qualifier.quote_term($term); |
214
|
|
|
|
|
|
|
} |
215
|
|
|
|
|
|
|
} |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
#TODO: apply cql_mapping |
218
|
|
|
|
|
|
|
elsif ($node->isa('CQL::ProxNode')) { |
219
|
0
|
|
|
|
|
|
my $distance = 1; |
220
|
0
|
|
|
|
|
|
my $qualifier = $node->left->getQualifier; |
221
|
0
|
|
|
|
|
|
my $term = escape_term(join(' ', $node->left->getTerm, $node->right->getTerm)); |
222
|
|
|
|
|
|
|
|
223
|
0
|
0
|
|
|
|
|
if (my ($n) = $node->op =~ $distance_modifier) { |
224
|
0
|
0
|
|
|
|
|
$distance = $n if $n > 1; |
225
|
|
|
|
|
|
|
} |
226
|
0
|
0
|
|
|
|
|
if ($qualifier =~ $any_field) { |
227
|
0
|
|
|
|
|
|
return qq("$term"~$distance); |
228
|
|
|
|
|
|
|
} else { |
229
|
0
|
|
|
|
|
|
return qq($qualifier:"$term"~$distance); |
230
|
|
|
|
|
|
|
} |
231
|
|
|
|
|
|
|
} |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
elsif ($node->isa('CQL::BooleanNode')) { |
234
|
0
|
|
|
|
|
|
my $lft = $node->left; |
235
|
0
|
|
|
|
|
|
my $rgt = $node->right; |
236
|
0
|
|
|
|
|
|
my $lft_q = $self->visit($lft); |
237
|
0
|
|
|
|
|
|
my $rgt_q = $self->visit($rgt); |
238
|
0
|
0
|
0
|
|
|
|
$lft_q = "( $lft_q)" unless $lft->isa('CQL::TermNode') || $lft->isa('CQL::ProxNode'); |
239
|
0
|
0
|
0
|
|
|
|
$rgt_q = "( $rgt_q)" unless $rgt->isa('CQL::TermNode') || $rgt->isa('CQL::ProxNode'); |
240
|
|
|
|
|
|
|
|
241
|
0
|
|
|
|
|
|
return join ' ', $lft_q, uc $node->op, $rgt_q; |
242
|
|
|
|
|
|
|
} |
243
|
|
|
|
|
|
|
} |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
1; |
246
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
=head1 NAME |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
Catmandu::Store::Solr::CQL - Converts a CQL query string to a Solr query string |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
=head1 SYNOPSIS |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
$solr_query_string = Catmandu::Store::Solr::CQL->parse($cql_query_string); |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
=head1 DESCRIPTION |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
This package currently parses most of CQL 1.1: |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
and |
260
|
|
|
|
|
|
|
or |
261
|
|
|
|
|
|
|
not |
262
|
|
|
|
|
|
|
prox |
263
|
|
|
|
|
|
|
prox/distance<$n |
264
|
|
|
|
|
|
|
srw.allRecords |
265
|
|
|
|
|
|
|
srw.serverChoice |
266
|
|
|
|
|
|
|
srw.anywhere |
267
|
|
|
|
|
|
|
cql.allRecords |
268
|
|
|
|
|
|
|
cql.serverChoice |
269
|
|
|
|
|
|
|
cql.anywhere |
270
|
|
|
|
|
|
|
= |
271
|
|
|
|
|
|
|
scr |
272
|
|
|
|
|
|
|
=/fuzzy |
273
|
|
|
|
|
|
|
scr/fuzzy |
274
|
|
|
|
|
|
|
< |
275
|
|
|
|
|
|
|
> |
276
|
|
|
|
|
|
|
<= |
277
|
|
|
|
|
|
|
>= |
278
|
|
|
|
|
|
|
<> |
279
|
|
|
|
|
|
|
exact |
280
|
|
|
|
|
|
|
all |
281
|
|
|
|
|
|
|
any |
282
|
|
|
|
|
|
|
within |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
=head1 METHODS |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
=head2 parse |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
Parses the given CQL query string with L<CQL::Parser> and converts it to a Solr query string. |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
=head2 visit |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
Converts the given L<CQL::Node> to a Solr query string. |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
=head1 TODO |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
support cql 1.2, more modifiers (esp. masked), sortBy, encloses |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
=head1 SEE ALSO |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
L<CQL::Parser>. |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=cut |