File Coverage

blib/lib/Search/Indexer/Incremental/MD5.pm
Criterion Covered Total %
statement 23 25 92.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 32 34 94.1


line stmt bran cond sub pod time code
1             package Search::Indexer::Incremental::MD5 ;
2              
3 1     1   33841 use strict;
  1         2  
  1         28  
4 1     1   5 use warnings ;
  1         2  
  1         24  
5 1     1   5 use Carp qw(carp croak confess) ;
  1         11  
  1         102  
6              
7             BEGIN 
8             {
9 1         17 use Sub::Exporter -setup =>
10             {
11             exports =>
12             [
13             qw
14             (
15             delete_indexing_databases
16             show_database_information
17             add_files
18             remove_files
19             check_index
20             search_string
21             )
22             ],
23            
24             groups  =>
25             {
26             all  => [
27             qw
28             (
29             delete_indexing_databases
30             show_database_information
31             add_files
32             remove_files
33             check_index
34             search_string
35             )
36             ],
37             }
38 1     1   986 };
  1         29442  
39            
40 1     1   570 use vars qw ($VERSION);
  1         4  
  1         62  
41 1     1   22 $VERSION     = '0.06';
42             }
43              
44             #----------------------------------------------------------------------------------------------------------
45              
46 1     1   2048 use File::stat;
  1         9409  
  1         7  
47 1     1   1070 use Time::localtime;
  1         2817  
  1         55  
48 1     1   471 use BerkeleyDB;
  0            
  0            
49             use List::Util qw/sum/;
50              
51             use Search::Indexer::Incremental::MD5::Indexer qw() ;
52             use Search::Indexer::Incremental::MD5::Searcher qw() ;
53             use Search::Indexer::Incremental::MD5::Language::Perl qw(get_perl_word_regex_and_stopwords) ;
54              
55             use Digest::MD5 ;
56             use English qw( -no_match_vars ) ;
57              
58             use Readonly ;
59             Readonly my $EMPTY_STRING => q{} ;
60              
61             #----------------------------------------------------------------------------------------------------------
62              
63             =head1 NAME
64            
65             Search::Indexer::Incremental::MD5 - Incrementally index your files
66            
67             =head1 SYNOPSIS
68            
69             use File::Find::Rule ;
70            
71             use Readonly ;
72             Readonly my $DEFAUT_MAX_FILE_SIZE_INDEXING_THRESHOLD => 300 << 10 ; # 300KB
73            
74             my $indexer
75             = Search::Indexer::Incremental::MD5::Indexer->new
76             (
77             USE_POSITIONS => 1,
78             INDEX_DIRECTORY => 'text_index',
79             get_perl_word_regex_and_stopwords(),
80             ) ;
81            
82             my @files = File::Find::Rule
83             ->file()
84             ->name( '*.pm', '*.pod' )
85             ->size( "<=$DEFAUT_MAX_FILE_SIZE_INDEXING_THRESHOLD" )
86             ->not_name(qr[auto | unicore | DateTime/TimeZone | DateTime/Locale])
87             ->in('.') ;
88            
89             indexer->add_files(@files) ;
90             indexer->add_files(@more_files) ;
91             indexer = undef ;
92            
93             my $search_string = 'find_me' ;
94             my $searcher =
95             eval
96             {
97             Search::Indexer::Incremental::MD5::Searcher->new
98             (
99             USE_POSITIONS => 1,
100             INDEX_DIRECTORY => 'text_index',
101             get_perl_word_regex_and_stopwords(),
102             )
103             } or croak "No full text index found! $@\n" ;
104            
105             my $results = $searcher->search($search_string) ;
106            
107             # sort in decreasing score order
108             my @indexes = map { $_->[0] }
109             reverse
110             sort { $a->[1] <=> $b->[1] }
111             map { [$_, $results->[$_]{SCORE}] }
112             0 .. $#$results ;
113            
114             for (@indexes)
115             {
116             print {* STDOUT} "$results->[$_]{PATH} [$results->[$_]{SCORE}].\n" ;
117             }
118            
119             $searcher = undef ;
120            
121            
122             =head1 DESCRIPTION
123            
124             This module implements an incremental text indexer and searcher based on L<Search::Indexer>.
125            
126             =head1 DOCUMENTATION
127            
128             Given a list of files, this module will allow you to create an indexed text database that you can later
129             query for matches. You can also use the B<siim> command line application installed with this module.
130            
131             =head1 SUBROUTINES/METHODS
132            
133             =cut
134              
135             #----------------------------------------------------------------------------------------------------------
136              
137             Readonly my $ID_TO_METADATA_FILE => 'id_to_docs_metadata.bdb' ;
138              
139             #----------------------------------------------------------------------------------------------------------
140              
141             sub show_database_information
142             {
143              
144             =head2 show_database_information($index_directory)
145            
146             I<Arguments>
147            
148             =over 2
149            
150             =item * $index_directory - location of the index databases
151            
152             =back
153            
154             I<Returns> - A hash reference. Keys represent an information field.
155            
156             I<Exceptions> - Error opening the indexing database
157            
158             =cut
159              
160             my ($index_directory) = @_ ;
161              
162             croak 'Error: index directory not defined!' unless defined $index_directory ;
163              
164             Readonly my $ID_TO_METADATA_FILE_AND_PATH => "$index_directory/$ID_TO_METADATA_FILE" ;
165              
166             # use id_to_docs_metadata.bdb, to store a lookup from the uniq id
167             # to the document metadata {$doc_id => "$md5\t$path"}
168             tie my %id_to_metadata, 'BerkeleyDB::Hash', ## no critic (Miscellanea::ProhibitTies)
169             -Filename => $ID_TO_METADATA_FILE_AND_PATH,
170             -Flags    => DB_CREATE
171             or croak "Error: opening '$ID_TO_METADATA_FILE_AND_PATH': $^E $BerkeleyDB::Error";
172              
173             return
174             {
175             entries => scalar(grep {defined $id_to_metadata{$_}} keys %id_to_metadata),
176             size => sum(map {-s} (glob("$index_directory/*.bdb"), $ID_TO_METADATA_FILE_AND_PATH)),
177             update_date => ctime(stat($ID_TO_METADATA_FILE_AND_PATH)->mtime),
178             } ;
179             }
180              
181             #----------------------------------------------------------------------------------------------------------
182              
183             sub delete_indexing_databases
184             {
185              
186             =head2 delete_indexing_databases($index_directory)
187            
188             Removes all the index databases in the passed directory
189            
190             I<Arguments>
191            
192             =over 2
193            
194             =item * $index_directory - location of the index databases
195            
196             =back
197            
198             I<Returns> - Nothing
199            
200             I<Exceptions> - Can't remove index databases.
201            
202             =cut
203              
204             my ($index_directory) = @_ ;
205              
206             croak "Error: Invalid or undefined index directory!\n" unless defined $index_directory ;
207              
208             for my $file_to_remove
209             (
210             "$index_directory/$ID_TO_METADATA_FILE",
211             "$index_directory/ixd.bdb",
212             "$index_directory/ixw.bdb",
213             )
214             {
215             unlink $file_to_remove or croak "Error: Can't unlink '$file_to_remove': $!" ;
216             }
217              
218             return ;
219             }
220              
221             #----------------------------------------------------------------------------------------------------------
222              
223             sub search_string
224             {
225              
226             =head2 search_string(\%arguments)
227            
228             Displays all the files matching the search query.
229            
230             I<Arguments>
231            
232             =over 2
233            
234             =item \%arguments -
235            
236             =over 2
237            
238             =item -
239            
240             =item $arguments->{perl_mode} - Boolean - Use Perl specific word regex and stopwords
241            
242             =item $arguments->{index_directory} - The location of the index database
243            
244             =item $arguments->{use_position} - See L<Sear::Indexer> for a complete documentation
245            
246             =item $arguments->{search} - String - The search query
247            
248             =item $arguments->{verbose} - Boolean - Display the document id and score if set
249            
250             =back
251            
252             =item $search_string -
253            
254             =back
255            
256             I<Returns> - Nothing
257            
258             I<Exceptions> - None
259            
260             =cut
261              
262             my ($arguments) = @_ ;
263              
264             my $searcher
265             = eval
266             {
267             Search::Indexer::Incremental::MD5::Searcher->new
268             (
269             INDEX_DIRECTORY => $arguments->{index_directory},
270             USE_POSITIONS => $arguments->{use_position},
271             );
272             } or croak "No full text index found! $@\n" ;
273              
274             my $results = $searcher->search(SEARCH_STRING => $arguments->{search}) ;
275              
276             ## no critic (ProhibitDoubleSigils)
277             my @indexes = map { $_->[0] }
278             reverse
279             sort { $a->[1] <=> $b->[1] }
280             map { [$_, $results->[$_]{SCORE}] }
281             0 .. $#$results ;
282              
283             for my $index (@indexes)
284             {
285             my $matching_file = $results->[$index]{PATH} ;
286            
287             if($arguments->{verbose})
288             {
289             print {* STDOUT} "'$matching_file' [id:$results->[$index]{ID}] with score $results->[$index]{SCORE}.\n" ;
290             }
291             else
292             {
293             print {* STDOUT} "$matching_file\n" ;
294             }
295             }
296            
297             return ;
298             }
299              
300             #----------------------------------------------------------------------------------------------------------
301              
302             sub add_files
303             {
304              
305             =head2 add_files(\%arguments, \@files)
306            
307             Adds files to index, if the files are modified, and displays their name.
308            
309             I<Arguments>
310            
311             =over 2
312            
313             =item \%arguments -
314            
315             =over 2
316            
317             =item $arguments->{perl_mode} - Boolean - Use Perl specific word regex and stopwords
318            
319             =item $arguments->{stopwords_file} - Optional- Name of the file containing the stopwords to use (overridden by the perl option)
320            
321             =item $arguments->{index_directory} - The location of the index database
322            
323             =item $arguments->{use_position} - See L<Sear::Indexer> for a complete documentation
324            
325             =item $arguments->{maximum_document_size} - Integer - Only files with size inferior to this limit will be added
326            
327             =item $arguments->{verbose} - Boolean - Display the document id and score if set
328            
329             =back
330            
331             =item \@files - Files to be added in the index
332            
333             =back
334            
335             I<Returns> - Nothing
336            
337             I<Exceptions> - None
338            
339             =cut
340              
341             my ($arguments, $files) = @_ ;
342              
343             my @perl_extra_arguments ;
344             @perl_extra_arguments = get_perl_word_regex_and_stopwords() if($arguments->{perl_mode}) ;
345              
346             my @stopwords ;
347             @stopwords = (STOPWORDS => $arguments->{stopwords_file}) if($arguments->{stopwords_file}) ;
348              
349             my $indexer
350             = Search::Indexer::Incremental::MD5::Indexer->new
351             (
352             INDEX_DIRECTORY => $arguments->{index_directory},
353             USE_POSITIONS => $arguments->{use_position},
354             WORD_REGEX => qr/\w+/smx,
355             @stopwords,
356             @perl_extra_arguments,
357             ) ;
358              
359             $indexer->add_files
360             (
361             FILES => [sort @{$files}],
362             MAXIMUM_DOCUMENT_SIZE => $arguments->{maximum_document_size},
363             DONE_ONE_FILE_CALLBACK =>
364             sub
365             {
366             my ($file, $description, $file_info) = @_ ;
367            
368             if($file_info->{STATE} == 0)
369             {
370             if($arguments->{verbose})
371             {
372             printf {* STDOUT} "'$file' [id:$file_info->{ID}] up to date %.3f s.\n", $file_info->{TIME} ;
373             }
374             }
375             elsif($file_info->{STATE} == 1)
376             {
377             if($arguments->{verbose})
378             {
379             printf {* STDOUT} "'$file' [id:$file_info->{ID}] re-indexed in %.3f s.\n", $file_info->{TIME} ;
380             }
381             else
382             {
383             print {* STDOUT} "$file\n" ;
384             }
385             }
386             elsif($file_info->{STATE} == 2)
387             {
388             if($arguments->{verbose})
389             {
390             printf {* STDOUT} "'$file' [id:$file_info->{ID}] new file %.3f s.\n", $file_info->{TIME} ;
391             }
392             else
393             {
394             print {* STDOUT} "$file\n" ;
395             }
396             }
397             else
398             {
399             croak "Error: Unexpected file '$file' state!\n" ;
400             }
401             }
402             ) ;
403              
404             return
405             }
406              
407             #----------------------------------------------------------------------------------------------------------
408              
409             sub remove_files
410             {
411              
412             =head2 remove_files(\%arguments, \@files)
413            
414             Remove the passed files from the index
415            
416             I<Arguments>
417            
418             =over 2
419            
420             =item $\%arguments -
421            
422             =over 2
423            
424             =item $arguments->{perl_mode} - Boolean - Use Perl specific word regex and stopwords
425            
426             =item $arguments->{stopwords_file} - Optional- Name of the file containing the stopwords to use (overridden by the perl option)
427            
428             =item $arguments->{index_directory} - The location of the index database
429            
430             =item $arguments->{use_position} - See L<Sear::Indexer> for a complete documentation
431            
432             =item $arguments->{verbose} - Boolean - Display the document id and score if set
433            
434             =back
435            
436             =item \@files - Files to be removed
437            
438             =back
439            
440             I<Returns> - Nothing
441            
442             I<Exceptions> - None
443            
444             =cut
445              
446             my ($arguments, $files) = @_ ;
447              
448             my @perl_extra_arguments ;
449             @perl_extra_arguments = get_perl_word_regex_and_stopwords() if($arguments->{perl_mode}) ;
450              
451             my @stopwords ;
452             @stopwords = (STOPWORDS => $arguments->{stopwords_file}) if($arguments->{stopwords_file}) ;
453              
454             my $indexer
455             = Search::Indexer::Incremental::MD5::Indexer->new
456             (
457             INDEX_DIRECTORY => $arguments->{index_directory},
458             USE_POSITIONS => $arguments->{use_position},
459             WORD_REGEX => qr/\w+/smx,
460             @stopwords,
461             @perl_extra_arguments,
462             ) ;
463              
464             $indexer->remove_files
465             (
466             FILES => $files,
467             DONE_ONE_FILE_CALLBACK =>
468             sub
469             {
470             my ($file, $description, $file_info) = @_ ;
471              
472             if($file_info->{STATE} == 0)
473             {
474             if($arguments->{verbose})
475             {
476             printf {* STDOUT} "'$file' [id:$file_info->{ID}] removed in %.3f s.\n", $file_info->{TIME} ;
477             }
478             else
479             {
480             print {* STDOUT} "$file\n" ;
481             }
482             }
483             elsif($file_info->{STATE} == 1)
484             {
485             if($arguments->{verbose})
486             {
487             printf {* STDOUT} "'$file' not found in %.3f s.\n", $file_info->{TIME} ;
488             }
489             }
490             else
491             {
492             croak "Error: Unexpected file '$file' state!\n" ;
493             }
494             }
495             ) ;
496            
497             return ;
498             }
499              
500             #----------------------------------------------------------------------------------------------------------
501              
502             sub check_index
503             {
504              
505             =head2 check_index(\%arguments)
506            
507             check the files in the index
508            
509             I<Arguments>
510            
511             =over 2
512            
513             =item \%arguments -
514            
515             =over 2
516            
517             =item $arguments->{perl_mode} - Boolean - Use Perl specific word regex and stopwords
518            
519             =item $arguments->{stopwords_file} - Optional- Name of the file containing the stopwords to use (overridden by the perl option)
520            
521             =item $arguments->{index_directory} - The location of the index database
522            
523             =item $arguments->{use_position} - See L<Sear::Indexer> for a complete documentation
524            
525             =item $arguments->{verbose} - Boolean - Display the document id and score if set
526            
527             =back
528            
529             =back
530            
531             I<Returns> - Nothing
532            
533             I<Exceptions> - None
534            
535             =cut
536              
537             my ($arguments) = @_ ;
538              
539             my @perl_extra_arguments ;
540             @perl_extra_arguments = get_perl_word_regex_and_stopwords() if($arguments->{perl_mode}) ;
541              
542             my @stopwords ;
543             @stopwords = (STOPWORDS => $arguments->{stopwords_file}) if($arguments->{stopwords_file}) ;
544              
545             my $indexer
546             = Search::Indexer::Incremental::MD5::Indexer->new
547             (
548             INDEX_DIRECTORY => $arguments->{index_directory},
549             USE_POSITIONS => $arguments->{use_position},
550             WORD_REGEX => qr/\w+/smx,
551             @stopwords,
552             @perl_extra_arguments,
553             ) ;
554              
555             $indexer->check_indexed_files
556             (
557             DONE_ONE_FILE_CALLBACK =>
558             sub
559             {
560             my ($file, $description,$file_info) = @_ ;
561              
562             if($file_info->{STATE} == 0)
563             {
564             if($arguments->{verbose})
565             {
566             printf {* STDOUT} "'$file' [id:$file_info->{ID}] found and identical in %.3f s.\n", $file_info->{TIME} ;
567             }
568             else
569             {
570             print {* STDOUT} "$file\n" ;
571             }
572             }
573             elsif($file_info->{STATE} == 1)
574             {
575             if($arguments->{verbose})
576             {
577             printf {* STDOUT} "'$file' [id:$file_info->{ID}] file found, contents differ %.3f s.\n", $file_info->{TIME} ;
578             }
579             else
580             {
581             print {* STDOUT} "$file\n" ;
582             }
583             }
584             elsif($file_info->{STATE} == 2)
585             {
586             if($arguments->{verbose})
587             {
588             printf {* STDOUT} "'$file' [id:$file_info->{ID}] not found in %.3f s.\n", $file_info->{TIME} ;
589             }
590             else
591             {
592             print {* STDOUT} "$file\n" ;
593             }
594             }
595             else
596             {
597             croak "Error: Unexpected file '$file' state!\n" ;
598             }
599             }
600             ) ;
601              
602             return ;
603             }
604              
605             #----------------------------------------------------------------------------------------------------------
606              
607             sub get_file_MD5
608             {
609              
610             =head2 get_file_MD5($file)
611            
612             Returns the MD5 of the I<$file> argument.
613            
614             I<Arguments>
615            
616             =over 2
617            
618             =item $file - The location of the file to compute an MD5 for
619            
620             =back
621            
622             I<Returns> - A string containing the file md5
623            
624             I<Exceptions> - fails if the file can't be open
625            
626             =cut
627              
628             my ($file) = @_ ;
629             open(my $fh, '<', $file) or croak "Error: Can't open '$file' to compute MD5: $!";
630             binmode($fh);
631              
632             my $md5 = Digest::MD5->new->addfile($fh)->hexdigest ;
633              
634             close $fh or croak 'Error: Can not close file!' ;
635              
636             return $md5 ;
637             }
638              
639             #----------------------------------------------------------------------------------------------------------
640              
641             1 ;
642              
643             =head1 BUGS AND LIMITATIONS
644            
645             None so far.
646            
647             =head1 AUTHOR
648            
649             Nadim ibn hamouda el Khemir
650             CPAN ID: NKH
651             mailto: nadim@cpan.org
652            
653             =head1 LICENSE AND COPYRIGHT
654            
655             This program is free software; you can redistribute
656             it and/or modify it under the same terms as Perl itself.
657            
658             =head1 SUPPORT
659            
660             You can find documentation for this module with the perldoc command.
661            
662             perldoc Search::Indexer::Incremental::MD5
663            
664             You can also look for information at:
665            
666             =over 4
667            
668             =item * AnnoCPAN: Annotated CPAN documentation
669            
670             L<http://annocpan.org/dist/Search-Indexer-Incremental-MD5>
671            
672             =item * RT: CPAN's request tracker
673            
674             Please report any bugs or feature requests to L <bug-search-indexer-incremental-md5@rt.cpan.org>.
675            
676             We will be notified, and then you'll automatically be notified of progress on
677             your bug as we make changes.
678            
679             =item * Search CPAN
680            
681             L<http://search.cpan.org/dist/Search-Indexer-Incremental-MD5>
682            
683             =back
684            
685             =head1 SEE ALSO
686            
687             L<Search::Indexer>
688            
689             L<Search::Indexer::Incremental::MD5::Indexer> and L<Search::Indexer::Incremental::MD5::Searcher>
690            
691             =cut
692