5 use OpenSRF::Utils::JSON;
22 sub facet_class_count {
24 return @{$self->facet_classes};
27 sub search_class_count {
29 return @{$self->search_classes};
34 return @{$self->filters};
39 return @{$self->modifiers};
44 $class = ref($class) || $class;
46 $parser_config{$class}{custom_data} ||= {};
47 return $parser_config{$class}{custom_data};
52 $class = ref($class) || $class;
54 $parser_config{$class}{operators} ||= {};
55 return $parser_config{$class}{operators};
60 $class = ref($class) || $class;
62 $parser_config{$class}{filters} ||= [];
63 return $parser_config{$class}{filters};
68 $class = ref($class) || $class;
70 $parser_config{$class}{modifiers} ||= [];
71 return $parser_config{$class}{modifiers};
76 $class = ref($class) || $class;
80 my $self = bless {} => $class;
82 for my $o (keys %{QueryParser->operators}) {
83 $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
86 for my $opt ( keys %opts) {
87 $self->$opt( $opts{$opt} ) if ($self->can($opt));
95 my $pkg = ref($self) || $self;
96 return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
99 sub add_search_filter {
101 $pkg = ref($pkg) || $pkg;
104 return $filter if (grep { $_ eq $filter } @{$pkg->filters});
105 push @{$pkg->filters}, $filter;
109 sub add_search_modifier {
111 $pkg = ref($pkg) || $pkg;
112 my $modifier = shift;
114 return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
115 push @{$pkg->modifiers}, $modifier;
119 sub add_facet_class {
121 $pkg = ref($pkg) || $pkg;
124 return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
126 push @{$pkg->facet_classes}, $class;
127 $pkg->facet_fields->{$class} = [];
132 sub add_search_class {
134 $pkg = ref($pkg) || $pkg;
137 return $class if (grep { $_ eq $class } @{$pkg->search_classes});
139 push @{$pkg->search_classes}, $class;
140 $pkg->search_fields->{$class} = [];
141 $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
148 $class = ref($class) || $class;
152 return undef unless ($opname);
154 $parser_config{$class}{operators} ||= {};
155 $parser_config{$class}{operators}{$opname} = $op if ($op);
157 return $parser_config{$class}{operators}{$opname};
162 $class = ref($class) || $class;
165 $parser_config{$class}{facet_classes} ||= [];
166 $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
167 return $parser_config{$class}{facet_classes};
172 $class = ref($class) || $class;
175 $parser_config{$class}{classes} ||= [];
176 $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
177 return $parser_config{$class}{classes};
180 sub add_query_normalizer {
182 $pkg = ref($pkg) || $pkg;
186 my $params = shift || [];
188 # do not add if function AND params are identical to existing member
189 return $func if (grep {
190 $_->{function} eq $func and
191 OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
192 } @{$pkg->query_normalizers->{$class}->{$field}});
194 push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
199 sub query_normalizers {
201 $pkg = ref($pkg) || $pkg;
206 $parser_config{$pkg}{normalizers} ||= {};
209 $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
210 return $parser_config{$pkg}{normalizers}{$class}{$field};
212 return $parser_config{$pkg}{normalizers}{$class};
216 return $parser_config{$pkg}{normalizers};
219 sub add_filter_normalizer {
221 $pkg = ref($pkg) || $pkg;
224 my $params = shift || [];
226 return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
228 push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
233 sub filter_normalizers {
235 $pkg = ref($pkg) || $pkg;
239 $parser_config{$pkg}{filter_normalizers} ||= {};
241 $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
242 return $parser_config{$pkg}{filter_normalizers}{$filter};
245 return $parser_config{$pkg}{filter_normalizers};
248 sub default_search_class {
250 $pkg = ref($pkg) || $pkg;
252 $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
254 return $QueryParser::parser_config{$pkg}{default_class};
257 sub remove_facet_class {
259 $pkg = ref($pkg) || $pkg;
262 return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
264 $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
265 delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
270 sub remove_search_class {
272 $pkg = ref($pkg) || $pkg;
275 return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
277 $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
278 delete $QueryParser::parser_config{$pkg}{fields}{$class};
283 sub add_facet_field {
285 $pkg = ref($pkg) || $pkg;
289 $pkg->add_facet_class( $class );
291 return { $class => $field } if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
293 push @{$pkg->facet_fields->{$class}}, $field;
295 return { $class => $field };
300 $class = ref($class) || $class;
302 $parser_config{$class}{facet_fields} ||= {};
303 return $parser_config{$class}{facet_fields};
306 sub add_search_field {
308 $pkg = ref($pkg) || $pkg;
312 $pkg->add_search_class( $class );
314 return { $class => $field } if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
316 push @{$pkg->search_fields->{$class}}, $field;
318 return { $class => $field };
323 $class = ref($class) || $class;
325 $parser_config{$class}{fields} ||= {};
326 return $parser_config{$class}{fields};
329 sub add_search_class_alias {
331 $pkg = ref($pkg) || $pkg;
335 $pkg->add_search_class( $class );
337 return { $class => $alias } if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
339 push @{$pkg->search_class_aliases->{$class}}, $alias;
341 return { $class => $alias };
344 sub search_class_aliases {
346 $class = ref($class) || $class;
348 $parser_config{$class}{class_map} ||= {};
349 return $parser_config{$class}{class_map};
352 sub add_search_field_alias {
354 $pkg = ref($pkg) || $pkg;
359 return { $class => { $field => $alias } } if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
361 push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
363 return { $class => { $field => $alias } };
366 sub search_field_aliases {
368 $class = ref($class) || $class;
370 $parser_config{$class}{field_alias_map} ||= {};
371 return $parser_config{$class}{field_alias_map};
374 sub remove_facet_field {
376 $pkg = ref($pkg) || $pkg;
380 return { $class => $field } if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
382 $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
384 return { $class => $field };
387 sub remove_search_field {
389 $pkg = ref($pkg) || $pkg;
393 return { $class => $field } if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
395 $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
397 return { $class => $field };
400 sub remove_search_field_alias {
402 $pkg = ref($pkg) || $pkg;
407 return { $class => { $field => $alias } } if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
409 $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
411 return { $class => { $field => $alias } };
414 sub remove_search_class_alias {
416 $pkg = ref($pkg) || $pkg;
420 return { $class => $alias } if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
422 $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
424 return { $class => $alias };
430 $self->{_debug} = $q if (defined $q);
431 return $self->{_debug};
437 $self->{_query} = $q if (defined $q);
438 return $self->{_query};
444 $self->{_parse_tree} = $q if (defined $q);
445 return $self->{_parse_tree};
450 my $pkg = ref($self) || $self;
451 warn " ** parse package is $pkg\n" if $self->debug;
454 $self->query( shift() )
463 my $pkg = ref($self) || $self;
465 warn " ** decompose package is $pkg\n" if $self->debug;
468 my $current_class = shift || $self->default_search_class;
470 my $recursing = shift || 0;
471 my $phrase_helper = shift || 0;
473 # Build the search class+field uber-regexp
474 my $search_class_re = '^\s*(';
478 for my $class ( keys %{$pkg->search_fields} ) {
480 for my $field ( @{$pkg->search_fields->{$class}} ) {
482 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
484 s/(^|\s+)$alias[:=]/$1$class\|$field:/g;
488 $search_class_re .= '|' unless ($first_class);
490 $search_class_re .= $class . '(?:\|\w+)*';
491 $seen_classes{$class} = 1;
494 for my $class ( keys %{$pkg->search_class_aliases} ) {
496 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
498 s/(^|[^|])\b$alias\|/$1$class\|/g;
499 s/(^|[^|])\b$alias[:=]/$1$class:/g;
502 if (!$seen_classes{$class}) {
503 $search_class_re .= '|' unless ($first_class);
506 $search_class_re .= $class . '(?:\|\w+)*';
507 $seen_classes{$class} = 1;
510 $search_class_re .= '):';
512 warn " ** Search class RE: $search_class_re\n" if $self->debug;
514 my $required_re = $pkg->operator('required');
515 $required_re = qr/\Q$required_re\E/;
517 my $disallowed_re = $pkg->operator('disallowed');
518 $disallowed_re = qr/\Q$disallowed_re\E/;
520 my $and_re = $pkg->operator('and');
521 $and_re = qr/^\s*\Q$and_re\E/;
523 my $or_re = $pkg->operator('or');
524 $or_re = qr/^\s*\Q$or_re\E/;
526 my $group_start_re = $pkg->operator('group_start');
527 $group_start_re = qr/^\s*\Q$group_start_re\E/;
529 my $group_end = $pkg->operator('group_end');
530 my $group_end_re = qr/^\s*\Q$group_end\E/;
532 my $modifier_tag_re = $pkg->operator('modifier');
533 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
536 # Build the filter and modifier uber-regexps
537 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
538 warn " Facet RE: $facet_re\n" if $self->debug;
540 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
541 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
543 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
544 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
546 my $struct = $self->new_plan( level => $recursing );
550 while (!$remainder) {
551 if (/^\s*$/) { # end of an explicit group
553 } elsif (/$group_end_re/) { # end of an explicit group
554 warn "Encountered explicit group end\n" if $self->debug;
557 $remainder = $struct->top_plan ? '' : $';
560 } elsif ($self->filter_count && /$filter_re/) { # found a filter
561 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
563 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
565 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
568 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
569 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
571 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
573 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
576 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
577 warn "Encountered search modifier: $1\n" if $self->debug;
580 if (!$struct->top_plan) {
581 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
583 $struct->new_modifier($1);
587 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
588 warn "Encountered search modifier: $1\n" if $self->debug;
593 if (!$struct->top_plan) {
594 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
595 } elsif ($2 =~ /^[ty1]/i) {
596 $struct->new_modifier($mod);
600 } elsif (/$group_start_re/) { # start of an explicit group
601 warn "Encountered explicit group start\n" if $self->debug;
603 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
604 $struct->add_node( $substruct ) if ($substruct);
608 } elsif (/$and_re/) { # ANDed expression
610 next if ($last_type eq 'AND');
611 next if ($last_type eq 'OR');
612 warn "Encountered AND\n" if $self->debug;
614 $struct->joiner( '&' );
617 } elsif (/$or_re/) { # ORed expression
619 next if ($last_type eq 'AND');
620 next if ($last_type eq 'OR');
621 warn "Encountered OR\n" if $self->debug;
623 $struct->joiner( '|' );
626 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
627 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
629 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
631 my $facet_value = [ split '\s*#\s*', $3 ];
632 $struct->new_facet( $facet => $facet_value, $negate );
636 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
638 if ($last_type eq 'CLASS') {
639 $struct->remove_last_node( $current_class );
640 warn "Encountered class change with no searches!\n" if $self->debug;
643 warn "Encountered class change: $1\n" if $self->debug;
646 $struct->classed_node( $current_class );
649 $last_type = 'CLASS';
650 } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
651 warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
653 my $req_ness = $1 || '';
656 if (!$phrase_helper) {
657 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
659 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
660 $struct->add_node( $substruct ) if ($substruct);
663 warn "Directly parsing the phrase subquery\n" if $self->debug;
664 $struct->joiner( '&' );
666 my $class_node = $struct->classed_node($current_class);
668 if ($req_ness eq $pkg->operator('disallowed')) {
669 $class_node->add_dummy_atom( node => $class_node );
670 $class_node->add_unphrase( $phrase );
672 #$phrase =~ s/(^|\s)\b/$1-/g;
674 $class_node->add_phrase( $phrase );
682 # } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
683 # warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
687 # my $class_node = $struct->classed_node($current_class);
688 # $class_node->add_phrase( $phrase );
690 # $struct->joiner( '&' );
693 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
694 warn "Encountered atom: $1\n" if $self->debug;
695 warn "Remainder: $'\n" if $self->debug;
703 my $class_node = $struct->classed_node($current_class);
705 my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
706 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
708 if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
709 # $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
710 # $class_node->add_unphrase( $atom ) if ($prefix eq '!');
712 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
713 $struct->joiner( '&' );
721 $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
723 return $struct if !wantarray;
724 return ($struct, $remainder);
727 sub find_class_index {
731 my ($class_part, @field_parts) = split '\|', $class;
732 $class_part ||= $class;
734 for my $idx ( 0 .. scalar(@$query) - 1 ) {
735 next unless ref($$query[$idx]);
736 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
739 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
746 $self->{core_limit} = $l if ($l);
747 return $self->{core_limit};
753 $self->{superpage} = $l if ($l);
754 return $self->{superpage};
760 $self->{superpage_size} = $l if ($l);
761 return $self->{superpage_size};
765 #-------------------------------
766 package QueryParser::query_plan;
770 return undef unless ref($self);
771 return $self->{QueryParser};
776 $pkg = ref($pkg) || $pkg;
777 my %args = (query => [], joiner => '&', @_);
779 return bless \%args => $pkg;
784 my $pkg = ref($self) || $self;
785 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
786 $self->add_node( $node );
792 my $pkg = ref($self) || $self;
797 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
798 $self->add_node( $node );
805 my $pkg = ref($self) || $self;
810 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
811 $self->add_filter( $node );
819 return undef unless ($needle);
820 return grep { $_->name eq $needle } @{ $self->filters };
826 return undef unless ($needle);
827 return grep { $_->name eq $needle } @{ $self->modifiers };
832 my $pkg = ref($self) || $self;
835 my $node = do{$pkg.'::modifier'}->new( $name );
836 $self->add_modifier( $node );
843 my $requested_class = shift;
846 for my $n (@{$self->{query}}) {
847 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
848 if ($n->requested_class eq $requested_class) {
855 $node = $self->new_node;
856 $node->requested_class( $requested_class );
862 sub remove_last_node {
864 my $requested_class = shift;
866 my $old = pop(@{$self->query_nodes});
867 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
874 return $self->{query};
881 $self->{query} ||= [];
882 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
883 push(@{$self->{query}}, $node);
891 return $self->{level} ? 0 : 1;
896 return $self->{level};
903 $self->{joiner} = $joiner if ($joiner);
904 return $self->{joiner};
909 $self->{modifiers} ||= [];
910 return $self->{modifiers};
915 my $modifier = shift;
917 $self->{modifiers} ||= [];
918 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
920 push(@{$self->{modifiers}}, $modifier);
927 $self->{facets} ||= [];
928 return $self->{facets};
935 $self->{facets} ||= [];
936 return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
938 push(@{$self->{facets}}, $facet);
945 $self->{filters} ||= [];
946 return $self->{filters};
953 $self->{filters} ||= [];
954 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
956 push(@{$self->{filters}}, $filter);
962 #-------------------------------
963 package QueryParser::query_plan::node;
967 $pkg = ref($pkg) || $pkg;
970 return bless \%args => $pkg;
975 my $pkg = ref($self) || $self;
976 return do{$pkg.'::atom'}->new( @_ );
979 sub requested_class { # also split into classname and fields
984 my ($class_part, @field_parts) = split '\|', $class;
985 $class_part ||= $class;
987 $self->{requested_class} = $class;
988 $self->{classname} = $class_part;
989 $self->{fields} = \@field_parts;
992 return $self->{requested_class};
999 $self->{plan} = $plan if ($plan);
1000 return $self->{plan};
1007 $self->{classname} = $class if ($class);
1008 return $self->{classname};
1015 $self->{fields} ||= [];
1016 $self->{fields} = \@fields if (@fields);
1017 return $self->{fields};
1024 $self->{phrases} ||= [];
1025 $self->{phrases} = \@phrases if (@phrases);
1026 return $self->{phrases};
1033 $self->{unphrases} ||= [];
1034 $self->{unphrases} = \@phrases if (@phrases);
1035 return $self->{unphrases};
1042 push(@{$self->phrases}, $phrase);
1051 push(@{$self->unphrases}, $phrase);
1058 my @query_atoms = @_;
1060 $self->{query_atoms} ||= [];
1061 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1062 return $self->{query_atoms};
1070 my $content = $atom;
1073 $atom = $self->new_atom( content => $content, @parts );
1076 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1077 push(@{$self->query_atoms}, $atom);
1082 sub add_dummy_atom {
1086 my $atom = $self->new_atom( @parts, dummy => 1 );
1088 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1089 push(@{$self->query_atoms}, $atom);
1094 #-------------------------------
1095 package QueryParser::query_plan::node::atom;
1099 $pkg = ref($pkg) || $pkg;
1102 return bless \%args => $pkg;
1107 return undef unless (ref $self);
1108 return $self->{node};
1113 return undef unless (ref $self);
1114 return $self->{content};
1119 return undef unless (ref $self);
1120 return $self->{prefix};
1125 return undef unless (ref $self);
1126 return $self->{suffix};
1129 #-------------------------------
1130 package QueryParser::query_plan::filter;
1134 $pkg = ref($pkg) || $pkg;
1137 return bless \%args => $pkg;
1142 return $self->{plan};
1147 return $self->{name};
1152 return $self->{negate};
1157 return $self->{args};
1160 #-------------------------------
1161 package QueryParser::query_plan::facet;
1165 $pkg = ref($pkg) || $pkg;
1168 return bless \%args => $pkg;
1173 return $self->{plan};
1178 return $self->{name};
1183 return $self->{negate};
1188 return $self->{'values'};
1191 #-------------------------------
1192 package QueryParser::query_plan::modifier;
1196 $pkg = ref($pkg) || $pkg;
1197 my $modifier = shift;
1199 return bless \$modifier => $pkg;