5 use OpenSRF::Utils::JSON;
21 sub facet_class_count {
23 return @{$self->facet_classes};
26 sub search_class_count {
28 return @{$self->search_classes};
33 return @{$self->filters};
38 return @{$self->modifiers};
43 $class = ref($class) || $class;
45 $parser_config{$class}{custom_data} ||= {};
46 return $parser_config{$class}{custom_data};
51 $class = ref($class) || $class;
53 $parser_config{$class}{operators} ||= {};
54 return $parser_config{$class}{operators};
59 $class = ref($class) || $class;
61 $parser_config{$class}{filters} ||= [];
62 return $parser_config{$class}{filters};
67 $class = ref($class) || $class;
69 $parser_config{$class}{modifiers} ||= [];
70 return $parser_config{$class}{modifiers};
75 $class = ref($class) || $class;
79 my $self = bless {} => $class;
81 for my $o (keys %{QueryParser->operators}) {
82 $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
85 for my $opt ( keys %opts) {
86 $self->$opt( $opts{$opt} ) if ($self->can($opt));
94 my $pkg = ref($self) || $self;
95 return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
98 sub add_search_filter {
100 $pkg = ref($pkg) || $pkg;
103 return $filter if (grep { $_ eq $filter } @{$pkg->filters});
104 push @{$pkg->filters}, $filter;
108 sub add_search_modifier {
110 $pkg = ref($pkg) || $pkg;
111 my $modifier = shift;
113 return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
114 push @{$pkg->modifiers}, $modifier;
118 sub add_facet_class {
120 $pkg = ref($pkg) || $pkg;
123 return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
125 push @{$pkg->facet_classes}, $class;
126 $pkg->facet_fields->{$class} = [];
131 sub add_search_class {
133 $pkg = ref($pkg) || $pkg;
136 return $class if (grep { $_ eq $class } @{$pkg->search_classes});
138 push @{$pkg->search_classes}, $class;
139 $pkg->search_fields->{$class} = [];
140 $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
147 $class = ref($class) || $class;
151 return undef unless ($opname);
153 $parser_config{$class}{operators} ||= {};
154 $parser_config{$class}{operators}{$opname} = $op if ($op);
156 return $parser_config{$class}{operators}{$opname};
161 $class = ref($class) || $class;
164 $parser_config{$class}{facet_classes} ||= [];
165 $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
166 return $parser_config{$class}{facet_classes};
171 $class = ref($class) || $class;
174 $parser_config{$class}{classes} ||= [];
175 $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
176 return $parser_config{$class}{classes};
179 sub add_query_normalizer {
181 $pkg = ref($pkg) || $pkg;
185 my $params = shift || [];
187 # do not add if function AND params are identical to existing member
188 return $func if (grep {
189 $_->{function} eq $func and
190 OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
191 } @{$pkg->query_normalizers->{$class}->{$field}});
193 push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
198 sub query_normalizers {
200 $pkg = ref($pkg) || $pkg;
205 $parser_config{$pkg}{normalizers} ||= {};
208 $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
209 return $parser_config{$pkg}{normalizers}{$class}{$field};
211 return $parser_config{$pkg}{normalizers}{$class};
215 return $parser_config{$pkg}{normalizers};
218 sub add_filter_normalizer {
220 $pkg = ref($pkg) || $pkg;
223 my $params = shift || [];
225 return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
227 push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
232 sub filter_normalizers {
234 $pkg = ref($pkg) || $pkg;
238 $parser_config{$pkg}{filter_normalizers} ||= {};
240 $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
241 return $parser_config{$pkg}{filter_normalizers}{$filter};
244 return $parser_config{$pkg}{filter_normalizers};
247 sub default_search_class {
249 $pkg = ref($pkg) || $pkg;
251 $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
253 return $QueryParser::parser_config{$pkg}{default_class};
256 sub remove_facet_class {
258 $pkg = ref($pkg) || $pkg;
261 return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
263 $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
264 delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
269 sub remove_search_class {
271 $pkg = ref($pkg) || $pkg;
274 return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
276 $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
277 delete $QueryParser::parser_config{$pkg}{fields}{$class};
282 sub add_facet_field {
284 $pkg = ref($pkg) || $pkg;
288 $pkg->add_facet_class( $class );
290 return { $class => $field } if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
292 push @{$pkg->facet_fields->{$class}}, $field;
294 return { $class => $field };
299 $class = ref($class) || $class;
301 $parser_config{$class}{facet_fields} ||= {};
302 return $parser_config{$class}{facet_fields};
305 sub add_search_field {
307 $pkg = ref($pkg) || $pkg;
311 $pkg->add_search_class( $class );
313 return { $class => $field } if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
315 push @{$pkg->search_fields->{$class}}, $field;
317 return { $class => $field };
322 $class = ref($class) || $class;
324 $parser_config{$class}{fields} ||= {};
325 return $parser_config{$class}{fields};
328 sub add_search_class_alias {
330 $pkg = ref($pkg) || $pkg;
334 $pkg->add_search_class( $class );
336 return { $class => $alias } if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
338 push @{$pkg->search_class_aliases->{$class}}, $alias;
340 return { $class => $alias };
343 sub search_class_aliases {
345 $class = ref($class) || $class;
347 $parser_config{$class}{class_map} ||= {};
348 return $parser_config{$class}{class_map};
351 sub add_search_field_alias {
353 $pkg = ref($pkg) || $pkg;
358 return { $class => { $field => $alias } } if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
360 push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
362 return { $class => { $field => $alias } };
365 sub search_field_aliases {
367 $class = ref($class) || $class;
369 $parser_config{$class}{field_alias_map} ||= {};
370 return $parser_config{$class}{field_alias_map};
373 sub remove_facet_field {
375 $pkg = ref($pkg) || $pkg;
379 return { $class => $field } if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
381 $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
383 return { $class => $field };
386 sub remove_search_field {
388 $pkg = ref($pkg) || $pkg;
392 return { $class => $field } if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
394 $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
396 return { $class => $field };
399 sub remove_search_field_alias {
401 $pkg = ref($pkg) || $pkg;
406 return { $class => { $field => $alias } } if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
408 $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
410 return { $class => { $field => $alias } };
413 sub remove_search_class_alias {
415 $pkg = ref($pkg) || $pkg;
419 return { $class => $alias } if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
421 $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
423 return { $class => $alias };
429 $self->{_debug} = $q if (defined $q);
430 return $self->{_debug};
436 $self->{_query} = $q if (defined $q);
437 return $self->{_query};
443 $self->{_parse_tree} = $q if (defined $q);
444 return $self->{_parse_tree};
449 my $pkg = ref($self) || $self;
450 warn " ** parse package is $pkg\n" if $self->debug;
453 $self->query( shift() )
462 my $pkg = ref($self) || $self;
464 warn " ** decompose package is $pkg\n" if $self->debug;
467 my $current_class = shift || $self->default_search_class;
469 my $recursing = shift || 0;
471 # Build the search class+field uber-regexp
472 my $search_class_re = '^\s*(';
476 for my $class ( keys %{$pkg->search_fields} ) {
478 for my $field ( @{$pkg->search_fields->{$class}} ) {
480 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
482 s/(^|\s+)$alias[:=]/$1$class\|$field:/g;
486 $search_class_re .= '|' unless ($first_class);
488 $search_class_re .= $class . '(?:\|\w+)*';
489 $seen_classes{$class} = 1;
492 for my $class ( keys %{$pkg->search_class_aliases} ) {
494 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
496 s/(^|[^|])\b$alias\|/$1$class\|/g;
497 s/(^|[^|])\b$alias[:=]/$1$class:/g;
500 $search_class_re .= '|' unless ($first_class);
503 $search_class_re .= $class . '(?:\|\w+)*' if (!$seen_classes{$class});
504 $seen_classes{$class} = 1;
506 $search_class_re .= '):';
508 warn " ** Search class RE: $search_class_re\n" if $self->debug;
510 my $required_re = $pkg->operator('required');
511 $required_re = qr/^\s*\Q$required_re\E/;
512 my $and_re = $pkg->operator('and');
513 $and_re = qr/^\s*\Q$and_re\E/;
515 my $or_re = $pkg->operator('or');
516 $or_re = qr/^\s*\Q$or_re\E/;
518 my $group_start_re = $pkg->operator('group_start');
519 $group_start_re = qr/^\s*\Q$group_start_re\E/;
521 my $group_end = $pkg->operator('group_end');
522 my $group_end_re = qr/^\s*\Q$group_end\E/;
524 my $modifier_tag_re = $pkg->operator('modifier');
525 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
528 # Build the filter and modifier uber-regexps
529 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
530 warn " Facet RE: $facet_re\n" if $self->debug;
532 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
533 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
535 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
536 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
538 my $struct = $self->new_plan( level => $recursing );
542 while (!$remainder) {
543 if (/^\s*$/) { # end of an explicit group
545 } elsif (/$group_end_re/) { # end of an explicit group
546 warn "Encountered explicit group end\n" if $self->debug;
549 $remainder = $struct->top_plan ? '' : $';
552 } elsif ($self->filter_count && /$filter_re/) { # found a filter
553 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
555 my $negate = ($1 eq '-') ? 1 : 0;
557 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
560 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
561 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
563 my $negate = ($1 eq '-') ? 1 : 0;
565 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
568 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
569 warn "Encountered search modifier: $1\n" if $self->debug;
572 if (!$struct->top_plan) {
573 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
575 $struct->new_modifier($1);
579 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
580 warn "Encountered search modifier: $1\n" if $self->debug;
585 if (!$struct->top_plan) {
586 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
587 } elsif ($2 =~ /^[ty1]/i) {
588 $struct->new_modifier($mod);
592 } elsif (/$group_start_re/) { # start of an explicit group
593 warn "Encountered explicit group start\n" if $self->debug;
595 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
596 $struct->add_node( $substruct ) if ($substruct);
600 } elsif (/$and_re/) { # ANDed expression
602 next if ($last_type eq 'AND');
603 next if ($last_type eq 'OR');
604 warn "Encountered AND\n" if $self->debug;
606 $struct->joiner( '&' );
609 } elsif (/$or_re/) { # ORed expression
611 next if ($last_type eq 'AND');
612 next if ($last_type eq 'OR');
613 warn "Encountered OR\n" if $self->debug;
615 $struct->joiner( '|' );
618 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
619 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
621 my $negate = ($1 eq '-') ? 1 : 0;
623 my $facet_value = [ split '\s*#\s*', $3 ];
624 $struct->new_facet( $facet => $facet_value, $negate );
628 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
630 if ($last_type eq 'CLASS') {
631 $struct->remove_last_node( $current_class );
632 warn "Encountered class change with no searches!\n" if $self->debug;
635 warn "Encountered class change: $1\n" if $self->debug;
638 $struct->classed_node( $current_class );
641 $last_type = 'CLASS';
642 } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
643 warn "Encountered phrase: $1\n" if $self->debug;
645 $struct->joiner( '&' );
648 my $class_node = $struct->classed_node($current_class);
649 $class_node->add_phrase( $phrase );
653 } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
654 warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
658 my $class_node = $struct->classed_node($current_class);
659 $class_node->add_phrase( $phrase );
661 $struct->joiner( '&' );
664 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
665 warn "Encountered atom: $1\n" if $self->debug;
666 warn "Remainder: $'\n" if $self->debug;
674 my $negator = ($atom =~ s/^-//o) ? '!' : '';
675 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
677 if (!grep { $atom eq $_ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
678 my $class_node = $struct->classed_node($current_class);
679 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
680 $struct->joiner( '&' );
688 $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
690 return $struct if !wantarray;
691 return ($struct, $remainder);
694 sub find_class_index {
698 my ($class_part, @field_parts) = split '\|', $class;
699 $class_part ||= $class;
701 for my $idx ( 0 .. scalar(@$query) - 1 ) {
702 next unless ref($$query[$idx]);
703 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
706 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
713 $self->{core_limit} = $l if ($l);
714 return $self->{core_limit};
720 $self->{superpage} = $l if ($l);
721 return $self->{superpage};
727 $self->{superpage_size} = $l if ($l);
728 return $self->{superpage_size};
732 #-------------------------------
733 package QueryParser::query_plan;
737 return undef unless ref($self);
738 return $self->{QueryParser};
743 $pkg = ref($pkg) || $pkg;
744 my %args = (query => [], joiner => '&', @_);
746 return bless \%args => $pkg;
751 my $pkg = ref($self) || $self;
752 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
753 $self->add_node( $node );
759 my $pkg = ref($self) || $self;
764 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
765 $self->add_node( $node );
772 my $pkg = ref($self) || $self;
777 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
778 $self->add_filter( $node );
786 return undef unless ($needle);
787 return grep { $_->name eq $needle } @{ $self->filters };
793 return undef unless ($needle);
794 return grep { $_->name eq $needle } @{ $self->modifiers };
799 my $pkg = ref($self) || $self;
802 my $node = do{$pkg.'::modifier'}->new( $name );
803 $self->add_modifier( $node );
810 my $requested_class = shift;
813 for my $n (@{$self->{query}}) {
814 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
815 if ($n->requested_class eq $requested_class) {
822 $node = $self->new_node;
823 $node->requested_class( $requested_class );
829 sub remove_last_node {
831 my $requested_class = shift;
833 my $old = pop(@{$self->query_nodes});
834 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
841 return $self->{query};
848 $self->{query} ||= [];
849 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
850 push(@{$self->{query}}, $node);
858 return $self->{level} ? 0 : 1;
863 return $self->{level};
870 $self->{joiner} = $joiner if ($joiner);
871 return $self->{joiner};
876 $self->{modifiers} ||= [];
877 return $self->{modifiers};
882 my $modifier = shift;
884 $self->{modifiers} ||= [];
885 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
887 push(@{$self->{modifiers}}, $modifier);
894 $self->{facets} ||= [];
895 return $self->{facets};
902 $self->{facets} ||= [];
903 return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
905 push(@{$self->{facets}}, $facet);
912 $self->{filters} ||= [];
913 return $self->{filters};
920 $self->{filters} ||= [];
921 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
923 push(@{$self->{filters}}, $filter);
929 #-------------------------------
930 package QueryParser::query_plan::node;
934 $pkg = ref($pkg) || $pkg;
937 return bless \%args => $pkg;
942 my $pkg = ref($self) || $self;
943 return do{$pkg.'::atom'}->new( @_ );
946 sub requested_class { # also split into classname and fields
951 my ($class_part, @field_parts) = split '\|', $class;
952 $class_part ||= $class;
954 $self->{requested_class} = $class;
955 $self->{classname} = $class_part;
956 $self->{fields} = \@field_parts;
959 return $self->{requested_class};
966 $self->{plan} = $plan if ($plan);
967 return $self->{plan};
974 $self->{classname} = $class if ($class);
975 return $self->{classname};
982 $self->{fields} ||= [];
983 $self->{fields} = \@fields if (@fields);
984 return $self->{fields};
991 $self->{phrases} ||= [];
992 $self->{phrases} = \@phrases if (@phrases);
993 return $self->{phrases};
1000 push(@{$self->phrases}, $phrase);
1007 my @query_atoms = @_;
1009 $self->{query_atoms} ||= [];
1010 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1011 return $self->{query_atoms};
1019 my $content = $atom;
1022 $atom = $self->new_atom( content => $content, @parts );
1025 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1026 push(@{$self->query_atoms}, $atom);
1031 #-------------------------------
1032 package QueryParser::query_plan::node::atom;
1036 $pkg = ref($pkg) || $pkg;
1039 return bless \%args => $pkg;
1044 return undef unless (ref $self);
1045 return $self->{node};
1050 return undef unless (ref $self);
1051 return $self->{content};
1056 return undef unless (ref $self);
1057 return $self->{prefix};
1062 return undef unless (ref $self);
1063 return $self->{suffix};
1066 #-------------------------------
1067 package QueryParser::query_plan::filter;
1071 $pkg = ref($pkg) || $pkg;
1074 return bless \%args => $pkg;
1079 return $self->{plan};
1084 return $self->{name};
1089 return $self->{negate};
1094 return $self->{args};
1097 #-------------------------------
1098 package QueryParser::query_plan::facet;
1102 $pkg = ref($pkg) || $pkg;
1105 return bless \%args => $pkg;
1110 return $self->{plan};
1115 return $self->{name};
1120 return $self->{negate};
1125 return $self->{'values'};
1128 #-------------------------------
1129 package QueryParser::query_plan::modifier;
1133 $pkg = ref($pkg) || $pkg;
1134 my $modifier = shift;
1136 return bless \$modifier => $pkg;