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;
472 # Build the search class+field uber-regexp
473 my $search_class_re = '^\s*(';
477 for my $class ( keys %{$pkg->search_fields} ) {
479 for my $field ( @{$pkg->search_fields->{$class}} ) {
481 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
483 s/(^|\s+)$alias[:=]/$1$class\|$field:/g;
487 $search_class_re .= '|' unless ($first_class);
489 $search_class_re .= $class . '(?:\|\w+)*';
490 $seen_classes{$class} = 1;
493 for my $class ( keys %{$pkg->search_class_aliases} ) {
495 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
497 s/(^|[^|])\b$alias\|/$1$class\|/g;
498 s/(^|[^|])\b$alias[:=]/$1$class:/g;
501 if (!$seen_classes{$class}) {
502 $search_class_re .= '|' unless ($first_class);
505 $search_class_re .= $class . '(?:\|\w+)*';
506 $seen_classes{$class} = 1;
509 $search_class_re .= '):';
511 warn " ** Search class RE: $search_class_re\n" if $self->debug;
513 my $required_re = $pkg->operator('required');
514 $required_re = qr/\Q$required_re\E/;
516 my $disallowed_re = $pkg->operator('disallowed');
517 $disallowed_re = qr/\Q$disallowed_re\E/;
519 my $and_re = $pkg->operator('and');
520 $and_re = qr/^\s*\Q$and_re\E/;
522 my $or_re = $pkg->operator('or');
523 $or_re = qr/^\s*\Q$or_re\E/;
525 my $group_start_re = $pkg->operator('group_start');
526 $group_start_re = qr/^\s*\Q$group_start_re\E/;
528 my $group_end = $pkg->operator('group_end');
529 my $group_end_re = qr/^\s*\Q$group_end\E/;
531 my $modifier_tag_re = $pkg->operator('modifier');
532 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
535 # Build the filter and modifier uber-regexps
536 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
537 warn " Facet RE: $facet_re\n" if $self->debug;
539 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
540 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
542 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
543 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
545 my $struct = $self->new_plan( level => $recursing );
549 while (!$remainder) {
550 if (/^\s*$/) { # end of an explicit group
552 } elsif (/$group_end_re/) { # end of an explicit group
553 warn "Encountered explicit group end\n" if $self->debug;
556 $remainder = $struct->top_plan ? '' : $';
559 } elsif ($self->filter_count && /$filter_re/) { # found a filter
560 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
562 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
564 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
567 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
568 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
570 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
572 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
575 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
576 warn "Encountered search modifier: $1\n" if $self->debug;
579 if (!$struct->top_plan) {
580 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
582 $struct->new_modifier($1);
586 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
587 warn "Encountered search modifier: $1\n" if $self->debug;
592 if (!$struct->top_plan) {
593 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
594 } elsif ($2 =~ /^[ty1]/i) {
595 $struct->new_modifier($mod);
599 } elsif (/$group_start_re/) { # start of an explicit group
600 warn "Encountered explicit group start\n" if $self->debug;
602 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
603 $struct->add_node( $substruct ) if ($substruct);
607 } elsif (/$and_re/) { # ANDed expression
609 next if ($last_type eq 'AND');
610 next if ($last_type eq 'OR');
611 warn "Encountered AND\n" if $self->debug;
613 $struct->joiner( '&' );
616 } elsif (/$or_re/) { # ORed expression
618 next if ($last_type eq 'AND');
619 next if ($last_type eq 'OR');
620 warn "Encountered OR\n" if $self->debug;
622 $struct->joiner( '|' );
625 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
626 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
628 my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
630 my $facet_value = [ split '\s*#\s*', $3 ];
631 $struct->new_facet( $facet => $facet_value, $negate );
635 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
637 if ($last_type eq 'CLASS') {
638 $struct->remove_last_node( $current_class );
639 warn "Encountered class change with no searches!\n" if $self->debug;
642 warn "Encountered class change: $1\n" if $self->debug;
645 $struct->classed_node( $current_class );
648 $last_type = 'CLASS';
649 } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
650 warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
652 $struct->joiner( '&' );
656 my $class_node = $struct->classed_node($current_class);
658 if ($req_ness eq $pkg->operator('disallowed')) {
659 $class_node->add_dummy_atom( node => $class_node );
660 $class_node->add_unphrase( $phrase );
662 #$phrase =~ s/(^|\s)\b/$1-/g;
664 $class_node->add_phrase( $phrase );
669 # } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
670 # warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
674 # my $class_node = $struct->classed_node($current_class);
675 # $class_node->add_phrase( $phrase );
677 # $struct->joiner( '&' );
680 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
681 warn "Encountered atom: $1\n" if $self->debug;
682 warn "Remainder: $'\n" if $self->debug;
690 my $class_node = $struct->classed_node($current_class);
692 my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
693 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
695 if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
696 # $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
697 # $class_node->add_unphrase( $atom ) if ($prefix eq '!');
699 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
700 $struct->joiner( '&' );
708 $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
710 return $struct if !wantarray;
711 return ($struct, $remainder);
714 sub find_class_index {
718 my ($class_part, @field_parts) = split '\|', $class;
719 $class_part ||= $class;
721 for my $idx ( 0 .. scalar(@$query) - 1 ) {
722 next unless ref($$query[$idx]);
723 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
726 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
733 $self->{core_limit} = $l if ($l);
734 return $self->{core_limit};
740 $self->{superpage} = $l if ($l);
741 return $self->{superpage};
747 $self->{superpage_size} = $l if ($l);
748 return $self->{superpage_size};
752 #-------------------------------
753 package QueryParser::query_plan;
757 return undef unless ref($self);
758 return $self->{QueryParser};
763 $pkg = ref($pkg) || $pkg;
764 my %args = (query => [], joiner => '&', @_);
766 return bless \%args => $pkg;
771 my $pkg = ref($self) || $self;
772 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
773 $self->add_node( $node );
779 my $pkg = ref($self) || $self;
784 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
785 $self->add_node( $node );
792 my $pkg = ref($self) || $self;
797 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
798 $self->add_filter( $node );
806 return undef unless ($needle);
807 return grep { $_->name eq $needle } @{ $self->filters };
813 return undef unless ($needle);
814 return grep { $_->name eq $needle } @{ $self->modifiers };
819 my $pkg = ref($self) || $self;
822 my $node = do{$pkg.'::modifier'}->new( $name );
823 $self->add_modifier( $node );
830 my $requested_class = shift;
833 for my $n (@{$self->{query}}) {
834 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
835 if ($n->requested_class eq $requested_class) {
842 $node = $self->new_node;
843 $node->requested_class( $requested_class );
849 sub remove_last_node {
851 my $requested_class = shift;
853 my $old = pop(@{$self->query_nodes});
854 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
861 return $self->{query};
868 $self->{query} ||= [];
869 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
870 push(@{$self->{query}}, $node);
878 return $self->{level} ? 0 : 1;
883 return $self->{level};
890 $self->{joiner} = $joiner if ($joiner);
891 return $self->{joiner};
896 $self->{modifiers} ||= [];
897 return $self->{modifiers};
902 my $modifier = shift;
904 $self->{modifiers} ||= [];
905 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
907 push(@{$self->{modifiers}}, $modifier);
914 $self->{facets} ||= [];
915 return $self->{facets};
922 $self->{facets} ||= [];
923 return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
925 push(@{$self->{facets}}, $facet);
932 $self->{filters} ||= [];
933 return $self->{filters};
940 $self->{filters} ||= [];
941 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
943 push(@{$self->{filters}}, $filter);
949 #-------------------------------
950 package QueryParser::query_plan::node;
954 $pkg = ref($pkg) || $pkg;
957 return bless \%args => $pkg;
962 my $pkg = ref($self) || $self;
963 return do{$pkg.'::atom'}->new( @_ );
966 sub requested_class { # also split into classname and fields
971 my ($class_part, @field_parts) = split '\|', $class;
972 $class_part ||= $class;
974 $self->{requested_class} = $class;
975 $self->{classname} = $class_part;
976 $self->{fields} = \@field_parts;
979 return $self->{requested_class};
986 $self->{plan} = $plan if ($plan);
987 return $self->{plan};
994 $self->{classname} = $class if ($class);
995 return $self->{classname};
1002 $self->{fields} ||= [];
1003 $self->{fields} = \@fields if (@fields);
1004 return $self->{fields};
1011 $self->{phrases} ||= [];
1012 $self->{phrases} = \@phrases if (@phrases);
1013 return $self->{phrases};
1020 $self->{unphrases} ||= [];
1021 $self->{unphrases} = \@phrases if (@phrases);
1022 return $self->{unphrases};
1029 push(@{$self->phrases}, $phrase);
1038 push(@{$self->unphrases}, $phrase);
1045 my @query_atoms = @_;
1047 $self->{query_atoms} ||= [];
1048 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1049 return $self->{query_atoms};
1057 my $content = $atom;
1060 $atom = $self->new_atom( content => $content, @parts );
1063 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1064 push(@{$self->query_atoms}, $atom);
1069 sub add_dummy_atom {
1073 my $atom = $self->new_atom( @parts, dummy => 1 );
1075 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1076 push(@{$self->query_atoms}, $atom);
1081 #-------------------------------
1082 package QueryParser::query_plan::node::atom;
1086 $pkg = ref($pkg) || $pkg;
1089 return bless \%args => $pkg;
1094 return undef unless (ref $self);
1095 return $self->{node};
1100 return undef unless (ref $self);
1101 return $self->{content};
1106 return undef unless (ref $self);
1107 return $self->{prefix};
1112 return undef unless (ref $self);
1113 return $self->{suffix};
1116 #-------------------------------
1117 package QueryParser::query_plan::filter;
1121 $pkg = ref($pkg) || $pkg;
1124 return bless \%args => $pkg;
1129 return $self->{plan};
1134 return $self->{name};
1139 return $self->{negate};
1144 return $self->{args};
1147 #-------------------------------
1148 package QueryParser::query_plan::facet;
1152 $pkg = ref($pkg) || $pkg;
1155 return bless \%args => $pkg;
1160 return $self->{plan};
1165 return $self->{name};
1170 return $self->{negate};
1175 return $self->{'values'};
1178 #-------------------------------
1179 package QueryParser::query_plan::modifier;
1183 $pkg = ref($pkg) || $pkg;
1184 my $modifier = shift;
1186 return bless \$modifier => $pkg;