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 if (!$seen_classes{$class}) {
501 $search_class_re .= '|' unless ($first_class);
504 $search_class_re .= $class . '(?:\|\w+)*';
505 $seen_classes{$class} = 1;
508 $search_class_re .= '):';
510 warn " ** Search class RE: $search_class_re\n" if $self->debug;
512 my $required_re = $pkg->operator('required');
513 $required_re = qr/^\s*\Q$required_re\E/;
514 my $and_re = $pkg->operator('and');
515 $and_re = qr/^\s*\Q$and_re\E/;
517 my $or_re = $pkg->operator('or');
518 $or_re = qr/^\s*\Q$or_re\E/;
520 my $group_start_re = $pkg->operator('group_start');
521 $group_start_re = qr/^\s*\Q$group_start_re\E/;
523 my $group_end = $pkg->operator('group_end');
524 my $group_end_re = qr/^\s*\Q$group_end\E/;
526 my $modifier_tag_re = $pkg->operator('modifier');
527 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
530 # Build the filter and modifier uber-regexps
531 my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
532 warn " Facet RE: $facet_re\n" if $self->debug;
534 my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
535 my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
537 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
538 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
540 my $struct = $self->new_plan( level => $recursing );
544 while (!$remainder) {
545 if (/^\s*$/) { # end of an explicit group
547 } elsif (/$group_end_re/) { # end of an explicit group
548 warn "Encountered explicit group end\n" if $self->debug;
551 $remainder = $struct->top_plan ? '' : $';
554 } elsif ($self->filter_count && /$filter_re/) { # found a filter
555 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
557 my $negate = ($1 eq '-') ? 1 : 0;
559 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
562 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
563 warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
565 my $negate = ($1 eq '-') ? 1 : 0;
567 $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
570 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
571 warn "Encountered search modifier: $1\n" if $self->debug;
574 if (!$struct->top_plan) {
575 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
577 $struct->new_modifier($1);
581 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
582 warn "Encountered search modifier: $1\n" if $self->debug;
587 if (!$struct->top_plan) {
588 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
589 } elsif ($2 =~ /^[ty1]/i) {
590 $struct->new_modifier($mod);
594 } elsif (/$group_start_re/) { # start of an explicit group
595 warn "Encountered explicit group start\n" if $self->debug;
597 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
598 $struct->add_node( $substruct ) if ($substruct);
602 } elsif (/$and_re/) { # ANDed expression
604 next if ($last_type eq 'AND');
605 next if ($last_type eq 'OR');
606 warn "Encountered AND\n" if $self->debug;
608 $struct->joiner( '&' );
611 } elsif (/$or_re/) { # ORed expression
613 next if ($last_type eq 'AND');
614 next if ($last_type eq 'OR');
615 warn "Encountered OR\n" if $self->debug;
617 $struct->joiner( '|' );
620 } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
621 warn "Encountered facet: $1$2 => $3\n" if $self->debug;
623 my $negate = ($1 eq '-') ? 1 : 0;
625 my $facet_value = [ split '\s*#\s*', $3 ];
626 $struct->new_facet( $facet => $facet_value, $negate );
630 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
632 if ($last_type eq 'CLASS') {
633 $struct->remove_last_node( $current_class );
634 warn "Encountered class change with no searches!\n" if $self->debug;
637 warn "Encountered class change: $1\n" if $self->debug;
640 $struct->classed_node( $current_class );
643 $last_type = 'CLASS';
644 } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
645 warn "Encountered phrase: $1\n" if $self->debug;
647 $struct->joiner( '&' );
650 my $class_node = $struct->classed_node($current_class);
651 $class_node->add_phrase( $phrase );
655 } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
656 warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
660 my $class_node = $struct->classed_node($current_class);
661 $class_node->add_phrase( $phrase );
663 $struct->joiner( '&' );
666 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
667 warn "Encountered atom: $1\n" if $self->debug;
668 warn "Remainder: $'\n" if $self->debug;
676 my $negator = ($atom =~ s/^-//o) ? '!' : '';
677 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
679 if (!grep { $atom eq $_ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
680 my $class_node = $struct->classed_node($current_class);
681 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
682 $struct->joiner( '&' );
690 $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
692 return $struct if !wantarray;
693 return ($struct, $remainder);
696 sub find_class_index {
700 my ($class_part, @field_parts) = split '\|', $class;
701 $class_part ||= $class;
703 for my $idx ( 0 .. scalar(@$query) - 1 ) {
704 next unless ref($$query[$idx]);
705 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
708 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
715 $self->{core_limit} = $l if ($l);
716 return $self->{core_limit};
722 $self->{superpage} = $l if ($l);
723 return $self->{superpage};
729 $self->{superpage_size} = $l if ($l);
730 return $self->{superpage_size};
734 #-------------------------------
735 package QueryParser::query_plan;
739 return undef unless ref($self);
740 return $self->{QueryParser};
745 $pkg = ref($pkg) || $pkg;
746 my %args = (query => [], joiner => '&', @_);
748 return bless \%args => $pkg;
753 my $pkg = ref($self) || $self;
754 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
755 $self->add_node( $node );
761 my $pkg = ref($self) || $self;
766 my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
767 $self->add_node( $node );
774 my $pkg = ref($self) || $self;
779 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
780 $self->add_filter( $node );
788 return undef unless ($needle);
789 return grep { $_->name eq $needle } @{ $self->filters };
795 return undef unless ($needle);
796 return grep { $_->name eq $needle } @{ $self->modifiers };
801 my $pkg = ref($self) || $self;
804 my $node = do{$pkg.'::modifier'}->new( $name );
805 $self->add_modifier( $node );
812 my $requested_class = shift;
815 for my $n (@{$self->{query}}) {
816 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
817 if ($n->requested_class eq $requested_class) {
824 $node = $self->new_node;
825 $node->requested_class( $requested_class );
831 sub remove_last_node {
833 my $requested_class = shift;
835 my $old = pop(@{$self->query_nodes});
836 pop(@{$self->query_nodes}) if (@{$self->query_nodes});
843 return $self->{query};
850 $self->{query} ||= [];
851 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
852 push(@{$self->{query}}, $node);
860 return $self->{level} ? 0 : 1;
865 return $self->{level};
872 $self->{joiner} = $joiner if ($joiner);
873 return $self->{joiner};
878 $self->{modifiers} ||= [];
879 return $self->{modifiers};
884 my $modifier = shift;
886 $self->{modifiers} ||= [];
887 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
889 push(@{$self->{modifiers}}, $modifier);
896 $self->{facets} ||= [];
897 return $self->{facets};
904 $self->{facets} ||= [];
905 return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
907 push(@{$self->{facets}}, $facet);
914 $self->{filters} ||= [];
915 return $self->{filters};
922 $self->{filters} ||= [];
923 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
925 push(@{$self->{filters}}, $filter);
931 #-------------------------------
932 package QueryParser::query_plan::node;
936 $pkg = ref($pkg) || $pkg;
939 return bless \%args => $pkg;
944 my $pkg = ref($self) || $self;
945 return do{$pkg.'::atom'}->new( @_ );
948 sub requested_class { # also split into classname and fields
953 my ($class_part, @field_parts) = split '\|', $class;
954 $class_part ||= $class;
956 $self->{requested_class} = $class;
957 $self->{classname} = $class_part;
958 $self->{fields} = \@field_parts;
961 return $self->{requested_class};
968 $self->{plan} = $plan if ($plan);
969 return $self->{plan};
976 $self->{classname} = $class if ($class);
977 return $self->{classname};
984 $self->{fields} ||= [];
985 $self->{fields} = \@fields if (@fields);
986 return $self->{fields};
993 $self->{phrases} ||= [];
994 $self->{phrases} = \@phrases if (@phrases);
995 return $self->{phrases};
1002 push(@{$self->phrases}, $phrase);
1009 my @query_atoms = @_;
1011 $self->{query_atoms} ||= [];
1012 $self->{query_atoms} = \@query_atoms if (@query_atoms);
1013 return $self->{query_atoms};
1021 my $content = $atom;
1024 $atom = $self->new_atom( content => $content, @parts );
1027 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1028 push(@{$self->query_atoms}, $atom);
1033 #-------------------------------
1034 package QueryParser::query_plan::node::atom;
1038 $pkg = ref($pkg) || $pkg;
1041 return bless \%args => $pkg;
1046 return undef unless (ref $self);
1047 return $self->{node};
1052 return undef unless (ref $self);
1053 return $self->{content};
1058 return undef unless (ref $self);
1059 return $self->{prefix};
1064 return undef unless (ref $self);
1065 return $self->{suffix};
1068 #-------------------------------
1069 package QueryParser::query_plan::filter;
1073 $pkg = ref($pkg) || $pkg;
1076 return bless \%args => $pkg;
1081 return $self->{plan};
1086 return $self->{name};
1091 return $self->{negate};
1096 return $self->{args};
1099 #-------------------------------
1100 package QueryParser::query_plan::facet;
1104 $pkg = ref($pkg) || $pkg;
1107 return bless \%args => $pkg;
1112 return $self->{plan};
1117 return $self->{name};
1122 return $self->{negate};
1127 return $self->{'values'};
1130 #-------------------------------
1131 package QueryParser::query_plan::modifier;
1135 $pkg = ref($pkg) || $pkg;
1136 my $modifier = shift;
1138 return bless \$modifier => $pkg;