root/branches/release-33/lib/MT/App/Search.pm @ 1757

Revision 1757, 24.7 kB (checked in by mpaschal, 20 months ago)

Search all kinds of entries when searching entries
BugzID: 75420

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::App::Search;
8
9use strict;
10use base qw( MT::App );
11
12use MT::Util qw( encode_html encode_url );
13
14sub id { 'new_search' }
15
16sub init {
17    my $app = shift;
18    $app->SUPER::init(@_) or return;
19    $app->set_no_cache;
20    $app->{default_mode} = 'default';
21
22    $app->mode('tag') if $app->param('tag');
23    ## process pathinfo
24    #if ( my $pi = $app->path_info ) {
25    #    $pi =~ s!^/!!;
26    #    my ($mode, $tag, @args) = split /\//, $pi;
27    #    $app->mode($mode);
28    #    $app->param($mode, $tag);
29    #    for my $arg (@args) {
30    #        my ($k, $v) = split /=/, $arg, 2;
31    #        $app->param($k, $v);
32    #    }
33    #}
34    $app->_register_core_callbacks({
35        'search_post_execute' => \&log_search,
36        'search_post_render'  => \&cache_out,
37    });
38    $app;
39}
40
41sub core_methods {
42    my $app = shift;
43    return {
44        'default' => \&process,
45        'tag'     => '$Core::MT::App::Search::TagSearch::process',
46    };
47}
48
49sub core_parameters {
50    my $app = shift;
51    return {
52        params => [ qw( searchTerms search count limit startIndex offset
53            category author )],
54        types  => {
55            #author => {
56            #    columns => [ qw( name nickname email url ) ],
57            #    'sort' => 'created_on',
58            #    terms   => { status => 1 }, #MT::Author::ACTIVE()
59            #},
60            entry => {
61                columns => {
62                    title     => 'like',
63                    keywords  => 'like',
64                    text      => 'like',
65                    text_more => 'like'
66                },
67                'sort'  => 'authored_on',
68                terms   => { status => 2, class => '*' }, #MT::Entry::RELEASE()
69                filter_types => {
70                    author   => \&_join_author,
71                    category => \&_join_category,
72                },
73            },
74        },
75        cache_driver => {
76            'package' => 'MT::Cache::Negotiate',
77        },
78    };
79}
80
81sub init_request{
82    my $app = shift;
83    $app->SUPER::init_request(@_);
84    my $q = $app->param;
85
86    my $params = $app->registry( $app->mode, 'params' );
87    foreach ( @$params ) {
88        delete $app->{$_} if exists $app->{$_}
89    }
90
91    my %no_override;
92    foreach my $no ( split /\s*,\s*/, $app->config->SearchNoOverride ) {
93        $no_override{ $no } = 1;
94        $no_override{ "Search$no" } = 1
95            if $no !~ /^Search.+/;
96    }
97
98    ## Set other search params--prefer per-query setting, default to
99    ## config file.
100    for my $key (qw( SearchResultDisplay SearchMaxResults SearchSortBy )) {
101        $app->{searchparam}{$key} = $no_override{$key} ?
102            $app->config->$key() : ($q->param($key) || $app->config->$key());
103    }
104
105    $app->{searchparam}{Type} = 'entry';
106    if ( my $type = $q->param('type') ) {
107        return $app->errtrans('Invalid type: [_1]', encode_html($type) )
108            if $type !~ /[\w\.]+/;
109        $app->{searchparam}{Type} = $type;
110    }
111
112    $app->generate_cache_keys();
113    $app->init_cache_driver();
114
115    my $processed = 0;
116    my $list      = {};
117    if ( MT->run_callbacks( 'search_blog_list', $app, $list, \$processed ) ) {
118        if ( $processed ) {
119            $app->{searchparam}{IncludeBlogs} = $list;
120        }
121        else {
122            my $blog_list = $app->create_blog_list( %no_override );
123            $app->{searchparam}{IncludeBlogs} = $blog_list->{IncludeBlogs}
124                if $blog_list && %$blog_list
125                && $blog_list->{IncludeBlogs}
126                && %{ $blog_list->{IncludeBlogs} };
127            if ( !exists($app->{searchparam}{IncludeBlogs})
128              && ( my $blog_id = $q->param('blog_id') ) ) {
129                $blog_id =~ s/\D//g;
130                $app->{searchparam}{IncludeBlogs}{$blog_id} = 1
131                    if $blog_id;
132            }
133        }
134    }
135    else {
136        return $app->error( $app->translate('Invalid request.') );
137    }
138}
139
140sub generate_cache_keys {
141    my $app = shift;
142
143    my $q = $app->param;
144    my @p = sort { $a cmp $b } $q->param;
145    my ( $key, $count_key );
146    $key .= lc($_) . encode_url($q->param($_))
147        foreach @p;
148    $count_key .= lc($_) . encode_url($q->param($_))
149        foreach grep { ('limit' ne lc($_)) && ('offset' ne lc($_)) } @p;
150    $app->{cache_keys} = { result => $key, count => $count_key };
151}
152
153sub init_cache_driver {
154    my $app = shift;
155
156    my $registry = $app->registry( $app->mode, 'cache_driver' );
157    my $cache_driver = $registry->{'package'} || 'MT::Cache::Negotiate';
158    eval "require $cache_driver;";
159    if ( my $e = $@ ) {
160        require MT::Log;
161        $app->log({
162            message =>
163                $app->translate("Search: failed storing results in cache.  [_1] is not available: [_2]",
164                    $cache_driver, $e ),
165            level => MT::Log::INFO(),
166            class => 'search',
167        });
168        return;
169    }
170    $app->{cache_driver} = $cache_driver->new( ttl => $app->config->ThrottleSeconds );
171}
172
173sub create_blog_list {
174    my $app = shift;
175    my ( %no_override ) = @_;
176
177    my $q = $app->param;
178    my $cfg = $app->config;
179
180    unless ( %no_override ) {
181        my %no_override;
182        foreach my $no ( split /\s*,\s*/, $app->config->SearchNoOverride ) {
183            $no_override{ $no } = 1;
184            $no_override{ "Search$no" } = 1
185                if $no !~ /^Search.+/;
186        }
187    }
188
189    my %blog_list;
190    ## Combine user-selected included/excluded blogs
191    ## with config file settings.
192    for my $type (qw( IncludeBlogs ExcludeBlogs )) {
193        $blog_list{$type} = {};
194        if (my $list = $cfg->$type()) {
195            $blog_list{$type} =
196                { map { $_ => 1 } split /\s*,\s*/, $list };
197        }
198        next if exists($no_override{$type}) && $no_override{$type};
199        for my $blog_id ($q->param($type)) {
200            if ($blog_id =~ m/,/) {
201                my @ids = split /,/, $blog_id;
202                s/\D+//g for @ids; # only numeric values.
203                foreach my $id (@ids) {
204                    next unless $id;
205                    $blog_list{$type}{$id} = 1;
206                }
207            } else {
208                $blog_id =~ s/\D+//g; # only numeric values.
209                $blog_list{$type}{$blog_id} = 1;
210            }
211        }
212    }
213
214    ## If IncludeBlogs has not been set, we need to build a list of
215    ## the blogs to search. If ExcludeBlogs was set, exclude any blogs
216    ## set in that list from our final list.
217    unless ( exists $blog_list{IncludeBlogs} ) {
218        my $exclude = $blog_list{ExcludeBlogs};
219        my $iter = $app->model('blog')->load_iter;
220        while (my $blog = $iter->()) {
221            $blog_list{IncludeBlogs}{$blog->id} = 1
222                unless $exclude && $exclude->{$blog->id};
223        }
224    }
225
226    \%blog_list;
227}
228
229sub check_cache {
230    my $app = shift;
231
232    my $cache = $app->{cache_driver}->get_multi(
233        values %{ $app->{cache_keys} } );
234
235    my $count = $cache->{ $app->{cache_keys}{count} }
236        if exists $cache->{ $app->{cache_keys}{count} };
237    my $result = $cache->{ $app->{cache_keys}{result} }
238        if exists $cache->{ $app->{cache_keys}{result} };
239
240    ( $count, $result );
241}
242
243sub process {
244    my $app = shift;
245
246    my ( $count, $out ) = $app->check_cache();
247    if ( defined $out ) {
248        $app->run_callbacks( 'search_cache_hit', $count, $out );
249        return $out;
250    }
251
252    my @arguments = $app->search_terms();
253    return $app->error($app->errstr) if $app->errstr;
254
255    $count = 0;
256    my $iter;
257    if ( @arguments ) {
258        ( $count, $iter ) = $app->execute( @arguments );
259        return $app->error($app->errstr) unless $iter;
260
261        $app->run_callbacks( 'search_post_execute', $app, \$count, \$iter );
262    }
263
264    my $format = q();
265    if ( $format = $app->param('format') ) {
266        return $app->errtrans('Invalid format: [_1]', encode_html($format))
267            if $format !~ /\w+/;
268    }
269    my $method = "render$format";
270    $method = 'render' unless $app->can($method);
271    $out = $app->$method( $count, $iter );
272
273    my $result;
274    if (ref($out) && ($out->isa('MT::Template'))) {
275        defined( $result = $app->build_page($out) )
276            or return $app->error($out->errstr);
277    }
278    else {
279        $result = $out;
280    }
281
282    $app->run_callbacks( 'search_post_render', $app, $count, $result );
283    $result;
284}
285
286sub count {
287    my $app = shift;
288    my ( $class, $terms, $args ) = @_;
289    my $count = $app->{cache_driver}->get($app->{cache_keys}{count});
290    return $count if defined $count;
291
292    $count = $class->count( $terms, $args );
293    return $app->error($class->errstr) unless defined $count;
294
295    my $cache_driver = $app->{cache_driver};
296    $cache_driver->set( $app->{cache_keys}{count}, $count, $app->config->ThrottleSeconds );
297
298    $count;
299}
300
301sub execute {
302    my $app = shift;
303    my ( $terms, $args ) = @_;
304
305    my $class = $app->model( $app->{searchparam}{Type} )
306        or return $app->errtrans('Unsupported type: [_1]', encode_html($app->{searchparam}{Type}));
307
308    my $count = $app->count( $class, $terms, $args );
309    return $app->errtrans("Invalid query.  [_1]", $app->errstr) unless defined $count;
310
311    my $iter = $class->load_iter( $terms, $args )
312        or $app->error($class->errstr);
313    ( $count, $iter );
314}
315
316sub search_terms {
317    my $app = shift;
318    my $q = $app->param;
319
320    my $search_string = $q->param('searchTerms') || $q->param('search')
321        or return ( undef, undef );
322    $app->{search_string} = $search_string;
323    my $offset = $q->param('startIndex') || $q->param('offset') || 0;
324    return $app->errtrans('Invalid value: [_1]', encode_html($offset))
325        if $offset && $offset !~ /^\d+$/;
326    my $limit = $q->param('count') || $q->param('limit');
327    return $app->errtrans('Invalid value: [_1]', encode_html($limit))
328        if $limit && $limit !~ /^\d+$/;
329    my $max = $app->{searchparam}{SearchMaxResults};
330    $max =~ s/\D//g if defined $max;
331    $limit = $max if !$limit || ( $limit - $offset > $max );
332
333    my $params = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
334    my %def_terms = exists( $params->{terms} )
335          ? %{ $params->{terms} }
336          : ();
337    delete $def_terms{'plugin'}; #FIXME: why is this in here?
338
339    if ( exists $app->{searchparam}{IncludeBlogs} ) {
340        $def_terms{blog_id} = [ keys %{ $app->{searchparam}{IncludeBlogs} } ];
341    }
342
343    my @terms;
344    if (%def_terms) {
345        # If we have a term for the model's class column, add it separately, so
346        # array search() doesn't add the default class column term.
347        my $type = $app->{searchparam}{Type};
348        my $model_class = MT->model($type);
349        if (my $class_col = $model_class->properties->{class_column}) {
350            if ($def_terms{$class_col}) {
351                push @terms, { $class_col => delete $def_terms{$class_col} };
352            }
353        }
354
355        push @terms, \%def_terms;
356    }
357
358    my $columns = $params->{columns};
359    delete $columns->{'plugin'}; #FIXME: why is this in here?
360    return $app->errtrans('No column was specified to search for [_1].', $app->{searchparam}{Type})
361        unless $columns && %$columns;
362
363    my $parsed = $app->query_parse( %$columns );
364    return $app->errtrans('Parse error: [1]', $app->errstr)
365        unless $parsed && %$parsed;
366
367    push @terms, $parsed->{terms} if exists $parsed->{terms};
368
369    my $sort = $params->{'sort'};
370    if ( $sort !~ /\w+\!$/ && $app->{searchparam}{SearchSortBy} ) {
371        my $sort_by = $app->{searchparam}{SearchSortBy};
372        $sort_by =~ s/[\w\-\.]+//g;
373        $sort = $sort_by;
374    }
375
376    my %args = (
377      exists( $parsed->{args} ) ? %{ $parsed->{args} } : (),
378      $limit  ? ( 'limit' => $limit ) : (),
379      $offset ? ( 'offset' => $offset ) : (),
380      $sort   ? ( 'sort' => [
381            { desc   => 'descend' eq $app->{searchparam}{SearchResultDisplay} ? 'DESC' : 'ASC',
382              column => $sort }
383        ] ) : (),
384    );
385
386    if ( exists $app->{searchparam}{IncludeBlogs} ) {
387        unshift @{ $args{'sort'} },
388          { desc => 'ASC',
389            column    => 'blog_id' };
390    }
391
392    ( \@terms, \%args );
393}
394
395sub cache_out {
396    my ( $cb, $app, $count, $out ) = @_;
397
398    my $result;
399    if (ref($out) && ($out->isa('MT::Template'))) {
400        defined($result = $app->build_page($out))
401            or die $out->errstr;
402    }
403    else {
404        $result = $out;
405    }
406
407    my $cache_driver = $app->{cache_driver};
408    $cache_driver->set( $app->{cache_keys}{result}, $out, $app->config->ThrottleSeconds );
409}
410
411sub log_search {
412    my ( $cb, $app, $count_ref, $iter_ref ) = @_;
413
414    #FIXME: template name may not be 'feed' for search feed
415    unless ( $app->param('template') && ( 'feed' eq $app->param('template') ) ) {
416        my $blog_id = $app->first_blog_id();
417        require MT::Log;
418        $app->log({
419            message => $app->translate("Search: query for '[_1]'",
420                  $app->{search_string}),
421            level => MT::Log::INFO(),
422            class => 'search',
423            category => 'straight_search',
424            $blog_id ? (blog_id => $blog_id) : ()
425        });
426    }
427}
428
429sub template_paths {
430    my $app = shift;
431    my @paths = $app->SUPER::template_paths;
432    ( $app->config->SearchTemplatePath, @paths );
433}
434
435sub first_blog_id {
436    my $app = shift;
437    my $q = $app->param;
438
439    my $blog_id;
440    if ( $q->param('IncludeBlogs') ) {
441        my @ids = split ',', $q->param('IncludeBlogs');
442        $blog_id = $ids[0];
443    }
444    elsif ( exists($app->{searchparam}{IncludeBlogs})
445      && keys(%{ $app->{searchparam}{IncludeBlogs} }) ) {
446        my @blog_ids = keys %{ $app->{searchparam}{IncludeBlogs} };
447        $blog_id = $blog_ids[0] if @blog_ids;
448    }
449    $blog_id;
450}
451
452sub prepare_context {
453    my $app = shift;
454    my $q = $app->param;
455    my ( $count, $iter ) = @_;
456
457    ## Initialize and set up the context object.
458    require MT::Template::Context::Search;
459    my $ctx = MT::Template::Context::Search->new;
460    if ( my $str = $app->{search_string} ) {
461        $ctx->stash('search_string', encode_html($str));
462    }
463    if ( $q->param('type') ) {
464        $ctx->stash('type', $app->{searchparam}{Type});
465    }
466    if ( $app->{default_mode} ne $app->mode ) {
467        $ctx->stash('mode', $app->mode);
468    }
469    if ( my $template = $q->param('Template') ) {
470        $template =~ s/[\w\-\.]//g;
471        $ctx->stash('template_id', $template);
472    }
473    $ctx->stash('stash_key'  , $app->{searchparam}{Type} );
474    $ctx->stash('maxresults' , $app->{searchparam}{SearchMaxResults});
475    $ctx->stash('include_blogs',
476        join ',', keys %{ $app->{searchparam}{IncludeBlogs} });
477    $ctx->stash('results'    , $iter);
478    $ctx->stash('count'      , $count);
479    $ctx->stash('offset'     , $q->param('startIndex') || $q->param('offset') || 0);
480    $ctx->stash('limit'      , $q->param('count') || $q->param('limit'));
481    $ctx->stash('format'     , $q->param('format')) if $q->param('format');
482
483    my $blog_id = $app->first_blog_id();
484    if ( $blog_id ) {
485        $ctx->stash('blog_id', $blog_id);
486        $ctx->stash('blog',    $app->model('blog')->load($blog_id));
487    }
488    $ctx;
489}
490
491sub load_search_tmpl {
492    my $app = shift;
493    my $q = $app->param;
494    my ( $ctx ) = @_;
495
496    my $tmpl;
497    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) {
498        # load specified template
499        my $filename;
500        if (my @tmpls = (
501          $app->config->default('SearchAltTemplate'),
502          $app->config->SearchAltTemplate) ) {
503            for my $tmpl (@tmpls) {
504                next unless defined $tmpl;
505                my ( $nickname, $file ) = split /\s+/, $tmpl;
506                if ( $nickname eq $q->param('Template') ) {
507                    $filename = $file;
508                    last;
509                }
510            }
511        }
512        return $app->errtrans( "No alternate template is specified for the Template '[_1]'",
513          encode_html( $q->param('Template') ) )
514            unless $filename;
515        # template_paths method does the magic
516        $tmpl = $app->load_tmpl( $filename )
517            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" );
518    }
519    else {
520        # load default template
521        # first look for appropriate blog_id
522        if ( my $blog_id = $ctx->stash('blog_id') ) {
523            # look for 'search_results'
524            my $tmpl_class = $app->model('template');
525            $tmpl = $tmpl_class->load(
526                { blog_id => $blog_id, type => 'search_results' }
527            );
528        }
529        unless ( $tmpl ) {
530            # load template from search_template path
531            # template_paths method does the magic
532            $tmpl = $app->load_tmpl( $app->config->SearchDefaultTemplate );
533        }
534    }
535    return $app->error($app->errstr)
536        unless $tmpl;
537
538    $ctx->var('system_template', '1');
539    $ctx->var('search_results', '1');
540
541    $tmpl->context($ctx);
542    $tmpl;
543}
544
545sub render {
546    my $app = shift;
547    my ( $count, $iter ) = @_;
548
549    my @arguments = $app->prepare_context( $count, $iter )
550        or return $app->error($app->errstr);
551    my $tmpl = $app->load_search_tmpl( @arguments )
552        or return $app->error($app->errstr);
553    $tmpl;
554}
555
556sub renderjs {
557    my $app = shift;
558    my ( $count, $iter ) = @_;
559
560    my ( $ctx ) = $app->prepare_context( $count, $iter )
561        or return $app->json_error($app->errstr);
562    my $search_tmpl = $app->load_search_tmpl( $ctx )
563        or return $app->json_error($app->errstr);
564    my $result_node = $search_tmpl->getElementById('search_results')
565        or return $app->json_error('Search template does not have markup for search results.');
566    my $t = $result_node->innerHTML();
567
568    require MT::Template;
569    my $tmpl = MT::Template->new( type => 'scalarref', source => \$t );
570    $ctx->stash('format', q()); # don't propagate "js" format
571    $tmpl->context( $ctx );
572    my $content = $tmpl->output
573        or return $app->json_error($tmpl->errstr);
574
575    my $next_link = $ctx->_hdlr_next_link();
576    return $app->json_result({ content => $content, next_url => $next_link });
577}
578
579sub query_parse {
580    my $app = shift;
581    my ( %columns ) = @_;
582
583    my $search = $app->{search_string};
584
585    my $reg = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
586    my $filter_types = $reg->{ 'filter_types' };
587    foreach my $type ( keys %$filter_types ) {
588        if ( my $filter = $app->param($type) ) {
589            $search .= " $type:$filter";
590        }
591    }
592
593    require Lucene::QueryParser;
594    my $lucene_struct = Lucene::QueryParser::parse_query( $search );
595    my ( $terms, $joins ) = $app->_query_parse_core( $lucene_struct, \%columns, $filter_types );
596    my $return = {
597        $terms && @$terms ? (terms => $terms) : ()
598    };
599    if ( $joins && @$joins ) {
600        my $args = {};
601        _create_join_arg( $args, $joins );
602        if ( $args && %$args ) {
603            $return->{args} = $args;
604        }
605    }
606    $return;
607}
608
609sub _create_join_arg {
610    my ( $args, $joins ) = @_;
611    my $join = shift @$joins;
612    return unless $join && @$join;
613    my $next = $join->[3];
614    if ( defined $next ) {
615        if ( exists $next->{'join'} ) {
616            $next = $next->{'join'}->[3];
617        }
618    }
619    else {
620        $next = {};
621        $join->[3] = $next;
622    }
623    _create_join_arg($next, $joins);
624    $args->{'join'} = $join;
625}
626
627sub _query_parse_core {
628    my $app = shift;
629    my ( $lucene_struct, $columns, $filter_types ) = @_;
630
631    my $rvalue = sub {
632        my %rvalues = (
633            NORMALlike => { like => '%' . $_[1] . '%' },
634            NORMAL1    => $_[1],
635            PROHIBITEDlike => { not_like => '%' . $_[1] . '%' },
636            PROHIBITED1    => { not => $_[1] }
637        );
638        $rvalues{$_[0]};
639    };
640
641    my ( @structure, @joins );
642    for my $term ( @$lucene_struct ) {
643        if ( exists $term->{field} ) {
644            unless ( exists $columns->{ $term->{field} } ) {
645                next if $filter_types && %$filter_types
646                    && !exists( $filter_types->{ $term->{field} } );
647            }
648        }
649
650        my @tmp;
651        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
652            my $test;
653            if ( exists( $term->{field} ) ) {
654                if ( $filter_types && %$filter_types
655                  && exists( $filter_types->{ $term->{field} } ) ) {
656                    my $code = $app->handler_to_coderef($filter_types->{ $term->{field} });
657                    if ( $code ) {
658                        my $join_args = $code->( $app, $term );
659                        push @joins, $join_args;
660                        next;
661                    }
662                }
663                elsif ( exists $columns->{ $term->{field} } ) {
664                    my $test = $rvalue->(
665                        ( $term->{type} || '' ) . $columns->{ $term->{field} },
666                        $term->{term}
667                    );
668                    push @tmp, { $term->{field} => $test };
669                }
670            }
671            else {
672                my @cols = keys %$columns;
673                my $number = scalar @cols;
674                for ( my $i = 0; $i < $number; $i++ ) {
675                    my $test = $rvalue->(
676                        ( $term->{type} || '' ) . $columns->{ $cols[$i] },
677                        $term->{term}
678                    );
679                    push @tmp, { $cols[$i] => $test };
680                    unless ( $i == $number - 1 ) {
681                        push @tmp, '-or';
682                    }
683                }
684            }
685        }
686        elsif ( 'SUBQUERY' eq $term->{query} ) {
687            my ( $test, $more_joins ) = $app->_query_parse_core(
688                $term->{subquery}, $columns, $filter_types );
689            next unless $test && @$test;
690            if ( @structure ) {
691                push @structure, 'PROHIBITED' eq $term->{type}
692                  ? '-and_not'
693                  : '-and';
694            }
695            push @structure, $test->[0];
696            push @joins, @$more_joins;
697            next;
698        }
699
700        if ( exists($term->{conj}) && ( 'OR' eq $term->{conj} ) ) {
701            if ( my $prev = pop @structure ) {
702                push @structure, [ $prev, -or => \@tmp ];
703            }
704        }
705        else {
706            if ( @structure ) {
707                push @structure, '-and';
708            }
709            push @structure, \@tmp;
710        }
711    }
712    ( \@structure, \@joins );
713}
714
715# add category filter to entry search
716sub _join_category {
717    my ( $app, $term ) = @_;
718
719    my $query = $term->{term};
720    if ( 'PHRASE' eq $term->{query} ) {
721        $query =~ s/'/"/g;
722    }
723
724    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
725    if ( 'PROHIBITED' eq $term->{type} ) {
726        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
727    }
728    # search for exact match
729    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { label => 1 }, {} );
730    return unless $terms && @$terms;
731    push @$terms, '-and', {
732        id => \'= placement_category_id',
733        blog_id => \'= entry_blog_id',
734    };
735
736    require MT::Placement;
737    require MT::Category;
738    return MT::Placement->join_on( undef,
739        { entry_id => \'= entry_id', blog_id => \'= entry_blog_id' },
740        { join => MT::Category->join_on( undef, $terms, {} ),
741          unique => 1 }
742    );
743}
744
745# add author filter to entry search
746sub _join_author {
747    my ( $app, $term ) = @_;
748
749    my $query = $term->{term};
750    if ( 'PHRASE' eq $term->{query} ) {
751        $query =~ s/'/"/g;
752    }
753
754    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
755    if ( 'PROHIBITED' eq $term->{type} ) {
756        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
757    }
758    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { nickname => 'like' }, {} );
759    return unless $terms && @$terms;
760    push @$terms, '-and', {
761        id => \'= entry_author_id',
762    };
763    require MT::Author;
764    return MT::Author->join_on( undef,
765        $terms,
766        { unique => 1 }
767    );
768}
769
7701;
771__END__
772
773=head1 NAME
774
775MT::App::Search
776
777=head1 Callbacks
778
779Callbacks called by the package are as follows:
780
781=over 4
782
783=item search_post_execute
784
785    callback($cb, $app, \$count, \$iter)
786
787Called immediately after the search from the database (or however
788search executed depending on the algorithm).
789
790=item search_post_render
791
792    callback($cb, $app, $count, $out_html)
793
794Called immediately after the search template was loaded and its
795context populated.
796
797=item search_cache_hit
798
799    callback($cb, $app, $count, $out_html)
800
801Called immediately after cached results was retrieved.
802
803=item search_blog_list
804
805    callback($cb, $app, \%list, \$processed)
806
807Called during init_request in which a plugin can fill %list.
808The list must has the following data structure.
809
810    %list = ( 1 => 1, 2 => 1, 3 => 1 );
811
812where the hash keys (1, 2, and 3) are the IDs of the blogs to search for.
813
814Plugins must also set $processed = 1 in order to specify the app that
815the app must not overwrite the blog list created by the plugin.
816
817=head1 AUTHOR & COPYRIGHT
818
819Please see L<MT/AUTHOR & COPYRIGHT>.
820
821=cut
Note: See TracBrowser for help on using the browser.