root/branches/release-31/lib/MT/App/Search/TagSearch.pm @ 1516

Revision 1516, 8.1 kB (checked in by fumiakiy, 21 months ago)

Use the new caching layer in MT::App::Search::TagSearch. BugId:69031

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