]> git.deb.at Git - deb/packages.git/blob - lib/Packages/Search.pm
Import data from squeeze-backports
[deb/packages.git] / lib / Packages / Search.pm
1 #
2 # Packages::Search
3 #
4 # Copyright (C) 2004-2007 Frank Lichtenheld <frank@lichtenheld.de>
5 #
6 # The code is based on the old search_packages.pl script that
7 # was:
8 #
9 # Copyright (C) 1998 James Treacy
10 # Copyright (C) 2000, 2001 Josip Rodin
11 # Copyright (C) 2001 Adam Heath
12 # Copyright (C) 2004 Martin Schulze
13 #
14 #    This program is free software; you can redistribute it and/or modify
15 #    it under the terms of the GNU General Public License as published by
16 #    the Free Software Foundation; either version 1 of the License, or
17 #    (at your option) any later version.
18 #
19 #    This program is distributed in the hope that it will be useful,
20 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
21 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 #    GNU General Public License for more details.
23 #
24 #    You should have received a copy of the GNU General Public License
25 #    along with this program; if not, write to the Free Software
26 #    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27 #
28
29 =head1 NAME
30
31 Packages::Search - 
32
33 =head1 SYNOPSIS
34
35 =head1 DESCRIPTION
36
37 =over 4
38
39 =cut
40
41 package Packages::Search;
42
43 use strict;
44 use warnings;
45
46 use POSIX;
47 use HTML::Entities;
48 use DB_File;
49 use Lingua::Stem v0.82;
50 use Search::Xapian qw(:ops);
51
52 use Deb::Versions;
53 use Packages::CGI;
54 use Exporter;
55
56 our @ISA = qw( Exporter );
57
58 our @EXPORT_OK = qw( read_entry read_entry_all read_entry_simple
59                      read_src_entry read_src_entry_all find_binaries
60                      do_names_search do_fulltext_search do_xapian_search
61                      find_similar
62                      );
63 our %EXPORT_TAGS = ( all => [ @EXPORT_OK ] );
64
65 our $VERSION = 0.01;
66
67 our $too_many_hits = 0;
68
69 sub read_entry_all {
70     my ($hash, $key, $results, $non_results, $opts) = @_;
71     my ($virt, $result) = split /\000/o, $hash->{$key} || "-\01-", 2;
72
73     my %virt = split /\01/o, $virt;
74     while (my ($suite, $provides) = each %virt) {
75         next if $suite eq '-';
76         if ($opts->{h_suites}{$suite}) {
77             push @$results, [ $key, "-", $suite, 'virtual', 'v', 'v', 'v', 'v', 'v',
78                               $provides];
79         } else {
80             push @$non_results, [ $key, "-", $suite, 'virtual', 'v', 'v', 'v', 'v', 'v',
81                                   $provides];
82         }
83     }
84
85     foreach (split(/\000/o, $result||'')) {
86         my @data = split ( /\s/o, $_, 9 );
87         debug( "Considering entry ".join( ':', @data), 2) if DEBUG;
88         if ($opts->{h_suites}{$data[1]}
89             && ($opts->{h_archs}{$data[2]} || $data[2] eq 'all')
90             && $opts->{h_sections}{$data[3]}) {
91             debug( "Using entry ".join( ':', @data), 2) if DEBUG;
92             push @$results, [ $key, @data ];
93         } else {
94             push @$non_results, [ $key, @data ];
95         }
96     }
97 }
98 sub read_entry {
99     my ($hash, $key, $results, $opts) = @_;
100     my @non_results;
101     read_entry_all( $hash, $key, $results, \@non_results, $opts );
102 }
103
104 #FIXME: make configurable
105 my %fallback_suites = (
106                        'etch-backports' => 'etch',
107                        'etch-volatile' => 'etch',
108                        'lenny-backports' => 'lenny',
109                        'lenny-volatile' => 'lenny',
110                        'squeeze-backports' => 'squeeze',
111                        experimental => 'sid' );
112
113 sub read_entry_simple {
114     my ($hash, $key, $archives, $suite) = @_;
115     # FIXME: drop $archives
116
117     my ($virt, $result) = split /\000/o, $hash->{$key} || "-\01-\0", 2;
118     my %virt = split /\01/o, $virt; 
119     debug( "read_entry_simple: key=$key, archives=".
120            join(" ",(keys %$archives)).", suite=$suite", 1) if DEBUG;
121     debug( "read_entry_simple: virt=".join(" ",(%virt)), 2) if DEBUG;
122     # FIXME: not all of the 2^4=16 combinations of empty(results),
123     # empty(virt{suite}), empty(fb_result), empty(virt{fb_suite}) are dealt
124     # with correctly, but it's adequate enough for now
125     return [ $virt{$suite} ] unless defined $result;
126     foreach (split /\000/o, $result) {
127         my @data = split ( /\s/o, $_, 9 );
128         debug( "use entry: @data", 2 ) if DEBUG && $data[1] eq $suite;
129         return [ $virt{$suite}, @data ] if $data[1] eq $suite;
130     }
131     if (my $fb_suite = $fallback_suites{$suite}) {
132         my $fb_result = read_entry_simple( $hash, $key, $archives, $fb_suite );
133         my $fb_virt = shift(@$fb_result);
134         $virt{$suite} .= $virt{$suite} ? " $fb_virt" : $fb_virt if $fb_virt;
135         return [ $virt{$suite}, @$fb_result ] if @$fb_result;
136     }
137     return [ $virt{$suite} ];
138 }
139
140 sub read_src_entry_all {
141     my ($hash, $key, $results, $non_results, $opts) = @_;
142     my $result = $hash->{$key} || '';
143     debug( "read_src_entry_all: key=$key", 1) if DEBUG;
144     foreach (split /\000/o, $result) {
145         my @data = split ( /\s/o, $_, 6 );
146         debug( "Considering entry ".join( ':', @data), 2) if DEBUG;
147         if ($opts->{h_archives}{$data[0]}
148             && $opts->{h_suites}{$data[1]}
149             && $opts->{h_sections}{$data[2]}) {
150             debug( "Using entry ".join( ':', @data), 2) if DEBUG;
151             push @$results, [ $key, @data ];
152         } else {
153             push @$non_results, [ $key, @data ];
154         }
155     }
156 }
157 sub read_src_entry {
158     my ($hash, $key, $results, $opts) = @_;
159     my @non_results;
160     read_src_entry_all( $hash, $key, $results, \@non_results, $opts );
161 }
162 sub do_names_search {
163     my ($keywords, $packages, $postfixes, $read_entry, $opts,
164         $results, $non_results) = @_;
165
166     my $first_keyword = lc shift @$keywords;
167     @$keywords = map { lc $_ } @$keywords;
168
169     my ($key, $prefixes) = ($first_keyword, '');
170     my (%pkgs, %pkgs_min);
171     $postfixes->seq( $key, $prefixes, R_CURSOR );
172     while (index($key, $first_keyword) >= 0) {
173         if ($prefixes =~ /^(\^)?\001(\d+)/o) {
174             debug("$key has too many hits", 2 ) if DEBUG;
175             $too_many_hits += $2;
176             if ($1) { # use the empty prefix
177                 foreach my $k (@$keywords) {
178                     next unless $key =~ /\Q$k\E/;
179                 }
180                 debug("add key $key", 2) if DEBUG;
181                 $pkgs{$key}++;
182                 $pkgs_min{$key}++;
183             }
184         } else {
185           PREFIX:
186             foreach (split /\000/o, $prefixes) {
187                 $_ = '' if $_ eq '^';
188                 my $word = "$_$key";
189                 foreach my $k (@$keywords) {
190                     next PREFIX unless $word =~ /\Q$k\E/;
191                 }
192                 debug("add word $word", 2) if DEBUG;
193                 $pkgs{$word}++;
194                 $pkgs_min{$word}++ if $_ eq '';
195             }
196         }
197         last if $postfixes->seq( $key, $prefixes, R_NEXT ) != 0;
198         last if keys %pkgs_min >= 100;
199     }
200
201     my $nr = keys %pkgs;
202     my $min_nr = keys %pkgs_min;
203     debug("nr=$nr min_nr=$min_nr too_many_hits=$too_many_hits", 1) if DEBUG;
204     if ($nr >= 100) {
205         $too_many_hits += $nr - $min_nr + 1;
206         %pkgs = %pkgs_min;
207     }
208     foreach my $pkg (sort keys %pkgs) {
209         &$read_entry( $packages, $pkg, $results, $non_results, $opts );
210     }
211 }
212
213 sub do_xapian_search {
214     my ($keywords, $dbpath, $did2pkg, $packages, $read_entry, $opts,
215         $results, $non_results) = @_;
216
217 # NOTE: this needs to correspond with parse-packages!
218     my @tmp;
219     foreach my $keyword (@$keywords) {
220         $keyword =~ s;[^\w/+]+; ;og;
221         push @tmp, $keyword;
222     }
223     my $stemmer = Lingua::Stem->new();
224     my @stemmed_keywords = grep { length($_) } @{$stemmer->stem( @tmp )};
225
226     my $db = Search::Xapian::Database->new( $dbpath );
227     my $enq = $db->enquire( OP_OR, @$keywords, @stemmed_keywords );
228     debug( "Xapian Query was: ".$enq->get_query()->get_description(), 1) if DEBUG;
229     my @matches = $enq->matches(0, 999);
230
231     my (@order, %tmp_results);
232     foreach my $match ( @matches ) {
233         my $id = $match->get_docid();
234         my $result = $did2pkg->{$id};
235
236         foreach (split /\000/o, $result) {
237             my @data = split /\s/, $_, 3;
238             debug ("Considering $data[0], arch = $data[2], relevance=".$match->get_percent(), 3) if DEBUG;
239 #           next unless $data[2] eq 'all' || $opts->{h_archs}{$data[2]};
240 #           debug ("Ok", 3) if DEBUG;
241             unless ($tmp_results{$data[0]}++) {
242                 push @order, $data[0];
243             }
244         }
245         last if @order > 100;
246     }
247     undef $db;
248     $too_many_hits++ if @order > 100;
249
250     debug ("ORDER: @order", 2) if DEBUG;
251     foreach my $pkg (@order) {
252         &$read_entry( $packages, $pkg, $results, $non_results, $opts );
253     }
254 }
255
256 sub find_similar {
257     my ($pkg, $dbpath, $did2pkg) = @_;
258
259     my $db = Search::Xapian::Database->new( $dbpath );
260     my $enq = $db->enquire( "P$pkg" );
261     debug( "Xapian Query was: ".$enq->get_query()->get_description(), 1) if DEBUG;
262     my $first_match = ($enq->matches(0,1))[0]->get_document();
263
264     my @terms;
265     my $term_it = $first_match->termlist_begin();
266     my $term_end = $first_match->termlist_end();
267
268     for (; $term_it ne $term_end; $term_it++) {
269         debug( "TERM: ".$term_it->get_termname(), 3);
270         push @terms, $term_it->get_termname();
271     }
272
273     my $rel_enq = $db->enquire( OP_OR, @terms );
274     debug( "Xapian Query was: ".$rel_enq->get_query()->get_description(), 1) if DEBUG;
275     my @rel_pkg = $rel_enq->matches(2,20);
276
277 #    use Data::Dumper;
278 #    debug(Dumper(\@rel_pkg),1);
279
280     my (@order, %tmp_results);
281     foreach my $match ( @rel_pkg ) {
282         my $id = $match->get_docid();
283         my $result = $did2pkg->{$id};
284
285         foreach (split /\000/o, $result) {
286             my @data = split /\s/, $_, 3;
287             debug ("Considering $data[0], arch = $data[2], relevance=".$match->get_percent(), 3) if DEBUG;
288             next if $data[0] eq $pkg;
289             unless ($tmp_results{$data[0]}++) {
290                 push @order, $data[0];
291             }
292         }
293     }
294     undef $db;
295
296     debug ("ORDER: @order", 2) if DEBUG;
297     my $last = 10;
298     $last = $#order if $#order < $last;
299     return @order[0..$last];
300 }
301
302 sub find_binaries {
303     my ($pkg, $archive, $suite, $src2bin) = @_;
304
305     my $bins = $src2bin->{$pkg} || '';
306     my %bins;
307     foreach (split /\000/o, $bins) {
308         my @data = split /\s/, $_, 5;
309
310         debug( "find_binaries: considering @data", 3 ) if DEBUG;
311         if (($data[0] eq $archive)
312             && ($data[1] eq $suite)) {
313             $bins{$data[2]}++;
314             debug( "find_binaries: using @data", 3 ) if DEBUG;
315         }
316     }
317
318     return [ keys %bins ];
319 }
320
321
322 1;