[rt #1301] refactor makeIndex.pl into separate CGI and command-line tools
authorTony Cook <tony@develop-help.com>
Mon, 6 Feb 2012 11:17:38 +0000 (22:17 +1100)
committerTony Cook <tony@develop-help.com>
Wed, 8 Feb 2012 00:08:13 +0000 (11:08 +1100)
MANIFEST
MANIFEST.SKIP
site/cgi-bin/admin/makeIndex.pl
site/cgi-bin/modules/BSE/Index.pm [new file with mode: 0644]
site/data/db/bse_background_tasks.data
site/data/db/bse_msg_base.data
site/data/db/bse_msg_defaults.data
site/templates/admin/makeindex.tmpl [new file with mode: 0644]
site/util/bse_makeindex.pl [new file with mode: 0644]

index 34298ef..69b144a 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -109,6 +109,7 @@ site/cgi-bin/modules/BSE/ImportSourceXLS.pm
 site/cgi-bin/modules/BSE/ImportTargetArticle.pm
 site/cgi-bin/modules/BSE/ImportTargetBase.pm
 site/cgi-bin/modules/BSE/ImportTargetProduct.pm
+site/cgi-bin/modules/BSE/Index.pm
 site/cgi-bin/modules/BSE/Index/Base.pm
 site/cgi-bin/modules/BSE/Index/BSE.pm
 site/cgi-bin/modules/BSE/Jobs/AuditClean.pm
@@ -546,6 +547,7 @@ site/templates/admin/log/entry.tmpl
 site/templates/admin/log/log.tmpl
 site/templates/admin/log/mail.tmpl
 site/templates/admin/logon.tmpl
+site/templates/admin/makeindex.tmpl
 site/templates/admin/memberupdate/import.tmpl
 site/templates/admin/memberupdate/preview.tmpl
 site/templates/admin/memberupdate/request.tmpl
@@ -772,6 +774,7 @@ site/templates/user/unsubone_base.tmpl
 site/templates/user/userpage_base.tmpl
 site/templates/xbase.tmpl
 site/util/bse_back.pl
+site/util/bse_makeindex.pl
 site/util/bse_mkgitversion.pl
 site/util/bse_msgcheck.pl      Scan for undefined message ids
 site/util/bse_nightly.pl
index 03df954..2f718be 100644 (file)
@@ -21,3 +21,4 @@
 ^site/cgi-bin/modules/BSE/Modules\.pm$
 ^site/cgi-bin/modules/Squirrel/ImageEditor\.pm$
 ^\.?\#
+/\.?\#
index 3b6a6c6..39f6cf6 100755 (executable)
 #!/usr/bin/perl -w
+# -d:ptkdb
+BEGIN { $ENV{DISPLAY} = '192.168.32.195:0.0' }
 use strict;
 use FindBin;
 use lib "$FindBin::Bin/../modules";
-use Articles;
-use Constants qw($BASEDIR $MAXPHRASE $DATADIR @SEARCH_EXCLUDE @SEARCH_INCLUDE $SEARCH_LEVEL);
-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;
+use BSE::Request;
+use BSE::Index;
+use BSE::Util::HTML;
+use Carp 'confess';
+
+$SIG{__DIE__} = sub { confess $@ };
+
+{
   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;
+  if (has_access($req)) {
+    do_regen($req);
   }
-
-  $cfg = BSE::Cfg->new;
-  BSE::DB->init($cfg);
 }
 
-my $urlbase = $cfg->entryVar('site', 'url');
-
-my $articles = 'Articles';
-
-# scores depending on where the term appears
-# these may need some tuning
-# preferably, keep these to single digits
-my %scores =
-  (
-   title=>5,
-   body=>3,
-   keyword=>4,
-   pageTitle=>5,
-   author=>4,
-   file_displayName => 2,
-   file_description=>2,
-   file_notes => 1,
-   summary => 0,
-   description => 0,
-   product_code => 0,
-  );
-
-for my $name (keys %scores) {
-  my $score = $cfg->entry('search index scores', $name);
-  if (defined($score) && $score =~ /^\d+$/) {
-    $scores{$name} = $score;
+sub has_access {
+  my ($req) = @_;
+
+  my $cfg = $req->cfg;
+  my ($code, $msg, $url);
+  if ($req->check_admin_logon()) {
+    unless ($req->user_can("bse_makeindex")) {
+      ($code, $msg) = ( "ACCESS", [ "msg:bse/admin/generic/accessdenied", [ "bse_makeindex" ] ] );
+    }
+  }
+  else {
+    ($code, $msg, $url) = ( "LOGON", [ "msg:bse/admin/logon/needlogon" ], $cfg->admin_url("logon") );
+  }
+  if ($code) {
+    $url ||= $cfg->admin_url("menu");
+    if ($req->want_json_response) {
+      if (ref $msg) {
+       $msg = $req->catmsg(@$msg);
+      }
+      $req->output_result
+       ($req->json_content
+        ({
+          success => 0,
+          error_code => $code,
+          message => $msg,
+          errors => {},
+         }));
+    }
+    else {
+      $req->flash_error(@$msg);
+      $req->output_result($req->get_refresh($url));
+    }
+    return;
   }
-}
 
-# if the level of the article is higher than this, store it's parentid 
-# instead
-my $max_level = $SEARCH_LEVEL;
-
-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,
-   verbose => $verbose,
-  );
-
-eval {
-  vnote($start, $verbose, "s1::Starting index");
-  $indexer->start_index();
-  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";
+  return 1;
 }
 
-if ($in_cgi && !$verbose) {
-  refresh_to_admin($cfg, "/cgi-bin/admin/menu.pl");
-}
+sub do_regen {
+  my ($req) = @_;
 
-sub makeIndex {
-  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;
-    # find the section
-    my $article = $articles->getByPkey($id);
-    next unless ($article->{listed} || $article->{flags} =~ /I/);
-    next unless $article->is_linked;
-    next if $article->{flags} =~ /[CN]/;
-    my $section = $article;
-    while ($section->{parentid} >= 1) {
-      $section = $articles->getByPkey($section->{parentid});
-      next INDEX if $section->{flags} =~ /C/;
+  my $outputcb = sub {
+    my $text = shift;
+    defined $text or confess "undef output";
+    if ($req->charset) {
+      require Encode;
+      Encode->import;
+      $text = Encode::encode($req->charset, $text, Encode::FB_DEFAULT());
     }
-    my $id = $article->{id};
-    my $indexas = $article->{level} > $max_level ? $article->{parentid} : $id;
-    my $sectionid = $section->{id};
-    eval "use $article->{generator}";
-    $@ and die $@;
-    my $gen = $article->{generator}->new(top=>$article, cfg=>$cfg);
-    next unless $gen->visible($article) or $do_search{$sectionid};
-    
-    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;
+    print $text;
+  };
+
+  my $cgi = $req->cgi;
+  my $cfg = $req->cfg;
+  my $verbose = $cgi->param("verbose") || 0;
+  my $type = $cgi->param("type") || "html";
+  my ($suffix, $permessage);
+  my %acts;
+  my ($errorcb, $notecb);
+  if ($verbose) {
+    ++$|;
+    if ($type eq "html") {
+      require BSE::Template;
+      %acts = $req->admin_tags;
+      my $temp_result = BSE::Template->get_page("admin/makeindex", $req->cfg, \%acts);
+      (my ($prefix), $permessage, $suffix) =
+       split /<:\s*iterator\s+(?:begin|end)\s+messages\s*:>/, $temp_result;
+      print "Content-Type: ", BSE::Template->html_type($cfg), "\n\n";
+      $outputcb->($prefix);
+      my $out = sub {
+       my ($error, @msg) = @_;
+       $acts{ifError} = $error;
+       $acts{message} = escape_html(join("", @msg));
+       $outputcb->(BSE::Template->replace($permessage, $req->cfg, \%acts));
+      };
+      $errorcb = sub {
+       my (@msg) = @_;
+       
+       $out->(1, @_);
+      };
+      $notecb = sub {
+       $out->(0, @_);
+      };
     }
+    else {
+      my $charset = $cfg->charset;
+      print "Content-Type: text/plain; charset=$charset\n\n";
+      $errorcb = $notecb = sub {
+       my @msg = @_;
+       $outputcb->(join("", @msg, "\n"));
+      }
+    }
+  }
+  
+  my $good = eval {
+    my $indexer = BSE::Index->new
+      (
+       error => $errorcb,
+       note => $notecb,
+      );
+    $indexer->do_index();
     
-    vnote($start, $verbose, "i:$id:Indexing '$article->{title}'");
-    
-    my %fields;
-    for my $field (sort { $scores{$b} <=> $scores{$a} } keys %scores) {
-
-      next unless $scores{$field};
-      # strip out markup
-      my $text;
-      if (exists $article->{$field}) {
-       $text = $article->{$field};
+    1;
+  };
+  if ($good) {
+    if ($verbose) {
+      my $msg = $req->catmsg("msg:bse/admin/makeindex/complete");
+      $notecb->($msg);
+      $outputcb->($suffix) if defined $suffix;
+    }
+    else {
+      if ($req->want_json_response) {
+       $req->output_result
+         ($req->json_content
+          ({
+            success => 1,
+           })
+         );
       }
       else {
-       if ($field =~ /^file_(.*)/) {
-          my $file_field = $1;
-          @files = $article->files unless $got_files++;
-          $text = join "\n", map $_->{$file_field}, @files;
-       }
+       my $url = $cgi->param("r") || $cfg->admin_url("menu");
+       $req->flash("msg:bse/admin/makeindex/complete");
+       $req->output_result($req->get_refresh($url));
       }
-      #next if $text =~ m!^\<html\>!i; # I don't know how to do this (yet)
-      if ($field eq 'body') {
-       $gen->remove_block($articles, [], \$text);
-       $text =~ s/[abi]\[([^\]]+)\]/$1/g;
+    }
+  }
+  else {
+    my $msg = $@;
+    if ($verbose) {
+      $errorcb->($msg);
+    }
+    else {
+      if ($req->want_json_response) {
+       $req->output_result
+         ($req->json_content
+          ({
+            success => 0,
+            error_code => "UNKNOWN",
+            message => $msg,
+            errors => {},
+           }));
       }
+      else {
+       my $url = $cfg->admin_url("menu");
 
-      next unless defined $text;
-
-      $fields{$field} = $text;
+       $req->flash_error($msg);
+       $req->output_result($req->get_refresh($url));
+      }
     }
-    $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";
-}
+=head1 NAME
+
+makeIndex.pl - regenerate the search index (CGI only)
+
+=head1 SYNOPSIS
+
+  http//example.com/cgi-bin/admin/makeIndex.pl
+  http//example.com/cgi-bin/admin/makeIndex.pl?verbose=1
+  http//example.com/cgi-bin/admin/makeIndex.pl?verbose=1&type=text
+
+=head1 DESCRIPTION
+
+Regenerates the BSE search index using the currently configured search
+engine indexing module.
+
+Parameters:
+
+=over
+
+=item *
+
+C<verbose> - if a true perl value, display progress to the user, see
+C<type> for the format of the progress.
+
+If false, perform the indexing silently, flash a completion message
+and refresh to the url indicated by the C<r> parameter, or the admin
+menu by default.
+
+=item *
+
+C<type> - the type of output when C<verbose> is true.  The default,
+"html", produces HTML output based on the template
+C<admin/makeindex.tmpl>.
+
+Otherwise produce C<text/plain> output in the current BSE character
+set.
+
+=item *
+
+C<r> - the URL to refresh to after non-verbose search index regen.
+Defaults to the main admin menu.
+
+=back
+
+=head1 SEE ALSO
+
+bse_makeindex.pl, BSE::Index
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=cut
+
diff --git a/site/cgi-bin/modules/BSE/Index.pm b/site/cgi-bin/modules/BSE/Index.pm
new file mode 100644 (file)
index 0000000..02ab299
--- /dev/null
@@ -0,0 +1,187 @@
+package BSE::Index;
+use strict;
+use Time::HiRes qw(time);
+use Constants qw($BASEDIR $MAXPHRASE $DATADIR @SEARCH_EXCLUDE @SEARCH_INCLUDE $SEARCH_LEVEL);
+use Articles;
+
+our $VERSION = "1.000";
+
+my %scores =
+  (
+   title=>5,
+   body=>3,
+   keyword=>4,
+   pageTitle=>5,
+   author=>4,
+   file_displayName => 2,
+   file_description=>2,
+   file_notes => 1,
+   summary => 0,
+   description => 0,
+   product_code => 0,
+  );
+
+sub new {
+  my ($class, %opts) = @_;
+
+  $opts{scores} ||= \%scores;
+  $opts{start} = time;
+  $opts{max_level} ||= $SEARCH_LEVEL;
+
+  return bless \%opts, $class;
+}
+
+sub indexer {
+  my ($self) = @_;
+
+  unless ($self->{indexer}) {
+    my $cfg = BSE::Cfg->single;
+    my $indexer_class = $cfg->entry('search', 'indexer', 'BSE::Index::BSE');
+    (my $indexer_file = $indexer_class . ".pm") =~ s!::!/!g;
+    require $indexer_file;
+
+    $self->{indexer} = $indexer_class->new
+      (
+       cfg => $cfg,
+       scores => $self->{scores},
+       verbose => $self->{verbose},
+      );
+  }
+
+  return $self->{indexer};
+}
+
+sub do_index {
+  my ($self) = @_;
+
+  my $indexer = $self->indexer;
+  eval {
+    $self->vnote("s1::Starting index");
+    $indexer->start_index();
+    $self->vnote("s2::Starting article scan");
+    $self->make_index();
+    $self->vnote("f2::Populating search index");
+    $indexer->end_index();
+    $self->vnote("f1::Indexing complete");
+  };
+  if ($@) {
+    $self->_error("Indexing error: $@");
+    return;
+  }
+  return 1;
+}
+
+sub make_index {
+  my ($self) = @_;
+
+  my %dont_search;
+  my %do_search;
+  @dont_search{@SEARCH_EXCLUDE} = @SEARCH_EXCLUDE;
+  @do_search{@SEARCH_INCLUDE} = @SEARCH_INCLUDE;
+  $self->vnote("s::Loading article ids");
+  my @ids = Articles->allids;
+  my $count = @ids;
+  $self->vnote("c:$count:$count articles to index");
+  my $cfg = BSE::Cfg->single;
+  my $indexer = $self->indexer;
+
+ INDEX: for my $id (@ids) {
+    my @files;
+    my $got_files;
+    # find the section
+    my $article = Articles->getByPkey($id);
+    next unless $article;
+    next unless ($article->{listed} || $article->{flags} =~ /I/);
+    next unless $article->is_linked;
+    next if $article->{flags} =~ /[CN]/;
+    my $section = $article;
+    while ($section->{parentid} >= 1) {
+      $section = Articles->getByPkey($section->{parentid});
+      next INDEX if $section->{flags} =~ /C/;
+    }
+    my $id = $article->{id};
+    my $indexas = $article->{level} > $self->{max_level} ? $article->{parentid} : $id;
+    my $sectionid = $section->{id};
+    eval "use $article->{generator}";
+    $@ and die $@;
+    my $gen = $article->{generator}->new(top=>$article, cfg=>$cfg);
+    next unless $gen->visible($article) or $do_search{$sectionid};
+    
+    next if $dont_search{$sectionid};
+
+    $article = $gen->get_real_article($article);
+
+    unless ($article) {
+      $self->error("$id:Full article for $id not found");
+      next;
+    }
+
+    $self->vnote("i:$id:Indexing '$article->{title}'");
+
+    my %fields;
+    for my $field (sort { $scores{$b} <=> $scores{$a} } keys %scores) {
+
+      next unless $scores{$field};
+      # strip out markup
+      my $text;
+      if (exists $article->{$field}) {
+       $text = $article->{$field};
+      }
+      else {
+       if ($field =~ /^file_(.*)/) {
+          my $file_field = $1;
+          @files = $article->files unless $got_files++;
+          $text = join "\n", map $_->{$file_field}, @files;
+       }
+      }
+      #next if $text =~ m!^\<html\>!i; # I don't know how to do this (yet)
+      if ($field eq 'body') {
+       $gen->remove_block("Articles", [], \$text);
+       $text =~ s/[abi]\[([^\]]+)\]/$1/g;
+      }
+
+      next unless defined $text;
+
+      $fields{$field} = $text;
+    }
+    $indexer->process_article($article, $section, $indexas, \%fields);
+  }
+  $self->vnote("f::Article scan complete");
+}
+
+sub error {
+  my ($self, @msg) = @_;
+
+  $self->_error($self->_time_passed, ":e:", @msg);
+}
+
+sub _error {
+  my ($self, @error) = @_;
+
+  if ($self->{error}) {
+    $self->{error}->(@error);
+  }
+  else {
+    print STDERR @error;
+  }
+}
+
+sub _time_passed {
+  my ($self) = @_;
+
+  return sprintf("%.3f", time() - $self->{start});
+}
+
+sub vnote {
+  my ($self, @msg) = @_;
+
+  $self->_note($self->_time_passed, ":", @msg);
+}
+
+sub _note {
+  my ($self, @msg) = @_;
+
+  if ($self->{note}) {
+    $self->{note}->(@msg);
+  }
+}
index d01c6ae..991d761 100644 (file)
@@ -1,5 +1,5 @@
 --
-# VERSION=1.001
+# VERSION=1.002
 id: bse_gen
 description: Regenerate Site
 binname: perl util/gen.pl
@@ -51,7 +51,7 @@ TEXT
 
 id: bse_make_index
 description: Rebuild the search index
-binname: perl cgi-bin/admin/makeIndex.pl
+binname: perl util/bse_makeindex.pl
 bin_opts: -v
 long_desc: <<TEXT
 Rebuild the search index.
index 8b01bf7..77fc3c0 100644 (file)
@@ -149,6 +149,18 @@ description: Category field errors
 id: bse/admin/edit/category/unknown
 description: if an unknown article category is received
 
+id: bse/admin/generic/
+description: General messages
+
+id: bse/admin/generic/accessdenied (%1 - required access)
+description: Displayed when the user doesn't have access due to permissions
+
+id: bse/admin/makeindex/
+description: makeIndex messages
+
+id: bse/admin/makeindex/complete
+description: Index completion message
+
 id: bse/admin/shop/
 description: Shop Administration
 
index f01815e..97856d4 100644 (file)
@@ -175,6 +175,12 @@ message: Email address '%1:{email}s' confirmed for user '%1:{userId}s'
 id: bse/admin/siteusers/membershipsaved
 message: Membership saved for group '%1:{name}s'
 
+id: bse/admin/generic/accessdenied
+message: You don't have access to this function (%1:s)
+
+id: bse/admin/makeindex/complete
+message: Search index rebuild complete
+
 id: bse/shop/cart/empty
 message: Cart emptied
 
diff --git a/site/templates/admin/makeindex.tmpl b/site/templates/admin/makeindex.tmpl
new file mode 100644 (file)
index 0000000..142fe30
--- /dev/null
@@ -0,0 +1,18 @@
+<:wrap admin/base.tmpl title => "Regenarate Search Index":>
+
+<h1>Regenerating Search Index</h1>
+
+<:iterator begin messages:>
+<:switch:>
+<:case Error:>
+<p class="error message"><:message:></p>
+<:case default:>
+<div><:message:></div>
+<:endswitch:>
+<:iterator end messages:>
+
+<p><a href="<:adminurl menu:>">Return to admin menu</a></p>
+
+<p><font size="-1">BSE Release <:release:></font></p>
+</body>
+</html>
diff --git a/site/util/bse_makeindex.pl b/site/util/bse_makeindex.pl
new file mode 100644 (file)
index 0000000..8b29694
--- /dev/null
@@ -0,0 +1,77 @@
+#!perl -w
+use strict;
+use Getopt::Long;
+use FindBin;
+use lib "$FindBin::Bin/../cgi-bin/modules";
+use BSE::Index;
+use BSE::API qw(bse_init bse_cfg);
+use Articles;
+use Encode;
+
+bse_init("../cgi-bin");
+
+Getopt::Long::Configure('bundling');
+my $verbose;
+my $article;
+GetOptions("v", \$verbose);
+$verbose = defined $verbose;
+
+my $cfg = bse_cfg();
+
+my $charset = $cfg->charset;
+my $cb;
+if ($verbose) {
+  ++$|;
+  $cb = sub {
+    my $text = join("", @_)."\n";
+    $text = encode($charset, $text, Encode::FB_DEFAULT);
+    print $text;
+  };
+}
+
+my $index = BSE::Index->new
+  (
+   note => $cb,
+   error => $cb,
+  );
+
+$index->do_index;
+
+exit;
+
+=head1 NAME
+
+bse_makeindex.pl - generate the BSE search index.
+
+=head1 SYNOPSIS
+
+  # generate the index silently
+  perl bse_makeindex.pl
+
+  # generate the index with progress
+  perl bse_makeindex.pl -v
+
+=head1 DESCRIPTION
+
+C<bse_makeindex.pl> is a command-line tool to regenerate the BSE
+search index.
+
+You can supply a C<-v> option to produce progress output.  This output
+assumes your terminal encoding matches the BSE configured character
+encoding.
+
+=head1 HISTORY
+
+Previously F<cgi-bin/admin/makeIndex.pl> worked in both modes, but the
+indexing logic was moved to L<BSE::Index> and the command-line
+specific code moved to F<bse_makeindex.pl>.
+
+=head1 AUTHOR
+
+Tony Cook <tony@develop-help.com>
+
+=head1 SEE ALSO
+
+makeIndex.pl, BSE::Index
+
+=cut