5 use OpenSRF::Utils::JSON;
9 QueryParser - basic QueryParser class
14 my $QParser = QueryParser->new(%args);
18 Main entrypoint into the QueryParser functionality.
24 # Note that the first key must match the name of the package.
25 our %parser_config = (
45 return QueryParser::Canonicalize::abstract_query2str_impl(
46 $self->parse_tree->to_abstract_query(@_)
51 =head2 facet_class_count
53 $count = $QParser->facet_class_count();
56 sub facet_class_count {
58 return @{$self->facet_classes};
61 =head2 search_class_count
63 $count = $QParser->search_class_count();
66 sub search_class_count {
68 return @{$self->search_classes};
73 $count = $QParser->filter_count();
78 return @{$self->filters};
83 $count = $QParser->modifier_count();
88 return @{$self->modifiers};
93 $data = $QParser->custom_data($class);
98 $class = ref($class) || $class;
100 $parser_config{$class}{custom_data} ||= {};
101 return $parser_config{$class}{custom_data};
106 $operators = $QParser->operators();
108 Returns hashref of the configured operators.
113 $class = ref($class) || $class;
115 $parser_config{$class}{operators} ||= {};
116 return $parser_config{$class}{operators};
119 sub allow_nested_modifiers {
122 $class = ref($class) || $class;
124 $parser_config{$class}{allow_nested_modifiers} = $v if (defined $v);
125 return $parser_config{$class}{allow_nested_modifiers};
130 $filters = $QParser->filters();
132 Returns arrayref of the configured filters.
137 $class = ref($class) || $class;
139 $parser_config{$class}{filters} ||= [];
140 return $parser_config{$class}{filters};
143 =head2 filter_callbacks
145 $filter_callbacks = $QParser->filter_callbacks();
147 Returns hashref of the configured filter callbacks.
150 sub filter_callbacks {
152 $class = ref($class) || $class;
154 $parser_config{$class}{filter_callbacks} ||= {};
155 return $parser_config{$class}{filter_callbacks};
160 $modifiers = $QParser->modifiers();
162 Returns arrayref of the configured modifiers.
167 $class = ref($class) || $class;
169 $parser_config{$class}{modifiers} ||= [];
170 return $parser_config{$class}{modifiers};
175 $QParser = QueryParser->new(%args);
177 Creates a new QueryParser object.
182 $class = ref($class) || $class;
186 my $self = bless {} => $class;
188 for my $o (keys %{QueryParser->operators}) {
189 $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
192 for my $opt ( keys %opts) {
193 $self->$opt( $opts{$opt} ) if ($self->can($opt));
201 $query_plan = $QParser->new_plan();
203 Create a new query plan.
208 my $pkg = ref($self) || $self;
209 return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
212 =head2 add_search_filter
214 $QParser->add_search_filter($filter, [$callback]);
216 Adds a filter with the specified name and an optional callback to the
217 QueryParser configuration.
220 sub add_search_filter {
222 $pkg = ref($pkg) || $pkg;
224 my $callback = shift;
226 return $filter if (grep { $_ eq $filter } @{$pkg->filters});
227 push @{$pkg->filters}, $filter;
228 $pkg->filter_callbacks->{$filter} = $callback if ($callback);
232 =head2 add_search_modifier
234 $QParser->add_search_modifier($modifier);
236 Adds a modifier with the specified name to the QueryParser configuration.
239 sub add_search_modifier {
241 $pkg = ref($pkg) || $pkg;
242 my $modifier = shift;
244 return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
245 push @{$pkg->modifiers}, $modifier;
249 =head2 add_facet_class
251 $QParser->add_facet_class($facet_class);
253 Adds a facet class with the specified name to the QueryParser configuration.
256 sub add_facet_class {
258 $pkg = ref($pkg) || $pkg;
261 return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
263 push @{$pkg->facet_classes}, $class;
264 $pkg->facet_fields->{$class} = [];
269 =head2 add_search_class
271 $QParser->add_search_class($class);
273 Adds a search class with the specified name to the QueryParser configuration.
276 sub add_search_class {
278 $pkg = ref($pkg) || $pkg;
281 return $class if (grep { $_ eq $class } @{$pkg->search_classes});
283 push @{$pkg->search_classes}, $class;
284 $pkg->search_fields->{$class} = [];
285 $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
290 =head2 add_search_modifier
292 $op = $QParser->operator($operator, [$newvalue]);
294 Retrieves or sets value for the specified operator. Valid operators and
295 their defaults are as follows:
303 =item * group_start => (
305 =item * group_end => )
307 =item * required => +
309 =item * disallowed => -
311 =item * modifier => #
319 $class = ref($class) || $class;
323 return undef unless ($opname);
325 $parser_config{$class}{operators} ||= {};
326 $parser_config{$class}{operators}{$opname} = $op if ($op);
328 return $parser_config{$class}{operators}{$opname};
333 $classes = $QParser->facet_classes([\@newclasses]);
335 Returns arrayref of all configured facet classes after optionally
336 replacing configuration.
341 $class = ref($class) || $class;
344 $parser_config{$class}{facet_classes} ||= [];
345 $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
346 return $parser_config{$class}{facet_classes};
349 =head2 search_classes
351 $classes = $QParser->search_classes([\@newclasses]);
353 Returns arrayref of all configured search classes after optionally
354 replacing the previous configuration.
359 $class = ref($class) || $class;
362 $parser_config{$class}{classes} ||= [];
363 $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
364 return $parser_config{$class}{classes};
367 =head2 add_query_normalizer
369 $function = $QParser->add_query_normalizer($class, $field, $func, [\@params]);
373 sub add_query_normalizer {
375 $pkg = ref($pkg) || $pkg;
379 my $params = shift || [];
381 # do not add if function AND params are identical to existing member
382 return $func if (grep {
383 $_->{function} eq $func and
384 OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
385 } @{$pkg->query_normalizers->{$class}->{$field}});
387 push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
392 =head2 query_normalizers
394 $normalizers = $QParser->query_normalizers($class, $field);
396 Returns a list of normalizers associated with the specified search class
400 sub query_normalizers {
402 $pkg = ref($pkg) || $pkg;
407 $parser_config{$pkg}{normalizers} ||= {};
410 $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
411 return $parser_config{$pkg}{normalizers}{$class}{$field};
413 return $parser_config{$pkg}{normalizers}{$class};
417 return $parser_config{$pkg}{normalizers};
420 =head2 add_filter_normalizer
422 $normalizer = $QParser->add_filter_normalizer($filter, $func, [\@params]);
424 Adds a normalizer function to the specified filter.
427 sub add_filter_normalizer {
429 $pkg = ref($pkg) || $pkg;
432 my $params = shift || [];
434 return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
436 push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
441 =head2 filter_normalizers
443 $normalizers = $QParser->filter_normalizers($filter);
445 Return arrayref of normalizer functions associated with the specified filter.
448 sub filter_normalizers {
450 $pkg = ref($pkg) || $pkg;
454 $parser_config{$pkg}{filter_normalizers} ||= {};
456 $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
457 return $parser_config{$pkg}{filter_normalizers}{$filter};
460 return $parser_config{$pkg}{filter_normalizers};
463 =head2 default_search_class
465 $default_class = $QParser->default_search_class([$class]);
467 Set or return the default search class.
470 sub default_search_class {
472 $pkg = ref($pkg) || $pkg;
474 $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
476 return $QueryParser::parser_config{$pkg}{default_class};
479 =head2 remove_facet_class
481 $QParser->remove_facet_class($class);
483 Remove the specified facet class from the configuration.
486 sub remove_facet_class {
488 $pkg = ref($pkg) || $pkg;
491 return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
493 $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
494 delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
499 =head2 remove_search_class
501 $QParser->remove_search_class($class);
503 Remove the specified search class from the configuration.
506 sub remove_search_class {
508 $pkg = ref($pkg) || $pkg;
511 return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
513 $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
514 delete $QueryParser::parser_config{$pkg}{fields}{$class};
519 =head2 add_facet_field
521 $QParser->add_facet_field($class, $field);
523 Adds the specified field (and facet class if it doesn't already exist)
524 to the configuration.
527 sub add_facet_field {
529 $pkg = ref($pkg) || $pkg;
533 $pkg->add_facet_class( $class );
535 return { $class => $field } if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
537 push @{$pkg->facet_fields->{$class}}, $field;
539 return { $class => $field };
544 $fields = $QParser->facet_fields($class);
546 Returns arrayref with list of fields for specified facet class.
551 $class = ref($class) || $class;
553 $parser_config{$class}{facet_fields} ||= {};
554 return $parser_config{$class}{facet_fields};
557 =head2 add_search_field
559 $QParser->add_search_field($class, $field);
561 Adds the specified field (and facet class if it doesn't already exist)
562 to the configuration.
565 sub add_search_field {
567 $pkg = ref($pkg) || $pkg;
571 $pkg->add_search_class( $class );
573 return { $class => $field } if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
575 push @{$pkg->search_fields->{$class}}, $field;
577 return { $class => $field };
582 $fields = $QParser->search_fields();
584 Returns arrayref with list of configured search fields.
589 $class = ref($class) || $class;
591 $parser_config{$class}{fields} ||= {};
592 return $parser_config{$class}{fields};
595 =head2 add_search_class_alias
597 $QParser->add_search_class_alias($class, $alias);
600 sub add_search_class_alias {
602 $pkg = ref($pkg) || $pkg;
606 $pkg->add_search_class( $class );
608 return { $class => $alias } if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
610 push @{$pkg->search_class_aliases->{$class}}, $alias;
612 return { $class => $alias };
615 =head2 search_class_aliases
617 $aliases = $QParser->search_class_aliases($class);
620 sub search_class_aliases {
622 $class = ref($class) || $class;
624 $parser_config{$class}{class_map} ||= {};
625 return $parser_config{$class}{class_map};
628 =head2 add_search_field_alias
630 $QParser->add_search_field_alias($class, $field, $alias);
633 sub add_search_field_alias {
635 $pkg = ref($pkg) || $pkg;
640 return { $class => { $field => $alias } } if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
642 push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
644 return { $class => { $field => $alias } };
647 =head2 search_field_aliases
649 $aliases = $QParser->search_field_aliases();
652 sub search_field_aliases {
654 $class = ref($class) || $class;
656 $parser_config{$class}{field_alias_map} ||= {};
657 return $parser_config{$class}{field_alias_map};
660 =head2 remove_facet_field
662 $QParser->remove_facet_field($class, $field);
665 sub remove_facet_field {
667 $pkg = ref($pkg) || $pkg;
671 return { $class => $field } if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
673 $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
675 return { $class => $field };
678 =head2 remove_search_field
680 $QParser->remove_search_field($class, $field);
683 sub remove_search_field {
685 $pkg = ref($pkg) || $pkg;
689 return { $class => $field } if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
691 $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
693 return { $class => $field };
696 =head2 remove_search_field_alias
698 $QParser->remove_search_field_alias($class, $field, $alias);
701 sub remove_search_field_alias {
703 $pkg = ref($pkg) || $pkg;
708 return { $class => { $field => $alias } } if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
710 $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
712 return { $class => { $field => $alias } };
715 =head2 remove_search_class_alias
717 $QParser->remove_search_class_alias($class, $alias);
720 sub remove_search_class_alias {
722 $pkg = ref($pkg) || $pkg;
726 return { $class => $alias } if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
728 $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
730 return { $class => $alias };
735 $debug = $QParser->debug([$debug]);
737 Return or set whether debugging output is enabled.
743 $self->{_debug} = $q if (defined $q);
744 return $self->{_debug};
749 $query = $QParser->query([$query]);
751 Return or set the query.
757 $self->{_query} = " $q " if (defined $q);
758 return $self->{_query};
763 $parse_tree = $QParser->parse_tree([$parse_tree]);
765 Return or set the parse tree associated with the QueryParser.
771 $self->{_parse_tree} = $q if (defined $q);
772 return $self->{_parse_tree};
778 $self->{_top} = $q if (defined $q);
779 return $self->{_top};
784 $QParser->parse([$query]);
786 Parse the specified query, or the query already associated with the QueryParser
792 my $pkg = ref($self) || $self;
793 warn " ** parse package is $pkg\n" if $self->debug;
796 # $self->query( shift() )
800 $self->decompose( $self->query( shift() ) );
802 if ($self->floating_plan) {
803 $self->floating_plan->add_node( $self->parse_tree );
804 $self->parse_tree( $self->floating_plan );
807 $self->parse_tree->plan_level(0);
814 ($struct, $remainder) = $QParser->decompose($querystring, [$current_class], [$recursing], [$phrase_helper]);
816 This routine does the heavy work of parsing the query string recursively.
817 Returns the top level query plan, or the query plan from a lower level plus
818 the portion of the query string that needs to be processed at a higher level.
821 our $last_class = '';
828 my $pkg = ref($self) || $self;
832 my $current_class = shift || $self->default_search_class;
834 my $recursing = shift || 0;
835 my $phrase_helper = shift || 0;
837 # Build the search class+field uber-regexp
838 my $search_class_re = '^\s*(';
841 warn ' 'x$recursing." ** decompose package is $pkg\n" if $self->debug;
844 for my $class ( keys %{$pkg->search_field_aliases} ) {
845 warn ' 'x$recursing." *** ... Looking for search fields in $class\n" if $self->debug;
847 for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
848 warn ' 'x$recursing." *** ... Looking for aliases of $field\n" if $self->debug;
850 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
851 my $aliasr = qr/$alias/;
852 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
853 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
854 warn ' 'x$recursing." *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
858 $search_class_re .= '|' unless ($first_class);
860 $search_class_re .= $class . '(?:[|#][^:|]+)*';
861 $seen_classes{$class} = 1;
864 for my $class ( keys %{$pkg->search_class_aliases} ) {
866 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
867 my $aliasr = qr/$alias/;
868 s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
869 s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
870 warn ' 'x$recursing." *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
873 if (!$seen_classes{$class}) {
874 $search_class_re .= '|' unless ($first_class);
877 $search_class_re .= $class . '(?:[|#][^:|]+)*';
878 $seen_classes{$class} = 1;
881 $search_class_re .= '):';
883 warn ' 'x$recursing." ** Rewritten query: $_\n" if $self->debug;
884 warn ' 'x$recursing." ** Search class RE: $search_class_re\n" if $self->debug;
886 my $required_op = $pkg->operator('required');
887 my $required_re = qr/\Q$required_op\E/;
889 my $disallowed_op = $pkg->operator('disallowed');
890 my $disallowed_re = qr/\Q$disallowed_op\E/;
892 my $and_op = $pkg->operator('and');
893 my $and_re = qr/^\s*\Q$and_op\E/;
895 my $or_op = $pkg->operator('or');
896 my $or_re = qr/^\s*\Q$or_op\E/;
898 my $group_start = $pkg->operator('group_start');
899 my $group_start_re = qr/^\s*\Q$group_start\E/;
901 my $group_end = $pkg->operator('group_end');
902 my $group_end_re = qr/^\s*\Q$group_end\E/;
904 my $float_start = $pkg->operator('float_start');
905 my $float_start_re = qr/^\s*\Q$float_start\E/;
907 my $float_end = $pkg->operator('float_end');
908 my $float_end_re = qr/^\s*\Q$float_end\E/;
910 my $modifier_tag = $pkg->operator('modifier');
911 my $modifier_tag_re = qr/^\s*\Q$modifier_tag\E/;
913 # Group start/end normally are ( and ), but can be overridden.
914 # We thus include ( and ) specifically due to filters, as well as : for classes.
915 my $phrase_cleanup_re = qr/\s*(\Q$required_op\E|\Q$disallowed_op\E|\Q$and_op\E|\Q$or_op\E|\Q$group_start\E|\Q$group_end\E|\Q$float_start\E|\Q$float_end\E|\Q$modifier_tag\E|:|\(|\))/;
917 # Build the filter and modifier uber-regexps
918 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
919 warn ' 'x$recursing." ** Facet RE: $facet_re\n" if $self->debug;
921 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
922 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
924 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
925 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
927 my $struct = shift || $self->new_plan( level => $recursing );
928 $self->parse_tree( $struct ) if (!$self->parse_tree);
932 while (!$remainder) {
933 warn ' 'x$recursing."Start of the loop. last_type: $last_type, joiner: ".$struct->joiner.", struct: $struct\n" if $self->debug;
934 if ($last_type eq 'FEND' and $fstart and $fstart != $struct) { # fall back further
937 } elsif ($last_type eq 'FEND') {
942 if (/^\s*$/) { # end of an explicit group
943 local $last_type = '';
945 } elsif (/$float_end_re/) { # end of an explicit group
946 warn ' 'x$recursing."Encountered explicit float end, remainder: $'\n" if $self->debug;
954 } elsif (/$group_end_re/) { # end of an explicit group
955 warn ' 'x$recursing."Encountered explicit group end, remainder: $'\n" if $self->debug;
960 local $last_type = '';
961 } elsif ($self->filter_count && /$filter_re/) { # found a filter
962 warn ' 'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
964 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
968 my $params = [ split '[,]+', $3 ];
970 if ($pkg->filter_callbacks->{$filter}) {
971 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
972 $_ = "$replacement $_" if ($replacement);
974 $struct->new_filter( $filter => $params, $negate );
978 local $last_type = '';
979 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
980 warn ' 'x$recursing."Encountered search filter: $1$2 set to $3\n" if $self->debug;
982 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
986 my $params = [ split '[,]+', $3 ];
988 if ($pkg->filter_callbacks->{$filter}) {
989 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
990 $_ = "$replacement $_" if ($replacement);
992 $struct->new_filter( $filter => $params, $negate );
995 local $last_type = '';
996 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
997 warn ' 'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1000 if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1001 warn ' 'x$recursing." Search modifiers only allowed at the top level of the query\n" if $self->debug;
1003 $struct->new_modifier($1);
1006 local $last_type = '';
1007 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
1008 warn ' 'x$recursing."Encountered search modifier: $1\n" if $self->debug;
1013 if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
1014 warn ' 'x$recursing." Search modifiers only allowed at the top level of the query\n" if $self->debug;
1015 } elsif ($2 =~ /^[ty1]/i) {
1016 $struct->new_modifier($mod);
1019 local $last_type = '';
1020 } elsif (/$float_start_re/) { # start of an explicit float
1021 warn ' 'x$recursing."Encountered explicit float start\n" if $self->debug;
1025 $last_class = $current_class;
1026 $current_class = undef;
1028 $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
1030 # pass the floating_plan struct to be modified by the float'ed chunk
1031 my ($floating_plan, $subremainder) = $self->new( debug => $self->debug )->decompose( $', undef, undef, undef, $self->floating_plan);
1033 warn ' 'x$recursing."Remainder after explicit float: $_\n" if $self->debug;
1035 $current_class = $last_class;
1038 } elsif (/$group_start_re/) { # start of an explicit group
1039 warn ' 'x$recursing."Encountered explicit group start\n" if $self->debug;
1041 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
1042 $struct->add_node( $substruct ) if ($substruct);
1044 warn ' 'x$recursing."Query remainder after bool group: $_\n" if $self->debug;
1046 local $last_type = '';
1048 } elsif (/$and_re/) { # ANDed expression
1050 warn ' 'x$recursing."Encountered AND\n" if $self->debug;
1051 do {warn ' 'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1052 do {warn ' 'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1053 local $last_type = 'AND';
1055 warn ' 'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1057 #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1058 my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 1 );
1061 warn ' 'x$recursing."RHS built\n" if $self->debug;
1062 warn ' 'x$recursing."Post-AND remainder: $subremainder\n" if $self->debug;
1064 my $wrapper = $self->new_plan( level => $recursing + 1 );
1066 if ($LHS->floating) {
1067 $wrapper->{query} = $LHS->{query};
1068 my $outer_wrapper = $self->new_plan( level => $recursing + 1 );
1069 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1070 $LHS->{query} = [$outer_wrapper];
1073 $wrapper->add_node($_) for ($LHS, $RHS);
1074 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1075 $struct = $self->new_plan( level => $recursing );
1076 $struct->add_node($wrapper);
1079 $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1081 local $last_type = '';
1082 } elsif (/$or_re/) { # ORed expression
1084 warn ' 'x$recursing."Encountered OR\n" if $self->debug;
1085 do {warn ' 'x$recursing."!!! Already doing the bool dance for AND\n" if $self->debug; next} if ($last_type eq 'AND');
1086 do {warn ' 'x$recursing."!!! Already doing the bool dance for OR\n" if $self->debug; next} if ($last_type eq 'OR');
1087 local $last_type = 'OR';
1089 warn ' 'x$recursing."Saving LHS, building RHS\n" if $self->debug;
1091 #my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1092 my ($RHS, $subremainder) = $self->decompose( $_, $current_class, $recursing + 2 );
1095 warn ' 'x$recursing."RHS built\n" if $self->debug;
1096 warn ' 'x$recursing."Post-OR remainder: $subremainder\n" if $self->debug;
1098 my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1100 if ($LHS->floating) {
1101 $wrapper->{query} = $LHS->{query};
1102 my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '|' );
1103 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
1104 $LHS->{query} = [$outer_wrapper];
1107 $wrapper->add_node($_) for ($LHS, $RHS);
1108 $wrapper->plan_level($wrapper->plan_level); # reset levels all the way down
1109 $struct = $self->new_plan( level => $recursing );
1110 $struct->add_node($wrapper);
1113 $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1115 local $last_type = '';
1116 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
1117 warn ' 'x$recursing."Encountered facet: $1$2 => $3\n" if $self->debug;
1119 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1121 my $facet_value = [ split '\s*#\s*', $3 ];
1122 $struct->new_facet( $facet => $facet_value, $negate );
1125 local $last_type = '';
1126 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
1128 if ($last_type eq 'CLASS') {
1129 $struct->remove_last_node( $current_class );
1130 warn ' 'x$recursing."Encountered class change with no searches!\n" if $self->debug;
1133 warn ' 'x$recursing."Encountered class change: $1\n" if $self->debug;
1135 $current_class = $struct->classed_node( $1 )->requested_class();
1138 local $last_type = 'CLASS';
1139 } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
1140 warn ' 'x$recursing.'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
1142 my $req_ness = $1 || '';
1145 if (!$phrase_helper) {
1146 warn ' 'x$recursing."Recursing into decompose with the phrase as a subquery\n" if $self->debug;
1148 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
1149 $struct->add_node( $substruct ) if ($substruct);
1152 warn ' 'x$recursing."Directly parsing the phrase subquery\n" if $self->debug;
1153 $struct->joiner( '&' );
1155 my $class_node = $struct->classed_node($current_class);
1157 if ($req_ness eq $pkg->operator('disallowed')) {
1158 $class_node->add_dummy_atom( node => $class_node );
1159 $class_node->add_unphrase( $phrase );
1161 #$phrase =~ s/(^|\s)\b/$1-/g;
1163 $class_node->add_phrase( $phrase );
1166 # Cleanup the phrase to make it so that we don't parse things in it as anything other than atoms
1167 $phrase =~ s/$phrase_cleanup_re/ /g;
1173 local $last_type = '';
1175 } elsif (/^\s*($required_re|$disallowed_re)([^${group_end}${float_end}\s"]+)/) { # convert require/disallow word to {un}phrase
1176 warn ' 'x$recursing."Encountered required atom (mini phrase), transforming for phrase parse: $1\n" if $self->debug;
1178 $_ = $1 . '"' . $2 . '"' . $';
1180 local $last_type = '';
1181 } elsif (/^\s*([^${group_end}${float_end}\s]+)/o) { # atom
1182 warn ' 'x$recursing."Encountered atom: $1\n" if $self->debug;
1183 warn ' 'x$recursing."Remainder: $'\n" if $self->debug;
1189 local $last_type = '';
1191 my $class_node = $struct->classed_node($current_class);
1193 my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
1194 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
1196 if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
1197 # $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
1198 # $class_node->add_unphrase( $atom ) if ($prefix eq '!');
1200 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
1201 $struct->joiner( '&' );
1204 local $last_type = '';
1212 scalar(@{$struct->query_nodes}) == 0 &&
1213 scalar(@{$struct->filters}) == 0 &&
1216 return $struct if !wantarray;
1217 return ($struct, $remainder);
1220 =head2 find_class_index
1222 $index = $QParser->find_class_index($class, $query);
1225 sub find_class_index {
1229 my ($class_part, @field_parts) = split '\|', $class;
1230 $class_part ||= $class;
1232 for my $idx ( 0 .. scalar(@$query) - 1 ) {
1233 next unless ref($$query[$idx]);
1234 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
1237 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
1243 $limit = $QParser->core_limit([$limit]);
1245 Return and/or set the core_limit.
1251 $self->{core_limit} = $l if ($l);
1252 return $self->{core_limit};
1257 $superpage = $QParser->superpage([$superpage]);
1259 Return and/or set the superpage.
1265 $self->{superpage} = $l if ($l);
1266 return $self->{superpage};
1269 =head2 superpage_size
1271 $size = $QParser->superpage_size([$size]);
1273 Return and/or set the superpage size.
1276 sub superpage_size {
1279 $self->{superpage_size} = $l if ($l);
1280 return $self->{superpage_size};
1284 #-------------------------------
1285 package QueryParser::_util;
1287 # At this level, joiners are always & or |. This is not
1288 # the external, configurable representation of joiners that
1289 # defaults to # && and ||.
1293 return (not ref $str and ($str eq '&' or $str eq '|'));
1296 sub default_joiner { '&' }
1298 # 0 for different, 1 for the same.
1299 sub compare_abstract_atoms {
1300 my ($left, $right) = @_;
1302 foreach (qw/prefix suffix content/) {
1303 no warnings; # undef can stand in for '' here
1304 return 0 unless $left->{$_} eq $right->{$_};
1310 sub fake_abstract_atom_from_phrase {
1313 my $qp_class = shift || 'QueryParser';
1318 $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
1323 "type" => "atom", "prefix" => $prefix, "suffix" => '"',
1324 "content" => $phrase
1328 sub find_arrays_in_abstract {
1332 foreach my $key (keys %$hash) {
1333 if (ref $hash->{$key} eq "ARRAY") {
1334 push @arrays, $hash->{$key};
1335 foreach (@{$hash->{$key}}) {
1336 push @arrays, find_arrays_in_abstract($_);
1344 #-------------------------------
1345 package QueryParser::Canonicalize; # not OO
1348 sub _abstract_query2str_filter {
1350 my $qp_class = shift || 'QueryParser';
1351 my $qpconfig = $QueryParser::parser_config{$qp_class};
1355 $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
1357 join(",", @{$f->{args}})
1361 sub _abstract_query2str_modifier {
1363 my $qp_class = shift || 'QueryParser';
1364 my $qpconfig = $QueryParser::parser_config{$qp_class};
1366 return $qpconfig->{operators}{modifier} . $f;
1370 my $children = shift;
1371 my $op = (keys %$children)[0];
1372 return @{$$children{$op}};
1376 # This should produce an equivalent query to the original, given an
1378 sub abstract_query2str_impl {
1379 my $abstract_query = shift;
1380 my $depth = shift || 0;
1382 my $qp_class ||= shift || 'QueryParser';
1383 my $force_qp_node = shift || 0;
1384 my $qpconfig = $QueryParser::parser_config{$qp_class};
1386 my $fs = $qpconfig->{operators}{float_start};
1387 my $fe = $qpconfig->{operators}{float_end};
1388 my $gs = $qpconfig->{operators}{group_start};
1389 my $ge = $qpconfig->{operators}{group_end};
1390 my $and = $qpconfig->{operators}{and};
1391 my $or = $qpconfig->{operators}{or};
1397 if (exists $abstract_query->{type}) {
1398 if ($abstract_query->{type} eq 'query_plan') {
1399 $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
1400 exists $abstract_query->{filters};
1402 $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
1403 exists $abstract_query->{modifiers};
1405 $size = _kid_list($abstract_query->{children});
1406 $isnode = 1 if ($size > 1 and ($force_qp_node or $depth));
1407 #warn "size: $size, depth: $depth, isnode: $isnode, AQ: ".Dumper($abstract_query);
1408 } elsif ($abstract_query->{type} eq 'node') {
1409 if ($abstract_query->{alias}) {
1410 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
1411 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
1413 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1414 $q .= "|$_" foreach @{$abstract_query->{fields}};
1418 } elsif ($abstract_query->{type} eq 'atom') {
1419 my $prefix = $abstract_query->{prefix} || '';
1420 $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
1421 $q .= ($q ? ' ' : '') . $prefix .
1422 ($abstract_query->{content} || '') .
1423 ($abstract_query->{suffix} || '');
1424 } elsif ($abstract_query->{type} eq 'facet') {
1425 # facet syntax [ # ] is hardcoded I guess?
1426 my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1427 $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1428 join(" # ", @{$abstract_query->{values}}) . "]";
1432 my $next_depth = int($size > 1);
1434 if (exists $abstract_query->{children}) {
1436 my $op = (keys(%{$abstract_query->{children}}))[0];
1438 if ($abstract_query->{floating}) { # always the top node!
1439 my $sub_node = pop @{$abstract_query->{children}{$op}};
1441 $abstract_query->{floating} = 0;
1442 $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class, 1) . $fe. " ";
1444 $abstract_query = $sub_node;
1447 if ($abstract_query && exists $abstract_query->{children}) {
1448 $op = (keys(%{$abstract_query->{children}}))[0];
1449 $q .= ($q ? ' ' : '') . join(
1450 ($op eq '&' ? ' ' : " $or "),
1452 my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1453 } @{$abstract_query->{children}{$op}}
1456 } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1457 my $op = (keys(%{$abstract_query}))[0];
1458 $q .= ($q ? ' ' : '') . join(
1459 ($op eq '&' ? ' ' : " $or "),
1461 my $x = abstract_query2str_impl($_, $depth + $next_depth, $qp_class, $force_qp_node); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1462 } @{$abstract_query->{$op}}
1466 $q = "$gs$q$ge" if ($isnode);
1471 #-------------------------------
1472 package QueryParser::query_plan;
1476 return undef unless ref($self);
1477 return $self->{QueryParser};
1482 $pkg = ref($pkg) || $pkg;
1483 my %args = (query => [], joiner => '&', @_);
1485 return bless \%args => $pkg;
1490 my $pkg = ref($self) || $self;
1491 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1492 $self->add_node( $node );
1498 my $pkg = ref($self) || $self;
1503 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1504 $self->add_node( $node );
1511 my $pkg = ref($self) || $self;
1516 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1517 $self->add_filter( $node );
1523 sub _merge_filters {
1524 my $left_filter = shift;
1525 my $right_filter = shift;
1528 return undef unless $left_filter or $right_filter;
1529 return $right_filter unless $left_filter;
1530 return $left_filter unless $right_filter;
1532 my $args = $left_filter->{args} || [];
1535 push(@$args, @{$right_filter->{args}});
1538 # find the intersect values
1540 map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1541 $args = [ grep { $new_vals{$_} } @$args ];
1544 $left_filter->{args} = $args;
1545 return $left_filter;
1548 sub collapse_filters {
1552 # start by merging any filters at this level.
1553 # like-level filters are always ORed together
1556 my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1558 $cur_filter = shift @cur_filters;
1559 my $args = $cur_filter->{args} || [];
1560 $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1563 # next gather the collapsed filters from sub-plans and
1564 # merge them with our own
1566 my @subquery = @{$self->{query}};
1569 my $blob = shift @subquery;
1570 shift @subquery; # joiner
1571 next unless $blob->isa('QueryParser::query_plan');
1572 my $sub_filter = $blob->collapse_filters($name);
1573 $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1576 if ($self->QueryParser->debug) {
1577 my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1578 warn "collapse_filters($name) => [@args]\n";
1586 my $needle = shift;;
1587 return undef unless ($needle);
1589 my $filter = $self->collapse_filters($needle);
1591 warn "find_filter($needle) => " .
1592 (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n"
1593 if $self->QueryParser->debug;
1595 return $filter ? ($filter) : ();
1600 my $needle = shift;;
1601 return undef unless ($needle);
1602 return grep { $_->name eq $needle } @{ $self->modifiers };
1607 my $pkg = ref($self) || $self;
1610 my $node = do{$pkg.'::modifier'}->new( $name );
1611 $self->add_modifier( $node );
1618 my $requested_class = shift;
1621 for my $n (@{$self->{query}}) {
1622 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1623 if ($n->requested_class eq $requested_class) {
1630 $node = $self->new_node;
1631 $node->requested_class( $requested_class );
1637 sub remove_last_node {
1639 my $requested_class = shift;
1641 my $old = pop(@{$self->query_nodes});
1642 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1649 return $self->{query};
1655 $self->{floating} = $f if (defined $f);
1656 return $self->{floating};
1663 $self->{query} ||= [];
1664 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1665 push(@{$self->{query}}, $node);
1673 return $self->{level} ? 0 : 1;
1680 if (defined $level) {
1681 $self->{level} = $level;
1682 for (@{$self->query_nodes}) {
1683 $_->plan_level($level + 1) if (ref and $_->isa('QueryParser::query_plan'));
1687 return $self->{level};
1694 $self->{joiner} = $joiner if ($joiner);
1695 return $self->{joiner};
1700 $self->{modifiers} ||= [];
1701 return $self->{modifiers};
1706 my $modifier = shift;
1708 $self->{modifiers} ||= [];
1709 $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1711 push(@{$self->{modifiers}}, $modifier);
1718 $self->{facets} ||= [];
1719 return $self->{facets};
1726 $self->{facets} ||= [];
1727 $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1729 push(@{$self->{facets}}, $facet);
1736 $self->{filters} ||= [];
1737 return $self->{filters};
1744 $self->{filters} ||= [];
1746 push(@{$self->{filters}}, $filter);
1751 # %opts supports two options at this time:
1753 # If true, do not do anything to the phrases and unphrases
1754 # fields on any discovered nodes.
1756 # If true, also return the query parser config as part of the blob.
1757 # This will get set back to 0 before recursion to avoid repetition.
1758 sub to_abstract_query {
1762 my $pkg = ref $self->QueryParser || $self->QueryParser;
1764 my $abstract_query = {
1765 type => "query_plan",
1766 floating => $self->floating,
1767 level => $self->plan_level,
1768 filters => [map { $_->to_abstract_query } @{$self->filters}],
1769 modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1772 if ($opts{with_config}) {
1773 $opts{with_config} = 0;
1774 $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1779 for my $qnode (@{$self->query_nodes}) {
1780 # Remember: qnode can be a joiner string, a node, or another query_plan
1782 if (QueryParser::_util::is_joiner($qnode)) {
1783 if ($abstract_query->{children}) {
1784 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1785 next if $open_joiner eq $qnode;
1787 my $oldroot = $abstract_query->{children};
1789 $abstract_query->{children} = {$qnode => $kids};
1791 $abstract_query->{children} = {$qnode => $kids};
1794 push @$kids, $qnode->to_abstract_query(%opts);
1798 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1799 return $abstract_query;
1803 #-------------------------------
1804 package QueryParser::query_plan::node;
1806 $Data::Dumper::Indent = 0;
1810 $pkg = ref($pkg) || $pkg;
1813 return bless \%args => $pkg;
1818 my $pkg = ref($self) || $self;
1819 return do{$pkg.'::atom'}->new( @_ );
1822 sub requested_class { # also split into classname, fields and alias
1828 my (undef, $alias) = split '#', $class;
1830 $class =~ s/#[^|]+//;
1831 ($alias, @afields) = split '\|', $alias;
1834 my @fields = @afields;
1835 my ($class_part, @field_parts) = split '\|', $class;
1836 for my $f (@field_parts) {
1837 push(@fields, $f) unless (grep { $f eq $_ } @fields);
1840 $class_part ||= $class;
1842 $self->{requested_class} = $class;
1843 $self->{alias} = $alias if $alias;
1844 $self->{alias_fields} = \@afields if $alias;
1845 $self->{classname} = $class_part;
1846 $self->{fields} = \@fields;
1849 return $self->{requested_class};
1856 $self->{plan} = $plan if ($plan);
1857 return $self->{plan};
1864 $self->{alias} = $alias if ($alias);
1865 return $self->{alias};
1872 $self->{alias_fields} = $alias if ($alias);
1873 return $self->{alias_fields};
1880 $self->{classname} = $class if ($class);
1881 return $self->{classname};
1888 $self->{fields} ||= [];
1889 $self->{fields} = \@fields if (@fields);
1890 return $self->{fields};
1897 $self->{phrases} ||= [];
1898 $self->{phrases} = \@phrases if (@phrases);
1899 return $self->{phrases};
1906 $self->{unphrases} ||= [];
1907 $self->{unphrases} = \@phrases if (@phrases);
1908 return $self->{unphrases};
1915 push(@{$self->phrases}, $phrase);
1924 push(@{$self->unphrases}, $phrase);
1931 my @query_atoms = @_;
1933 $self->{query_atoms} ||= [];
1934 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1935 return $self->{query_atoms};
1943 my $content = $atom;
1946 $atom = $self->new_atom( content => $content, @parts );
1949 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1950 push(@{$self->query_atoms}, $atom);
1955 sub add_dummy_atom {
1959 my $atom = $self->new_atom( @parts, dummy => 1 );
1961 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1962 push(@{$self->query_atoms}, $atom);
1967 # This will find up to one occurence of @$short_list within @$long_list, and
1968 # replace it with the single atom $replacement.
1969 sub replace_phrase_in_abstract_query {
1970 my ($self, $short_list, $long_list, $replacement) = @_;
1974 my $goal = scalar @$short_list;
1976 for (my $i = 0; $i < scalar (@$long_list); $i++) {
1977 my $right = $long_list->[$i];
1979 if (QueryParser::_util::compare_abstract_atoms(
1980 $short_list->[scalar @already], $right
1983 } elsif (scalar @already) {
1988 if (scalar @already == $goal) {
1989 splice @$long_list, $already[0], scalar(@already), $replacement;
1998 sub to_abstract_query {
2002 my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
2004 my $abstract_query = {
2006 "alias" => $self->alias,
2007 "alias_fields" => $self->alias_fields,
2008 "class" => $self->classname,
2009 "fields" => $self->fields
2014 for my $qatom (@{$self->query_atoms}) {
2015 if (QueryParser::_util::is_joiner($qatom)) {
2016 if ($abstract_query->{children}) {
2017 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
2018 next if $open_joiner eq $qatom;
2020 my $oldroot = $abstract_query->{children};
2022 $abstract_query->{children} = {$qatom => $kids};
2024 $abstract_query->{children} = {$qatom => $kids};
2027 push @$kids, $qatom->to_abstract_query;
2031 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2033 if ($self->{phrases} and not $opts{no_phrases}) {
2034 for my $phrase (@{$self->{phrases}}) {
2035 # Phrases appear duplication in a real QP tree, and we don't want
2036 # that duplication in our abstract query. So for all our phrases,
2037 # break them into atoms as QP would, and remove any matching
2038 # sequences of atoms from our abstract query.
2040 my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
2042 # For a well-behaved phrase, we should now have only one node
2043 # in the $tmptree query plan, and that node should have an
2044 # orderly list of atoms and joiners.
2046 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2050 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2052 )->{children}->{'&'}->[0]->{children}->{'&'};
2057 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
2059 last if $self->replace_phrase_in_abstract_query(
2062 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
2070 # Do the same as the preceding block for unphrases (negated phrases).
2071 if ($self->{unphrases} and not $opts{no_phrases}) {
2072 for my $phrase (@{$self->{unphrases}}) {
2073 my $tmptree = $self->{plan}->{QueryParser}->new(
2074 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
2076 )->parse->parse_tree;
2079 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
2083 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
2085 )->{children}->{'&'}->[0]->{children}->{'&'};
2090 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
2092 last if $self->replace_phrase_in_abstract_query(
2095 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
2103 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2104 return $abstract_query;
2107 #-------------------------------
2108 package QueryParser::query_plan::node::atom;
2112 $pkg = ref($pkg) || $pkg;
2115 return bless \%args => $pkg;
2120 return undef unless (ref $self);
2121 return $self->{node};
2126 return undef unless (ref $self);
2127 return $self->{content};
2132 return undef unless (ref $self);
2133 return $self->{prefix};
2138 return undef unless (ref $self);
2139 return $self->{suffix};
2142 sub to_abstract_query {
2146 (map { $_ => $self->$_ } qw/prefix suffix content/),
2150 #-------------------------------
2151 package QueryParser::query_plan::filter;
2155 $pkg = ref($pkg) || $pkg;
2158 return bless \%args => $pkg;
2163 return $self->{plan};
2168 return $self->{name};
2173 return $self->{negate};
2178 return $self->{args};
2181 sub to_abstract_query {
2185 map { $_ => $self->$_ } qw/name negate args/
2189 #-------------------------------
2190 package QueryParser::query_plan::facet;
2194 $pkg = ref($pkg) || $pkg;
2197 return bless \%args => $pkg;
2202 return $self->{plan};
2207 return $self->{name};
2212 return $self->{negate};
2217 return $self->{'values'};
2220 sub to_abstract_query {
2224 (map { $_ => $self->$_ } qw/name negate values/),
2229 #-------------------------------
2230 package QueryParser::query_plan::modifier;
2234 $pkg = ref($pkg) || $pkg;
2235 my $modifier = shift;
2238 return bless { name => $modifier, negate => $negate } => $pkg;
2243 return $self->{name};
2248 return $self->{negate};
2251 sub to_abstract_query {