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 );
811 ($struct, $remainder) = $QParser->decompose($querystring, [$current_class], [$recursing], [$phrase_helper]);
813 This routine does the heavy work of parsing the query string recursively.
814 Returns the top level query plan, or the query plan from a lower level plus
815 the portion of the query string that needs to be processed at a higher level.
820 my $pkg = ref($self) || $self;
822 warn " ** decompose package is $pkg\n" if $self->debug;
825 my $current_class = shift || $self->default_search_class;
827 my $recursing = shift || 0;
828 my $phrase_helper = shift || 0;
830 # Build the search class+field uber-regexp
831 my $search_class_re = '^\s*(';
835 for my $class ( keys %{$pkg->search_field_aliases} ) {
836 warn " *** ... Looking for search fields in $class\n" if $self->debug;
838 for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
839 warn " *** ... Looking for aliases of $field\n" if $self->debug;
841 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
842 my $aliasr = qr/$alias/;
843 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
844 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
845 warn " *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
849 $search_class_re .= '|' unless ($first_class);
851 $search_class_re .= $class . '(?:[|#][^:|]+)*';
852 $seen_classes{$class} = 1;
855 for my $class ( keys %{$pkg->search_class_aliases} ) {
857 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
858 my $aliasr = qr/$alias/;
859 s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
860 s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
861 warn " *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
864 if (!$seen_classes{$class}) {
865 $search_class_re .= '|' unless ($first_class);
868 $search_class_re .= $class . '(?:[|#][^:|]+)*';
869 $seen_classes{$class} = 1;
872 $search_class_re .= '):';
874 warn " ** Rewritten query: $_\n" if $self->debug;
875 warn " ** Search class RE: $search_class_re\n" if $self->debug;
877 my $required_re = $pkg->operator('required');
878 $required_re = qr/\Q$required_re\E/;
880 my $disallowed_re = $pkg->operator('disallowed');
881 $disallowed_re = qr/\Q$disallowed_re\E/;
883 my $and_re = $pkg->operator('and');
884 $and_re = qr/^\s*\Q$and_re\E/;
886 my $or_re = $pkg->operator('or');
887 $or_re = qr/^\s*\Q$or_re\E/;
889 my $group_start = $pkg->operator('group_start');
890 my $group_start_re = qr/^\s*\Q$group_start\E/;
892 my $group_end = $pkg->operator('group_end');
893 my $group_end_re = qr/^\s*\Q$group_end\E/;
895 my $float_start = $pkg->operator('float_start');
896 my $float_start_re = qr/^\s*\Q$float_start\E/;
898 my $float_end = $pkg->operator('float_end');
899 my $float_end_re = qr/^\s*\Q$float_end\E/;
901 my $modifier_tag_re = $pkg->operator('modifier');
902 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
905 # Build the filter and modifier uber-regexps
906 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
907 warn " ** Facet RE: $facet_re\n" if $self->debug;
909 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
910 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
912 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
913 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
915 my $struct = shift || $self->new_plan( level => $recursing );
916 $self->parse_tree( $struct ) if (!$self->parse_tree);
921 while (!$remainder) {
922 if (/^\s*$/) { # end of an explicit group
924 } elsif (/$float_end_re/) { # end of an explicit group
925 warn "Encountered explicit float end\n" if $self->debug;
931 } elsif (/$group_end_re/) { # end of an explicit group
932 warn "Encountered explicit group end\n" if $self->debug;
935 $remainder = $struct->top_plan ? '' : $';
938 } elsif ($self->filter_count && /$filter_re/) { # found a filter
939 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
941 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
945 my $params = [ split '[,]+', $3 ];
947 if ($pkg->filter_callbacks->{$filter}) {
948 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
949 $_ = "$replacement $_" if ($replacement);
951 $struct->new_filter( $filter => $params, $negate );
956 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
957 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
959 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
963 my $params = [ split '[,]+', $3 ];
965 if ($pkg->filter_callbacks->{$filter}) {
966 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
967 $_ = "$replacement $_" if ($replacement);
969 $struct->new_filter( $filter => $params, $negate );
973 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
974 warn "Encountered search modifier: $1\n" if $self->debug;
977 if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
978 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
980 $struct->new_modifier($1);
984 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
985 warn "Encountered search modifier: $1\n" if $self->debug;
990 if (!($struct->top_plan || $parser_config{$pkg}->{allow_nested_modifiers})) {
991 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
992 } elsif ($2 =~ /^[ty1]/i) {
993 $struct->new_modifier($mod);
997 } elsif (/$float_start_re/) { # start of an explicit float
998 warn "Encountered explicit float start\n" if $self->debug;
1000 $self->floating_plan( $self->new_plan( floating => 1 ) ) if (!$self->floating_plan);
1001 # pass the floating_plan struct to be modified by the float'ed chunk
1002 my ($floating_plan, $subremainder) = $self->new->decompose( $', undef, undef, undef, $self->floating_plan);
1006 } elsif (/$group_start_re/) { # start of an explicit group
1007 warn "Encountered explicit group start\n" if $self->debug;
1009 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
1010 $struct->add_node( $substruct ) if ($substruct);
1014 } elsif (/$and_re/) { # ANDed expression
1016 next if ($last_type eq 'AND');
1017 next if ($last_type eq 'OR');
1018 warn "Encountered AND\n" if $self->debug;
1021 my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1024 $struct = $self->new_plan( level => $recursing, joiner => '&', floating => $LHS->floating );
1025 if ($LHS->floating) {
1026 $self->floating_plan($struct);
1030 $struct->add_node($_) for ($LHS, $RHS);
1032 $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1035 } elsif (/$or_re/) { # ORed expression
1037 next if ($last_type eq 'AND');
1038 next if ($last_type eq 'OR');
1039 warn "Encountered OR\n" if $self->debug;
1042 my ($RHS, $subremainder) = $self->decompose( "$group_start $_ $group_end", $current_class, $recursing + 1 );
1045 $struct = $self->new_plan( level => $recursing, joiner => '|' );
1046 $struct->add_node($_) for ($LHS, $RHS);
1048 $self->parse_tree( $struct ) if ($self->parse_tree == $LHS);
1051 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
1052 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
1054 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
1056 my $facet_value = [ split '\s*#\s*', $3 ];
1057 $struct->new_facet( $facet => $facet_value, $negate );
1061 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
1063 if ($last_type eq 'CLASS') {
1064 $struct->remove_last_node( $current_class );
1065 warn "Encountered class change with no searches!\n" if $self->debug;
1068 warn "Encountered class change: $1\n" if $self->debug;
1070 $current_class = $struct->classed_node( $1 )->requested_class();
1073 $last_type = 'CLASS';
1074 } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
1075 warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
1077 my $req_ness = $1 || '';
1080 if (!$phrase_helper) {
1081 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
1083 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
1084 $struct->add_node( $substruct ) if ($substruct);
1087 warn "Directly parsing the phrase subquery\n" if $self->debug;
1088 $struct->joiner( '&' );
1090 my $class_node = $struct->classed_node($current_class);
1092 if ($req_ness eq $pkg->operator('disallowed')) {
1093 $class_node->add_dummy_atom( node => $class_node );
1094 $class_node->add_unphrase( $phrase );
1096 #$phrase =~ s/(^|\s)\b/$1-/g;
1098 $class_node->add_phrase( $phrase );
1106 # } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
1107 # warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
1111 # my $class_node = $struct->classed_node($current_class);
1112 # $class_node->add_phrase( $phrase );
1113 # $_ = $phrase . $';
1114 # $struct->joiner( '&' );
1117 } elsif (/^\s*([^${group_end}${float_end}\s]+)/o) { # atom
1118 warn "Encountered atom: $1\n" if $self->debug;
1119 warn "Remainder: $'\n" if $self->debug;
1127 my $class_node = $struct->classed_node($current_class);
1129 my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
1130 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
1132 if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
1133 # $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
1134 # $class_node->add_unphrase( $atom ) if ($prefix eq '!');
1136 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
1137 $struct->joiner( '&' );
1146 scalar(@{$struct->query_nodes}) == 0 &&
1147 scalar(@{$struct->filters}) == 0 &&
1150 return $struct if !wantarray;
1151 return ($struct, $remainder);
1154 =head2 find_class_index
1156 $index = $QParser->find_class_index($class, $query);
1159 sub find_class_index {
1163 my ($class_part, @field_parts) = split '\|', $class;
1164 $class_part ||= $class;
1166 for my $idx ( 0 .. scalar(@$query) - 1 ) {
1167 next unless ref($$query[$idx]);
1168 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
1171 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
1177 $limit = $QParser->core_limit([$limit]);
1179 Return and/or set the core_limit.
1185 $self->{core_limit} = $l if ($l);
1186 return $self->{core_limit};
1191 $superpage = $QParser->superpage([$superpage]);
1193 Return and/or set the superpage.
1199 $self->{superpage} = $l if ($l);
1200 return $self->{superpage};
1203 =head2 superpage_size
1205 $size = $QParser->superpage_size([$size]);
1207 Return and/or set the superpage size.
1210 sub superpage_size {
1213 $self->{superpage_size} = $l if ($l);
1214 return $self->{superpage_size};
1218 #-------------------------------
1219 package QueryParser::_util;
1221 # At this level, joiners are always & or |. This is not
1222 # the external, configurable representation of joiners that
1223 # defaults to # && and ||.
1227 return (not ref $str and ($str eq '&' or $str eq '|'));
1230 sub default_joiner { '&' }
1232 # 0 for different, 1 for the same.
1233 sub compare_abstract_atoms {
1234 my ($left, $right) = @_;
1236 foreach (qw/prefix suffix content/) {
1237 no warnings; # undef can stand in for '' here
1238 return 0 unless $left->{$_} eq $right->{$_};
1244 sub fake_abstract_atom_from_phrase {
1247 my $qp_class = shift || 'QueryParser';
1252 $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
1257 "type" => "atom", "prefix" => $prefix, "suffix" => '"',
1258 "content" => $phrase
1262 sub find_arrays_in_abstract {
1266 foreach my $key (keys %$hash) {
1267 if (ref $hash->{$key} eq "ARRAY") {
1268 push @arrays, $hash->{$key};
1269 foreach (@{$hash->{$key}}) {
1270 push @arrays, find_arrays_in_abstract($_);
1278 #-------------------------------
1279 package QueryParser::Canonicalize; # not OO
1281 sub _abstract_query2str_filter {
1283 my $qp_class = shift || 'QueryParser';
1284 my $qpconfig = $QueryParser::parser_config{$qp_class};
1288 $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
1290 join(",", @{$f->{args}})
1294 sub _abstract_query2str_modifier {
1296 my $qp_class = shift || 'QueryParser';
1297 my $qpconfig = $QueryParser::parser_config{$qp_class};
1299 return $qpconfig->{operators}{modifier} . $f;
1303 my $children = shift;
1304 my $op = (keys %$children)[0];
1305 return @{$$children{$op}};
1308 # This should produce an equivalent query to the original, given an
1310 sub abstract_query2str_impl {
1311 my $abstract_query = shift;
1312 my $depth = shift || 0;
1314 my $qp_class ||= shift || 'QueryParser';
1315 my $qpconfig = $QueryParser::parser_config{$qp_class};
1317 my $fs = $qpconfig->{operators}{float_start};
1318 my $fe = $qpconfig->{operators}{float_end};
1319 my $gs = $qpconfig->{operators}{group_start};
1320 my $ge = $qpconfig->{operators}{group_end};
1321 my $and = $qpconfig->{operators}{and};
1322 my $or = $qpconfig->{operators}{or};
1327 if (exists $abstract_query->{type}) {
1328 if ($abstract_query->{type} eq 'query_plan') {
1329 $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
1330 exists $abstract_query->{filters};
1332 $q .= ($q ? ' ' : '') . join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
1333 exists $abstract_query->{modifiers};
1335 if (!$abstract_query->{floating} && exists $abstract_query->{children} && _kid_list($abstract_query->{children}) > 1);
1336 } elsif ($abstract_query->{type} eq 'node') {
1337 if ($abstract_query->{alias}) {
1338 $q .= ($q ? ' ' : '') . $abstract_query->{alias};
1339 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
1341 $q .= ($q ? ' ' : '') . $abstract_query->{class};
1342 $q .= "|$_" foreach @{$abstract_query->{fields}};
1346 } elsif ($abstract_query->{type} eq 'atom') {
1347 my $prefix = $abstract_query->{prefix} || '';
1348 $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
1349 $q .= ($q ? ' ' : '') . $prefix .
1350 ($abstract_query->{content} || '') .
1351 ($abstract_query->{suffix} || '');
1352 } elsif ($abstract_query->{type} eq 'facet') {
1353 # facet syntax [ # ] is hardcoded I guess?
1354 my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
1355 $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
1356 join(" # ", @{$abstract_query->{values}}) . "]";
1360 if (exists $abstract_query->{children}) {
1362 my $op = (keys(%{$abstract_query->{children}}))[0];
1364 if ($abstract_query->{floating}) { # always the top node!
1365 my $sub_node = pop @{$abstract_query->{children}{$op}};
1367 $abstract_query->{floating} = 0;
1368 $q = $fs . " " . abstract_query2str_impl($abstract_query,0,$qp_class) . $fe. " ";
1370 $abstract_query = $sub_node;
1373 if ($abstract_query && exists $abstract_query->{children}) {
1374 $op = (keys(%{$abstract_query->{children}}))[0];
1375 $q .= ($q ? ' ' : '') . join(
1376 ($op eq '&' ? ' ' : " $or "),
1378 my $x = abstract_query2str_impl($_, $depth + 1, $qp_class); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1379 } @{$abstract_query->{children}{$op}}
1382 } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
1383 my $op = (keys(%{$abstract_query}))[0];
1384 $q .= ($q ? ' ' : '') . join(
1385 ($op eq '&' ? ' ' : " $or "),
1387 my $x = abstract_query2str_impl($_, $depth + 1, $qp_class); $x =~ s/^\s+//; $x =~ s/\s+$//; $x;
1388 } @{$abstract_query->{$op}}
1392 $q = "$gs$q$ge" if ($isnode);
1397 #-------------------------------
1398 package QueryParser::query_plan;
1402 return undef unless ref($self);
1403 return $self->{QueryParser};
1408 $pkg = ref($pkg) || $pkg;
1409 my %args = (query => [], joiner => '&', @_);
1411 return bless \%args => $pkg;
1416 my $pkg = ref($self) || $self;
1417 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1418 $self->add_node( $node );
1424 my $pkg = ref($self) || $self;
1429 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1430 $self->add_node( $node );
1437 my $pkg = ref($self) || $self;
1442 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1443 $self->add_filter( $node );
1449 sub _merge_filters {
1450 my $left_filter = shift;
1451 my $right_filter = shift;
1454 return undef unless $left_filter or $right_filter;
1455 return $right_filter unless $left_filter;
1456 return $left_filter unless $right_filter;
1458 my $args = $left_filter->{args} || [];
1461 push(@$args, @{$right_filter->{args}});
1464 # find the intersect values
1466 map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1467 $args = [ grep { $new_vals{$_} } @$args ];
1470 $left_filter->{args} = $args;
1471 return $left_filter;
1474 sub collapse_filters {
1478 # start by merging any filters at this level.
1479 # like-level filters are always ORed together
1482 my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1484 $cur_filter = shift @cur_filters;
1485 my $args = $cur_filter->{args} || [];
1486 $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1489 # next gather the collapsed filters from sub-plans and
1490 # merge them with our own
1492 my @subquery = @{$self->{query}};
1495 my $blob = shift @subquery;
1496 shift @subquery; # joiner
1497 next unless $blob->isa('QueryParser::query_plan');
1498 my $sub_filter = $blob->collapse_filters($name);
1499 $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1502 if ($self->QueryParser->debug) {
1503 my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1504 warn "collapse_filters($name) => [@args]\n";
1512 my $needle = shift;;
1513 return undef unless ($needle);
1515 my $filter = $self->collapse_filters($needle);
1517 warn "find_filter($needle) => " .
1518 (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n"
1519 if $self->QueryParser->debug;
1521 return $filter ? ($filter) : ();
1526 my $needle = shift;;
1527 return undef unless ($needle);
1528 return grep { $_->name eq $needle } @{ $self->modifiers };
1533 my $pkg = ref($self) || $self;
1536 my $node = do{$pkg.'::modifier'}->new( $name );
1537 $self->add_modifier( $node );
1544 my $requested_class = shift;
1547 for my $n (@{$self->{query}}) {
1548 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1549 if ($n->requested_class eq $requested_class) {
1556 $node = $self->new_node;
1557 $node->requested_class( $requested_class );
1563 sub remove_last_node {
1565 my $requested_class = shift;
1567 my $old = pop(@{$self->query_nodes});
1568 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1575 return $self->{query};
1581 $self->{floating} = $f if (defined $f);
1582 return $self->{floating};
1589 $self->{query} ||= [];
1590 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1591 push(@{$self->{query}}, $node);
1599 return $self->{level} ? 0 : 1;
1604 return $self->{level};
1611 $self->{joiner} = $joiner if ($joiner);
1612 return $self->{joiner};
1617 $self->{modifiers} ||= [];
1618 return $self->{modifiers};
1623 my $modifier = shift;
1625 $self->{modifiers} ||= [];
1626 $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1628 push(@{$self->{modifiers}}, $modifier);
1635 $self->{facets} ||= [];
1636 return $self->{facets};
1643 $self->{facets} ||= [];
1644 $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1646 push(@{$self->{facets}}, $facet);
1653 $self->{filters} ||= [];
1654 return $self->{filters};
1661 $self->{filters} ||= [];
1663 push(@{$self->{filters}}, $filter);
1668 # %opts supports two options at this time:
1670 # If true, do not do anything to the phrases and unphrases
1671 # fields on any discovered nodes.
1673 # If true, also return the query parser config as part of the blob.
1674 # This will get set back to 0 before recursion to avoid repetition.
1675 sub to_abstract_query {
1679 my $pkg = ref $self->QueryParser || $self->QueryParser;
1681 my $abstract_query = {
1682 type => "query_plan",
1683 floating => $self->floating,
1684 filters => [map { $_->to_abstract_query } @{$self->filters}],
1685 modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1688 if ($opts{with_config}) {
1689 $opts{with_config} = 0;
1690 $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1695 for my $qnode (@{$self->query_nodes}) {
1696 # Remember: qnode can be a joiner string, a node, or another query_plan
1698 if (QueryParser::_util::is_joiner($qnode)) {
1699 if ($abstract_query->{children}) {
1700 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1701 next if $open_joiner eq $qnode;
1703 my $oldroot = $abstract_query->{children};
1705 $abstract_query->{children} = {$qnode => $kids};
1707 $abstract_query->{children} = {$qnode => $kids};
1710 push @$kids, $qnode->to_abstract_query(%opts);
1714 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1715 return $abstract_query;
1719 #-------------------------------
1720 package QueryParser::query_plan::node;
1722 $Data::Dumper::Indent = 0;
1726 $pkg = ref($pkg) || $pkg;
1729 return bless \%args => $pkg;
1734 my $pkg = ref($self) || $self;
1735 return do{$pkg.'::atom'}->new( @_ );
1738 sub requested_class { # also split into classname, fields and alias
1744 my (undef, $alias) = split '#', $class;
1746 $class =~ s/#[^|]+//;
1747 ($alias, @afields) = split '\|', $alias;
1750 my @fields = @afields;
1751 my ($class_part, @field_parts) = split '\|', $class;
1752 for my $f (@field_parts) {
1753 push(@fields, $f) unless (grep { $f eq $_ } @fields);
1756 $class_part ||= $class;
1758 $self->{requested_class} = $class;
1759 $self->{alias} = $alias if $alias;
1760 $self->{alias_fields} = \@afields if $alias;
1761 $self->{classname} = $class_part;
1762 $self->{fields} = \@fields;
1765 return $self->{requested_class};
1772 $self->{plan} = $plan if ($plan);
1773 return $self->{plan};
1780 $self->{alias} = $alias if ($alias);
1781 return $self->{alias};
1788 $self->{alias_fields} = $alias if ($alias);
1789 return $self->{alias_fields};
1796 $self->{classname} = $class if ($class);
1797 return $self->{classname};
1804 $self->{fields} ||= [];
1805 $self->{fields} = \@fields if (@fields);
1806 return $self->{fields};
1813 $self->{phrases} ||= [];
1814 $self->{phrases} = \@phrases if (@phrases);
1815 return $self->{phrases};
1822 $self->{unphrases} ||= [];
1823 $self->{unphrases} = \@phrases if (@phrases);
1824 return $self->{unphrases};
1831 push(@{$self->phrases}, $phrase);
1840 push(@{$self->unphrases}, $phrase);
1847 my @query_atoms = @_;
1849 $self->{query_atoms} ||= [];
1850 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1851 return $self->{query_atoms};
1859 my $content = $atom;
1862 $atom = $self->new_atom( content => $content, @parts );
1865 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1866 push(@{$self->query_atoms}, $atom);
1871 sub add_dummy_atom {
1875 my $atom = $self->new_atom( @parts, dummy => 1 );
1877 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1878 push(@{$self->query_atoms}, $atom);
1883 # This will find up to one occurence of @$short_list within @$long_list, and
1884 # replace it with the single atom $replacement.
1885 sub replace_phrase_in_abstract_query {
1886 my ($self, $short_list, $long_list, $replacement) = @_;
1890 my $goal = scalar @$short_list;
1892 for (my $i = 0; $i < scalar (@$long_list); $i++) {
1893 my $right = $long_list->[$i];
1895 if (QueryParser::_util::compare_abstract_atoms(
1896 $short_list->[scalar @already], $right
1899 } elsif (scalar @already) {
1904 if (scalar @already == $goal) {
1905 splice @$long_list, $already[0], scalar(@already), $replacement;
1914 sub to_abstract_query {
1918 my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
1920 my $abstract_query = {
1922 "alias" => $self->alias,
1923 "alias_fields" => $self->alias_fields,
1924 "class" => $self->classname,
1925 "fields" => $self->fields
1930 for my $qatom (@{$self->query_atoms}) {
1931 if (QueryParser::_util::is_joiner($qatom)) {
1932 if ($abstract_query->{children}) {
1933 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1934 next if $open_joiner eq $qatom;
1936 my $oldroot = $abstract_query->{children};
1938 $abstract_query->{children} = {$qatom => $kids};
1940 $abstract_query->{children} = {$qatom => $kids};
1943 push @$kids, $qatom->to_abstract_query;
1947 if ($self->{phrases} and not $opts{no_phrases}) {
1948 for my $phrase (@{$self->{phrases}}) {
1949 # Phrases appear duplication in a real QP tree, and we don't want
1950 # that duplication in our abstract query. So for all our phrases,
1951 # break them into atoms as QP would, and remove any matching
1952 # sequences of atoms from our abstract query.
1954 my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
1956 # For a well-behaved phrase, we should now have only one node
1957 # in the $tmptree query plan, and that node should have an
1958 # orderly list of atoms and joiners.
1960 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1964 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1966 )->{children}->{'&'}->[0]->{children}->{'&'};
1971 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1973 last if $self->replace_phrase_in_abstract_query(
1976 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
1984 # Do the same as the preceding block for unphrases (negated phrases).
1985 if ($self->{unphrases} and not $opts{no_phrases}) {
1986 for my $phrase (@{$self->{unphrases}}) {
1987 my $tmptree = $self->{plan}->{QueryParser}->new(
1988 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
1990 )->parse->parse_tree;
1993 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1997 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1999 )->{children}->{'&'}->[0]->{children}->{'&'};
2004 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
2006 last if $self->replace_phrase_in_abstract_query(
2009 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
2017 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
2018 return $abstract_query;
2021 #-------------------------------
2022 package QueryParser::query_plan::node::atom;
2026 $pkg = ref($pkg) || $pkg;
2029 return bless \%args => $pkg;
2034 return undef unless (ref $self);
2035 return $self->{node};
2040 return undef unless (ref $self);
2041 return $self->{content};
2046 return undef unless (ref $self);
2047 return $self->{prefix};
2052 return undef unless (ref $self);
2053 return $self->{suffix};
2056 sub to_abstract_query {
2060 (map { $_ => $self->$_ } qw/prefix suffix content/),
2064 #-------------------------------
2065 package QueryParser::query_plan::filter;
2069 $pkg = ref($pkg) || $pkg;
2072 return bless \%args => $pkg;
2077 return $self->{plan};
2082 return $self->{name};
2087 return $self->{negate};
2092 return $self->{args};
2095 sub to_abstract_query {
2099 map { $_ => $self->$_ } qw/name negate args/
2103 #-------------------------------
2104 package QueryParser::query_plan::facet;
2108 $pkg = ref($pkg) || $pkg;
2111 return bless \%args => $pkg;
2116 return $self->{plan};
2121 return $self->{name};
2126 return $self->{negate};
2131 return $self->{'values'};
2134 sub to_abstract_query {
2138 (map { $_ => $self->$_ } qw/name negate values/),
2143 #-------------------------------
2144 package QueryParser::query_plan::modifier;
2148 $pkg = ref($pkg) || $pkg;
2149 my $modifier = shift;
2152 return bless { name => $modifier, negate => $negate } => $pkg;
2157 return $self->{name};
2162 return $self->{negate};
2165 sub to_abstract_query {