]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/SearchIdx.pm
lei: add some labels support
[public-inbox.git] / lib / PublicInbox / SearchIdx.pm
index 7e2843e938989d067a2c8f089f5fad6f6314c968..ca1f358833e1afae56baac6ef6200d55e2023e6a 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2015-2020 all contributors <meta@public-inbox.org>
+# Copyright (C) 2015-2021 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
 #
@@ -11,6 +11,7 @@ use strict;
 use v5.10.1;
 use parent qw(PublicInbox::Search PublicInbox::Lock Exporter);
 use PublicInbox::Eml;
+use PublicInbox::Search qw(xap_terms);
 use PublicInbox::InboxWritable;
 use PublicInbox::MID qw(mids_for_index mids);
 use PublicInbox::MsgIter;
@@ -22,7 +23,8 @@ use PublicInbox::OverIdx;
 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
+use PublicInbox::Address;
+our @EXPORT_OK = qw(log2stack is_ancestor check_size prepare_stack
        index_text term_generator add_val is_bad_blob);
 my $X = \%PublicInbox::Search::X;
 our ($DB_CREATE_OR_OPEN, $DB_OPEN);
@@ -33,6 +35,7 @@ use constant DEBUG => !!$ENV{DEBUG};
 my $xapianlevels = qr/\A(?:full|medium)\z/;
 my $hex = '[a-f0-9]';
 my $OID = $hex .'{40,}';
+my @VMD_MAP = (kw => 'K', L => 'L');
 our $INDEXLEVELS = qr/\A(?:full|medium|basic)\z/;
 
 sub new {
@@ -64,7 +67,6 @@ sub new {
                $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;
@@ -103,12 +105,14 @@ 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) |
-               (eval($xap.'::minor_version()') << 8);
+               (eval($xap.'::minor_version()') << 8) |
+               eval($xap.'::revision()');
        $DB_NO_SYNC = 0x4 if $ver >= 0x10400;
+       # Xapian v1.2.21..v1.2.24 were missing close-on-exec on OFD locks
+       $X->{CLOEXEC_UNSET} = 1 if $ver >= 0x010215 && $ver <= 0x010218;
        1;
 }
 
@@ -157,22 +161,44 @@ sub term_generator ($) { # write-only
        }
 }
 
+sub index_phrase ($$$$) {
+       my ($self, $text, $wdf_inc, $prefix) = @_;
+
+       my $tg = term_generator($self);
+       $tg->index_text($text, $wdf_inc, $prefix);
+       $tg->increase_termpos;
+}
+
 sub index_text ($$$$) {
        my ($self, $text, $wdf_inc, $prefix) = @_;
-       my $tg = term_generator($self); # man Search::Xapian::TermGenerator
 
        if ($self->{indexlevel} eq 'full') {
-               $tg->index_text($text, $wdf_inc, $prefix);
-               $tg->increase_termpos;
+               index_phrase($self, $text, $wdf_inc, $prefix);
        } else {
+               my $tg = term_generator($self);
                $tg->index_text_without_positions($text, $wdf_inc, $prefix);
        }
 }
 
 sub index_headers ($$) {
        my ($self, $smsg) = @_;
-       my @x = (from => 'A', # Author
-               subject => 'S', to => 'XTO', cc => 'XCC');
+       my @x = (from => 'A', to => 'XTO', cc => 'XCC'); # A: Author
+       while (my ($field, $pfx) = splice(@x, 0, 2)) {
+               my $val = $smsg->{$field};
+               next if $val eq '';
+               # include "(comments)" after the address, too, so not using
+               # PublicInbox::Address::names or pairs
+               index_text($self, $val, 1, $pfx);
+
+               # we need positional info for email addresses since they
+               # can be considered phrases
+               if ($self->{indexlevel} eq 'medium') {
+                       for my $addr (PublicInbox::Address::emails($val)) {
+                               index_phrase($self, $addr, 1, $pfx);
+                       }
+               }
+       }
+       @x = (subject => 'S');
        while (my ($field, $pfx) = splice(@x, 0, 2)) {
                my $val = $smsg->{$field};
                index_text($self, $val, 1, $pfx) if $val ne '';
@@ -185,7 +211,11 @@ sub index_diff_inc ($$$$) {
                index_text($self, join("\n", @$xnq), 1, 'XNQ');
                @$xnq = ();
        }
-       index_text($self, $text, 1, $pfx);
+       if ($pfx eq 'XDFN') {
+               index_phrase($self, $text, 1, $pfx);
+       } else {
+               index_text($self, $text, 1, $pfx);
+       }
 }
 
 sub index_old_diff_fn {
@@ -291,7 +321,7 @@ sub index_xapian { # msg_iter callback
        my $ct = $part->content_type || 'text/plain';
        my $fn = $part->filename;
        if (defined $fn && $fn ne '') {
-               index_text($self, $fn, 1, 'XFN');
+               index_phrase($self, $fn, 1, 'XFN');
        }
        if ($part->{is_submsg}) {
                my $mids = mids_for_index($part);
@@ -329,20 +359,20 @@ sub index_list_id ($$$) {
                $l =~ /<([^>]+)>/ or next;
                my $lid = lc $1;
                $doc->add_boolean_term('G' . $lid);
-               index_text($self, $lid, 1, 'XL'); # probabilistic
+               index_phrase($self, $lid, 1, 'XL'); # probabilistic
        }
 }
 
 sub index_ids ($$$$) {
        my ($self, $doc, $hdr, $mids) = @_;
        for my $mid (@$mids) {
-               index_text($self, $mid, 1, 'XM');
+               index_phrase($self, $mid, 1, 'XM');
 
                # because too many Message-IDs are prefixed with
                # "Pine.LNX."...
                if ($mid =~ /\w{12,}/) {
                        my @long = ($mid =~ /(\w{3,}+)/g);
-                       index_text($self, join(' ', @long), 1, 'XM');
+                       index_phrase($self, join(' ', @long), 1, 'XM');
                }
        }
        $doc->add_boolean_term('Q' . $_) for @$mids;
@@ -379,7 +409,7 @@ sub eml2doc ($$$;$) {
        if (!$self->{-skip_docdata}) {
                # WWW doesn't need {to} or {cc}, only NNTP
                $smsg->{to} = $smsg->{cc} = '';
-               PublicInbox::OverIdx::parse_references($smsg, $eml, $mids);
+               $smsg->parse_references($eml, $mids);
                my $data = $smsg->to_doc_data;
                $doc->set_data($data);
        }
@@ -399,7 +429,16 @@ sub eml2doc ($$$;$) {
 
 sub add_xapian ($$$$) {
        my ($self, $eml, $smsg, $mids) = @_;
+       begin_txn_lazy($self);
+       my $merge_vmd = delete $smsg->{-merge_vmd};
        my $doc = eml2doc($self, $eml, $smsg, $mids);
+       if (my $old = $merge_vmd ? _get_doc($self, $smsg->{num}) : undef) {
+               my @x = @VMD_MAP;
+               while (my ($field, $pfx) = splice(@x, 0, 2)) {
+                       my $vals = xap_terms($pfx, $old);
+                       $doc->add_boolean_term($pfx.$_) for keys %$vals;
+               }
+       }
        $self->{xdb}->replace_document($smsg->{num}, $doc);
 }
 
@@ -416,6 +455,7 @@ sub _msgmap_init ($) {
 sub add_message {
        # mime = PublicInbox::Eml or Email::MIME object
        my ($self, $mime, $smsg, $sync) = @_;
+       begin_txn_lazy($self);
        my $mids = mids_for_index($mime);
        $smsg //= bless { blob => '' }, 'PublicInbox::Smsg'; # test-only compat
        $smsg->{mid} //= $mids->[0]; # v1 compatibility
@@ -464,7 +504,10 @@ sub add_eidx_info {
        begin_txn_lazy($self);
        my $doc = _get_doc($self, $docid) or return;
        term_generator($self)->set_document($doc);
+
+       # '.' is special for lei_store
        $doc->add_boolean_term('O'.$eidx_key) if $eidx_key ne '.';
+
        index_list_id($self, $doc, $eml);
        $self->{xdb}->replace_document($docid, $doc);
 }
@@ -498,66 +541,88 @@ sub remove_eidx_info {
        $self->{xdb}->replace_document($docid, $doc);
 }
 
-sub set_keywords {
-       my ($self, $docid, @kw) = @_;
+sub set_vmd {
+       my ($self, $docid, $vmd) = @_;
        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);
+       my ($end, @rm, @add);
+       my @x = @VMD_MAP;
+       while (my ($field, $pfx) = splice(@x, 0, 2)) {
+               my $set = $vmd->{$field} // next;
+               my %keep = map { $_ => 1 } @$set;
+               my %add = %keep;
+               $end //= $doc->termlist_end;
+               for (my $cur = $doc->termlist_begin; $cur != $end; $cur++) {
+                       $cur->skip_to($pfx);
+                       last if $cur == $end;
+                       my $v = $cur->get_termname;
+                       $v =~ s/\A$pfx//s or next;
+                       $keep{$v} ? delete($add{$v}) : push(@rm, $pfx.$v);
+               }
+               push(@add, map { $pfx.$_ } keys %add);
+       }
+       return unless scalar(@rm) || scalar(@add);
+       $doc->remove_term($_) for @rm;
+       $doc->add_boolean_term($_) for @add;
        $self->{xdb}->replace_document($docid, $doc);
 }
 
-sub add_keywords {
-       my ($self, $docid, @kw) = @_;
+sub add_vmd {
+       my ($self, $docid, $vmd) = @_;
        begin_txn_lazy($self);
        my $doc = _get_doc($self, $docid) or return;
-       $doc->add_boolean_term('K'.$_) for @kw;
+       my @x = @VMD_MAP;
+       while (my ($field, $pfx) = splice(@x, 0, 2)) {
+               my $add = $vmd->{$field} // next;
+               $doc->add_boolean_term($pfx . $_) for @$add;
+       }
        $self->{xdb}->replace_document($docid, $doc);
 }
 
-sub remove_keywords {
-       my ($self, $docid, @kw) = @_;
+sub remove_vmd {
+       my ($self, $docid, $vmd) = @_;
        begin_txn_lazy($self);
        my $doc = _get_doc($self, $docid) or return;
        my $replace;
-       eval {
-               $doc->remove_term('K'.$_);
-               $replace = 1
-       } for @kw;
+       my @x = @VMD_MAP;
+       while (my ($field, $pfx) = splice(@x, 0, 2)) {
+               my $rm = $vmd->{$field} // next;
+               for (@$rm) {
+                       eval {
+                               $doc->remove_term($pfx . $_);
+                               $replace = 1;
+                       };
+               }
+       }
        $self->{xdb}->replace_document($docid, $doc) if $replace;
 }
 
-sub get_val ($$) {
-       my ($doc, $col) = @_;
-       sortable_unserialise($doc->get_value($col));
-}
-
-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());
-       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);
-       $smsg;
+sub update_vmd {
+       my ($self, $docid, $vmd_mod) = @_;
+       begin_txn_lazy($self);
+       my $doc = _get_doc($self, $docid) or return;
+       my $updated = 0;
+       my @x = @VMD_MAP;
+       while (my ($field, $pfx) = splice(@x, 0, 2)) {
+               # field: "label" or "kw"
+               for my $val (@{$vmd_mod->{"-$field"} // []}) {
+                       eval {
+                               $doc->remove_term($pfx . $val);
+                               ++$updated;
+                       };
+               }
+               for my $val (@{$vmd_mod->{"+$field"} // []}) {
+                       $doc->add_boolean_term($pfx . $val);
+                       ++$updated;
+               }
+       }
+       $self->{xdb}->replace_document($docid, $doc) if $updated;
+       $updated;
 }
 
 sub xdb_remove {
        my ($self, @docids) = @_;
+       $self->begin_txn_lazy;
        my $xdb = $self->{xdb} or return;
        for my $docid (@docids) {
                eval { $xdb->delete_document($docid) };
@@ -565,13 +630,6 @@ sub xdb_remove {
        }
 }
 
-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, $num) if need_xapian($self);
-}
-
 sub index_git_blob_id {
        my ($doc, $pfx, $objid) = @_;
 
@@ -625,17 +683,6 @@ sub index_mm {
        }
 }
 
-# returns the number of bytes to add if given a non-CRLF arg
-sub crlf_adjust ($) {
-       if (index($_[0], "\r\n") < 0) {
-               # common case is LF-only, every \n needs an \r;
-               # so favor a cheap tr// over an expensive m//g
-               $_[0] =~ tr/\n/\n/;
-       } else { # count number of '\n' w/o '\r', expensive:
-               scalar(my @n = ($_[0] =~ m/(?<!\r)\n/g));
-       }
-}
-
 sub is_bad_blob ($$$$) {
        my ($oid, $type, $size, $expect_oid) = @_;
        if ($type ne 'blob') {
@@ -652,14 +699,15 @@ sub index_both { # git->cat_async callback
        my ($nr, $max) = @$sync{qw(nr max)};
        ++$$nr;
        $$max -= $size;
-       $size += crlf_adjust($$bref);
-       my $smsg = bless { bytes => $size, blob => $oid }, 'PublicInbox::Smsg';
+       my $smsg = bless { blob => $oid }, 'PublicInbox::Smsg';
+       $smsg->set_bytes($$bref, $size);
        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;
 }
@@ -674,6 +722,7 @@ sub unindex_both { # git->cat_async callback
        if (defined(my $cur_cmt = $sync->{cur_cmt})) {
                ${$sync->{latest_cmt}} = $cur_cmt;
        }
+       ++$self->{nidx};
 }
 
 sub with_umask {
@@ -1007,14 +1056,6 @@ sub commit_txn_lazy {
                $self->with_umask(\&_commit_txn, $self);
 }
 
-sub worker_done {
-       my ($self) = @_;
-       if (need_xapian($self)) {
-               die "$$ $0 xdb not released\n" if $self->{xdb};
-       }
-       die "$$ $0 still in transaction\n" if $self->{txn};
-}
-
 sub eidx_shard_new {
        my ($class, $eidx, $shard) = @_;
        my $self = bless {