17 sub search_class_count {
19 return @{$self->search_classes};
24 return @{$self->filters};
29 return @{$self->modifiers};
34 $class = ref($class) || $class;
36 $parser_config{$class}{custom_data} ||= {};
37 return $parser_config{$class}{custom_data};
42 $class = ref($class) || $class;
44 $parser_config{$class}{operators} ||= {};
45 return $parser_config{$class}{operators};
50 $class = ref($class) || $class;
52 $parser_config{$class}{filters} ||= [];
53 return $parser_config{$class}{filters};
58 $class = ref($class) || $class;
60 $parser_config{$class}{modifiers} ||= [];
61 return $parser_config{$class}{modifiers};
66 $class = ref($class) || $class;
70 my $self = bless {} => $class;
72 for my $o (keys %{QueryParser->operators}) {
73 $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
76 for my $opt ( keys %opts) {
77 $self->$opt( $opts{$opt} ) if ($self->can($opt));
85 my $pkg = ref($self) || $self;
86 return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
89 sub add_search_filter {
91 $pkg = ref($pkg) || $pkg;
94 return $filter if (grep { $_ eq $filter } @{$pkg->filters});
95 push @{$pkg->filters}, $filter;
99 sub add_search_modifier {
101 $pkg = ref($pkg) || $pkg;
102 my $modifier = shift;
104 return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
105 push @{$pkg->modifiers}, $modifier;
109 sub add_search_class {
111 $pkg = ref($pkg) || $pkg;
114 return $class if (grep { $_ eq $class } @{$pkg->search_classes});
116 push @{$pkg->search_classes}, $class;
117 $pkg->search_fields->{$class} = [];
118 $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
125 $class = ref($class) || $class;
129 return undef unless ($opname);
131 $parser_config{$class}{operators} ||= {};
132 $parser_config{$class}{operators}{$opname} = $op if ($op);
134 return $parser_config{$class}{operators}{$opname};
139 $class = ref($class) || $class;
142 $parser_config{$class}{classes} ||= [];
143 $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
144 return $parser_config{$class}{classes};
147 sub add_query_normalizer {
149 $pkg = ref($pkg) || $pkg;
153 my $params = shift || [];
155 return $func if (grep { $_ eq $func } @{$pkg->query_normalizers->{$class}->{$field}});
157 push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
162 sub query_normalizers {
164 $pkg = ref($pkg) || $pkg;
169 $parser_config{$pkg}{normalizers} ||= {};
172 $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
173 return $parser_config{$pkg}{normalizers}{$class}{$field};
175 return $parser_config{$pkg}{normalizers}{$class};
179 return $parser_config{$pkg}{normalizers};
182 sub default_search_class {
184 $pkg = ref($pkg) || $pkg;
186 $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
188 return $QueryParser::parser_config{$pkg}{default_class};
191 sub remove_search_class {
193 $pkg = ref($pkg) || $pkg;
196 return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
198 $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
199 delete $QueryParser::parser_config{$pkg}{fields}{$class};
204 sub add_search_field {
206 $pkg = ref($pkg) || $pkg;
210 $pkg->add_search_class( $class );
212 return { $class => $field } if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
214 push @{$pkg->search_fields->{$class}}, $field;
216 return { $class => $field };
221 $class = ref($class) || $class;
223 $parser_config{$class}{fields} ||= {};
224 return $parser_config{$class}{fields};
227 sub add_search_class_alias {
229 $pkg = ref($pkg) || $pkg;
233 $pkg->add_search_class( $class );
235 return { $class => $alias } if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
237 push @{$pkg->search_class_aliases->{$class}}, $alias;
239 return { $class => $alias };
242 sub search_class_aliases {
244 $class = ref($class) || $class;
246 $parser_config{$class}{class_map} ||= {};
247 return $parser_config{$class}{class_map};
250 sub add_search_field_alias {
252 $pkg = ref($pkg) || $pkg;
257 return { $class => { $field => $alias } } if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
259 push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
261 return { $class => { $field => $alias } };
264 sub search_field_aliases {
266 $class = ref($class) || $class;
268 $parser_config{$class}{field_alias_map} ||= {};
269 return $parser_config{$class}{field_alias_map};
272 sub remove_search_field {
274 $pkg = ref($pkg) || $pkg;
278 return { $class => $field } if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
280 $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
282 return { $class => $field };
285 sub remove_search_field_alias {
287 $pkg = ref($pkg) || $pkg;
292 return { $class => { $field => $alias } } if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
294 $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
296 return { $class => { $field => $alias } };
299 sub remove_search_class_alias {
301 $pkg = ref($pkg) || $pkg;
305 return { $class => $alias } if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
307 $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
309 return { $class => $alias };
315 $self->{_debug} = $q if (defined $q);
316 return $self->{_debug};
322 $self->{_query} = $q if (defined $q);
323 return $self->{_query};
329 $self->{_parse_tree} = $q if (defined $q);
330 return $self->{_parse_tree};
337 $self->query( shift() )
346 my $pkg = ref($self) || $self;;
349 my $current_class = shift || $self->default_search_class;
351 my $recursing = shift || 0;
353 # Build the search class+field uber-regexp
354 my $search_class_re = '^\s*(';
357 for my $class ( keys %{$pkg->search_field_aliases} ) {
359 for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
361 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
363 s/\b$alias[:=]/$class\|$field:/g;
366 $search_class_re .= '|' unless ($first_class);
369 $search_class_re .= $class;
373 for my $class ( keys %{$pkg->search_class_aliases} ) {
375 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
377 s/(^|[^|])\b$alias\|/$1$class\|/g;
378 s/(^|[^|])\b$alias[:=]/$1$class:/g;
381 $search_class_re .= '|' unless ($first_class);
384 $search_class_re .= $class . '(?:\|\w+)*';
386 $search_class_re .= '):';
388 my $required_re = $pkg->operator('required');
389 $required_re = qr/^\s*\Q$required_re\E/;
390 my $and_re = $pkg->operator('and');
391 $and_re = qr/^\s*\Q$and_re\E/;
393 my $or_re = $pkg->operator('or');
394 $or_re = qr/^\s*\Q$or_re\E/;
396 my $group_start_re = $pkg->operator('group_start');
397 $group_start_re = qr/^\s*\Q$group_start_re\E/;
399 my $group_end = $pkg->operator('group_end');
400 my $group_end_re = qr/^\s*\Q$group_end\E/;
402 my $modifier_tag_re = $pkg->operator('modifier');
403 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
406 # Build the filter and modifier uber-regexps
407 my $filter_re = '^\s*(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
408 my $filter_as_class_re = '^\s*(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
410 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
411 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
413 my $struct = $self->new_plan( level => $recursing );
417 while (!$remainder) {
418 if (/$group_end_re/) { # end of an explicit group
419 warn "Encountered explicit group end\n" if $self->debug;
425 } elsif ($self->filter_count && /$filter_re/) { # found a filter
426 warn "Encountered search filter: $1 set to $2\n" if $self->debug;
429 $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
432 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
433 warn "Encountered search filter: $1 set to $2\n" if $self->debug;
436 $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
439 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
440 warn "Encountered search modifier: $1\n" if $self->debug;
443 if (!$struct->top_plan) {
444 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
446 $struct->new_modifier($1);
450 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
451 warn "Encountered search modifier: $1\n" if $self->debug;
456 if (!$struct->top_plan) {
457 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
458 } elsif ($2 =~ /^[ty1]/i) {
459 $struct->new_modifier($mod);
463 } elsif (/$group_start_re/) { # start of an explicit group
464 warn "Encountered explicit group start\n" if $self->debug;
466 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
467 $struct->add_node( $substruct );
471 } elsif (/$and_re/) { # ANDed expression
473 next if ($last_type eq 'AND');
474 next if ($last_type eq 'OR');
475 warn "Encountered AND\n" if $self->debug;
477 $struct->joiner( '&' );
480 } elsif (/$or_re/) { # ORed expression
482 next if ($last_type eq 'AND');
483 next if ($last_type eq 'OR');
484 warn "Encountered OR\n" if $self->debug;
486 $struct->joiner( '|' );
489 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
490 warn "Encountered class change: $1\n" if $self->debug;
493 $struct->classed_node( $current_class );
497 } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
498 warn "Encountered phrase: $1\n" if $self->debug;
500 $struct->joiner( '&' );
503 my $class_node = $struct->classed_node($current_class);
504 $class_node->add_phrase( $phrase );
508 } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
509 warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
513 my $class_node = $struct->classed_node($current_class);
514 $class_node->add_phrase( $phrase );
516 $struct->joiner( '&' );
519 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
520 warn "Encountered atom: $1\n" if $self->debug;
521 warn "Remainder: $'\n" if $self->debug;
526 my $class_node = $struct->classed_node($current_class);
527 my $negator = ($atom =~ s/^-//o) ? '!' : '';
528 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
530 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
531 $struct->joiner( '&' );
541 return $struct if !wantarray;
542 return ($struct, $remainder);
545 sub find_class_index {
549 my ($class_part, @field_parts) = split '\|', $class;
550 $class_part ||= $class;
552 for my $idx ( 0 .. scalar(@$query) - 1 ) {
553 next unless ref($$query[$idx]);
554 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
557 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
564 $self->{core_limit} = $l if ($l);
565 return $self->{core_limit};
571 $self->{superpage} = $l if ($l);
572 return $self->{superpage};
578 $self->{superpage_size} = $l if ($l);
579 return $self->{superpage_size};
583 #-------------------------------
584 package QueryParser::query_plan;
588 return undef unless ref($self);
589 return $self->{QueryParser};
594 $pkg = ref($pkg) || $pkg;
595 my %args = (joiner => '&', @_);
597 return bless \%args => $pkg;
602 my $pkg = ref($self) || $self;
603 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
604 $self->add_node( $node );
610 my $pkg = ref($self) || $self;
614 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args );
615 $self->add_filter( $node );
623 return undef unless ($needle);
624 return grep { $_->name eq $needle } @{ $self->filters };
630 return undef unless ($needle);
631 return grep { $_->name eq $needle } @{ $self->modifiers };
636 my $pkg = ref($self) || $self;
639 my $node = do{$pkg.'::modifier'}->new( $name );
640 $self->add_modifier( $node );
647 my $requested_class = shift;
650 for my $n (@{$self->{query}}) {
651 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
652 if ($n->requested_class eq $requested_class) {
659 $node = $self->new_node;
660 $node->requested_class( $requested_class );
668 return $self->{query};
675 $self->{query} ||= [];
676 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
677 push(@{$self->{query}}, $node);
685 return $self->{level} ? 0 : 1;
690 return $self->{level};
697 $self->{joiner} = $joiner if ($joiner);
698 return $self->{joiner};
703 $self->{modifiers} ||= [];
704 return $self->{modifiers};
709 my $modifier = shift;
711 $self->{modifiers} ||= [];
712 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
714 push(@{$self->{modifiers}}, $modifier);
721 $self->{filters} ||= [];
722 return $self->{filters};
729 $self->{filters} ||= [];
730 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
732 push(@{$self->{filters}}, $filter);
738 #-------------------------------
739 package QueryParser::query_plan::node;
743 $pkg = ref($pkg) || $pkg;
746 return bless \%args => $pkg;
751 my $pkg = ref($self) || $self;
752 return do{$pkg.'::atom'}->new( @_ );
755 sub requested_class { # also split into classname and fields
760 my ($class_part, @field_parts) = split '\|', $class;
761 $class_part ||= $class;
763 $self->{requested_class} = $class;
764 $self->{classname} = $class_part;
765 $self->{fields} = \@field_parts;
768 return $self->{requested_class};
775 $self->{plan} = $plan if ($plan);
776 return $self->{plan};
783 $self->{classname} = $class if ($class);
784 return $self->{classname};
791 $self->{fields} ||= [];
792 $self->{fields} = \@fields if (@fields);
793 return $self->{fields};
800 $self->{phrases} ||= [];
801 $self->{phrases} = \@phrases if (@phrases);
802 return $self->{phrases};
809 push(@{$self->phrases}, $phrase);
816 my @query_atoms = @_;
818 $self->{query_atoms} ||= [];
819 $self->{query_atoms} = \@query_atoms if (@query_atoms);
820 return $self->{query_atoms};
831 $atom = $self->new_atom( content => $content, @parts );
834 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
835 push(@{$self->query_atoms}, $atom);
840 #-------------------------------
841 package QueryParser::query_plan::node::atom;
845 $pkg = ref($pkg) || $pkg;
848 return bless \%args => $pkg;
853 return undef unless (ref $self);
854 return $self->{node};
859 return undef unless (ref $self);
860 return $self->{content};
865 return undef unless (ref $self);
866 return $self->{prefix};
871 return undef unless (ref $self);
872 return $self->{suffix};
875 #-------------------------------
876 package QueryParser::query_plan::filter;
880 $pkg = ref($pkg) || $pkg;
883 return bless \%args => $pkg;
888 return $self->{plan};
893 return $self->{name};
898 return $self->{args};
901 #-------------------------------
902 package QueryParser::query_plan::modifier;
906 $pkg = ref($pkg) || $pkg;
907 my $modifier = shift;
909 return bless \$modifier => $pkg;