File Coverage

blib/lib/Catmandu/Store/Solr/CQL.pm
Criterion Covered Total %
statement 15 135 11.1
branch 0 92 0.0
condition 0 24 0.0
subroutine 5 10 50.0
pod 2 4 50.0
total 22 265 8.3


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