5 use OpenSRF::Utils::JSON;
24 return QueryParser::Canonicalize::abstract_query2str_impl(
25 $self->parse_tree->to_abstract_query(@_)
30 sub facet_class_count {
32 return @{$self->facet_classes};
35 sub search_class_count {
37 return @{$self->search_classes};
42 return @{$self->filters};
47 return @{$self->modifiers};
52 $class = ref($class) || $class;
54 $parser_config{$class}{custom_data} ||= {};
55 return $parser_config{$class}{custom_data};
60 $class = ref($class) || $class;
62 $parser_config{$class}{operators} ||= {};
63 return $parser_config{$class}{operators};
68 $class = ref($class) || $class;
70 $parser_config{$class}{filters} ||= [];
71 return $parser_config{$class}{filters};
74 sub filter_callbacks {
76 $class = ref($class) || $class;
78 $parser_config{$class}{filter_callbacks} ||= {};
79 return $parser_config{$class}{filter_callbacks};
84 $class = ref($class) || $class;
86 $parser_config{$class}{modifiers} ||= [];
87 return $parser_config{$class}{modifiers};
92 $class = ref($class) || $class;
96 my $self = bless {} => $class;
98 for my $o (keys %{QueryParser->operators}) {
99 $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
102 for my $opt ( keys %opts) {
103 $self->$opt( $opts{$opt} ) if ($self->can($opt));
111 my $pkg = ref($self) || $self;
112 return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
115 sub add_search_filter {
117 $pkg = ref($pkg) || $pkg;
119 my $callback = shift;
121 return $filter if (grep { $_ eq $filter } @{$pkg->filters});
122 push @{$pkg->filters}, $filter;
123 $pkg->filter_callbacks->{$filter} = $callback if ($callback);
127 sub add_search_modifier {
129 $pkg = ref($pkg) || $pkg;
130 my $modifier = shift;
132 return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
133 push @{$pkg->modifiers}, $modifier;
137 sub add_facet_class {
139 $pkg = ref($pkg) || $pkg;
142 return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
144 push @{$pkg->facet_classes}, $class;
145 $pkg->facet_fields->{$class} = [];
150 sub add_search_class {
152 $pkg = ref($pkg) || $pkg;
155 return $class if (grep { $_ eq $class } @{$pkg->search_classes});
157 push @{$pkg->search_classes}, $class;
158 $pkg->search_fields->{$class} = [];
159 $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
166 $class = ref($class) || $class;
170 return undef unless ($opname);
172 $parser_config{$class}{operators} ||= {};
173 $parser_config{$class}{operators}{$opname} = $op if ($op);
175 return $parser_config{$class}{operators}{$opname};
180 $class = ref($class) || $class;
183 $parser_config{$class}{facet_classes} ||= [];
184 $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
185 return $parser_config{$class}{facet_classes};
190 $class = ref($class) || $class;
193 $parser_config{$class}{classes} ||= [];
194 $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
195 return $parser_config{$class}{classes};
198 sub add_query_normalizer {
200 $pkg = ref($pkg) || $pkg;
204 my $params = shift || [];
206 # do not add if function AND params are identical to existing member
207 return $func if (grep {
208 $_->{function} eq $func and
209 OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
210 } @{$pkg->query_normalizers->{$class}->{$field}});
212 push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
217 sub query_normalizers {
219 $pkg = ref($pkg) || $pkg;
224 $parser_config{$pkg}{normalizers} ||= {};
227 $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
228 return $parser_config{$pkg}{normalizers}{$class}{$field};
230 return $parser_config{$pkg}{normalizers}{$class};
234 return $parser_config{$pkg}{normalizers};
237 sub add_filter_normalizer {
239 $pkg = ref($pkg) || $pkg;
242 my $params = shift || [];
244 return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
246 push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
251 sub filter_normalizers {
253 $pkg = ref($pkg) || $pkg;
257 $parser_config{$pkg}{filter_normalizers} ||= {};
259 $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
260 return $parser_config{$pkg}{filter_normalizers}{$filter};
263 return $parser_config{$pkg}{filter_normalizers};
266 sub default_search_class {
268 $pkg = ref($pkg) || $pkg;
270 $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
272 return $QueryParser::parser_config{$pkg}{default_class};
275 sub remove_facet_class {
277 $pkg = ref($pkg) || $pkg;
280 return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
282 $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
283 delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
288 sub remove_search_class {
290 $pkg = ref($pkg) || $pkg;
293 return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
295 $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
296 delete $QueryParser::parser_config{$pkg}{fields}{$class};
301 sub add_facet_field {
303 $pkg = ref($pkg) || $pkg;
307 $pkg->add_facet_class( $class );
309 return { $class => $field } if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
311 push @{$pkg->facet_fields->{$class}}, $field;
313 return { $class => $field };
318 $class = ref($class) || $class;
320 $parser_config{$class}{facet_fields} ||= {};
321 return $parser_config{$class}{facet_fields};
324 sub add_search_field {
326 $pkg = ref($pkg) || $pkg;
330 $pkg->add_search_class( $class );
332 return { $class => $field } if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
334 push @{$pkg->search_fields->{$class}}, $field;
336 return { $class => $field };
341 $class = ref($class) || $class;
343 $parser_config{$class}{fields} ||= {};
344 return $parser_config{$class}{fields};
347 sub add_search_class_alias {
349 $pkg = ref($pkg) || $pkg;
353 $pkg->add_search_class( $class );
355 return { $class => $alias } if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
357 push @{$pkg->search_class_aliases->{$class}}, $alias;
359 return { $class => $alias };
362 sub search_class_aliases {
364 $class = ref($class) || $class;
366 $parser_config{$class}{class_map} ||= {};
367 return $parser_config{$class}{class_map};
370 sub add_search_field_alias {
372 $pkg = ref($pkg) || $pkg;
377 return { $class => { $field => $alias } } if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
379 push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
381 return { $class => { $field => $alias } };
384 sub search_field_aliases {
386 $class = ref($class) || $class;
388 $parser_config{$class}{field_alias_map} ||= {};
389 return $parser_config{$class}{field_alias_map};
392 sub remove_facet_field {
394 $pkg = ref($pkg) || $pkg;
398 return { $class => $field } if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
400 $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
402 return { $class => $field };
405 sub remove_search_field {
407 $pkg = ref($pkg) || $pkg;
411 return { $class => $field } if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
413 $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
415 return { $class => $field };
418 sub remove_search_field_alias {
420 $pkg = ref($pkg) || $pkg;
425 return { $class => { $field => $alias } } if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
427 $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
429 return { $class => { $field => $alias } };
432 sub remove_search_class_alias {
434 $pkg = ref($pkg) || $pkg;
438 return { $class => $alias } if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
440 $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
442 return { $class => $alias };
448 $self->{_debug} = $q if (defined $q);
449 return $self->{_debug};
455 $self->{_query} = $q if (defined $q);
456 return $self->{_query};
462 $self->{_parse_tree} = $q if (defined $q);
463 return $self->{_parse_tree};
468 my $pkg = ref($self) || $self;
469 warn " ** parse package is $pkg\n" if $self->debug;
472 $self->query( shift() )
481 my $pkg = ref($self) || $self;
483 warn " ** decompose package is $pkg\n" if $self->debug;
486 my $current_class = shift || $self->default_search_class;
488 my $recursing = shift || 0;
489 my $phrase_helper = shift || 0;
491 # Build the search class+field uber-regexp
492 my $search_class_re = '^\s*(';
496 for my $class ( keys %{$pkg->search_field_aliases} ) {
497 warn " *** ... Looking for search fields in $class\n" if $self->debug;
499 for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
500 warn " *** ... Looking for aliases of $field\n" if $self->debug;
502 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
503 my $aliasr = qr/$alias/;
504 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
505 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
506 warn " *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
510 $search_class_re .= '|' unless ($first_class);
512 $search_class_re .= $class . '(?:[|#][^:|]+)*';
513 $seen_classes{$class} = 1;
516 for my $class ( keys %{$pkg->search_class_aliases} ) {
518 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
519 my $aliasr = qr/$alias/;
520 s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
521 s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
522 warn " *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
525 if (!$seen_classes{$class}) {
526 $search_class_re .= '|' unless ($first_class);
529 $search_class_re .= $class . '(?:[|#][^:|]+)*';
530 $seen_classes{$class} = 1;
533 $search_class_re .= '):';
535 warn " ** Rewritten query: $_\n" if $self->debug;
536 warn " ** Search class RE: $search_class_re\n" if $self->debug;
538 my $required_re = $pkg->operator('required');
539 $required_re = qr/\Q$required_re\E/;
541 my $disallowed_re = $pkg->operator('disallowed');
542 $disallowed_re = qr/\Q$disallowed_re\E/;
544 my $and_re = $pkg->operator('and');
545 $and_re = qr/^\s*\Q$and_re\E/;
547 my $or_re = $pkg->operator('or');
548 $or_re = qr/^\s*\Q$or_re\E/;
550 my $group_start_re = $pkg->operator('group_start');
551 $group_start_re = qr/^\s*\Q$group_start_re\E/;
553 my $group_end = $pkg->operator('group_end');
554 my $group_end_re = qr/^\s*\Q$group_end\E/;
556 my $modifier_tag_re = $pkg->operator('modifier');
557 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
560 # Build the filter and modifier uber-regexps
561 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
562 warn " ** Facet RE: $facet_re\n" if $self->debug;
564 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
565 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
567 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
568 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
570 my $struct = $self->new_plan( level => $recursing );
574 while (!$remainder) {
575 if (/^\s*$/) { # end of an explicit group
577 } elsif (/$group_end_re/) { # end of an explicit group
578 warn "Encountered explicit group end\n" if $self->debug;
581 $remainder = $struct->top_plan ? '' : $';
584 } elsif ($self->filter_count && /$filter_re/) { # found a filter
585 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
587 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
591 my $params = [ split '[,]+', $3 ];
593 if ($pkg->filter_callbacks->{$filter}) {
594 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
595 $_ = "$replacement $_" if ($replacement);
597 $struct->new_filter( $filter => $params, $negate );
602 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
603 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
605 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
609 my $params = [ split '[,]+', $3 ];
611 if ($pkg->filter_callbacks->{$filter}) {
612 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
613 $_ = "$replacement $_" if ($replacement);
615 $struct->new_filter( $filter => $params, $negate );
619 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
620 warn "Encountered search modifier: $1\n" if $self->debug;
623 if (!$struct->top_plan) {
624 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
626 $struct->new_modifier($1);
630 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
631 warn "Encountered search modifier: $1\n" if $self->debug;
636 if (!$struct->top_plan) {
637 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
638 } elsif ($2 =~ /^[ty1]/i) {
639 $struct->new_modifier($mod);
643 } elsif (/$group_start_re/) { # start of an explicit group
644 warn "Encountered explicit group start\n" if $self->debug;
646 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
647 $struct->add_node( $substruct ) if ($substruct);
651 } elsif (/$and_re/) { # ANDed expression
653 next if ($last_type eq 'AND');
654 next if ($last_type eq 'OR');
655 warn "Encountered AND\n" if $self->debug;
658 my ($RHS, $subremainder) = $self->decompose( '('.$_.')', $current_class, $recursing + 1 );
661 $struct = $self->new_plan( level => $recursing, joiner => '&' );
662 $struct->add_node($_) for ($LHS, $RHS);
665 } elsif (/$or_re/) { # ORed expression
667 next if ($last_type eq 'AND');
668 next if ($last_type eq 'OR');
669 warn "Encountered OR\n" if $self->debug;
672 my ($RHS, $subremainder) = $self->decompose( '('.$_.')', $current_class, $recursing + 1 );
675 $struct = $self->new_plan( level => $recursing, joiner => '|' );
676 $struct->add_node($_) for ($LHS, $RHS);
679 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
680 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
682 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
684 my $facet_value = [ split '\s*#\s*', $3 ];
685 $struct->new_facet( $facet => $facet_value, $negate );
689 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
691 if ($last_type eq 'CLASS') {
692 $struct->remove_last_node( $current_class );
693 warn "Encountered class change with no searches!\n" if $self->debug;
696 warn "Encountered class change: $1\n" if $self->debug;
698 $current_class = $struct->classed_node( $1 )->requested_class();
701 $last_type = 'CLASS';
702 } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
703 warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
705 my $req_ness = $1 || '';
708 if (!$phrase_helper) {
709 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
711 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
712 $struct->add_node( $substruct ) if ($substruct);
715 warn "Directly parsing the phrase subquery\n" if $self->debug;
716 $struct->joiner( '&' );
718 my $class_node = $struct->classed_node($current_class);
720 if ($req_ness eq $pkg->operator('disallowed')) {
721 $class_node->add_dummy_atom( node => $class_node );
722 $class_node->add_unphrase( $phrase );
724 #$phrase =~ s/(^|\s)\b/$1-/g;
726 $class_node->add_phrase( $phrase );
734 # } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
735 # warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
739 # my $class_node = $struct->classed_node($current_class);
740 # $class_node->add_phrase( $phrase );
742 # $struct->joiner( '&' );
745 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
746 warn "Encountered atom: $1\n" if $self->debug;
747 warn "Remainder: $'\n" if $self->debug;
755 my $class_node = $struct->classed_node($current_class);
757 my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
758 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
760 if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
761 # $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
762 # $class_node->add_unphrase( $atom ) if ($prefix eq '!');
764 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
765 $struct->joiner( '&' );
774 scalar(@{$struct->query_nodes}) == 0 &&
775 scalar(@{$struct->filters}) == 0 &&
778 return $struct if !wantarray;
779 return ($struct, $remainder);
782 sub find_class_index {
786 my ($class_part, @field_parts) = split '\|', $class;
787 $class_part ||= $class;
789 for my $idx ( 0 .. scalar(@$query) - 1 ) {
790 next unless ref($$query[$idx]);
791 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
794 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
801 $self->{core_limit} = $l if ($l);
802 return $self->{core_limit};
808 $self->{superpage} = $l if ($l);
809 return $self->{superpage};
815 $self->{superpage_size} = $l if ($l);
816 return $self->{superpage_size};
820 #-------------------------------
821 package QueryParser::_util;
823 # At this level, joiners are always & or |. This is not
824 # the external, configurable representation of joiners that
825 # defaults to # && and ||.
829 return (not ref $str and ($str eq '&' or $str eq '|'));
832 sub default_joiner { '&' }
834 # 0 for different, 1 for the same.
835 sub compare_abstract_atoms {
836 my ($left, $right) = @_;
838 foreach (qw/prefix suffix content/) {
839 no warnings; # undef can stand in for '' here
840 return 0 unless $left->{$_} eq $right->{$_};
846 sub fake_abstract_atom_from_phrase {
849 my $qp_class = shift || 'QueryParser';
854 $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
859 "type" => "atom", "prefix" => $prefix, "suffix" => '"',
864 sub find_arrays_in_abstract {
868 foreach my $key (keys %$hash) {
869 if (ref $hash->{$key} eq "ARRAY") {
870 push @arrays, $hash->{$key};
871 foreach (@{$hash->{$key}}) {
872 push @arrays, find_arrays_in_abstract($_);
880 #-------------------------------
881 package QueryParser::Canonicalize; # not OO
883 sub _abstract_query2str_filter {
885 my $qp_class = shift || 'QueryParser';
886 my $qpconfig = $QueryParser::parser_config{$qp_class};
890 $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
892 join(",", @{$f->{args}})
896 sub _abstract_query2str_modifier {
898 my $qp_class = shift || 'QueryParser';
899 my $qpconfig = $QueryParser::parser_config{$qp_class};
901 return $qpconfig->{operators}{modifier} . $f;
904 # This should produce an equivalent query to the original, given an
906 sub abstract_query2str_impl {
907 my $abstract_query = shift;
908 my $depth = shift || 0;
910 my $qp_class ||= shift || 'QueryParser';
911 my $qpconfig = $QueryParser::parser_config{$qp_class};
913 my $gs = $qpconfig->{operators}{group_start};
914 my $ge = $qpconfig->{operators}{group_end};
915 my $and = $qpconfig->{operators}{and};
916 my $or = $qpconfig->{operators}{or};
921 if (exists $abstract_query->{type}) {
922 if ($abstract_query->{type} eq 'query_plan') {
923 $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
924 exists $abstract_query->{filters};
925 $needs_group += scalar(@{$abstract_query->{filters}}) if exists $abstract_query->{filters};
929 $q .= join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
930 exists $abstract_query->{modifiers};
931 $needs_group += scalar(@{$abstract_query->{modifiers}}) if exists $abstract_query->{modifiers};
932 } elsif ($abstract_query->{type} eq 'node') {
933 if ($abstract_query->{alias}) {
934 $q .= " " . $abstract_query->{alias};
935 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
937 $q .= " " . $abstract_query->{class};
938 $q .= "|$_" foreach @{$abstract_query->{fields}};
941 } elsif ($abstract_query->{type} eq 'atom') {
942 my $prefix = $abstract_query->{prefix} || '';
943 $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
945 ($abstract_query->{content} || '') .
946 ($abstract_query->{suffix} || '');
948 } elsif ($abstract_query->{type} eq 'facet') {
949 # facet syntax [ # ] is hardcoded I guess?
950 my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
951 $q .= $prefix . $abstract_query->{name} . "[" .
952 join(" # ", @{$abstract_query->{values}}) . "]";
957 if (exists $abstract_query->{children}) {
958 my $op = (keys(%{$abstract_query->{children}}))[0];
960 " " . ($op eq '&' ? '' : $or) . " ",
962 abstract_query2str_impl($_, $depth + 1, $qp_class)
963 } @{$abstract_query->{children}{$op}}
965 $needs_group += scalar(@{$abstract_query->{children}{$op}});
966 } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
967 my $op = (keys(%{$abstract_query}))[0];
969 " " . ($op eq '&' ? $and : $or) . " ",
971 abstract_query2str_impl($_, $depth + 1, $qp_class)
972 } @{$abstract_query->{$op}}
974 $needs_group += scalar(@{$abstract_query->{$op}});
978 $q = $gs . $q . $ge if ($needs_group > 1 and $depth);
983 #-------------------------------
984 package QueryParser::query_plan;
988 return undef unless ref($self);
989 return $self->{QueryParser};
994 $pkg = ref($pkg) || $pkg;
995 my %args = (query => [], joiner => '&', @_);
997 return bless \%args => $pkg;
1002 my $pkg = ref($self) || $self;
1003 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1004 $self->add_node( $node );
1010 my $pkg = ref($self) || $self;
1015 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1016 $self->add_node( $node );
1023 my $pkg = ref($self) || $self;
1028 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1029 $self->add_filter( $node );
1035 sub _merge_filters {
1036 my $left_filter = shift;
1037 my $right_filter = shift;
1040 return undef unless $left_filter or $right_filter;
1041 return $right_filter unless $left_filter;
1042 return $left_filter unless $right_filter;
1044 my $args = $left_filter->{args} || [];
1047 push(@$args, @{$right_filter->{args}});
1050 # find the intersect values
1052 map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1053 $args = [ grep { $new_vals{$_} } @$args ];
1056 $left_filter->{args} = $args;
1057 return $left_filter;
1060 sub collapse_filters {
1064 # start by merging any filters at this level.
1065 # like-level filters are always ORed together
1068 my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1070 $cur_filter = shift @cur_filters;
1071 my $args = $cur_filter->{args} || [];
1072 $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1075 # next gather the collapsed filters from sub-plans and
1076 # merge them with our own
1078 my @subquery = @{$self->{query}};
1081 my $blob = shift @subquery;
1082 shift @subquery; # joiner
1083 next unless $blob->isa('QueryParser::query_plan');
1084 my $sub_filter = $blob->collapse_filters($name);
1085 $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1088 if ($self->QueryParser->debug) {
1089 my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1090 warn "collapse_filters($name) => [@args]\n";
1098 my $needle = shift;;
1099 return undef unless ($needle);
1101 my $filter = $self->collapse_filters($needle);
1103 warn "find_filter($needle) => " .
1104 (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n"
1105 if $self->QueryParser->debug;
1107 return $filter ? ($filter) : ();
1112 my $needle = shift;;
1113 return undef unless ($needle);
1114 return grep { $_->name eq $needle } @{ $self->modifiers };
1119 my $pkg = ref($self) || $self;
1122 my $node = do{$pkg.'::modifier'}->new( $name );
1123 $self->add_modifier( $node );
1130 my $requested_class = shift;
1133 for my $n (@{$self->{query}}) {
1134 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1135 if ($n->requested_class eq $requested_class) {
1142 $node = $self->new_node;
1143 $node->requested_class( $requested_class );
1149 sub remove_last_node {
1151 my $requested_class = shift;
1153 my $old = pop(@{$self->query_nodes});
1154 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1161 return $self->{query};
1168 $self->{query} ||= [];
1169 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1170 push(@{$self->{query}}, $node);
1178 return $self->{level} ? 0 : 1;
1183 return $self->{level};
1190 $self->{joiner} = $joiner if ($joiner);
1191 return $self->{joiner};
1196 $self->{modifiers} ||= [];
1197 return $self->{modifiers};
1202 my $modifier = shift;
1204 $self->{modifiers} ||= [];
1205 $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1207 push(@{$self->{modifiers}}, $modifier);
1214 $self->{facets} ||= [];
1215 return $self->{facets};
1222 $self->{facets} ||= [];
1223 $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1225 push(@{$self->{facets}}, $facet);
1232 $self->{filters} ||= [];
1233 return $self->{filters};
1240 $self->{filters} ||= [];
1242 push(@{$self->{filters}}, $filter);
1247 # %opts supports two options at this time:
1249 # If true, do not do anything to the phrases and unphrases
1250 # fields on any discovered nodes.
1252 # If true, also return the query parser config as part of the blob.
1253 # This will get set back to 0 before recursion to avoid repetition.
1254 sub to_abstract_query {
1258 my $pkg = ref $self->QueryParser || $self->QueryParser;
1260 my $abstract_query = {
1261 type => "query_plan",
1262 filters => [map { $_->to_abstract_query } @{$self->filters}],
1263 modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1266 if ($opts{with_config}) {
1267 $opts{with_config} = 0;
1268 $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1273 for my $qnode (@{$self->query_nodes}) {
1274 # Remember: qnode can be a joiner string, a node, or another query_plan
1276 if (QueryParser::_util::is_joiner($qnode)) {
1277 if ($abstract_query->{children}) {
1278 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1279 next if $open_joiner eq $qnode;
1281 my $oldroot = $abstract_query->{children};
1283 $abstract_query->{children} = {$qnode => $kids};
1285 $abstract_query->{children} = {$qnode => $kids};
1288 push @$kids, $qnode->to_abstract_query(%opts);
1292 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1293 return $abstract_query;
1297 #-------------------------------
1298 package QueryParser::query_plan::node;
1300 $Data::Dumper::Indent = 0;
1304 $pkg = ref($pkg) || $pkg;
1307 return bless \%args => $pkg;
1312 my $pkg = ref($self) || $self;
1313 return do{$pkg.'::atom'}->new( @_ );
1316 sub requested_class { # also split into classname, fields and alias
1322 my (undef, $alias) = split '#', $class;
1324 $class =~ s/#[^|]+//;
1325 ($alias, @afields) = split '\|', $alias;
1328 my @fields = @afields;
1329 my ($class_part, @field_parts) = split '\|', $class;
1330 for my $f (@field_parts) {
1331 push(@fields, $f) unless (grep { $f eq $_ } @fields);
1334 $class_part ||= $class;
1336 $self->{requested_class} = $class;
1337 $self->{alias} = $alias if $alias;
1338 $self->{alias_fields} = \@afields if $alias;
1339 $self->{classname} = $class_part;
1340 $self->{fields} = \@fields;
1343 return $self->{requested_class};
1350 $self->{plan} = $plan if ($plan);
1351 return $self->{plan};
1358 $self->{alias} = $alias if ($alias);
1359 return $self->{alias};
1366 $self->{alias_fields} = $alias if ($alias);
1367 return $self->{alias_fields};
1374 $self->{classname} = $class if ($class);
1375 return $self->{classname};
1382 $self->{fields} ||= [];
1383 $self->{fields} = \@fields if (@fields);
1384 return $self->{fields};
1391 $self->{phrases} ||= [];
1392 $self->{phrases} = \@phrases if (@phrases);
1393 return $self->{phrases};
1400 $self->{unphrases} ||= [];
1401 $self->{unphrases} = \@phrases if (@phrases);
1402 return $self->{unphrases};
1409 push(@{$self->phrases}, $phrase);
1418 push(@{$self->unphrases}, $phrase);
1425 my @query_atoms = @_;
1427 $self->{query_atoms} ||= [];
1428 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1429 return $self->{query_atoms};
1437 my $content = $atom;
1440 $atom = $self->new_atom( content => $content, @parts );
1443 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1444 push(@{$self->query_atoms}, $atom);
1449 sub add_dummy_atom {
1453 my $atom = $self->new_atom( @parts, dummy => 1 );
1455 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1456 push(@{$self->query_atoms}, $atom);
1461 # This will find up to one occurence of @$short_list within @$long_list, and
1462 # replace it with the single atom $replacement.
1463 sub replace_phrase_in_abstract_query {
1464 my ($self, $short_list, $long_list, $replacement) = @_;
1468 my $goal = scalar @$short_list;
1470 for (my $i = 0; $i < scalar (@$long_list); $i++) {
1471 my $right = $long_list->[$i];
1473 if (QueryParser::_util::compare_abstract_atoms(
1474 $short_list->[scalar @already], $right
1477 } elsif (scalar @already) {
1482 if (scalar @already == $goal) {
1483 splice @$long_list, $already[0], scalar(@already), $replacement;
1492 sub to_abstract_query {
1496 my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
1498 my $abstract_query = {
1500 "alias" => $self->alias,
1501 "alias_fields" => $self->alias_fields,
1502 "class" => $self->classname,
1503 "fields" => $self->fields
1508 for my $qatom (@{$self->query_atoms}) {
1509 if (QueryParser::_util::is_joiner($qatom)) {
1510 if ($abstract_query->{children}) {
1511 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1512 next if $open_joiner eq $qatom;
1514 my $oldroot = $abstract_query->{children};
1516 $abstract_query->{children} = {$qatom => $kids};
1518 $abstract_query->{children} = {$qatom => $kids};
1521 push @$kids, $qatom->to_abstract_query;
1525 if ($self->{phrases} and not $opts{no_phrases}) {
1526 for my $phrase (@{$self->{phrases}}) {
1527 # Phrases appear duplication in a real QP tree, and we don't want
1528 # that duplication in our abstract query. So for all our phrases,
1529 # break them into atoms as QP would, and remove any matching
1530 # sequences of atoms from our abstract query.
1532 my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
1534 # For a well-behaved phrase, we should now have only one node
1535 # in the $tmptree query plan, and that node should have an
1536 # orderly list of atoms and joiners.
1538 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1542 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1544 )->{children}->{'&'}->[0]->{children}->{'&'};
1549 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1551 last if $self->replace_phrase_in_abstract_query(
1554 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
1562 # Do the same as the preceding block for unphrases (negated phrases).
1563 if ($self->{unphrases} and not $opts{no_phrases}) {
1564 for my $phrase (@{$self->{unphrases}}) {
1565 my $tmptree = $self->{plan}->{QueryParser}->new(
1566 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
1568 )->parse->parse_tree;
1571 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1575 $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1577 )->{children}->{'&'}->[0]->{children}->{'&'};
1582 QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1584 last if $self->replace_phrase_in_abstract_query(
1587 QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
1595 $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1596 return $abstract_query;
1599 #-------------------------------
1600 package QueryParser::query_plan::node::atom;
1604 $pkg = ref($pkg) || $pkg;
1607 return bless \%args => $pkg;
1612 return undef unless (ref $self);
1613 return $self->{node};
1618 return undef unless (ref $self);
1619 return $self->{content};
1624 return undef unless (ref $self);
1625 return $self->{prefix};
1630 return undef unless (ref $self);
1631 return $self->{suffix};
1634 sub to_abstract_query {
1638 (map { $_ => $self->$_ } qw/prefix suffix content/),
1642 #-------------------------------
1643 package QueryParser::query_plan::filter;
1647 $pkg = ref($pkg) || $pkg;
1650 return bless \%args => $pkg;
1655 return $self->{plan};
1660 return $self->{name};
1665 return $self->{negate};
1670 return $self->{args};
1673 sub to_abstract_query {
1677 map { $_ => $self->$_ } qw/name negate args/
1681 #-------------------------------
1682 package QueryParser::query_plan::facet;
1686 $pkg = ref($pkg) || $pkg;
1689 return bless \%args => $pkg;
1694 return $self->{plan};
1699 return $self->{name};
1704 return $self->{negate};
1709 return $self->{'values'};
1712 sub to_abstract_query {
1716 (map { $_ => $self->$_ } qw/name negate values/),
1721 #-------------------------------
1722 package QueryParser::query_plan::modifier;
1726 $pkg = ref($pkg) || $pkg;
1727 my $modifier = shift;
1730 return bless { name => $modifier, negate => $negate } => $pkg;
1735 return $self->{name};
1740 return $self->{negate};
1743 sub to_abstract_query {