File Coverage

blib/lib/Search/Query/Dialect/Lucy.pm
Criterion Covered Total %
statement 277 304 91.1
branch 164 200 82.0
condition 69 92 75.0
subroutine 26 26 100.0
pod 5 5 100.0
total 541 627 86.2


line stmt bran cond sub pod time code
1             package Search::Query::Dialect::Lucy;
2 4     4   617367 use Moo;
  4         16761  
  4         31  
3             extends 'Search::Query::Dialect::Native';
4 4     4   2473 use Carp;
  4         8  
  4         318  
5 4     4   733 use Data::Dump qw( dump );
  4         7801  
  4         229  
6 4     4   24 use Scalar::Util qw( blessed );
  4         13  
  4         205  
7 4     4   2685 use Search::Query::Field::Lucy;
  4         11  
  4         146  
8 4     4   3377 use Lucy::Search::ANDQuery;
  4         43497  
  4         183  
9 4     4   3164 use Lucy::Search::NoMatchQuery;
  4         502  
  4         135  
10 4     4   3838 use Lucy::Search::NOTQuery;
  4         436  
  4         126  
11 4     4   2504 use Lucy::Search::ORQuery;
  4         412  
  4         120  
12 4     4   2507 use Lucy::Search::PhraseQuery;
  4         408  
  4         115  
13 4     4   2429 use Lucy::Search::RangeQuery;
  4         427  
  4         123  
14 4     4   1975 use Lucy::Search::TermQuery;
  4         341  
  4         129  
15 4     4   3002 use LucyX::Search::ProximityQuery;
  4         462  
  4         132  
16 4     4   2850 use LucyX::Search::NOTWildcardQuery;
  4         21451  
  4         114  
17 4     4   27 use LucyX::Search::WildcardQuery;
  4         7  
  4         68  
18 4     4   2679 use LucyX::Search::NullTermQuery;
  4         12638  
  4         105  
19 4     4   27 use LucyX::Search::AnyTermQuery;
  4         9  
  4         65  
20              
21 4     4   19 use namespace::autoclean;
  4         7  
  4         45  
22              
23             our $VERSION = '0.202';
24              
25             has 'wildcard' => ( is => 'rw', default => sub {'*'} );
26             has 'fuzzify' => ( is => 'rw', default => sub {0} );
27             has 'ignore_order_in_proximity' => ( is => 'rw', default => sub {0} );
28             has 'allow_single_wildcards' => ( is => 'rw', default => sub {0} );
29              
30             =head1 NAME
31              
32             Search::Query::Dialect::Lucy - Lucy query dialect
33              
34             =head1 SYNOPSIS
35              
36             use Lucy;
37             use Search::Query;
38            
39             my ($idx, $query) = get_index_name_and_query();
40            
41             my $searcher = Lucy::Search::IndexSearcher->new( index => $idx );
42             my $schema = $searcher->get_schema();
43            
44             # build field mapping
45             my %fields;
46             for my $field_name ( @{ $schema->all_fields() } ) {
47             $fields{$field_name} = {
48             type => $schema->fetch_type($field_name),
49             analyzer => $schema->fetch_analyzer($field_name),
50             };
51             }
52            
53             my $query_parser = Search::Query->parser(
54             dialect => 'Lucy',
55             croak_on_error => 1,
56             default_field => 'foo', # applied to "bare" terms with no field
57             fields => \%fields
58             );
59            
60             my $parsed_query = $query_parser->parse($query);
61             my $lucy_query = $parsed_query->as_lucy_query();
62             my $hits = $searcher->hits( query => $lucy_query );
63              
64             =head1 DESCRIPTION
65              
66             Search::Query::Dialect::Lucy extends the Lucy::QueryParser syntax
67             to support wildcards, proximity and ranges, in addition to the standard
68             Search::Query features.
69              
70             =head1 METHODS
71              
72             This class is a subclass of Search::Query::Dialect. Only new or overridden
73             methods are documented here.
74              
75             =cut
76              
77             =head2 BUILD
78              
79             Sets Lucy-appropriate defaults.
80             Can take the following params, also available as standard attribute
81             methods.
82              
83             =over
84              
85             =item wildcard
86              
87             Default is '*'.
88              
89             =item allow_single_wildcards
90              
91             If true, terms like '*' and '?' are allowed as valid. If false,
92             the Parser will croak if any term consists solely of a wildcard.
93              
94             The default is false.
95              
96             =item fuzzify
97              
98             If true, a wildcard is automatically appended to each query term.
99              
100             =item ignore_order_in_proximity
101              
102             If true, the terms in a proximity query will be evaluated for
103             matches regardless of the order in which they appear. For example,
104             given a document excerpt like:
105              
106             foo bar bing
107              
108             and a query like:
109              
110             "bing foo"~5
111              
112             if ignore_order_in_proximity is true, the document would match.
113             If ignore_order_in_proximity is false (the default), the document would
114             not match.
115              
116             =back
117              
118             =cut
119              
120             sub BUILD {
121 207     207 1 1024 my $self = shift;
122              
123 207 100 100     926 if ( $self->{default_field} and !ref( $self->{default_field} ) ) {
124 3         11 $self->{default_field} = [ $self->{default_field} ];
125             }
126              
127 207         3658 return $self;
128             }
129              
130             =head2 stringify
131              
132             Returns the Query object as a normalized string.
133              
134             =cut
135              
136             my %op_map = (
137             '+' => ' AND ',
138             '' => ' OR ',
139             '-' => ' ',
140             );
141              
142             sub _get_clause_joiner {
143 178     178   212 my $self = shift;
144 178 100       610 if ( $self->parser->default_boolop eq '+' ) {
145 174         321 return 'AND';
146             }
147             else {
148 4         9 return 'OR';
149             }
150             }
151              
152             sub stringify {
153 119     119 1 21584 my $self = shift;
154 119   66     460 my $tree = shift || $self;
155              
156 119         517 my @q;
157 119         205 foreach my $prefix ( '+', '', '-' ) {
158 355         344 my @clauses;
159 355         567 my $joiner = $op_map{$prefix};
160 355 100       931 next unless exists $tree->{$prefix};
161 123         138 for my $clause ( @{ $tree->{$prefix} } ) {
  123         252  
162 163         374 push( @clauses, $self->stringify_clause( $clause, $prefix ) );
163             }
164 122 50       280 next if !@clauses;
165              
166 122 50       194 push @q, join( $joiner, grep { defined and length } @clauses );
  161         783  
167             }
168 118         257 my $clause_joiner = $self->_get_clause_joiner;
169 118         798 return join " $clause_joiner ", @q;
170             }
171              
172             sub _doctor_value {
173 178     178   254 my ( $self, $clause ) = @_;
174              
175 178         238 my $value = $clause->{value};
176              
177 178 100 100     754 if ( defined $value and $self->fuzzify ) {
178 8 100       24 $value .= '*' unless $value =~ m/[\*]/;
179             }
180              
181 178         382 return $value;
182             }
183              
184             =head2 stringify_clause( I, I )
185              
186             Called by stringify() to handle each Clause in the Query tree.
187              
188             =cut
189              
190             sub stringify_clause {
191 163     163 1 191 my $self = shift;
192 163         202 my $clause = shift;
193 163         187 my $prefix = shift;
194              
195             #warn '=' x 80;
196             #warn dump $clause;
197             #warn "prefix = '$prefix'";
198              
199 163 100       404 if ( $clause->{op} eq '()' ) {
200 33         85 my $str = $self->stringify( $clause->{value} );
201 33 100 66     99 if ( $clause->has_children and $clause->has_children == 1 ) {
202 17 100       1229 if ( $prefix eq '-' ) {
203 1         4 return "(NOT ($str))";
204             }
205             else {
206              
207             # try not to double up the () unnecessarily
208 16 100       45 if ( $str =~ m/^\(/ ) {
209 4         12 return $str;
210             }
211             else {
212 12         43 return "($str)";
213             }
214             }
215             }
216             else {
217 16 100       1457 if ( $prefix eq '-' ) {
218 2 50       6 if ( $str =~ m/^\(/ ) {
219 0         0 return "(NOT $str)";
220             }
221             else {
222 2         8 return "(NOT ($str))";
223             }
224             }
225             else {
226 14         155 return "($str)";
227             }
228             }
229             }
230              
231 130   100     504 my $quote = $clause->quote || '';
232 130   100     476 my $proximity = $clause->proximity || '';
233 130 100       274 if ($proximity) {
234 3         8 $proximity = '~' . $proximity;
235             }
236              
237             # make sure we have a field
238 130   100     615 my $default_field
239             = $self->default_field
240             || $self->parser->default_field
241             || undef; # not empty string or 0
242 130         130 my @fields;
243 130 100       298 if ( $clause->{field} ) {
    100          
244 96         228 @fields = ( $clause->{field} );
245             }
246             elsif ( defined $default_field ) {
247 20 50       42 if ( ref $default_field ) {
248 20         60 @fields = @$default_field;
249             }
250             else {
251 0         0 @fields = ($default_field);
252             }
253             }
254              
255             # what value
256             my $value
257             = ref $clause->{value}
258             ? $clause->{value}
259 130 100       374 : $self->_doctor_value($clause);
260              
261             # if we have no fields, then operator is ignored.
262 130 100       288 if ( !@fields ) {
263 14 50       285 $self->debug and warn "no fields for " . dump($clause);
264 14         628 my $str = qq/$quote$value$quote$proximity/;
265 14 100       63 return $prefix eq '-' ? ( 'NOT ' . $str ) : $str;
266             }
267              
268 116         1228 my $wildcard = $self->wildcard;
269              
270             # normalize operator
271 116   100     289 my $op = $clause->{op} || ":";
272 116         217 $op =~ s/=/:/g;
273 116 100       247 if ( $prefix eq '-' ) {
274 22 100       74 $op = '!' . $op unless $op =~ m/^!/;
275             }
276 116 100 100     864 if ( defined $value and $value =~ m/[\*\?]|\Q$wildcard/ ) {
277 23         57 $op =~ s/:/~/g;
278 23 100 66     104 if ( $value eq '*' or $value eq '?' ) {
279 1 50       6 if ( !$self->allow_single_wildcards ) {
280 1         6 croak "single wildcards are not allowed: $clause";
281             }
282             }
283             }
284              
285 115         123 my @buf;
286 115         177 NAME: for my $name (@fields) {
287 160         440 my $field = $self->get_field($name);
288              
289 160 50       2197 if ( defined $field->callback ) {
290 0         0 push( @buf, $field->callback->( $field, $op, $value ) );
291 0         0 next NAME;
292             }
293              
294 160 50       3125 ( $self->debug > 1 )
295             and warn "lucy string: "
296             . dump [ $name, $op, $prefix, $quote, $value, ];
297              
298 160 100       12446 if ( !defined $value ) {
299 4         7 $value = 'NULL';
300             }
301              
302             # invert fuzzy
303 160 100       621 if ( $op eq '!~' ) {
    100          
    100          
    100          
    100          
304 2 50       18 $value .= $wildcard unless $value =~ m/\Q$wildcard/;
305 2         10 push(
306             @buf,
307             join( '',
308             '(NOT ', $name, ':', qq/$quote$value$quote$proximity/,
309             ')' )
310             );
311             }
312              
313             # fuzzy
314             elsif ( $op eq '~' ) {
315 35 100       119 $value .= $wildcard unless $value =~ m/\Q$wildcard/;
316 35         117 push( @buf,
317             join( '', $name, ':', qq/$quote$value$quote$proximity/ ) );
318             }
319              
320             # invert
321             elsif ( $op eq '!:' ) {
322              
323             # double negative
324 34 100 100     1563 if ( $prefix eq '-' and $clause->{op} eq '!:' ) {
325 1         5 push @buf,
326             join( '', $name, ':', qq/$quote$value$quote$proximity/ );
327             }
328             else {
329 33         134 push(
330             @buf,
331             join( '',
332             '(NOT ', $name, ':', qq/$quote$value$quote$proximity/,
333             ')' )
334             );
335             }
336             }
337              
338             # range
339             elsif ( $op eq '..' ) {
340 5 50 33     31 if ( ref $value ne 'ARRAY' or @$value != 2 ) {
341 0         0 croak "range of values must be a 2-element ARRAY";
342             }
343              
344             push(
345 5         25 @buf,
346             join( '',
347             $name, ':', '(', $value->[0], '..', $value->[1], ')' )
348             );
349              
350             }
351              
352             # invert range
353             elsif ( $op eq '!..' ) {
354 6 50 33     37 if ( ref $value ne 'ARRAY' or @$value != 2 ) {
355 0         0 croak "range of values must be a 2-element ARRAY";
356             }
357              
358             push(
359 6         23 @buf,
360             join( '',
361             $name, '!:', '(', $value->[0], '..', $value->[1], ')' )
362             );
363             }
364              
365             # standard
366             else {
367 78         275 push( @buf,
368             join( '', $name, ':', qq/$quote$value$quote$proximity/ ) );
369             }
370             }
371 115 100       286 my $joiner = $prefix eq '-' ? ' AND ' : ' OR ';
372             return
373 115 100       704 ( scalar(@buf) > 1 ? '(' : '' )
    100          
374             . join( $joiner, @buf )
375             . ( scalar(@buf) > 1 ? ')' : '' );
376             }
377              
378             =head2 as_lucy_query
379              
380             Returns the Dialect object as a Lucy::Search::Query-based object.
381             The Dialect object is walked and converted to a
382             Lucy::Searcher-compatible tree.
383              
384             =cut
385              
386             my %lucy_class_map = (
387             '+' => 'AND',
388             '' => 'OR',
389             '-' => 'NOT',
390             );
391              
392             sub as_lucy_query {
393 60     60 1 2941 my $self = shift;
394 60   66     229 my $tree = shift || $self;
395              
396 60         254 my @q;
397 60         98 foreach my $prefix ( '+', '', '-' ) {
398 180         173 my @clauses;
399 180         253 my $joiner = $lucy_class_map{$prefix};
400 180 100       427 next unless exists $tree->{$prefix};
401 62         75 my $has_explicit_fields = 0;
402 62         74 for my $clause ( @{ $tree->{$prefix} } ) {
  62         125  
403 84         226 my @lucy_clauses = $self->_lucy_clause( $clause, $prefix );
404 84 50       217 if ( !@lucy_clauses ) {
405              
406             #warn "No lucy_clauses for $clause";
407 0         0 next;
408             }
409 84         106 push( @clauses, @lucy_clauses );
410 84 100       196 if ( defined $clause->{field} ) {
411 68         124 $has_explicit_fields++;
412             }
413             }
414 62 50       127 next if !@clauses;
415              
416 62         126 my $lucy_class = 'Lucy::Search::' . $joiner . 'Query';
417 62 100       133 my $lucy_param_name = $joiner eq 'NOT' ? 'negated_query' : 'children';
418 62         85 @clauses = grep {defined} @clauses;
  84         158  
419 62 100 100     164 if ( $prefix eq '-' and @clauses > 1 ) {
420 1         2 $lucy_class = 'Lucy::Search::ANDQuery';
421 1         2 $lucy_param_name = 'children';
422             }
423              
424             #warn "$lucy_class -> new( $lucy_param_name => " . dump \@clauses;
425             #warn "has_explicit_fields=$has_explicit_fields";
426              
427 62 100 66     144 if ( @clauses == 1 ) {
    50          
428 48 100 100     210 if ( $prefix eq '-'
      100        
429             and $has_explicit_fields
430             and !$clauses[0]->isa($lucy_class) )
431             {
432 1         10 push @q, $lucy_class->new( $lucy_param_name => $clauses[0] );
433             }
434             else {
435 47         109 push @q, $clauses[0];
436             }
437             }
438             elsif ( !$has_explicit_fields and $prefix eq '-' ) {
439              
440 0         0 warn "do not wrap \@clauses in a $lucy_class";
441 0         0 push @q, @clauses;
442              
443             }
444             else {
445 14         168 push @q, $lucy_class->new( $lucy_param_name => \@clauses );
446             }
447              
448             }
449              
450             # possible we end up with nothing if StopFilter applied
451 60 50       117 if ( !@q ) {
452 0         0 return ();
453             }
454              
455 60         118 my $clause_joiner = $self->_get_clause_joiner;
456 60         101 my $lucy_class_joiner = 'Lucy::Search::' . $clause_joiner . 'Query';
457              
458 60 100       242 return @q == 1
459             ? $q[0]
460             : $lucy_class_joiner->new( children => \@q );
461             }
462              
463             sub _lucy_clause {
464 84     84   96 my $self = shift;
465 84         96 my $clause = shift;
466 84         95 my $prefix = shift;
467              
468             #warn dump $clause;
469             #warn "prefix = '$prefix'";
470              
471 84 100       189 if ( $clause->{op} eq '()' ) {
472 18         64 return $self->as_lucy_query( $clause->{value} );
473             }
474              
475             # make sure we have a field
476 66   100     202 my $default_field = $self->default_field || $self->parser->default_field;
477 66         65 my @fields;
478 66 100       137 if ( $clause->{field} ) {
    50          
479 50         121 @fields = ( $clause->{field} );
480             }
481             elsif ( defined $default_field ) {
482 16 50       40 if ( ref $default_field ) {
483 16         44 @fields = @$default_field;
484             }
485             else {
486 0         0 @fields = ($default_field);
487             }
488             }
489              
490             # what value
491             my $value
492             = ref $clause->{value}
493             ? $clause->{value}
494 66 100       190 : $self->_doctor_value($clause);
495              
496             # if we have no fields, we can't proceed, because Lucy
497             # requires a field for every term.
498 66 50       133 if ( !@fields ) {
499 0         0 croak
500             "No field specified for term '$value' -- set a default_field in Parser or Dialect";
501             }
502              
503 66         119 my $wildcard = $self->wildcard;
504              
505             # normalize operator
506 66   100     168 my $op = $clause->{op} || ":";
507 66         133 $op =~ s/=/:/g;
508 66 100       134 if ( $prefix eq '-' ) {
509 14 100       45 $op = '!' . $op unless $op =~ m/^!/;
510             }
511 66 100 100     528 if ( defined $value and $value =~ m/[\*\?]|\Q$wildcard/ ) {
512 13         34 $op =~ s/:/~/;
513 13 50 33     61 if ( $value eq '*' or $value eq '?' ) {
514 0 0       0 if ( !$self->allow_single_wildcards ) {
515 0         0 croak "single wildcards are not allowed: $clause";
516             }
517             }
518             }
519              
520 66   100     231 my $quote = $clause->quote || '';
521 66 100       127 my $is_phrase = $quote eq '"' ? 1 : 0;
522 66   100     257 my $proximity = $clause->proximity || '';
523              
524 66         69 my @buf;
525 66         97 FIELD: for my $name (@fields) {
526 108         955 my $field = $self->get_field($name);
527              
528 108 50       1423 if ( defined $field->callback ) {
529 0         0 push( @buf, $field->callback->( $field, $op, $value ) );
530 0         0 next FIELD;
531             }
532              
533 108 50       2143 if ( $self->debug ) {
534 0         0 warn "as_lucy_query:\n";
535             warn dump(
536             { name => $name,
537             op => $op,
538             prefix => $prefix,
539             quote => $quote,
540             value => $value,
541             clause_op => $clause->{op},
542             }
543 0         0 ) . "\n";
544             }
545              
546             # NULL
547 108 100       787 if ( !defined $value ) {
548 4 100       10 if ( $op eq '!:' ) {
549 3 100       7 if ( $prefix eq '-' ) {
550              
551             # original op was inverted above by $prefix
552 2 100       11 if ( $clause->{op} eq ':' ) {
553              
554             # appears to be a double negative, but necessary
555             # to get the logic and serialization correct.
556             # e.g. NOT foo:NULL
557 1         6 push @buf,
558             Lucy::Search::NOTQuery->new(
559             negated_query =>
560             $field->nullterm_query_class->new(
561             field => $name
562             )
563             );
564              
565             }
566             else {
567             # true double negative
568             # e.g. NOT foo!:NULL => foo:NULL
569 1         12 push @buf,
570             $field->nullterm_query_class->new(
571             field => $name );
572              
573             }
574              
575             }
576             else {
577 1         12 push @buf,
578             $field->anyterm_query_class->new( field => $name );
579             }
580             }
581             else {
582 1         9 push @buf,
583             $field->nullterm_query_class->new( field => $name );
584             }
585 4         532 next FIELD;
586             }
587              
588             # range is un-analyzed
589 104 100       267 if ( $op eq '..' ) {
    100          
590 3 50 33     17 if ( ref $value ne 'ARRAY' or @$value != 2 ) {
591 0         0 croak "range of values must be a 2-element ARRAY";
592             }
593              
594 3         87 my $range_query = $field->range_query_class->new(
595             field => $name,
596             lower_term => $value->[0],
597             upper_term => $value->[1],
598             include_lower => 1,
599             include_upper => 1,
600             );
601              
602 3         6 push( @buf, $range_query );
603 3         6 next FIELD;
604              
605             }
606              
607             # invert range
608             elsif ( $op eq '!..' ) {
609 4 50 33     27 if ( ref $value ne 'ARRAY' or @$value != 2 ) {
610 0         0 croak "range of values must be a 2-element ARRAY";
611             }
612              
613 4         86 my $range_query = $field->range_query_class->new(
614             field => $name,
615             lower_term => $value->[0],
616             upper_term => $value->[1],
617             include_lower => 1,
618             include_upper => 1,
619             );
620 4         48 push( @buf,
621             Lucy::Search::NOTQuery->new( negated_query => $range_query )
622             );
623 4         11 next FIELD;
624             }
625              
626             #$self->debug and warn "value before:$value";
627 97         172 my @values = ($value);
628              
629             # if the field has an analyzer, use it on $value
630 97 100 66     610 if ( blessed( $field->analyzer ) && !ref $value ) {
631              
632             # preserve any wildcards
633 92 100       338 if ( $value =~ m/[$wildcard\*\?]/ ) {
634              
635             # can't use full PolyAnalyzer since it will tokenize
636             # and strip the wildcards off.
637              
638             # assume CaseFolder
639             # TODO do not assume
640 27         48 $value = lc($value);
641              
642             # split on whitespace, not token regex
643 27         68 my @tok = split( m/\s+/, $value );
644              
645             # if stemmer, apply only to prefix if at all.
646 27         26 my $stemmer;
647              
648 27 50       97 if ( $field->analyzer->isa('Lucy::Analysis::PolyAnalyzer') ) {
    0          
649 27         145 my $analyzers = $field->analyzer->get_analyzers();
650 27         49 for my $ana (@$analyzers) {
651 81 100       287 if ( $ana->isa('Lucy::Analysis::SnowballStemmer') ) {
652 27         31 $stemmer = $ana;
653 27         56 last;
654             }
655              
656             }
657             }
658             elsif (
659             $field->analyzer->isa('Lucy::Analysis::SnowballStemmer') )
660             {
661 0         0 $stemmer = $field->analyzer;
662             }
663              
664 27 50       59 if ($stemmer) {
665 27         37 for my $tok (@tok) {
666 27 100       115 if ( $tok =~ s/^(\w+)\*$/$1/ ) {
667 18         149 my $stemmed = $stemmer->split($tok);
668              
669             # re-append the wildcard
670             # TODO ever have multiple?
671 18         60 $tok = $stemmed->[0] . '*';
672             }
673             }
674             }
675              
676 27         74 @values = @tok;
677              
678             }
679             else {
680 78 50       475 @values = grep { defined and length }
681 65         70 @{ $field->analyzer->split($value) };
  65         1108  
682              
683             # if we have a StopFilter in our analyzer chain,
684             # it's possible the @values ends up empty.
685             # The Lucy QueryParser will handle that case
686             # by trying twice with a NoMatchQuery.
687             # TODO what should our behavior be?
688             # a stopword will never match since the word
689             # won't be in the index...
690             }
691             }
692              
693             #$self->debug and warn "value after :" . dump( \@values );
694              
695             # StopFilter or similar case
696 97 50       267 if ( !@values ) {
697 0         0 return ();
698             }
699              
700 97 100 66     352 if ( $is_phrase or @values > 1 ) {
701 16 100       30 if ($proximity) {
702              
703 12 100       27 if ( $self->ignore_order_in_proximity ) {
704 4         5 my $n_values = scalar @values;
705 4         6 my @permutations;
706 4         10 while ( $n_values-- > 0 ) {
707 8         73 push(
708             @permutations,
709             $field->proximity_query_class->new(
710             field => $name,
711             terms => [@values], # new array
712             within => $proximity,
713             )
714             );
715 8         42 push( @values, shift(@values) ); # shuffle
716              
717             }
718             $self->debug
719 4 50       80 and dump [ map { $_->get_terms } @permutations ];
  0         0  
720 4         65 push(
721             @buf,
722             Lucy::Search::ORQuery->new(
723             children => \@permutations,
724             )
725             );
726             }
727             else {
728 8         123 push(
729             @buf,
730             $field->proximity_query_class->new(
731             field => $name,
732             terms => \@values,
733             within => $proximity,
734             )
735             );
736             }
737             }
738             else {
739             # invert
740 4 100       11 if ( $op eq '!:' ) {
741 2         34 push(
742             @buf,
743             Lucy::Search::NOTQuery->new(
744             negated_query => $field->phrase_query_class->new(
745             field => $name,
746             terms => \@values,
747             )
748             )
749             );
750             }
751              
752             # standard
753             else {
754              
755 2         70 push(
756             @buf,
757             $field->phrase_query_class->new(
758             field => $name,
759             terms => \@values,
760             )
761             );
762             }
763             }
764             }
765             else {
766 81         108 my $term = $values[0];
767              
768             # TODO why would this happen?
769 81 50 33     265 if ( !defined $term or !length $term ) {
770 0         0 warn "No term defined";
771 0         0 next FIELD;
772             }
773              
774             # invert fuzzy
775 81 100 66     687 if ( $op eq '!~'
    100 66        
    100 66        
      66        
776             || ( $op eq '!:' and $term =~ m/[$wildcard\*\?]/ ) )
777             {
778 2 50       12 $term .= $wildcard unless $term =~ m/\Q$wildcard/;
779              
780             # if the prefix is already NOT do not apply a double negative
781 2 100       10 if ( $prefix eq '-' ) {
    50          
782 1         7 push(
783             @buf,
784             $field->wildcard_query_class->new(
785             field => $name,
786             term => $term,
787             )
788             );
789             }
790             elsif ( $op =~ m/^\!/ ) {
791 1         8 push(
792             @buf,
793             Lucy::Search::NOTQuery->new(
794             negated_query =>
795             $field->wildcard_query_class->new(
796             field => $name,
797             term => $term,
798             )
799             )
800             );
801             }
802             else {
803 0         0 push(
804             @buf,
805             $field->wildcard_query_class->new(
806             field => $name,
807             term => $term,
808             )
809             );
810             }
811             }
812              
813             # fuzzy
814             elsif ( $op eq '~'
815             || ( $op eq ':' and $term =~ m/[$wildcard\*\?]/ ) )
816             {
817 26 100       95 $term .= $wildcard unless $term =~ m/\Q$wildcard/;
818              
819 26         166 push(
820             @buf,
821             $field->wildcard_query_class->new(
822             field => $name,
823             term => $term,
824             )
825             );
826             }
827              
828             # invert
829             elsif ( $op eq '!:' ) {
830 20         243 push(
831             @buf,
832             Lucy::Search::NOTQuery->new(
833             negated_query => $field->term_query_class->new(
834             field => $name,
835             term => $term,
836             )
837             )
838             );
839             }
840              
841             # standard
842             else {
843 33         313 push(
844             @buf,
845             $field->term_query_class->new(
846             field => $name,
847             term => $term,
848             )
849             );
850             }
851              
852             } # TERM
853             }
854              
855             # possible that stop_filter results in nothing in buffer
856 66 50       2025 if ( !@buf ) {
857 0         0 return ();
858             }
859 66 100       151 if ( @buf == 1 ) {
860 52         194 return $buf[0];
861             }
862 14 100       30 my $joiner = $prefix eq '-' ? 'AND' : 'OR';
863 14         26 my $lucy_class = 'Lucy::Search::' . $joiner . 'Query';
864 14         151 return $lucy_class->new( children => \@buf );
865             }
866              
867             =head2 field_class
868              
869             Returns "Search::Query::Field::Lucy".
870              
871             =cut
872              
873 26     26 1 44431 sub field_class {'Search::Query::Field::Lucy'}
874              
875             1;
876              
877             __END__