root/branches/release-30/lib/MT/App/Search.pm @ 1421

Revision 1421, 12.0 kB (checked in by fumiakiy, 21 months ago)

Recover commented out code for debugging.

Also removed a few unnecessary loading of other packages. BugId:69030, BugId:69029

  • 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 );
13use MT::Entry qw( :constants );
14
15sub id { 'new_search' }
16
17sub init {
18    my $app = shift;
19    $app->SUPER::init(@_) or return;
20    $app->set_no_cache;
21    $app->{default_mode} = 'search';
22    $app;
23}
24
25sub core_methods {
26    my $app = shift;
27    return {
28        'search' => \&process,
29        'author' => \&process,
30    };
31}
32
33sub core_query_params {
34    my $app = shift;
35    return [
36        'searchTerms',
37        'search',
38        'count',
39        'limit',
40        'startIndex',
41        'offset',
42    ];
43}
44
45sub init_request{
46    my $app = shift;
47    $app->SUPER::init_request(@_);
48    my $q = $app->param;
49
50    my $params = $app->registry('params');
51    foreach ( @$params ) {
52        delete $app->{$_} if exists $app->{$_}
53    }
54
55    my %no_override = map { $_ => 1 } split /\s*,\s*/, $app->config->NoOverride;
56    my $blog_list = $app->create_blog_list( %no_override );
57    $app->{searchparam}{IncludeBlogs} = $blog_list->{IncludeBlogs}
58        if $blog_list && %$blog_list
59        && $blog_list->{IncludeBlogs}
60        && %{ $blog_list->{IncludeBlogs} };
61
62    ## Set other search params--prefer per-query setting, default to
63    ## config file.
64    for my $key (qw( ResultDisplay MaxResults SearchSortBy )) {
65        $app->{searchparam}{$key} = $no_override{$key} ?
66            $app->config->$key() : ($q->param($key) || $app->config->$key());
67    }
68}
69
70sub create_blog_list {
71    my $app = shift;
72    my ( %no_override ) = @_;
73
74    my $q = $app->param;
75    my $cfg = $app->config;
76
77    %no_override = map { $_ => 1 } split /\s*,\s*/, $cfg->NoOverride
78        unless %no_override;
79
80    my %blog_list;
81    ## Combine user-selected included/excluded blogs
82    ## with config file settings.
83    for my $type (qw( IncludeBlogs ExcludeBlogs )) {
84        $blog_list{$type} = {};
85        if (my $list = $cfg->$type()) {
86            $blog_list{$type} =
87                { map { $_ => 1 } split /\s*,\s*/, $list };
88        }
89        next if exists($no_override{$type}) && $no_override{$type};
90        for my $blog_id ($q->param($type)) {
91            if ($blog_id =~ m/,/) {
92                my @ids = split /,/, $blog_id;
93                s/\D+//g for @ids; # only numeric values.
94                foreach my $id (@ids) {
95                    next unless $id;
96                    $blog_list{$type}{$id} = 1;
97                }
98            } else {
99                $blog_id =~ s/\D+//g; # only numeric values.
100                $blog_list{$type}{$blog_id} = 1;
101            }
102        }
103    }
104
105    ## If IncludeBlogs has not been set, we need to build a list of
106    ## the blogs to search. If ExcludeBlogs was set, exclude any blogs
107    ## set in that list from our final list.
108    unless ( exists $blog_list{IncludeBlogs} ) {
109        my $exclude = $blog_list{ExcludeBlogs};
110        my $iter = $app->model('blog')->load_iter;
111        while (my $blog = $iter->()) {
112            $blog_list{IncludeBlogs}{$blog->id} = 1
113                unless $exclude && $exclude->{$blog->id};
114        }
115    }
116
117    \%blog_list;
118}
119
120sub process {
121    my $app = shift;
122
123    my ( $terms, $args ) = $app->search_terms();
124    return $app->error($app->errstr) if $app->errstr;
125
126    my $count = 0;
127    my $iter;
128    if ( $terms && @$terms ) {
129        ( $count, $iter ) = $app->execute( $terms, $args );
130        return $app->error($app->errstr) unless $iter;
131
132        $iter = $app->post_search( $count, $iter );
133    }
134
135    return $app->render( $count, $iter );
136}
137
138sub count {
139    my $app = shift;
140    my ( $class, $terms, $args ) = @_;
141    #TODO: cache!
142    $class->count( $terms, $args );
143}
144
145sub execute {
146    my $app = shift;
147    my ( $terms, $args ) = @_;
148
149    my $class = $app->model( $app->mode eq 'search' ? 'entry' : $app->mode );
150    my $count = $app->count( $class, $terms, $args );
151    #TODO: cache!
152    my $iter = $class->load_iter( $terms, $args )
153        or $app->error($class->errstr);
154    ( $count, $iter );
155}
156
157sub search_terms {
158    my $app = shift;
159    my $q = $app->param;
160
161    my $search_string = $q->param('searchTerms') || $q->param('search')
162        or return ( undef, undef );
163    $app->{search_string} = $search_string;
164    my $offset = $q->param('startIndex') || $q->param('offset') || 0;
165    my $limit = $q->param('count') || $q->param('limit');
166    my $max = $app->{searchparam}{MaxResults};
167    $limit = $max if !$limit || ( $limit - $offset > $max );
168
169    my $type = $q->param('type');
170    my $entry_type = $app->mode eq 'search'
171      ? 1
172      : $type
173        ? ( 'entry' eq lc($type) || 'page' eq lc($type)
174          ? 1
175          : 0 )
176        : 0;
177
178    my $def_terms = {
179        $entry_type ? ( status => MT::Entry::RELEASE() ) : (),
180        $type ? ( class  => $type ) : (),
181        exists( $app->{searchparam}{IncludeBlogs} )
182          ? ( blog_id => [ keys %{ $app->{searchparam}{IncludeBlogs} } ] )
183          : (),
184    };
185    my @terms;
186    push @terms, $def_terms if %$def_terms;
187
188    my $columns = $app->mode eq 'search'
189      ? [ [ qw( title keywords text text_more ) ] ]
190      : $app->registry( $app->mode, 'columns' );
191    $columns = $columns->[0]; # FIXME: Why?
192    return $app->errtrans('No columns to search for was specified for [_1]', $app->mode)
193        unless $columns && @$columns;
194    my $number = scalar @$columns;
195    my @and;
196    for ( my $i = 0; $i < $number; $i++) {
197        push @and, { $columns->[$i] => { like => '%'.$search_string.'%' } };
198        unless ( $i == $number - 1 ) {
199            push @and, '-or';
200        }
201    }
202    push @terms, '-and' if @terms;
203    push @terms, \@and;
204
205    my %args = (
206      $limit ? ( 'limit' => $limit ) : (),
207      $offset ? ( 'offset' => $offset ) : (),
208      'sort' => [
209        { desc   => 'descend' eq $app->{searchparam}{ResultDisplay} ? 'DESC' : 'ASC',
210          column => $entry_type ? 'authored_on' : 'created_on' }
211      ]
212    );
213
214    if ( exists $app->{searchparam}{IncludeBlogs} ) {
215        unshift @{ $args{'sort'} },
216          { desc => 'ASC',
217            column    => 'blog_id' };
218    }
219
220    ( \@terms, \%args );
221}
222
223sub post_search {
224    my $app = shift;
225    my ( $count, $iter ) = @_;
226    # TODO: cache here?
227    $iter;
228}
229
230sub template_paths {
231    my $app = shift;
232    my @paths = $app->SUPER::template_paths;
233    ( $app->config->SearchTemplatePath, @paths );
234}
235
236sub load_search_tmpl {
237    my $app = shift;
238    my $q = $app->param;
239    my ( $count, $iter ) = @_;
240
241    my $blog_id;
242    if ( $q->param('IncludeBlogs') ) {
243        my @ids = split ',', $q->param('IncludeBlogs');
244        $blog_id = $ids[0];
245    }
246    elsif ( exists $app->{searchparam}{IncludeBlogs} ) {
247        $blog_id = @{ keys %{ $app->{searchparam}{IncludeBlogs} } }[0];
248    }
249
250    my $tmpl;
251    if ( $q->param('Template') && ( 'default' ne $q->param('Template') ) ) {
252        # load specified template
253        my $filename;
254        if (my @tmpls = ($app->config->default('AltTemplate'), $app->config->AltTemplate)) {
255            for my $tmpl (@tmpls) {
256                next unless defined $tmpl;
257                my ( $nickname, $file ) = split /\s+/, $tmpl;
258                if ( $nickname eq $q->param('Template') ) {
259                    $filename = $file;
260                    last;
261                }
262            }
263        }
264        return $app->errtrans("No alternate template is specified for the Template '[_1]'", $q->param('Template'))
265            unless $filename;
266        # template_paths method does the magic
267        $tmpl = $app->load_tmpl( $filename )
268            or return $app->errtrans( "Opening local file '[_1]' failed: [_2]", $filename, "$!" );
269    }
270    else {
271        # load default template
272        # first look for appropriate blog_id
273        if ( $blog_id ) {
274            # look for 'search_results'
275            my $tmpl_class = $app->model('template');
276            $tmpl = $tmpl_class->load(
277                { blog_id => $blog_id, type => 'search_results' }
278            );
279        }
280        unless ( $tmpl ) {
281            # load template from search_template path
282            # template_paths method does the magic
283            $tmpl = $app->load_tmpl( $app->config->DefaultTemplate );
284        }
285    }
286    return $app->error($app->errstr)
287        unless $tmpl;
288
289    ## Initialize and set up the context object.
290    require MT::Template::Context::Search;
291    my $ctx = MT::Template::Context::Search->new;
292    if ( $blog_id ) {
293        $ctx->stash('blog_id', $blog_id);
294        $ctx->stash('blog',    $app->model('blog')->load($blog_id));
295    }
296    $ctx->stash('results', $iter);
297    $ctx->stash('count',   $count);
298    $ctx->stash('stash_key', $app->mode)
299        if 'search' ne $app->mode;
300    $ctx->stash('include_blogs',
301        join ',', keys %{ $app->{searchparam}{IncludeBlogs} });
302    if ( my $str = $app->{search_string} ) {
303        $ctx->stash('search_string', encode_html($str));
304    }
305    $ctx->stash('template_id', $q->param('Template'));
306    $ctx->stash('maxresults' , $app->{searchparam}{MaxResults});
307
308    $tmpl->context($ctx);
309    $tmpl;
310}
311
312sub pre_render {
313    my $app = shift;
314    my ( $tmpl ) = @_;
315    $tmpl
316}
317
318sub render {
319    my $app = shift;
320    my ( $count, $iter ) = @_;
321
322    my $tmpl = $app->load_search_tmpl( $count, $iter );
323    $tmpl = $app->pre_render( $tmpl );
324
325    $tmpl;
326}
327
328sub query_parse {
329    my $app = shift;
330    return unless $app->{search_string};
331    use utf8;
332    #local $_ = MT::I18N::decode_utf8($app->{search_string});
333    local $_ = $app->{search_string_decoded};
334
335    s/^\s//;            # Remove leading whitespace
336    s/\s$//;            # Remove trailing whitespace
337    s/\s+AND\s+/ /g;    # Remove AND because it's implied
338    s/\s{2,}/ /g;       # Remove contiguous spaces
339
340    my @search_keys;
341    my @tokens = split;
342    while (my $atom = shift @tokens) {
343        my($type);
344        if ($atom eq 'NOT' || $atom eq 'AND') {
345            $type = $atom;
346            $atom = shift @tokens;
347            $atom = find_phrase($atom, \@tokens) if $atom =~ /^\"/;
348        } elsif ($atom eq 'OR') {
349            $atom = shift @tokens;
350            $atom = find_phrase($atom, \@tokens) if $atom =~ /^\"/;
351            ## OR new atom with last atom
352            $search_keys[-1]{atom} =
353                '(?:' . $search_keys[-1]{atom} .'|' . quotemeta($atom) . ')';
354            next;
355        } elsif ($atom =~ /^-(.*)/) {
356            $type = 'NOT';
357            $atom = $1;
358            $atom = find_phrase($atom, \@tokens) if $atom =~ /^\"/;
359        } else {
360            $type = 'AND';
361            $atom = find_phrase($atom, \@tokens) if $atom =~ /^\"/;
362        }
363        push @search_keys, { atom => quotemeta($atom),
364                             type => $type };
365    }
366
367    $app->{searchparam}{search_keys} = \@search_keys;
368    $app->query_optimize;
369}
370
371sub find_phrase {
372    my($atom, $tokenref) = @_;
373    while (my $next = shift @$tokenref) {
374        $atom = $atom . ' ' . $next;
375        last if $atom =~ /\"$/;
376    }
377    $atom =~ s/^"(.*)"$/$1/;
378    $atom;
379}
380
381sub query_optimize {
382    my $app = shift;
383
384    ## Sort keys longest to shortest for search efficiency.
385    $app->{searchparam}{search_keys} = [
386        reverse sort { length($a->{atom}) <=> length($b->{atom}) }
387        @{ $app->{searchparam}{search_keys} }
388    ];
389   
390    ## Sort keys by contents. Any ORs immediately get a lower priority.
391    my %terms;
392    for my $key (@{ $app->{searchparam}{search_keys} }) {
393        if ($key->{atom} =~ /\(.*\|.*\)/) {
394            push(@{ $terms{$key->{type}}{low} }, $key);
395        } else {
396            push(@{ $terms{$key->{type}}{high} }, $key);
397        }
398    }
399
400    ## Final priority: AND long, AND short, AND with OR (long/short),
401    ## NOT long/short
402    ##  This should give us the most efficient search in that it is
403    ##  searching for the harder-to-match keys first.
404    my %regex;
405    for my $type (qw( AND NOT )) {
406        for my $pri (qw( high low )) {
407            for my $obj (@{ $terms{$type}{$pri} }) {
408                push(@{ $regex{$type} }, $obj->{atom});
409            }
410        }
411    }
412
413    $app->{searchparam}{search_keys} = \%regex;
414}
415
4161;
417__END__
418
419=head1 NAME
420
421MT::App::Search
422
423=head1 AUTHOR & COPYRIGHT
424
425Please see L<MT/AUTHOR & COPYRIGHT>.
426
427=cut
Note: See TracBrowser for help on using the browser.