]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/Search.pm
update copyrights for 2021
[public-inbox.git] / lib / PublicInbox / Search.pm
index cfa942b2f75a794f3ed356bf743b7799b86dca72..547b6fbe32ac540c8eecc4e1fc386d32111df7f7 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 or flags
 #
@@ -6,7 +6,8 @@
 package PublicInbox::Search;
 use strict;
 use parent qw(Exporter);
-our @EXPORT_OK = qw(mdocid);
+our @EXPORT_OK = qw(retry_reopen int_val);
+use List::Util qw(max);
 
 # values for searching, changing the numeric value breaks
 # compatibility with old indices (so don't change them it)
@@ -53,11 +54,15 @@ use constant {
 
 use PublicInbox::Smsg;
 use PublicInbox::Over;
-my $QP_FLAGS;
-our %X = map { $_ => 0 } qw(BoolWeight Database Enquire QueryParser Stem);
+our $QP_FLAGS;
+our %X = map { $_ => 0 } qw(BoolWeight Database Enquire QueryParser Stem Query);
 our $Xap; # 'Search::Xapian' or 'Xapian'
-my $NVRP; # '$Xap::'.('NumberValueRangeProcessor' or 'NumberRangeProcessor')
-my $ENQ_ASCENDING;
+our $NVRP; # '$Xap::'.('NumberValueRangeProcessor' or 'NumberRangeProcessor')
+
+# ENQ_DESCENDING and ENQ_ASCENDING weren't in SWIG Xapian.pm prior to 1.4.16,
+# let's hope the ABI is stable
+our $ENQ_DESCENDING = 0;
+our $ENQ_ASCENDING = 1;
 
 sub load_xapian () {
        return 1 if defined $Xap;
@@ -83,15 +88,8 @@ sub load_xapian () {
                        'NumberRangeProcessor' : 'NumberValueRangeProcessor');
                $X{$_} = $Xap.'::'.$_ for (keys %X);
 
-               # ENQ_ASCENDING doesn't seem exported by SWIG Xapian.pm,
-               # so lets hope this part of the ABI is stable because it's
-               # just an integer:
-               $ENQ_ASCENDING = $x eq 'Xapian' ?
-                               1 : Search::Xapian::ENQ_ASCENDING();
-
-               # for Smsg:
-               *PublicInbox::Smsg::sortable_unserialise =
-                                               $Xap.'::sortable_unserialise';
+               *sortable_serialise = $x.'::sortable_serialise';
+               *sortable_unserialise = $x.'::sortable_unserialise';
                # n.b. FLAG_PURE_NOT is expensive not suitable for a public
                # website as it could become a denial-of-service vector
                # FLAG_PHRASE also seems to cause performance problems chert
@@ -192,36 +190,28 @@ sub xdir ($;$) {
        }
 }
 
-sub _xdb ($) {
+# returns all shards as separate Xapian::Database objects w/o combining
+sub xdb_shards_flat ($) {
        my ($self) = @_;
-       my $dir = xdir($self, 1);
-       my ($xdb, $slow_phrase);
-       my $qpf = \($self->{qp_flags} ||= $QP_FLAGS);
-       if ($self->{ibx_ver} >= 2) {
-               my @xdb;
-               opendir(my $dh, $dir) or return; # not initialized yet
-
+       my $xpfx = $self->{xpfx};
+       my (@xdb, $slow_phrase);
+       load_xapian();
+       if ($xpfx =~ m/xapian${\SCHEMA_VERSION}\z/) {
+               @xdb = ($X{Database}->new($xpfx));
+               $self->{qp_flags} |= FLAG_PHRASE() if !-f "$xpfx/iamchert";
+       } else {
+               opendir(my $dh, $xpfx) or return (); # not initialized yet
                # We need numeric sorting so shard[0] is first for reading
                # Xapian metadata, if needed
-               for (sort { $a <=> $b } grep(/\A[0-9]+\z/, readdir($dh))) {
-                       my $shard_dir = "$dir/$_";
-                       if (-d $shard_dir && -r _) {
-                               push @xdb, $X{Database}->new($shard_dir);
-                               $slow_phrase ||= -f "$shard_dir/iamchert";
-                       } else { # gaps from missing epochs throw off mdocid()
-                               warn "E: $shard_dir missing or unreadable\n";
-                               return;
-                       }
+               my $last = max(grep(/\A[0-9]+\z/, readdir($dh))) // return ();
+               for (0..$last) {
+                       my $shard_dir = "$self->{xpfx}/$_";
+                       push @xdb, $X{Database}->new($shard_dir);
+                       $slow_phrase ||= -f "$shard_dir/iamchert";
                }
-               $self->{nshard} = scalar(@xdb);
-               $xdb = shift @xdb;
-               $xdb->add_database($_) for @xdb;
-       } else {
-               $slow_phrase = -f "$dir/iamchert";
-               $xdb = $X{Database}->new($dir);
+               $self->{qp_flags} |= FLAG_PHRASE() if !$slow_phrase;
        }
-       $$qpf |= FLAG_PHRASE() unless $slow_phrase;
-       $xdb;
+       @xdb;
 }
 
 # v2 Xapian docids don't conflict, so they're identical to
@@ -235,39 +225,30 @@ sub mdocid {
 
 sub mset_to_artnums {
        my ($self, $mset) = @_;
-       my $nshard = $self->{nshard} // 1;
+       my $nshard = $self->{nshard};
        [ map { mdocid($nshard, $_) } $mset->items ];
 }
 
 sub xdb ($) {
        my ($self) = @_;
-       $self->{xdb} ||= do {
-               load_xapian();
-               _xdb($self);
+       $self->{xdb} //= do {
+               $self->{qp_flags} //= $QP_FLAGS;
+               my @xdb = $self->xdb_shards_flat or return;
+               $self->{nshard} = scalar(@xdb);
+               my $xdb = shift @xdb;
+               $xdb->add_database($_) for @xdb;
+               $xdb;
        };
 }
 
-sub xpfx_init ($) {
-       my ($self) = @_;
-       if ($self->{ibx_ver} == 1) {
-               $self->{xpfx} .= '/public-inbox/xapian' . SCHEMA_VERSION;
-       } else {
-               $self->{xpfx} .= '/xap'.SCHEMA_VERSION;
-       }
-}
-
 sub new {
        my ($class, $ibx) = @_;
        ref $ibx or die "BUG: expected PublicInbox::Inbox object: $ibx";
-       my $self = bless {
-               xpfx => $ibx->{inboxdir}, # for xpfx_init
+       my $xap = $ibx->version > 1 ? 'xap' : 'public-inbox/xapian';
+       bless {
+               xpfx => "$ibx->{inboxdir}/$xap" . SCHEMA_VERSION,
                altid => $ibx->{altid},
-               ibx_ver => $ibx->version,
        }, $class;
-       xpfx_init($self);
-       my $dir = xdir($self, 1);
-       $self->{over_ro} = PublicInbox::Over->new("$dir/over.sqlite3");
-       $self;
 }
 
 sub reopen {
@@ -284,20 +265,19 @@ sub mset {
        $opts ||= {};
        my $qp = $self->{qp} //= qparse_new($self);
        my $query = $qp->parse_query($query_string, $self->{qp_flags});
-       $opts->{relevance} = 1 unless exists $opts->{relevance};
        _do_enquire($self, $query, $opts);
 }
 
 sub retry_reopen {
-       my ($self, $cb, $arg) = @_;
+       my ($self, $cb, @arg) = @_;
        for my $i (1..10) {
                if (wantarray) {
                        my @ret;
-                       eval { @ret = $cb->($arg) };
+                       eval { @ret = $cb->($self, @arg) };
                        return @ret unless $@;
                } else {
                        my $ret;
-                       eval { $ret = $cb->($arg) };
+                       eval { $ret = $cb->($self, @arg) };
                        return $ret unless $@;
                }
                # Exception: The revision being read has been discarded -
@@ -317,7 +297,7 @@ sub retry_reopen {
 
 sub _do_enquire {
        my ($self, $query, $opts) = @_;
-       retry_reopen($self, \&_enquire_once, [ $self, $query, $opts ]);
+       retry_reopen($self, \&_enquire_once, $query, $opts);
 }
 
 # returns true if all docs have the THREADID value
@@ -327,19 +307,32 @@ sub has_threadid ($) {
 }
 
 sub _enquire_once { # retry_reopen callback
-       my ($self, $query, $opts) = @{$_[0]};
+       my ($self, $query, $opts) = @_;
        my $xdb = xdb($self);
+       if (defined(my $eidx_key = $opts->{eidx_key})) {
+               $query = $X{Query}->new(OP_FILTER(), $query, 'O'.$eidx_key);
+       }
+       if (defined(my $uid_range = $opts->{uid_range})) {
+               my $range = $X{Query}->new(OP_VALUE_RANGE(), UID,
+                                       sortable_serialise($uid_range->[0]),
+                                       sortable_serialise($uid_range->[1]));
+               $query = $X{Query}->new(OP_FILTER(), $query, $range);
+       }
        my $enquire = $X{Enquire}->new($xdb);
        $enquire->set_query($query);
        $opts ||= {};
         my $desc = !$opts->{asc};
-       if (($opts->{mset} || 0) == 2) { # mset == 2: ORDER BY docid/UID
+       my $rel = $opts->{relevance} // 0;
+       if ($rel == -1) { # ORDER BY docid/UID
+               $enquire->set_weighting_scheme($X{BoolWeight}->new);
                $enquire->set_docid_order($ENQ_ASCENDING);
+       } elsif ($rel == 0) {
+               $enquire->set_sort_by_value_then_relevance(TS, $desc);
+       } elsif ($rel == -2) {
                $enquire->set_weighting_scheme($X{BoolWeight}->new);
-       } elsif ($opts->{relevance}) {
+               $enquire->set_docid_order($ENQ_DESCENDING);
+       } else { # rel > 0
                $enquire->set_sort_by_relevance_then_value(TS, $desc);
-       } else {
-               $enquire->set_sort_by_value_then_relevance(TS, $desc);
        }
 
        # `mairix -t / --threads' or JMAP collapseThreads
@@ -351,7 +344,7 @@ sub _enquire_once { # retry_reopen callback
 
 sub mset_to_smsg {
        my ($self, $ibx, $mset) = @_;
-       my $nshard = $self->{nshard} // 1;
+       my $nshard = $self->{nshard};
        my $i = 0;
        my %order = map { mdocid($nshard, $_) => ++$i } $mset->items;
        my @msgs = sort {
@@ -425,4 +418,10 @@ sub help {
        \@ret;
 }
 
+sub int_val ($$) {
+       my ($doc, $col) = @_;
+       my $val = $doc->get_value($col) or return; # undefined is '' in Xapian
+       sortable_unserialise($val) + 0; # PV => IV conversion
+}
+
 1;