root/branches/release-34/lib/MT/App/Search.pm @ 1855

Revision 1855, 24.8 kB (checked in by fumiakiy, 20 months ago)

Stopped sorting search results by blog id in multi blog search thus mix search results from multiple blogs. BugId:75781

Added SearchResultsHeader and SearchResultsFooter template tags.

Gave up on resurrecting per blog limiting of number of results.

  • 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";
270    $method .= $format if $format && $app->can($method . $format);
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 $desc = 'descend' eq $app->{searchparam}{SearchResultDisplay} ? 'DESC' : 'ASC';
370    my @sort;
371    my $sort = $params->{'sort'};
372    if ( $sort !~ /\w+\!$/ && $app->{searchparam}{SearchSortBy} ) {
373        my $sort_by = $app->{searchparam}{SearchSortBy};
374        $sort_by =~ s/[^\w\-\.\,]+//g;
375        if ( $sort_by ) {
376            my @sort_bys = split ',', $sort_by;
377            foreach my $key ( @sort_bys ) {
378                push @sort, {
379                    desc   => $desc,
380                    column => $key
381                };
382            }
383        }
384    }
385    push @sort, {
386        desc   => $desc,
387        column => $sort
388    };
389
390    my %args = (
391      exists( $parsed->{args} ) ? %{ $parsed->{args} } : (),
392      $limit  ? ( 'limit' => $limit ) : (),
393      $offset ? ( 'offset' => $offset ) : (),
394      @sort   ? ( 'sort' => \@sort ) : (),
395    );
396
397    ( \@terms, \%args );
398}
399
400sub cache_out {
401    my ( $cb, $app, $count, $out ) = @_;
402
403    my $result;
404    if (ref($out) && ($out->isa('MT::Template'))) {
405        defined($result = $app->build_page($out))
406            or die $out->errstr;
407    }
408    else {
409        $result = $out;
410    }
411
412    my $cache_driver = $app->{cache_driver};
413    $cache_driver->set( $app->{cache_keys}{result}, $out, $app->config->ThrottleSeconds );
414}
415
416sub log_search {
417    my ( $cb, $app, $count_ref, $iter_ref ) = @_;
418
419    #FIXME: template name may not be 'feed' for search feed
420    unless ( $app->param('template') && ( 'feed' eq $app->param('template') ) ) {
421        my $blog_id = $app->first_blog_id();
422        require MT::Log;
423        $app->log({
424            message => $app->translate("Search: query for '[_1]'",
425                  $app->{search_string}),
426            level => MT::Log::INFO(),
427            class => 'search',
428            category => 'straight_search',
429            $blog_id ? (blog_id => $blog_id) : ()
430        });
431    }
432}
433
434sub template_paths {
435    my $app = shift;
436    my @paths = $app->SUPER::template_paths;
437    ( $app->config->SearchTemplatePath, @paths );
438}
439
440sub first_blog_id {
441    my $app = shift;
442    my $q = $app->param;
443
444    my $blog_id;
445    if ( $q->param('IncludeBlogs') ) {
446        my @ids = split ',', $q->param('IncludeBlogs');
447        $blog_id = $ids[0];
448    }
449    elsif ( exists($app->{searchparam}{IncludeBlogs})
450      && keys(%{ $app->{searchparam}{IncludeBlogs} }) ) {
451        my @blog_ids = keys %{ $app->{searchparam}{IncludeBlogs} };
452        $blog_id = $blog_ids[0] if @blog_ids;
453    }
454    $blog_id;
455}
456
457sub prepare_context {
458    my $app = shift;
459    my $q = $app->param;
460    my ( $count, $iter ) = @_;
461
462    ## Initialize and set up the context object.
463    require MT::Template::Context::Search;
464    my $ctx = MT::Template::Context::Search->new;
465    if ( my $str = $app->{search_string} ) {
466        $ctx->stash('search_string', encode_html($str));
467    }
468    if ( $q->param('type') ) {
469        $ctx->stash('type', $app->{searchparam}{Type});
470    }
471    if ( $app->{default_mode} ne $app->mode ) {
472        $ctx->stash('mode', $app->mode);
473    }
474    if ( my $template = $q->param('Template') ) {
475        $template =~ s/[\w\-\.]//g;
476        $ctx->stash('template_id', $template);
477    }
478    $ctx->stash('stash_key'  , $app->{searchparam}{Type} );
479    $ctx->stash('maxresults' , $app->{searchparam}{SearchMaxResults});
480    $ctx->stash('include_blogs',
481        join ',', keys %{ $app->{searchparam}{IncludeBlogs} });
482    $ctx->stash('results'    , $iter);
483    $ctx->stash('count'      , $count);
484    $ctx->stash('offset'     , $q->param('startIndex') || $q->param('offset') || 0);
485    $ctx->stash('limit'      , $q->param('count') || $q->param('limit'));
486    $ctx->stash('format'     , $q->param('format')) if $q->param('format');
487
488    my $blog_id = $app->first_blog_id();
489    if ( $blog_id ) {
490        $ctx->stash('blog_id', $blog_id);
491        $ctx->stash('blog',    $app->model('blog')->load($blog_id));
492    }
493    $ctx;
494}
495
496sub load_search_tmpl {
497    my $app = shift;
498    my $q = $app->param;
499    my ( $ctx ) = @_;
500
501    my $tmpl;
502    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) {
503        # load specified template
504        my $filename;
505        if (my @tmpls = (
506          $app->config->default('SearchAltTemplate'),
507          $app->config->SearchAltTemplate) ) {
508            for my $tmpl (@tmpls) {
509                next unless defined $tmpl;
510                my ( $nickname, $file ) = split /\s+/, $tmpl;
511                if ( $nickname eq $q->param('Template') ) {
512                    $filename = $file;
513                    last;
514                }
515            }
516        }
517        return $app->errtrans( "No alternate template is specified for the Template '[_1]'",
518          encode_html( $q->param('Template') ) )
519            unless $filename;
520        # template_paths method does the magic
521        $tmpl = $app->load_tmpl( $filename )
522            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" );
523    }
524    else {
525        # load default template
526        # first look for appropriate blog_id
527        if ( my $blog_id = $ctx->stash('blog_id') ) {
528            # look for 'search_results'
529            my $tmpl_class = $app->model('template');
530            $tmpl = $tmpl_class->load(
531                { blog_id => $blog_id, type => 'search_results' }
532            );
533        }
534        unless ( $tmpl ) {
535            # load template from search_template path
536            # template_paths method does the magic
537            $tmpl = $app->load_tmpl( $app->config->SearchDefaultTemplate );
538        }
539    }
540    return $app->error($app->errstr)
541        unless $tmpl;
542
543    $ctx->var('system_template', '1');
544    $ctx->var('search_results', '1');
545
546    $tmpl->context($ctx);
547    $tmpl;
548}
549
550sub render {
551    my $app = shift;
552    my ( $count, $iter ) = @_;
553
554    my @arguments = $app->prepare_context( $count, $iter )
555        or return $app->error($app->errstr);
556    my $tmpl = $app->load_search_tmpl( @arguments )
557        or return $app->error($app->errstr);
558    $tmpl;
559}
560
561sub renderjs {
562    my $app = shift;
563    my ( $count, $iter ) = @_;
564
565    my ( $ctx ) = $app->prepare_context( $count, $iter )
566        or return $app->json_error($app->errstr);
567    my $search_tmpl = $app->load_search_tmpl( $ctx )
568        or return $app->json_error($app->errstr);
569    my $result_node = $search_tmpl->getElementById('search_results')
570        or return $app->json_error('Search template does not have markup for search results.');
571    my $t = $result_node->innerHTML();
572
573    require MT::Template;
574    my $tmpl = MT::Template->new( type => 'scalarref', source => \$t );
575    $ctx->stash('format', q()); # don't propagate "js" format
576    $tmpl->context( $ctx );
577    my $content = $tmpl->output
578        or return $app->json_error($tmpl->errstr);
579
580    my $next_link = $ctx->_hdlr_next_link();
581    return $app->json_result({ content => $content, next_url => $next_link });
582}
583
584sub query_parse {
585    my $app = shift;
586    my ( %columns ) = @_;
587
588    my $search = $app->{search_string};
589
590    my $reg = $app->registry( $app->mode, 'types', $app->{searchparam}{Type} );
591    my $filter_types = $reg->{ 'filter_types' };
592    foreach my $type ( keys %$filter_types ) {
593        if ( my $filter = $app->param($type) ) {
594            $search .= " $type:$filter";
595        }
596    }
597
598    require Lucene::QueryParser;
599    my $lucene_struct = Lucene::QueryParser::parse_query( $search );
600    my ( $terms, $joins ) = $app->_query_parse_core( $lucene_struct, \%columns, $filter_types );
601    my $return = {
602        $terms && @$terms ? (terms => $terms) : ()
603    };
604    if ( $joins && @$joins ) {
605        my $args = {};
606        _create_join_arg( $args, $joins );
607        if ( $args && %$args ) {
608            $return->{args} = $args;
609        }
610    }
611    $return;
612}
613
614sub _create_join_arg {
615    my ( $args, $joins ) = @_;
616    my $join = shift @$joins;
617    return unless $join && @$join;
618    my $next = $join->[3];
619    if ( defined $next ) {
620        if ( exists $next->{'join'} ) {
621            $next = $next->{'join'}->[3];
622        }
623    }
624    else {
625        $next = {};
626        $join->[3] = $next;
627    }
628    _create_join_arg($next, $joins);
629    $args->{'join'} = $join;
630}
631
632sub _query_parse_core {
633    my $app = shift;
634    my ( $lucene_struct, $columns, $filter_types ) = @_;
635
636    my $rvalue = sub {
637        my %rvalues = (
638            NORMALlike => { like => '%' . $_[1] . '%' },
639            NORMAL1    => $_[1],
640            PROHIBITEDlike => { not_like => '%' . $_[1] . '%' },
641            PROHIBITED1    => { not => $_[1] }
642        );
643        $rvalues{$_[0]};
644    };
645
646    my ( @structure, @joins );
647    for my $term ( @$lucene_struct ) {
648        if ( exists $term->{field} ) {
649            unless ( exists $columns->{ $term->{field} } ) {
650                next if $filter_types && %$filter_types
651                    && !exists( $filter_types->{ $term->{field} } );
652            }
653        }
654
655        my @tmp;
656        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
657            my $test;
658            if ( exists( $term->{field} ) ) {
659                if ( $filter_types && %$filter_types
660                  && exists( $filter_types->{ $term->{field} } ) ) {
661                    my $code = $app->handler_to_coderef($filter_types->{ $term->{field} });
662                    if ( $code ) {
663                        my $join_args = $code->( $app, $term );
664                        push @joins, $join_args;
665                        next;
666                    }
667                }
668                elsif ( exists $columns->{ $term->{field} } ) {
669                    my $test = $rvalue->(
670                        ( $term->{type} || '' ) . $columns->{ $term->{field} },
671                        $term->{term}
672                    );
673                    push @tmp, { $term->{field} => $test };
674                }
675            }
676            else {
677                my @cols = keys %$columns;
678                my $number = scalar @cols;
679                for ( my $i = 0; $i < $number; $i++ ) {
680                    my $test = $rvalue->(
681                        ( $term->{type} || '' ) . $columns->{ $cols[$i] },
682                        $term->{term}
683                    );
684                    push @tmp, { $cols[$i] => $test };
685                    unless ( $i == $number - 1 ) {
686                        push @tmp, '-or';
687                    }
688                }
689            }
690        }
691        elsif ( 'SUBQUERY' eq $term->{query} ) {
692            my ( $test, $more_joins ) = $app->_query_parse_core(
693                $term->{subquery}, $columns, $filter_types );
694            next unless $test && @$test;
695            if ( @structure ) {
696                push @structure, 'PROHIBITED' eq $term->{type}
697                  ? '-and_not'
698                  : '-and';
699            }
700            push @structure, $test->[0];
701            push @joins, @$more_joins;
702            next;
703        }
704
705        if ( exists($term->{conj}) && ( 'OR' eq $term->{conj} ) ) {
706            if ( my $prev = pop @structure ) {
707                push @structure, [ $prev, -or => \@tmp ];
708            }
709        }
710        else {
711            if ( @structure ) {
712                push @structure, '-and';
713            }
714            push @structure, \@tmp;
715        }
716    }
717    ( \@structure, \@joins );
718}
719
720# add category filter to entry search
721sub _join_category {
722    my ( $app, $term ) = @_;
723
724    my $query = $term->{term};
725    if ( 'PHRASE' eq $term->{query} ) {
726        $query =~ s/'/"/g;
727    }
728
729    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
730    if ( 'PROHIBITED' eq $term->{type} ) {
731        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
732    }
733    # search for exact match
734    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { label => 1 }, {} );
735    return unless $terms && @$terms;
736    push @$terms, '-and', {
737        id => \'= placement_category_id',
738        blog_id => \'= entry_blog_id',
739    };
740
741    require MT::Placement;
742    require MT::Category;
743    return MT::Placement->join_on( undef,
744        { entry_id => \'= entry_id', blog_id => \'= entry_blog_id' },
745        { join => MT::Category->join_on( undef, $terms, {} ),
746          unique => 1 }
747    );
748}
749
750# add author filter to entry search
751sub _join_author {
752    my ( $app, $term ) = @_;
753
754    my $query = $term->{term};
755    if ( 'PHRASE' eq $term->{query} ) {
756        $query =~ s/'/"/g;
757    }
758
759    my $lucene_struct = Lucene::QueryParser::parse_query( $query );
760    if ( 'PROHIBITED' eq $term->{type} ) {
761        $_->{type} = 'PROHIBITED' foreach @$lucene_struct;
762    }
763    my ( $terms ) = $app->_query_parse_core( $lucene_struct, { nickname => 'like' }, {} );
764    return unless $terms && @$terms;
765    push @$terms, '-and', {
766        id => \'= entry_author_id',
767    };
768    require MT::Author;
769    return MT::Author->join_on( undef,
770        $terms,
771        { unique => 1 }
772    );
773}
774
7751;
776__END__
777
778=head1 NAME
779
780MT::App::Search
781
782=head1 Callbacks
783
784Callbacks called by the package are as follows:
785
786=over 4
787
788=item search_post_execute
789
790    callback($cb, $app, \$count, \$iter)
791
792Called immediately after the search from the database (or however
793search executed depending on the algorithm).
794
795=item search_post_render
796
797    callback($cb, $app, $count, $out_html)
798
799Called immediately after the search template was loaded and its
800context populated.
801
802=item search_cache_hit
803
804    callback($cb, $app, $count, $out_html)
805
806Called immediately after cached results was retrieved.
807
808=item search_blog_list
809
810    callback($cb, $app, \%list, \$processed)
811
812Called during init_request in which a plugin can fill %list.
813The list must has the following data structure.
814
815    %list = ( 1 => 1, 2 => 1, 3 => 1 );
816
817where the hash keys (1, 2, and 3) are the IDs of the blogs to search for.
818
819Plugins must also set $processed = 1 in order to specify the app that
820the app must not overwrite the blog list created by the plugin.
821
822=head1 AUTHOR & COPYRIGHT
823
824Please see L<MT/AUTHOR & COPYRIGHT>.
825
826=cut
Note: See TracBrowser for help on using the browser.