]> git.deb.at Git - deb/packages.git/blob - lib/Packages/Search.pm
1c369868db94014cc26e8c7bf3186f4da00f8013
[deb/packages.git] / lib / Packages / Search.pm
1 #
2 # Packages::Search
3 #
4 # Copyright (C) 2004-2006 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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                      );
62 our %EXPORT_TAGS = ( all => [ @EXPORT_OK ] );
63
64 our $VERSION = 0.01;
65
66 our $too_many_hits = 0;
67
68 sub read_entry_all {
69     my ($hash, $key, $results, $non_results, $opts) = @_;
70     my ($virt, $result) = split /\000/o, $hash->{$key} || "-\01-", 2;
71
72     my %virt = split /\01/o, $virt;
73     while (my ($suite, $provides) = each %virt) {
74         next if $suite eq '-';
75         if ($opts->{h_suites}{$suite}) {
76             push @$results, [ $key, "-", $suite, 'virtual', 'v', 'v', 'v', 'v',
77                               $provides];
78         } else {
79             push @$non_results, [ $key, "-", $suite, 'virtual', 'v', 'v', 'v', 'v',
80                                   $provides];
81         }
82     }
83
84     foreach (split /\000/o, $result) {
85         my @data = split ( /\s/o, $_, 8 );
86         debug( "Considering entry ".join( ':', @data), 2) if DEBUG;
87         if ($opts->{h_suites}{$data[1]}
88             && ($opts->{h_archs}{$data[2]} || $data[2] eq 'all')
89             && $opts->{h_sections}{$data[3]}) {
90             debug( "Using entry ".join( ':', @data), 2) if DEBUG;
91             push @$results, [ $key, @data ];
92         } else {
93             push @$non_results, [ $key, @data ];
94         }
95     }
96 }
97 sub read_entry {
98     my ($hash, $key, $results, $opts) = @_;
99     my @non_results;
100     read_entry_all( $hash, $key, $results, \@non_results, $opts );
101 }
102
103 #FIXME: make configurable
104 my %fallback_suites = (
105                        'stable-backports' => 'stable',
106                        'stable-volatile' => 'stable',
107                        experimental => 'unstable' );
108
109 sub read_entry_simple {
110     my ($hash, $key, $archives, $suite) = @_;
111     # FIXME: drop $archives
112
113     my ($virt, $result) = split /\000/o, $hash->{$key} || "-\01-\0", 2;
114     my %virt = split /\01/o, $virt; 
115     debug( "read_entry_simple: key=$key, archives=".
116            join(" ",(keys %$archives)).", suite=$suite", 1) if DEBUG;
117     debug( "read_entry_simple: virt=".join(" ",(%virt)), 2) if DEBUG;
118     # FIXME: not all of the 2^4=16 combinations of empty(results),
119     # empty(virt{suite}), empty(fb_result), empty(virt{fb_suite}) are dealt
120     # with correctly, but it's adequate enough for now
121     return [ $virt{$suite} ] unless defined $result;
122     foreach (split /\000/o, $result) {
123         my @data = split ( /\s/o, $_, 8 );
124         debug( "use entry: @data", 2 ) if DEBUG && $data[1] eq $suite;
125         return [ $virt{$suite}, @data ] if $data[1] eq $suite;
126     }
127     if (my $fb_suite = $fallback_suites{$suite}) {
128         my $fb_result = read_entry_simple( $hash, $key, $archives, $fb_suite );
129         my $fb_virt = shift(@$fb_result);
130         $virt{$suite} .= $virt{$suite} ? " $fb_virt" : $fb_virt if $fb_virt;
131         return [ $virt{$suite}, @$fb_result ] if @$fb_result;
132     }
133     return [ $virt{$suite} ];
134 }
135
136 sub read_src_entry_all {
137     my ($hash, $key, $results, $non_results, $opts) = @_;
138     my $result = $hash->{$key} || '';
139     debug( "read_src_entry_all: key=$key", 1) if DEBUG;
140     foreach (split /\000/o, $result) {
141         my @data = split ( /\s/o, $_, 6 );
142         debug( "Considering entry ".join( ':', @data), 2) if DEBUG;
143         if ($opts->{h_archives}{$data[0]}
144             && $opts->{h_suites}{$data[1]}
145             && $opts->{h_sections}{$data[2]}) {
146             debug( "Using entry ".join( ':', @data), 2) if DEBUG;
147             push @$results, [ $key, @data ];
148         } else {
149             push @$non_results, [ $key, @data ];
150         }
151     }
152 }
153 sub read_src_entry {
154     my ($hash, $key, $results, $opts) = @_;
155     my @non_results;
156     read_src_entry_all( $hash, $key, $results, \@non_results, $opts );
157 }
158 sub do_names_search {
159     my ($keywords, $packages, $postfixes, $read_entry, $opts,
160         $results, $non_results) = @_;
161
162     my $first_keyword = lc shift @$keywords;
163     @$keywords = map { lc $_ } @$keywords;
164         
165     my ($key, $prefixes) = ($first_keyword, '');
166     my %pkgs;
167     $postfixes->seq( $key, $prefixes, R_CURSOR );
168     while (index($key, $first_keyword) >= 0) {
169         if ($prefixes =~ /^\001(\d+)/o) {
170             debug( "$key has too many hits", 2 ) if DEBUG;
171             $too_many_hits += $1;
172         } else {
173           PREFIX:
174             foreach (split /\000/o, $prefixes) {
175                 $_ = '' if $_ eq '^';
176                 my $word = "$_$key";
177                 foreach my $k (@$keywords) {
178                     next PREFIX unless $word =~ /\Q$k\E/;
179                 }
180                 debug( "add word $word", 2) if DEBUG;
181                 $pkgs{$word}++;
182             }
183         }
184         last if $postfixes->seq( $key, $prefixes, R_NEXT ) != 0;
185         last if $too_many_hits or keys %pkgs >= 100;
186     }
187     
188     my $no_results = keys %pkgs;
189     if ($too_many_hits || ($no_results >= 100)) {
190         $too_many_hits += $no_results;
191         %pkgs = ( $first_keyword => 1 ) unless @$keywords;
192     }
193     foreach my $pkg (sort keys %pkgs) {
194         &$read_entry( $packages, $pkg, $results, $non_results, $opts );
195     }
196 }
197 sub do_fulltext_search {
198     my ($keywords, $file, $did2pkg, $packages, $read_entry, $opts,
199         $results, $non_results) = @_;
200
201 # NOTE: this needs to correspond with parse-packages!
202     my @tmp;
203     foreach my $keyword (@$keywords) {
204         $keyword =~ tr [A-Z] [a-z];
205         if ($opts->{exact}) {
206             $keyword = " $keyword ";
207         }
208         $keyword =~ s/[(),.-]+//og;
209         $keyword =~ s;[^a-z0-9_/+]+; ;og;
210         push @tmp, $keyword;
211     }
212     my $first_keyword = shift @tmp;
213     @$keywords = @tmp;
214
215     my $numres = 0;
216     my %tmp_results;
217     # fgrep is seriously faster than using perl
218     open DESC, '-|', 'fgrep', '-n', '--', $first_keyword, $file
219         or die "couldn't open $file: $!";
220   LINE:
221     while (<DESC>) {
222         foreach my $k (@$keywords) {
223             next LINE unless /\Q$k\E/;
224         }
225         /^(\d+)/;
226         my $nr = $1;
227         debug( "Matched line $_", 2) if DEBUG;
228         my $result = $did2pkg->{$nr};
229         foreach (split /\000/o, $result) {
230             my @data = split /\s/, $_, 3;
231 #           debug ("Considering $data[0], arch = $data[2]", 3) if DEBUG;
232 #           next unless $data[2] eq 'all' || $opts->{h_archs}{$data[2]};
233 #           debug ("Ok", 3) if DEBUG;
234             $numres++ unless $tmp_results{$data[0]}++;
235         }
236         last if $numres > 100;
237     }
238     close DESC;
239     $too_many_hits++ if $numres > 100;
240
241     my @results;
242     foreach my $pkg (keys %tmp_results) {
243         &$read_entry( $packages, $pkg, $results, $non_results, $opts );
244     }
245  }
246
247 sub do_xapian_search {
248     my ($keywords, $db, $did2pkg, $packages, $read_entry, $opts,
249         $results, $non_results) = @_;
250
251 # NOTE: this needs to correspond with parse-packages!
252     my @tmp;
253     foreach my $keyword (@$keywords) {
254         $keyword =~ tr [A-Z] [a-z];
255         if ($opts->{exact}) {
256             $keyword = " $keyword ";
257         }
258         $keyword =~ s/[(),.-]+//og;
259         $keyword =~ s;[^a-z0-9_/+]+; ;og;
260         push @tmp, $keyword;
261     }
262     my $stemmer = Lingua::Stem->new();
263     $keywords = $stemmer->stem( @tmp );
264
265     my $db = Search::Xapian::Database->new( $db );
266     my $enq = $db->enquire( OP_AND, @$keywords );
267     debug( "Xapian Query was: ".$enq->get_query()->get_description(), 1) if DEBUG;
268     my @matches = $enq->matches(0, 100);
269
270     my $numres = 0;
271     my %tmp_results;
272     foreach my $match ( @matches ) {
273         my $id = $match->get_docid();
274         my $result = $did2pkg->{$id};
275
276         foreach (split /\000/o, $result) {
277             my @data = split /\s/, $_, 3;
278 #           debug ("Considering $data[0], arch = $data[2]", 3) if DEBUG;
279 #           next unless $data[2] eq 'all' || $opts->{h_archs}{$data[2]};
280 #           debug ("Ok", 3) if DEBUG;
281             $numres++ unless $tmp_results{$data[0]}++;
282         }
283         last if $numres > 100;
284     }
285     undef $db;
286     $too_many_hits++ if $numres > 100;
287
288     foreach my $pkg (keys %tmp_results) {
289         &$read_entry( $packages, $pkg, $results, $non_results, $opts );
290     }
291 }
292
293 sub find_binaries {
294     my ($pkg, $archive, $suite, $src2bin) = @_;
295
296     my $bins = $src2bin->{$pkg} || '';
297     my %bins;
298     foreach (split /\000/o, $bins) {
299         my @data = split /\s/, $_, 5;
300
301         debug( "find_binaries: considering @data", 3 ) if DEBUG;
302         if (($data[0] eq $archive)
303             && ($data[1] eq $suite)) {
304             $bins{$data[2]}++;
305             debug( "find_binaries: using @data", 3 ) if DEBUG;
306         }
307     }
308
309     return [ keys %bins ];
310 }
311
312
313 1;