File Coverage

blib/lib/Catmandu/Store/ElasticSearch/CQL.pm
Criterion Covered Total %
statement 15 190 7.8
branch 0 124 0.0
condition 0 18 0.0
subroutine 5 12 41.6
pod 0 2 0.0
total 20 346 5.7


line stmt bran cond sub pod time code
1             package Catmandu::Store::ElasticSearch::CQL;
2              
3 1     1   7 use Catmandu::Sane;
  1         2  
  1         20  
4              
5             our $VERSION = '1.01';
6              
7 1     1   183 use Catmandu::Util qw(require_package trim);
  1         2  
  1         72  
8 1     1   502 use CQL::Parser;
  1         27081  
  1         32  
9 1     1   8 use Moo;
  1         3  
  1         976  
10 1     1   332 use namespace::clean;
  1         36  
  1         7  
11              
12             has parser => (is => 'ro', lazy => 1, builder => '_build_parser');
13             has mapping => (is => 'ro', required => 1);
14             has id_key => (is => 'ro', required => 1);
15              
16             my $RE_ANY_FIELD = qr'^(srw|cql)\.(serverChoice|anywhere)$'i;
17             my $RE_MATCH_ALL = qr'^(srw|cql)\.allRecords$'i;
18             my $RE_DISTANCE_MODIFIER = qr'\s*\/\s*distance\s*<\s*(\d+)'i;
19              
20             sub _build_parser {
21 0     0     CQL::Parser->new;
22             }
23              
24             sub parse {
25 0     0 0   my ($self, $query) = @_;
26 0 0         my $node = eval {$self->parser->parse($query);} or do {
  0            
27 0           my $error = $@;
28 0           die "cql error: $error";
29             };
30 0           $self->parse_node($node);
31             }
32              
33             sub parse_node {
34 0     0 0   my ($self, $node) = @_;
35              
36 0           my $query = {};
37              
38 0 0         unless ($node->isa('CQL::BooleanNode')) {
39 0 0         $node->isa('CQL::TermNode')
40             ? $self->_parse_term_node($node, $query)
41             : $self->_parse_prox_node($node, $query);
42 0           return $query;
43             }
44              
45 0           my @stack = ($node);
46 0           my @query_stack = (my $q = $query);
47              
48 0           while (@stack) {
49 0           $node = shift @stack;
50 0           $q = shift @query_stack;
51              
52 0 0         if ($node->isa('CQL::BooleanNode')) {
    0          
53 0           push @stack, $node->left, $node->right;
54 0           push @query_stack, my $left = {}, my $right = {};
55 0 0         if ($node->op eq 'and') {
    0          
56 0           $q->{bool} = {must => [$left, $right]};
57             }
58             elsif ($node->op eq 'or') {
59 0           $q->{bool} = {should => [$left, $right]};
60             }
61             else {
62             $q->{bool}
63 0           = {must => [$left, {bool => {must_not => [$right]}}]};
64             }
65             }
66             elsif ($node->isa('CQL::TermNode')) {
67 0           $self->_parse_term_node($node, $q);
68             }
69             else {
70 0           $self->_parse_prox_node($node, $q);
71             }
72             }
73              
74 0           $query;
75             }
76              
77             sub _parse_term_node {
78 0     0     my ($self, $node, $query) = @_;
79              
80 0           my $term = $node->getTerm;
81              
82 0 0         if ($term =~ $RE_MATCH_ALL) {
83 0           return {match_all => {}};
84             }
85              
86 0           my $qualifier = $node->getQualifier;
87 0           my $relation = $node->getRelation;
88 0           my @modifiers = $relation->getModifiers;
89 0           my $base = lc $relation->getBase;
90              
91 0 0         if ($base eq 'scr') {
92 0 0 0       if ($self->mapping and my $rel = $self->mapping->{default_relation}) {
93 0           $base = $rel;
94             }
95             else {
96 0           $base = '=';
97             }
98             }
99              
100 0 0         if ($qualifier =~ $RE_ANY_FIELD) {
101 0 0 0       if ($self->mapping and my $idx = $self->mapping->{default_index}) {
102 0           $qualifier = $idx;
103             }
104             else {
105 0           $qualifier = '_all';
106             }
107             }
108              
109 0           my $nested;
110              
111 0 0 0       if ($self->mapping and my $indexes = $self->mapping->{indexes}) {
112 0           $qualifier = lc $qualifier;
113             $qualifier =~ s/(?<=[^_])_(?=[^_])//g
114 0 0         if $self->mapping->{strip_separating_underscores};
115 0 0         my $mapping = $indexes->{$qualifier}
116             or Catmandu::Error->throw("cql error: unknown index $qualifier");
117 0 0         $mapping->{op}{$base}
118             or
119             Catmandu::Error->throw("cql error: relation $base not allowed");
120 0           my $op = $mapping->{op}{$base};
121 0 0 0       if (ref $op && $op->{field}) {
    0          
122 0           $qualifier = $op->{field};
123             }
124             elsif ($mapping->{field}) {
125 0           $qualifier = $mapping->{field};
126             }
127              
128 0           my $filters;
129 0 0 0       if (ref $op && $op->{filter}) {
    0          
130 0           $filters = $op->{filter};
131             }
132             elsif ($mapping->{filter}) {
133 0           $filters = $mapping->{filter};
134             }
135 0 0         if ($filters) {
136 0           for my $filter (@$filters) {
137 0 0         if ($filter eq 'lowercase') {$term = lc $term;}
  0            
138             }
139             }
140 0 0 0       if (ref $op && $op->{cb}) {
    0          
141 0           my ($pkg, $sub) = @{$op->{cb}};
  0            
142 0           $term = require_package($pkg)->$sub($term);
143             }
144             elsif ($mapping->{cb}) {
145 0           my ($pkg, $sub) = @{$mapping->{cb}};
  0            
146 0           $term = require_package($pkg)->$sub($term);
147             }
148              
149 0           $nested = $mapping->{nested};
150             }
151              
152             # TODO just pass query around
153 0           my $es_node = $self->_term_node($base, $qualifier, $term, @modifiers);
154              
155 0 0         if ($nested) {
156 0 0         if ($nested->{query}) {
157 0           $es_node = {bool => {must => [$nested->{query}, $es_node,]}};
158             }
159 0           $es_node = {nested => {path => $nested->{path}, query => $es_node,}};
160             }
161              
162 0           for my $key (keys %$es_node) {
163 0           $query->{$key} = $es_node->{$key};
164             }
165              
166 0           $query;
167             }
168              
169             sub _parse_prox_node {
170 0     0     my ($self, $node, $query) = @_;
171              
172 0           my $slop = 0;
173 0           my $qualifier = $node->left->getQualifier;
174 0           my $term = join(' ', $node->left->getTerm, $node->right->getTerm);
175 0 0         if (my ($n) = $node->op =~ $RE_DISTANCE_MODIFIER) {
176 0 0         $slop = $n - 1 if $n > 1;
177             }
178 0 0         if ($qualifier =~ $RE_ANY_FIELD) {
179 0           $qualifier = '_all';
180             }
181              
182 0           $query->{match_phrase} = {$qualifier => {query => $term, slop => $slop}};
183             }
184              
185             sub _term_node {
186 0     0     my ($self, $base, $qualifier, $term, @modifiers) = @_;
187 0           my $q;
188 0 0         if ($base eq '=') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
189 0 0         if (ref $qualifier) {
190             return {
191             bool => {
192             should => [
193             map {
194 0 0         if ($_ eq $self->id_key) {
  0            
195 0           {ids => {values => [$term]}};
196             }
197             else {
198 0           $self->_text_node($_, $term, @modifiers);
199             }
200             } @$qualifier
201             ]
202             }
203             };
204             }
205             else {
206 0 0         if ($qualifier eq $self->id_key) {
207 0           return {ids => {values => [$term]}};
208             }
209 0           return $self->_text_node($qualifier, $term, @modifiers);
210             }
211             }
212             elsif ($base eq '<') {
213 0 0         if (ref $qualifier) {
214             return {
215             bool => {
216             should =>
217 0           [map {{range => {$_ => {lt => $term}}}} @$qualifier]
  0            
218             }
219             };
220             }
221             else {
222 0           return {range => {$qualifier => {lt => $term}}};
223             }
224             }
225             elsif ($base eq '>') {
226 0 0         if (ref $qualifier) {
227             return {
228             bool => {
229             should =>
230 0           [map {{range => {$_ => {gt => $term}}}} @$qualifier]
  0            
231             }
232             };
233             }
234             else {
235 0           return {range => {$qualifier => {gt => $term}}};
236             }
237             }
238             elsif ($base eq '<=') {
239 0 0         if (ref $qualifier) {
240             return {
241             bool => {
242             should =>
243 0           [map {{range => {$_ => {lte => $term}}}} @$qualifier]
  0            
244             }
245             };
246             }
247             else {
248 0           return {range => {$qualifier => {lte => $term}}};
249             }
250             }
251             elsif ($base eq '>=') {
252 0 0         if (ref $qualifier) {
253             return {
254             bool => {
255             should =>
256 0           [map {{range => {$_ => {gte => $term}}}} @$qualifier]
  0            
257             }
258             };
259             }
260             else {
261 0           return {range => {$qualifier => {gte => $term}}};
262             }
263             }
264             elsif ($base eq '<>') {
265 0 0         if (ref $qualifier) {
266             return {
267             bool => {
268             must_not => [
269             map {
270 0 0         if ($_ eq $self->id_key) {
  0            
271 0           {ids => {values => [$term]}};
272             }
273             else {
274 0           {match_phrase => {$_ => {query => $term}}};
275             }
276             } @$qualifier
277             ]
278             }
279             };
280             }
281             else {
282 0 0         if ($qualifier eq $self->id_key) {
283 0           return {bool => {must_not => [{ids => {values => [$term]}}]}};
284             }
285             return {
286 0           bool => {
287             must_not =>
288             [{match_phrase => {$qualifier => {query => $term}}}]
289             }
290             };
291             }
292             }
293             elsif ($base eq 'exact') {
294 0 0         if (ref $qualifier) {
295             return {
296             bool => {
297             should => [
298             map {
299 0 0         if ($_ eq $self->id_key) {
  0            
300 0           {ids => {values => [$term]}};
301             }
302             else {
303 0           {match_phrase => {$_ => {query => $term}}};
304             }
305             } @$qualifier
306             ]
307             }
308             };
309             }
310             else {
311 0 0         if ($qualifier eq $self->id_key) {
312 0           return {ids => {values => [$term]}};
313             }
314 0           return {match_phrase => {$qualifier => {query => $term}}};
315             }
316             }
317             elsif ($base eq 'any') {
318 0           $term = [split /\s+/, trim($term)];
319 0 0         if (ref $qualifier) {
320             return {
321             bool => {
322             should => [
323             map {
324 0           $q = $_;
  0            
325 0           map {$self->_text_node($q, $_)} @$term;
  0            
326             } @$qualifier
327             ]
328             }
329             };
330             }
331             else {
332 0 0         if ($qualifier eq $self->id_key) {
333 0           return {ids => {values => $term}};
334             }
335             return {
336             bool => {
337 0           should => [map {$self->_text_node($qualifier, $_)} @$term]
  0            
338             }
339             };
340             }
341             }
342             elsif ($base eq 'all') {
343 0           $term = [split /\s+/, trim($term)];
344 0 0         if (ref $qualifier) {
345             return {
346             bool => {
347             should => [
348             map {
349 0           $q = $_;
  0            
350             {
351             bool => {
352             must => [
353 0           map {$self->_text_node($q, $_)}
  0            
354             @$term
355             ]
356             }
357             };
358             } @$qualifier
359             ]
360             }
361             };
362             }
363             else {
364             return {
365             bool => {
366 0           must => [map {$self->_text_node($qualifier, $_)} @$term]
  0            
367             }
368             };
369             }
370             }
371             elsif ($base eq 'within') {
372 0           my @range = split /\s+/, $term;
373 0 0         if (@range == 1) {
374 0 0         if (ref $qualifier) {
375             return {
376             bool => {
377             should => [
378 0           map {{text => {$_ => {query => $term}}}}
  0            
379             @$qualifier
380             ]
381             }
382             };
383             }
384             else {
385 0           return {match => {$qualifier => {query => $term}}};
386             }
387             }
388 0 0         if (ref $qualifier) {
389             return {
390             bool => {
391             should => [
392             map {
393 0           {
394 0           range => {
395             $_ => {lte => $range[0], gte => $range[1]}
396             }
397             }
398             } @$qualifier
399             ]
400             }
401             };
402             }
403             else {
404             return {
405 0           range => {$qualifier => {lte => $range[0], gte => $range[1]}}
406             };
407             }
408             }
409              
410 0 0         if (ref $qualifier) {
411             return {
412             bool => {
413             should => [
414 0           map {$self->_text_node($_, $term, @modifiers);}
  0            
415             @$qualifier
416             ]
417             }
418             };
419             }
420             else {
421 0           return $self->_text_node($qualifier, $term, @modifiers);
422             }
423             }
424              
425             sub _text_node {
426 0     0     my ($self, $qualifier, $term, @modifiers) = @_;
427 0 0         if ($term =~ /[^\\][\*\?]/) {
428              
429             # escape spaces
430 0           $term =~ s/(?<!\\) /\\ /g;
431 0           $term =~ s/^ /\\ /;
432              
433             # escape colons
434 0           $term =~ s/(?<!\\):/\\:/g;
435 0           $term =~ s/^:/\\:/;
436 0           return {query_string => {query => qq|$qualifier:$term|}};
437             }
438              
439             # Unescape wildcards (when needed)...
440 0           $term =~ s{[\\]([\^\*\?])}{$1}g;
441 0           for my $m (@modifiers) {
442 0 0         if ($m->[1] eq 'fuzzy')
443             { # TODO only works for single terms, mapping fuzzy_factor
444 0           return {fuzzy =>
445             {$qualifier => {value => $term, max_expansions => 10}}
446             };
447             }
448             }
449 0 0         if ($term =~ /\s/) {
450 0           return {match_phrase => {$qualifier => {query => $term}}};
451             }
452 0           {match => {$qualifier => {query => $term}}};
453             }
454              
455             1;