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};
335 my $pkg = ref($self) || $self;
336 warn " ** parse package is $pkg\n" if $self->debug;
339 $self->query( shift() )
348 my $pkg = ref($self) || $self;
350 warn " ** decompose package is $pkg\n" if $self->debug;
353 my $current_class = shift || $self->default_search_class;
355 my $recursing = shift || 0;
357 # Build the search class+field uber-regexp
358 my $search_class_re = '^\s*(';
362 for my $class ( keys %{$pkg->search_fields} ) {
364 for my $field ( @{$pkg->search_fields->{$class}} ) {
366 for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
368 s/\b$alias[:=]/$class\|$field:/g;
372 $search_class_re .= '|' unless ($first_class);
374 $search_class_re .= $class . '(?:\|\w+)*';
375 $seeen_class{$class} = 1;
378 for my $class ( keys %{$pkg->search_class_aliases} ) {
380 for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
382 s/(^|[^|])\b$alias\|/$1$class\|/g;
383 s/(^|[^|])\b$alias[:=]/$1$class:/g;
386 $search_class_re .= '|' unless ($first_class);
389 $search_class_re .= $class . '(?:\|\w+)*' if (!$seeen_class{$class});
390 $seeen_class{$class} = 1;
392 $search_class_re .= '):';
394 warn " ** Search class RE: $search_class_re\n" if $self->debug;
396 my $required_re = $pkg->operator('required');
397 $required_re = qr/^\s*\Q$required_re\E/;
398 my $and_re = $pkg->operator('and');
399 $and_re = qr/^\s*\Q$and_re\E/;
401 my $or_re = $pkg->operator('or');
402 $or_re = qr/^\s*\Q$or_re\E/;
404 my $group_start_re = $pkg->operator('group_start');
405 $group_start_re = qr/^\s*\Q$group_start_re\E/;
407 my $group_end = $pkg->operator('group_end');
408 my $group_end_re = qr/^\s*\Q$group_end\E/;
410 my $modifier_tag_re = $pkg->operator('modifier');
411 $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
414 # Build the filter and modifier uber-regexps
415 my $filter_re = '^\s*(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
416 my $filter_as_class_re = '^\s*(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
418 my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
419 my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
421 my $struct = $self->new_plan( level => $recursing );
425 while (!$remainder) {
426 if (/$group_end_re/) { # end of an explicit group
427 warn "Encountered explicit group end\n" if $self->debug;
433 } elsif ($self->filter_count && /$filter_re/) { # found a filter
434 warn "Encountered search filter: $1 set to $2\n" if $self->debug;
437 $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
440 } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
441 warn "Encountered search filter: $1 set to $2\n" if $self->debug;
444 $struct->new_filter( $1 => [ split '[, ]+', $2 ] );
447 } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
448 warn "Encountered search modifier: $1\n" if $self->debug;
451 if (!$struct->top_plan) {
452 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
454 $struct->new_modifier($1);
458 } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
459 warn "Encountered search modifier: $1\n" if $self->debug;
464 if (!$struct->top_plan) {
465 warn " Search modifiers only allowed at the top level of the query\n" if $self->debug;
466 } elsif ($2 =~ /^[ty1]/i) {
467 $struct->new_modifier($mod);
471 } elsif (/$group_start_re/) { # start of an explicit group
472 warn "Encountered explicit group start\n" if $self->debug;
474 my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
475 $struct->add_node( $substruct );
479 } elsif (/$and_re/) { # ANDed expression
481 next if ($last_type eq 'AND');
482 next if ($last_type eq 'OR');
483 warn "Encountered AND\n" if $self->debug;
485 $struct->joiner( '&' );
488 } elsif (/$or_re/) { # ORed expression
490 next if ($last_type eq 'AND');
491 next if ($last_type eq 'OR');
492 warn "Encountered OR\n" if $self->debug;
494 $struct->joiner( '|' );
497 } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
498 warn "Encountered class change: $1\n" if $self->debug;
501 $struct->classed_node( $current_class );
505 } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
506 warn "Encountered phrase: $1\n" if $self->debug;
508 $struct->joiner( '&' );
511 my $class_node = $struct->classed_node($current_class);
512 $class_node->add_phrase( $phrase );
516 } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
517 warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
521 my $class_node = $struct->classed_node($current_class);
522 $class_node->add_phrase( $phrase );
524 $struct->joiner( '&' );
527 } elsif (/^\s*([^$group_end\s]+)/o) { # atom
528 warn "Encountered atom: $1\n" if $self->debug;
529 warn "Remainder: $'\n" if $self->debug;
534 my $class_node = $struct->classed_node($current_class);
535 my $negator = ($atom =~ s/^-//o) ? '!' : '';
536 my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
538 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
539 $struct->joiner( '&' );
549 return $struct if !wantarray;
550 return ($struct, $remainder);
553 sub find_class_index {
557 my ($class_part, @field_parts) = split '\|', $class;
558 $class_part ||= $class;
560 for my $idx ( 0 .. scalar(@$query) - 1 ) {
561 next unless ref($$query[$idx]);
562 return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
565 push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
572 $self->{core_limit} = $l if ($l);
573 return $self->{core_limit};
579 $self->{superpage} = $l if ($l);
580 return $self->{superpage};
586 $self->{superpage_size} = $l if ($l);
587 return $self->{superpage_size};
591 #-------------------------------
592 package QueryParser::query_plan;
596 return undef unless ref($self);
597 return $self->{QueryParser};
602 $pkg = ref($pkg) || $pkg;
603 my %args = (joiner => '&', @_);
605 return bless \%args => $pkg;
610 my $pkg = ref($self) || $self;
611 my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
612 $self->add_node( $node );
618 my $pkg = ref($self) || $self;
622 my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args );
623 $self->add_filter( $node );
631 return undef unless ($needle);
632 return grep { $_->name eq $needle } @{ $self->filters };
638 return undef unless ($needle);
639 return grep { $_->name eq $needle } @{ $self->modifiers };
644 my $pkg = ref($self) || $self;
647 my $node = do{$pkg.'::modifier'}->new( $name );
648 $self->add_modifier( $node );
655 my $requested_class = shift;
658 for my $n (@{$self->{query}}) {
659 next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
660 if ($n->requested_class eq $requested_class) {
667 $node = $self->new_node;
668 $node->requested_class( $requested_class );
676 return $self->{query};
683 $self->{query} ||= [];
684 push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
685 push(@{$self->{query}}, $node);
693 return $self->{level} ? 0 : 1;
698 return $self->{level};
705 $self->{joiner} = $joiner if ($joiner);
706 return $self->{joiner};
711 $self->{modifiers} ||= [];
712 return $self->{modifiers};
717 my $modifier = shift;
719 $self->{modifiers} ||= [];
720 return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
722 push(@{$self->{modifiers}}, $modifier);
729 $self->{filters} ||= [];
730 return $self->{filters};
737 $self->{filters} ||= [];
738 return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
740 push(@{$self->{filters}}, $filter);
746 #-------------------------------
747 package QueryParser::query_plan::node;
751 $pkg = ref($pkg) || $pkg;
754 return bless \%args => $pkg;
759 my $pkg = ref($self) || $self;
760 return do{$pkg.'::atom'}->new( @_ );
763 sub requested_class { # also split into classname and fields
768 my ($class_part, @field_parts) = split '\|', $class;
769 $class_part ||= $class;
771 $self->{requested_class} = $class;
772 $self->{classname} = $class_part;
773 $self->{fields} = \@field_parts;
776 return $self->{requested_class};
783 $self->{plan} = $plan if ($plan);
784 return $self->{plan};
791 $self->{classname} = $class if ($class);
792 return $self->{classname};
799 $self->{fields} ||= [];
800 $self->{fields} = \@fields if (@fields);
801 return $self->{fields};
808 $self->{phrases} ||= [];
809 $self->{phrases} = \@phrases if (@phrases);
810 return $self->{phrases};
817 push(@{$self->phrases}, $phrase);
824 my @query_atoms = @_;
826 $self->{query_atoms} ||= [];
827 $self->{query_atoms} = \@query_atoms if (@query_atoms);
828 return $self->{query_atoms};
839 $atom = $self->new_atom( content => $content, @parts );
842 push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
843 push(@{$self->query_atoms}, $atom);
848 #-------------------------------
849 package QueryParser::query_plan::node::atom;
853 $pkg = ref($pkg) || $pkg;
856 return bless \%args => $pkg;
861 return undef unless (ref $self);
862 return $self->{node};
867 return undef unless (ref $self);
868 return $self->{content};
873 return undef unless (ref $self);
874 return $self->{prefix};
879 return undef unless (ref $self);
880 return $self->{suffix};
883 #-------------------------------
884 package QueryParser::query_plan::filter;
888 $pkg = ref($pkg) || $pkg;
891 return bless \%args => $pkg;
896 return $self->{plan};
901 return $self->{name};
906 return $self->{args};
909 #-------------------------------
910 package QueryParser::query_plan::modifier;
914 $pkg = ref($pkg) || $pkg;
915 my $modifier = shift;
917 return bless \$modifier => $pkg;