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

Revision 1855, 8.3 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 Id Date Author 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::TagSearch;
8
9use strict;
10use MT::Util qw( epoch2ts offset_time_list );
11use HTTP::Date qw( str2time );
12
13sub process {
14    my $app = shift;
15    return $app->errtrans('TagSearch works with MT::App::Search.')
16        unless $app->isa('MT::App::Search');
17
18    my ( $count, $out ) = $app->check_cache();
19    if ( defined $out ) {
20        $app->run_callbacks( 'search_cache_hit', $count, $out );
21        return $out;
22    }
23
24    my ( $terms, $args ) = &search_terms( $app );
25    return $app->error($app->errstr) if $app->errstr;
26
27    $count = 0;
28    my $iter;
29    if ( $terms && $args ) {
30        ( $count, $iter ) = $app->execute( $terms, $args );
31        return $app->error($app->errstr) unless $iter;
32
33        $app->run_callbacks( 'search_post_execute', $app, \$count, \$iter );
34    }
35
36    my $format = $app->param('format') || q();
37    my $method = "render";
38    $method .= $format if $format && $app->can($method . $format);
39    $out = $app->$method( $count, $iter );
40
41    my $result;
42    if (ref($out) && ($out->isa('MT::Template'))) {
43        defined( $result = $app->build_page($out) )
44            or return $app->error($out->errstr);
45    }
46    else {
47        $result = $out;
48    }
49
50    $app->run_callbacks( 'search_post_render', $app, $count, $result );
51    $result;
52}
53
54sub _process_lucene_query {
55    my ($search_string) = @_;
56
57    require Lucene::QueryParser;
58    my $lucene_struct = Lucene::QueryParser::parse_query( $search_string );
59
60    my @or_tags;
61    for my $term ( @$lucene_struct ) {
62        next if exists $term->{field};
63        next if 'SUBQUERY' eq $term->{query};
64
65        if ( ( 'TERM' eq $term->{query} ) || ( 'PHRASE' eq $term->{query} ) ){
66            if ( 'OR' eq $term->{conj} ) {
67                if ( my $prev = pop @or_tags ) {
68                    push @or_tags, $prev . ',' . $term->{term};
69                    next;
70                }
71            }
72            push @or_tags, $term->{term};
73        }
74    }
75    @or_tags;
76}
77
78sub add_join {
79    my ( $class, $terms, $def_args, $depth, $tag_ids ) = @_;
80
81    my $alias = $class->datasource . '_' . $depth;
82    my $j_term = "= $alias.objecttag_object_id";
83    $depth++;
84    my $this_alias = $class->datasource . '_' . $depth;
85    my %args;
86    my $tag_id = shift @$tag_ids;
87    if ( $tag_id ) {
88        my $j_arg = &add_join( $class, $terms, $def_args, $depth, $tag_ids );
89        $args{'join'} = $j_arg if $j_arg && @$j_arg;
90    }
91    if ( $tag_id ) {
92        return $class->join_on( undef,
93          {
94            object_id => \$j_term,
95            tag_id => $tag_id,
96            %$terms
97          },
98          {
99            %args,
100            %$def_args,
101            'alias' => $this_alias
102          }
103        );
104    }
105    undef;
106}
107
108sub search_terms {
109    my $app = shift;
110    my $q = $app->param;
111
112    my $offset = $q->param('startIndex') || $q->param('offset') || 0;
113    my $limit = $q->param('count') || $q->param('limit');
114    my $max = $app->{searchparam}{SearchMaxResults};
115    $limit = $max if !$limit || ( $limit - $offset > $max );
116
117    my $tag_class = $app->model('tag');
118    my $search_string = $q->param('tag') || $q->param('search');
119    $app->{search_string} = $search_string;
120
121    my @or_tag_names;
122    if ( ( $search_string =~ /\s+OR\s+/ )
123      || ( $search_string =~ /\s+AND\s+/ )
124      || ( $search_string =~ /\s*"\s*/ ) ) {
125        @or_tag_names = &_process_lucene_query( $search_string );
126    }
127    else {
128        my $counter = sub {
129            my $tmp = $_[0];
130            my $count = $tmp =~ s/,/,/g;
131            $count;
132        };
133
134        # plus to split intersection, comma to split addition
135        @or_tag_names = split /\+\s*/, $search_string;
136        # sort by the number of tags in each section (smallest first)
137        @or_tag_names = sort { $counter->($a) cmp $counter->($b) } @or_tag_names;
138    }
139
140    my @or_tags;
141    my $terms = {
142        $app->config->SearchPrivateTags ? () : ( is_private => '0' )
143    };
144    foreach my $or_tag_name ( @or_tag_names ) {
145        my %tags = map { $_ => 1, $tag_class->normalize($_) => 1 } split(/,/, $or_tag_name);
146        $terms->{name} = [ keys %tags ];
147        my @tags = $tag_class->load($terms);
148        my @tmp;
149        foreach my $tag (@tags) {
150            push @tmp, $tag->id;
151            my @more = $tag_class->load({ n8d_id => $tag->n8d_id ? $tag->n8d_id : $tag->id });
152            push @tmp, $_->id foreach @more;
153        }
154        push @or_tags, \@tmp if @tmp;
155    }
156    return ( undef, undef ) unless @or_tags;
157
158    my $ot_class = $app->model('objecttag');
159    my $class = $app->model( $app->{searchparam}{Type} )
160        or return $app->error($app->errstr);
161
162    my $params = $app->registry( 'default', 'types', $app->{searchparam}{Type} );
163    my %terms = exists( $params->{terms} )
164          ? %{ $params->{terms} }
165          : ();
166    delete $terms{'plugin'}; #FIXME: why is this in here?
167
168    if ( exists $app->{searchparam}{IncludeBlogs} ) {
169        $terms{blog_id} = [ keys %{ $app->{searchparam}{IncludeBlogs} } ];
170    }
171
172    my $depth = 1;
173    my $alias = $ot_class->datasource . '_' . $depth;
174
175    my $or_tag = shift @or_tags;
176    my $join_on_arg;
177    if ( @or_tags ) {
178        $join_on_arg = &add_join(
179            $ot_class,
180            { object_datasource => $class->datasource },
181            { },
182            $depth,
183            \@or_tags
184        );
185    }
186    #TODO: what if pk is from multiple cols?
187    my $pk = $class->datasource . '_' . $class->properties->{primary_key};
188    my $join_on = $ot_class->join_on( undef,
189        { tag_id => $or_tag,
190          object_datasource => $class->datasource,
191          object_id => \"= $pk"
192        },
193        { alias => $alias,
194          $join_on_arg && @$join_on_arg ? ( 'join' => $join_on_arg ) : ()
195        }
196    );
197
198    my $desc = 'descend' eq $app->{searchparam}{SearchResultDisplay} ? 'DESC' : 'ASC';
199    my @sort;
200    my $sort = $params->{'sort'};
201    if ( $sort !~ /\w+\!$/ && $app->{searchparam}{SearchSortBy} ) {
202        my $sort_by = $app->{searchparam}{SearchSortBy};
203        $sort_by =~ s/[^\w\-\.\,]+//g;
204        if ( $sort_by ) {
205            my @sort_bys = split ',', $sort_by;
206            foreach my $key ( @sort_bys ) {
207                push @sort, {
208                    desc   => $desc,
209                    column => $key
210                };
211            }
212        }
213    }
214    push @sort, {
215        desc   => $desc,
216        column => $sort
217    };
218
219    my %args = (
220      'join' => $join_on,
221      $limit  ? ( 'limit' => $limit ) : (),
222      $offset ? ( 'offset' => $offset ) : (),
223      @sort   ? ( 'sort' => \@sort ) : (),
224    );
225
226    my $blog_class = $app->model('blog');
227    # Override SearchCutoff if If-Modified-Since header is present
228    if ((my $mod_since = $app->get_header('If-Modified-Since')) && $app->param('Template') eq 'feed') {
229        my $tz_offset = 15;  # Start with maximum possible offset to UTC
230        my $blog_selected;
231        my $iter;
232        if ($app->{searchparam}{IncludeBlogs}) {
233            $iter = $blog_class->load_iter({ id => [ keys %{ $app->{searchparam}{IncludeBlogs} }] });
234        } else {
235            $iter = $blog_class->load_iter;
236        }
237        while (my $blog = $iter->()) {
238            my $blog_offset = $blog->server_offset ?
239                $blog->server_offset : 0;
240            if ($blog_offset < $tz_offset) {
241                $tz_offset = $blog_offset;
242                $blog_selected = $blog;
243            }
244        }
245        $mod_since = epoch2ts($blog_selected, str2time($mod_since));
246        if ( ( 'entry' eq $app->{searchparam}{Type} )
247          || ( 'page'  eq $app->{searchparam}{Type} ) ) {
248            $terms{authored_on} = [ $mod_since ];
249            $args{range} = { authored_on => 1 };
250        }
251        else {
252            $terms{created_on} = [ $mod_since ];
253            $args{range} = { created_on => 1 };
254        }
255    }
256
257    ( \%terms, \%args );
258}
259
2601;
261__END__
262
263=head1 NAME
264
265MT::App::Search::TagSearch
266
267=head1 SYNTAX
268
269TagSearch allows the following syntax in url-encoded "tag" parameter.
270
271"MovableType,TypePad,Vox" matches objects which has either MovableType,
272TypePad, or Vox tag.
273
274"MovableType+TypePad+Vox" matches objects which has all three tags.
275
276"TypePad OR Vox AND "Movable Type"" matches objects which has
277either TypePad or Vox tag *and* "Movable Type" tag.
278
279=head1 AUTHOR & COPYRIGHT
280
281Please see L<MT/AUTHOR & COPYRIGHT>.
282
283=cut
Note: See TracBrowser for help on using the browser.