optionally build the search index using a file backing store
authorTony Cook <tony@develop-help.com>
Fri, 6 May 2011 07:43:02 +0000 (17:43 +1000)
committerTony Cook <tony@develop-help.com>
Fri, 6 May 2011 07:43:02 +0000 (17:43 +1000)
much slower, much less memory.  Can also provide a verbose option to
report progress.

site/cgi-bin/admin/bse_modules.pl
site/cgi-bin/admin/makeIndex.pl
site/cgi-bin/modules/BSE/Index/BSE.pm
site/docs/config.pod

index 74e2343..4c7f32e 100755 (executable)
@@ -69,6 +69,7 @@ my @base_check =
      'Date::Format' => 0,
      'Captcha::reCAPTCHA' => 0,
      'FLV::Info' => 0,
+     'DBM::Deep' => 2,
     },
    },
    {
index 26ba2e0..3b6a6c6 100755 (executable)
@@ -8,13 +8,45 @@ use BSE::DB;
 use Generate;
 use BSE::Cfg;
 use BSE::WebUtil 'refresh_to_admin';
+use Time::HiRes qw(time);
 my $in_cgi = exists $ENV{REQUEST_METHOD};
+my $verbose;
+my $start = time();
+
+my $cfg;
+$| = 1;
 if ($in_cgi) {
+  # make sure the user can do this
+  require BSE::Request;
+  my $req = BSE::Request->new;
+  $cfg = $req->cfg;
+  if (!$req->check_admin_logon) {
+    my $url = $req->url("logon", { m => "You must logon to dump the database" });
+    $req->output_result($req->get_refresh($url));
+    exit;
+  }
+  elsif (!$req->user_can("bse_makeindex")) {
+    my $url = $req->url("menu", { m => "You don't have access to build the search index" });
+    $req->output_result($req->get_refresh($url));
+    exit;
+  }
+
+  $verbose = $req->cgi->param("verbose") || 0;
+  print "Content-Type: text/plain\n\n" if $verbose;
   #eval "use CGI::Carp qw(fatalsToBrowser)";
 }
+else {
+  require Getopt::Long;
+  Getopt::Long->import;
+  GetOptions("v:i" => \$verbose);
+  if (defined $verbose && !$verbose) {
+    $verbose = 1;
+  }
+
+  $cfg = BSE::Cfg->new;
+  BSE::DB->init($cfg);
+}
 
-my $cfg = BSE::Cfg->new;
-BSE::DB->init($cfg);
 my $urlbase = $cfg->entryVar('site', 'url');
 
 my $articles = 'Articles';
@@ -52,27 +84,40 @@ my $indexer_class = $cfg->entry('search', 'indexer', 'BSE::Index::BSE');
 (my $indexer_file = $indexer_class . ".pm") =~ s!::!/!g;
 require $indexer_file;
 # key is phrase, value is hashref with $id -> $sectionid
-my $indexer = $indexer_class->new(cfg => $cfg, scores => \%scores);
+my $indexer = $indexer_class->new
+  (
+   cfg => $cfg,
+   scores => \%scores,
+   verbose => $verbose,
+  );
+
 eval {
+  vnote($start, $verbose, "s1::Starting index");
   $indexer->start_index();
-  makeIndex($articles);
+  vnote($start, $verbose, "s2::Starting article scan");
+  makeIndex($articles, $start, $verbose);
+  vnote($start, $verbose, "f2::Populating search index");
   $indexer->end_index();
+  vnote($start, $verbose, "f1::Indexing complete");
 };
 if ($@) {
   print STDERR "Indexing error: $@\n";
 }
 
-if ($in_cgi) {
+if ($in_cgi && !$verbose) {
   refresh_to_admin($cfg, "/cgi-bin/admin/menu.pl");
 }
 
 sub makeIndex {
-  my $articles = shift;
+  my ($articles, $start, $verbose) = @_;
   my %dont_search;
   my %do_search;
   @dont_search{@SEARCH_EXCLUDE} = @SEARCH_EXCLUDE;
   @do_search{@SEARCH_INCLUDE} = @SEARCH_INCLUDE;
+  vnote($start, $verbose, "s::Loading article ids");
   my @ids = $articles->allids;
+  my $count = @ids;
+  vnote($start, $verbose, "c:$count:$count articles to index");
  INDEX: for my $id (@ids) {
     my @files;
     my $got_files;
@@ -97,6 +142,13 @@ sub makeIndex {
     next if $dont_search{$sectionid};
 
     $article = $gen->get_real_article($article);
+
+    unless ($article) {
+      vnote($start, $verbose, "e:$id:Full article for $id not found");
+      next;
+    }
+    
+    vnote($start, $verbose, "i:$id:Indexing '$article->{title}'");
     
     my %fields;
     for my $field (sort { $scores{$b} <=> $scores{$a} } keys %scores) {
@@ -126,5 +178,12 @@ sub makeIndex {
     }
     $indexer->process_article($article, $section, $indexas, \%fields);
   }
+  vnote($start, $verbose, "f::Article scan complete");
 }
 
+sub vnote {
+  my ($start, $verbose, @text) = @_;
+
+  $verbose or return;
+  printf "%.3f:%s\n", time() - $start, "@text";
+}
index a8c3da6..0de5005 100644 (file)
@@ -4,7 +4,7 @@ use base 'BSE::Index::Base';
 use BSE::DB;
 use Constants qw($DATADIR $MAXPHRASE);
 
-our $VERSION = "1.000";
+our $VERSION = "1.001";
 
 sub new {
   my ($class, %opts) = @_;
@@ -16,7 +16,29 @@ sub new {
     or die "No dropIndex member in BSE::DB";
   $self->{insertIndex} = $self->{dh}->stmt('insertIndex')
     or die "No insertIndex member in BSE::DB";
-  $self->{index} = {};
+
+  my $priority = $self->{cfg}->entry("search", "index_priority", "speed");
+  if ($priority eq "speed") {
+    $self->{index} = {};
+  }
+  elsif ($priority eq "memory") {
+    require DBM::Deep;
+    require File::Temp;
+    my $fh = File::Temp->new;
+    $self->{index} = DBM::Deep->new
+      (
+       fh => $fh,
+       locking => 0,
+       autoflush => 0,
+       data_sector_size => 256,
+      );
+    $self->{fh} = $fh;
+    $self->{filename} = $fh->filename;
+  }
+  else {
+    die "Unknown [search].index_priority of '$priority'\n";
+  }
+  $self->{priority} = $priority;
 
   $self->{decay_multiplier} = 0.4;
 
@@ -90,26 +112,28 @@ sub process {
     
     for my $phrase (map { "@words[$start..$_]" } $start..$end) {
       if (lc $phrase ne $phrase && !$seen->{lc $phrase}++) {
-       if (exists $self->{index}{lc $phrase}{$id}) {
+       my $temp = $self->{index}{lc $phrase};
+       if (exists $temp->{$id}) {
          $weights->{lc $phrase} *= $self->{decay_multiplier};
-         $self->{index}{lc $phrase}{$id}[1] += 
-           $score * $weights->{lc $phrase};
+         $temp->{$id}[1] += $score * $weights->{lc $phrase};
        }
        else {
          $weights->{lc $phrase} = 1.0;
-         $self->{index}{lc $phrase}{$id} = [ $sectionid, $score ];
+         $temp->{$id} = [ $sectionid, $score ];
        }
+       $self->{index}{lc $phrase} = $temp;
       }
       if (!$seen->{$phrase}++) {
-       if (exists $self->{index}{$phrase}{$id}) {
+       my $temp = $self->{index}{$phrase};
+       if (exists $temp->{$id}) {
          $weights->{$phrase} *= $self->{decay_multiplier};
-         $self->{index}{$phrase}{$id}[1] += 
-           $score * $weights->{$phrase};
+         $temp->{$id}[1] += $score * $weights->{$phrase};
        }
        else {
          $weights->{$phrase} = 1.0;
-         $self->{index}{$phrase}{$id} = [ $sectionid, $score ];
+         $temp->{$id} = [ $sectionid, $score ];
        }
+       $self->{index}{$phrase} = $temp;
       }
     }
   }
@@ -133,6 +157,12 @@ sub end_index {
     $insertIndex->execute($key, "@ids", "@sections", "@scores")
       or die "Cannot insert into index: ", $insertIndex->errstr;
   }
+
+  if ($self->{priority} eq "memory") {
+    delete $self->{dbm};
+    delete $self->{fh};
+    unlink $self->{filename};
+  }
 }
 
 1;
index ca184dd..b7148b7 100644 (file)
@@ -715,6 +715,20 @@ indexing.  Default: \w+
 The field specific word match regular expression for the built-in
 search indexer.  Default: the value of C<wordre>.
 
+=item indexer
+
+Module used to build the search index.  Default: BSE::Index::BSE.
+
+=item index_priority
+
+For C<BSE::Index::BSE>, the optimization priority.  The default of
+C<speed> builds the index in memory and is very fast, but can consume
+a lot of memory.  Otherwise, set this to C<memory> to reduce memory
+usage.
+
+C<memory> priority index building requires that the DBM::Deep module
+be installed.
+
 =back
 
 =head2 [search highlight]