]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/SearchIdx.pm
Merge remote-tracking branch 'origin/master' into lorelei
[public-inbox.git] / lib / PublicInbox / SearchIdx.pm
index 18390602faada97021c0b774ac944550208ac70e..95f4234c4b2b26cc2ca85ccd784a7d56b577db88 100644 (file)
@@ -1,6 +1,6 @@
 # Copyright (C) 2015-2020 all contributors <meta@public-inbox.org>
 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
-# based on notmuch, but with no concept of folders, files or flags
+# based on notmuch, but with no concept of folders, files
 #
 # Indexes mail with Xapian and our (SQLite-based) ::Msgmap for use
 # with the web and NNTP interfaces.  This index maintains thread
@@ -15,7 +15,7 @@ use PublicInbox::InboxWritable;
 use PublicInbox::MID qw(mids_for_index mids);
 use PublicInbox::MsgIter;
 use PublicInbox::IdxStack;
-use Carp qw(croak);
+use Carp qw(croak carp);
 use POSIX qw(strftime);
 use Time::Local qw(timegm);
 use PublicInbox::OverIdx;
@@ -23,7 +23,7 @@ use PublicInbox::Spawn qw(spawn nodatacow_dir);
 use PublicInbox::Git qw(git_unquote);
 use PublicInbox::MsgTime qw(msg_timestamp msg_datestamp);
 our @EXPORT_OK = qw(crlf_adjust log2stack is_ancestor check_size prepare_stack
-       index_text term_generator add_val);
+       index_text term_generator add_val is_bad_blob);
 my $X = \%PublicInbox::Search::X;
 our ($DB_CREATE_OR_OPEN, $DB_OPEN);
 our $DB_NO_SYNC = 0;
@@ -54,20 +54,16 @@ sub new {
                }
        }
        $ibx = PublicInbox::InboxWritable->new($ibx);
-       my $self = bless {
-               ibx => $ibx,
-               xpfx => $inboxdir, # for xpfx_init
-               -altid => $altid,
-               ibx_ver => $version,
-               indexlevel => $indexlevel,
-       }, $class;
-       $self->xpfx_init;
+       my $self = PublicInbox::Search->new($ibx);
+       bless $self, $class;
+       $self->{ibx} = $ibx;
+       $self->{-altid} = $altid;
+       $self->{indexlevel} = $indexlevel;
        $self->{-set_indexlevel_once} = 1 if $indexlevel eq 'medium';
        if ($ibx->{-skip_docdata}) {
                $self->{-set_skip_docdata_once} = 1;
                $self->{-skip_docdata} = 1;
        }
-       $ibx->umask_prepare;
        if ($version == 1) {
                $self->{lock_path} = "$inboxdir/ssoma.lock";
                my $dir = $self->xdir;
@@ -106,7 +102,6 @@ sub load_xapian_writable () {
        }
        eval 'require '.$X->{WritableDatabase} or die;
        *sortable_serialise = $xap.'::sortable_serialise';
-       *sortable_unserialise = $xap.'::sortable_unserialise';
        $DB_CREATE_OR_OPEN = eval($xap.'::DB_CREATE_OR_OPEN()');
        $DB_OPEN = eval($xap.'::DB_OPEN()');
        my $ver = (eval($xap.'::major_version()') << 16) |
@@ -352,8 +347,9 @@ sub index_ids ($$$$) {
        index_list_id($self, $doc, $hdr);
 }
 
-sub add_xapian ($$$$) {
+sub eml2doc ($$$;$) {
        my ($self, $eml, $smsg, $mids) = @_;
+       $mids //= mids_for_index($eml);
        my $doc = $X->{Document}->new;
        add_val($doc, PublicInbox::Search::TS(), $smsg->{ts});
        my @ds = gmtime($smsg->{ds});
@@ -370,7 +366,7 @@ sub add_xapian ($$$$) {
        index_headers($self, $smsg);
 
        if (defined(my $eidx_key = $smsg->{eidx_key})) {
-               $doc->add_boolean_term('O'.$eidx_key);
+               $doc->add_boolean_term('O'.$eidx_key) if $eidx_key ne '.';
        }
        msg_iter($eml, \&index_xapian, [ $self, $doc ]);
        index_ids($self, $doc, $eml, $mids);
@@ -396,12 +392,18 @@ sub add_xapian ($$$$) {
                        }
                }
        }
+       $doc;
+}
+
+sub add_xapian ($$$$) {
+       my ($self, $eml, $smsg, $mids) = @_;
+       my $doc = eml2doc($self, $eml, $smsg, $mids);
        $self->{xdb}->replace_document($smsg->{num}, $doc);
 }
 
 sub _msgmap_init ($) {
        my ($self) = @_;
-       die "BUG: _msgmap_init is only for v1\n" if $self->{ibx_ver} != 1;
+       die "BUG: _msgmap_init is only for v1\n" if $self->{ibx}->version != 1;
        $self->{mm} //= eval {
                require PublicInbox::Msgmap;
                my $rw = $self->{ibx}->{-no_fsync} ? 2 : 1;
@@ -445,33 +447,33 @@ sub add_message {
        $smsg->{num};
 }
 
-sub _get_doc ($$$) {
-       my ($self, $docid, $oid) = @_;
+sub _get_doc ($$) {
+       my ($self, $docid) = @_;
        my $doc = eval { $self->{xdb}->get_document($docid) };
        $doc // do {
                warn "E: $@\n" if $@;
-               warn "E: #$docid $oid missing in Xapian\n";
+               warn "E: #$docid missing in Xapian\n";
                undef;
        }
 }
 
 sub add_eidx_info {
-       my ($self, $docid, $oid, $eidx_key, $eml) = @_;
+       my ($self, $docid, $eidx_key, $eml) = @_;
        begin_txn_lazy($self);
-       my $doc = _get_doc($self, $docid, $oid) or return;
+       my $doc = _get_doc($self, $docid) or return;
        term_generator($self)->set_document($doc);
-       $doc->add_boolean_term('O'.$eidx_key);
+       $doc->add_boolean_term('O'.$eidx_key) if $eidx_key ne '.';
        index_list_id($self, $doc, $eml);
        $self->{xdb}->replace_document($docid, $doc);
 }
 
 sub remove_eidx_info {
-       my ($self, $docid, $oid, $eidx_key, $eml) = @_;
+       my ($self, $docid, $eidx_key, $eml) = @_;
        begin_txn_lazy($self);
-       my $doc = _get_doc($self, $docid, $oid) or return;
+       my $doc = _get_doc($self, $docid) or return;
        eval { $doc->remove_term('O'.$eidx_key) };
        warn "W: ->remove_term O$eidx_key: $@\n" if $@;
-       for my $l ($eml->header_raw('List-Id')) {
+       for my $l ($eml ? $eml->header_raw('List-Id') : ()) {
                $l =~ /<([^>]+)>/ or next;
                my $lid = lc $1;
                eval { $doc->remove_term('G' . $lid) };
@@ -494,17 +496,53 @@ sub remove_eidx_info {
        $self->{xdb}->replace_document($docid, $doc);
 }
 
-sub get_val ($$) {
-       my ($doc, $col) = @_;
-       sortable_unserialise($doc->get_value($col));
+sub set_keywords {
+       my ($self, $docid, @kw) = @_;
+       begin_txn_lazy($self);
+       my $doc = _get_doc($self, $docid) or return;
+       my %keep = map { $_ => 1 } @kw;
+       my %add = %keep;
+       my @rm;
+       my $end = $doc->termlist_end;
+       for (my $cur = $doc->termlist_begin; $cur != $end; $cur++) {
+               $cur->skip_to('K');
+               last if $cur == $end;
+               my $kw = $cur->get_termname;
+               $kw =~ s/\AK//s or next;
+               $keep{$kw} ? delete($add{$kw}) : push(@rm, $kw);
+       }
+       return unless (scalar(@rm) + scalar(keys %add));
+       $doc->remove_term('K'.$_) for @rm;
+       $doc->add_boolean_term('K'.$_) for (keys %add);
+       $self->{xdb}->replace_document($docid, $doc);
+}
+
+sub add_keywords {
+       my ($self, $docid, @kw) = @_;
+       begin_txn_lazy($self);
+       my $doc = _get_doc($self, $docid) or return;
+       $doc->add_boolean_term('K'.$_) for @kw;
+       $self->{xdb}->replace_document($docid, $doc);
+}
+
+sub remove_keywords {
+       my ($self, $docid, @kw) = @_;
+       begin_txn_lazy($self);
+       my $doc = _get_doc($self, $docid) or return;
+       my $replace;
+       eval {
+               $doc->remove_term('K'.$_);
+               $replace = 1
+       } for @kw;
+       $self->{xdb}->replace_document($docid, $doc) if $replace;
 }
 
 sub smsg_from_doc ($) {
        my ($doc) = @_;
        my $data = $doc->get_data or return;
        my $smsg = bless {}, 'PublicInbox::Smsg';
-       $smsg->{ts} = get_val($doc, PublicInbox::Search::TS());
-       my $dt = get_val($doc, PublicInbox::Search::DT());
+       $smsg->{ts} = int_val($doc, PublicInbox::Search::TS());
+       my $dt = int_val($doc, PublicInbox::Search::DT());
        my ($yyyy, $mon, $dd, $hh, $mm, $ss) = unpack('A4A2A2A2A2A2', $dt);
        $smsg->{ds} = timegm($ss, $mm, $hh, $dd, $mon - 1, $yyyy);
        $smsg->load_from_data($data);
@@ -512,25 +550,19 @@ sub smsg_from_doc ($) {
 }
 
 sub xdb_remove {
-       my ($self, $oid, @removed) = @_;
+       my ($self, @docids) = @_;
        my $xdb = $self->{xdb} or return;
-       for my $num (@removed) {
-               my $doc = _get_doc($self, $num, $oid) or next;
-               my $smsg = smsg_from_doc($doc);
-               my $blob = $smsg->{blob}; # may be undef if --skip-docdata
-               if (!defined($blob) || $blob eq $oid) {
-                       $xdb->delete_document($num);
-               } else {
-                       warn "E: #$num $oid != $blob in Xapian\n";
-               }
+       for my $docid (@docids) {
+               eval { $xdb->delete_document($docid) };
+               warn "E: #$docid not in in Xapian? $@\n" if $@;
        }
 }
 
-sub remove_by_oid {
-       my ($self, $oid, $num) = @_;
-       die "BUG: remove_by_oid is v2-only\n" if $self->{oidx};
+sub remove_by_docid {
+       my ($self, $num) = @_;
+       die "BUG: remove_by_docid is v2-only\n" if $self->{oidx};
        $self->begin_txn_lazy;
-       xdb_remove($self, $oid, $num) if need_xapian($self);
+       xdb_remove($self, $num) if need_xapian($self);
 }
 
 sub index_git_blob_id {
@@ -566,7 +598,7 @@ sub unindex_eml {
        } else { # just in case msgmap and over.sqlite3 become desynched:
                $self->{mm}->mid_delete($mids->[0]);
        }
-       xdb_remove($self, $oid, keys %tmp) if need_xapian($self);
+       xdb_remove($self, keys %tmp) if need_xapian($self);
 }
 
 sub index_mm {
@@ -597,29 +629,46 @@ sub crlf_adjust ($) {
        }
 }
 
+sub is_bad_blob ($$$$) {
+       my ($oid, $type, $size, $expect_oid) = @_;
+       if ($type ne 'blob') {
+               carp "W: $expect_oid is not a blob (type=$type)";
+               return 1;
+       }
+       croak "BUG: $oid != $expect_oid" if $oid ne $expect_oid;
+       $size == 0 ? 1 : 0; # size == 0 means purged
+}
+
 sub index_both { # git->cat_async callback
        my ($bref, $oid, $type, $size, $sync) = @_;
+       return if is_bad_blob($oid, $type, $size, $sync->{oid});
        my ($nr, $max) = @$sync{qw(nr max)};
        ++$$nr;
        $$max -= $size;
        $size += crlf_adjust($$bref);
        my $smsg = bless { bytes => $size, blob => $oid }, 'PublicInbox::Smsg';
        my $self = $sync->{sidx};
+       local $self->{current_info} = "$self->{current_info}: $oid";
        my $eml = PublicInbox::Eml->new($bref);
        $smsg->{num} = index_mm($self, $eml, $oid, $sync) or
                die "E: could not generate NNTP article number for $oid";
        add_message($self, $eml, $smsg, $sync);
+       ++$self->{nidx};
        my $cur_cmt = $sync->{cur_cmt} // die 'BUG: {cur_cmt} missing';
        ${$sync->{latest_cmt}} = $cur_cmt;
 }
 
 sub unindex_both { # git->cat_async callback
        my ($bref, $oid, $type, $size, $sync) = @_;
-       unindex_eml($sync->{sidx}, $oid, PublicInbox::Eml->new($bref));
+       return if is_bad_blob($oid, $type, $size, $sync->{oid});
+       my $self = $sync->{sidx};
+       local $self->{current_info} = "$self->{current_info}: $oid";
+       unindex_eml($self, $oid, PublicInbox::Eml->new($bref));
        # may be undef if leftover
        if (defined(my $cur_cmt = $sync->{cur_cmt})) {
                ${$sync->{latest_cmt}} = $cur_cmt;
        }
+       ++$self->{nidx};
 }
 
 sub with_umask {
@@ -632,10 +681,11 @@ sub index_sync {
        my ($self, $opt) = @_;
        delete $self->{lock_path} if $opt->{-skip_lock};
        $self->with_umask(\&_index_sync, $self, $opt);
-       if ($opt->{reindex}) {
+       if ($opt->{reindex} && !$opt->{quit}) {
                my %again = %$opt;
                delete @again{qw(rethread reindex)};
                index_sync($self, \%again);
+               $opt->{quit} = $again{quit}; # propagate to caller
        }
 }
 
@@ -688,7 +738,7 @@ sub v1_checkpoint ($$;$) {
        if (my $pr = $sync->{-opt}->{-progress}) {
                $pr->("indexed $nr/$sync->{ntodo}\n") if $nr;
        }
-       if (!$stk) { # more to come
+       if (!$stk && !$sync->{quit}) { # more to come
                begin_txn_lazy($self);
                $self->{mm}->{dbh}->begin_work;
        }
@@ -709,6 +759,7 @@ sub process_stack {
        if (my @leftovers = keys %{delete($sync->{D}) // {}}) {
                warn('W: unindexing '.scalar(@leftovers)." leftovers\n");
                for my $oid (@leftovers) {
+                       last if $sync->{quit};
                        $oid = unpack('H*', $oid);
                        $git->cat_async($oid, \&unindex_both, $sync);
                }
@@ -717,7 +768,8 @@ sub process_stack {
                $sync->{index_oid} = \&index_both;
        }
        while (my ($f, $at, $ct, $oid, $cur_cmt) = $stk->pop_rec) {
-               my $arg = { %$sync, cur_cmt => $cur_cmt };
+               my $arg = { %$sync, cur_cmt => $cur_cmt, oid => $oid };
+               last if $sync->{quit};
                if ($f eq 'm') {
                        $arg->{autime} = $at;
                        $arg->{cotime} = $ct;
@@ -731,7 +783,7 @@ sub process_stack {
                        $git->cat_async($oid, \&unindex_both, $arg);
                }
        }
-       v1_checkpoint($self, $sync, $stk);
+       v1_checkpoint($self, $sync, $sync->{quit} ? undef : $stk);
 }
 
 sub log2stack ($$$) {
@@ -841,15 +893,30 @@ sub reindex_from ($$) {
        ref($reindex) eq 'HASH' ? $reindex->{from} : '';
 }
 
+sub quit_cb ($) {
+       my ($sync) = @_;
+       sub {
+               # we set {-opt}->{quit} too, so ->index_sync callers
+               # can abort multi-inbox loops this way
+               $sync->{quit} = $sync->{-opt}->{quit} = 1;
+               warn "gracefully quitting\n";
+       }
+}
+
 # indexes all unindexed messages (v1 only)
 sub _index_sync {
        my ($self, $opt) = @_;
        my $tip = $opt->{ref} || 'HEAD';
        my $ibx = $self->{ibx};
+       local $self->{current_info} = "$ibx->{inboxdir}";
        $self->{batch_bytes} = $opt->{batch_size} // $BATCH_BYTES;
        $ibx->git->batch_prepare;
        my $pr = $opt->{-progress};
        my $sync = { reindex => $opt->{reindex}, -opt => $opt, ibx => $ibx };
+       my $quit = quit_cb($sync);
+       local $SIG{QUIT} = $quit;
+       local $SIG{INT} = $quit;
+       local $SIG{TERM} = $quit;
        my $xdb = $self->begin_txn_lazy;
        $self->{oidx}->rethread_prepare($opt);
        my $mm = _msgmap_init($self);
@@ -870,7 +937,7 @@ sub _index_sync {
        my $stk = prepare_stack($sync, $range);
        $sync->{ntodo} = $stk ? $stk->num_records : 0;
        $pr->("$sync->{ntodo}\n") if $pr; # continue previous line
-       process_stack($self, $sync, $stk);
+       process_stack($self, $sync, $stk) if !$sync->{quit};
 }
 
 sub DESTROY {
@@ -918,6 +985,10 @@ sub set_metadata_once {
 
 sub _commit_txn {
        my ($self) = @_;
+       if (my $eidx = $self->{eidx}) {
+               $eidx->git->async_wait_all;
+               $eidx->{transact_bytes} = 0;
+       }
        if (my $xdb = $self->{xdb}) {
                set_metadata_once($self);
                $xdb->commit_transaction;
@@ -953,4 +1024,25 @@ sub eidx_shard_new {
        $self;
 }
 
+# ensure there's no stale Xapian docs by treating $over as canonical
+sub over_check {
+       my ($self, $over) = @_;
+       begin_txn_lazy($self);
+       my $sth = $over->dbh->prepare(<<'');
+SELECT COUNT(*) FROM over WHERE num = ?
+
+       my $xdb = $self->{xdb};
+       my $cur = $xdb->postlist_begin('');
+       my $end = $xdb->postlist_end('');
+       my $xdir = $self->xdir;
+       for (; $cur != $end; $cur++) {
+               my $docid = $cur->get_docid;
+               $sth->execute($docid);
+               my $x = $sth->fetchrow_array;
+               next if $x > 0;
+               warn "I: removing $xdir #$docid, not in `over'\n";
+               $xdb->delete_document($docid);
+       }
+}
+
 1;