]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/Search.pm
lei q: use git approxidate with d:, dt: and rt: ranges
[public-inbox.git] / lib / PublicInbox / Search.pm
index fb3e997518e3b790f5ee7d774282b81948342af7..f42d70e3c6c8fda84bd3be15e5d4c2a30b5bbbde 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,8 +6,9 @@
 package PublicInbox::Search;
 use strict;
 use parent qw(Exporter);
-our @EXPORT_OK = qw(retry_reopen int_val);
+our @EXPORT_OK = qw(retry_reopen int_val get_pct xap_terms);
 use List::Util qw(max);
+use POSIX qw(strftime);
 
 # values for searching, changing the numeric value breaks
 # compatibility with old indices (so don't change them it)
@@ -190,41 +191,29 @@ sub xdir ($;$) {
        }
 }
 
-sub xdb_sharded {
+# returns all shards as separate Xapian::Database objects w/o combining
+sub xdb_shards_flat ($) {
        my ($self) = @_;
-       opendir(my $dh, $self->{xpfx}) or return; # not initialized yet
-
-       # We need numeric sorting so shard[0] is first for reading
-       # Xapian metadata, if needed
-       my $last = max(grep(/\A[0-9]+\z/, readdir($dh))) // return;
+       my $xpfx = $self->{xpfx};
        my (@xdb, $slow_phrase);
-       for (0..$last) {
-               my $shard_dir = "$self->{xpfx}/$_";
-               if (-d $shard_dir && -r _) {
+       load_xapian();
+       $self->{qp_flags} //= $QP_FLAGS;
+       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
+               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";
-               } else { # gaps from missing epochs throw off mdocid()
-                       warn "E: $shard_dir missing or unreadable\n";
-                       return;
                }
+               $self->{qp_flags} |= FLAG_PHRASE() if !$slow_phrase;
        }
-       $self->{qp_flags} |= FLAG_PHRASE() if !$slow_phrase;
-       $self->{nshard} = scalar(@xdb);
-       my $xdb = shift @xdb;
-       $xdb->add_database($_) for @xdb;
-       $xdb;
-}
-
-sub _xdb {
-       my ($self) = @_;
-       my $dir = xdir($self, 1);
-       $self->{qp_flags} //= $QP_FLAGS;
-       if ($self->{ibx_ver} >= 2) {
-               xdb_sharded($self);
-       } else {
-               $self->{qp_flags} |= FLAG_PHRASE() if !-f "$dir/iamchert";
-               $X{Database}->new($dir);
-       }
+       @xdb;
 }
 
 # v2 Xapian docids don't conflict, so they're identical to
@@ -238,37 +227,29 @@ 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();
-               $self->_xdb;
+               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);
-       $self;
 }
 
 sub reopen {
@@ -279,6 +260,72 @@ sub reopen {
        $self; # make chaining easier
 }
 
+# Convert git "approxidate" ranges to something usable with our
+# Xapian indices.  At the moment, Xapian only offers a C++-only API
+# and neither the SWIG nor XS bindings allow us to use custom code
+# to parse dates (and libgit2 doesn't expose git__date_parse, either,
+# so we're running git-rev-parse(1)).
+sub date_range {
+       my ($git, $pfx, $range) = @_;
+       # are we inside a parenthesized statement?
+       my $end = $range =~ s/([\)\s]*)\z// ? $1 : '';
+       my @r = split(/\.\./, $range, 2);
+
+       # expand "d:20101002" => "d:20101002..20101003" and like
+       # n.b. git doesn't do YYYYMMDD w/o '-', it needs YYYY-MM-DD
+       if ($pfx eq 'd') {
+               if (!defined($r[1])) {
+                       $r[0] =~ s/\A([0-9]{4})([0-9]{2})([0-9]{2})\z/$1-$2-$3/;
+                       $r[0] = $git->date_parse($r[0]);
+                       $r[1] = $r[0] + 86400;
+                       for my $x (@r) {
+                               $x = strftime('%Y%m%d', gmtime($x));
+                       }
+               } else {
+                       for my $x (@r) {
+                               next if $x eq '' || $x =~ /\A[0-9]{8}\z/;
+                               $x = strftime('%Y%m%d',
+                                               gmtime($git->date_parse($x)));
+                       }
+               }
+       } elsif ($pfx eq 'dt') {
+               if (!defined($r[1])) { # git needs gaps and not /\d{14}/
+                       $r[0] =~ s/\A([0-9]{4})([0-9]{2})([0-9]{2})
+                                       ([0-9]{2})([0-9]{2})([0-9]{2})\z
+                               /$1-$2-$3 $4:$5:$6/x;
+                       $r[0] = $git->date_parse($r[0]);
+                       $r[1] = $r[0] + 86400;
+                       for my $x (@r) {
+                               $x = strftime('%Y%m%d%H%M%S', gmtime($x));
+                       }
+               } else {
+                       for my $x (@r) {
+                               next if $x eq '' || $x =~ /\A[0-9]{14}\z/;
+                               $x = strftime('%Y%m%d%H%M%S',
+                                               gmtime($git->date_parse($x)));
+                       }
+               }
+       } else { # "rt", let git interpret "YYYY", deal with Y10K later :P
+               for my $x (@r) {
+                       next if $x eq '' || $x =~ /\A[0-9]{5,}\z/;
+                       $x = $git->date_parse($x);
+               }
+               $r[1] //= $r[0] + 86400;
+       }
+       "$pfx:".join('..', @r).$end;
+}
+
+sub query_argv_to_string {
+       my (undef, $git, $argv) = @_;
+       join(' ', map {;
+               if (s!\b(d|rt|dt):(.+)\z!date_range($git, $1, $2)!sge) {
+                       $_;
+               } else {
+                       /\s/ ? (s/\A(\w+:)// ? qq{$1"$_"} : qq{"$_}) : $_
+               }
+       } @$argv);
+}
+
 # read-only
 sub mset {
        my ($self, $query_string, $opts) = @_;
@@ -356,7 +403,7 @@ sub _enquire_once { # retry_reopen callback
        }
 
        # `mairix -t / --threads' or JMAP collapseThreads
-       if ($opts->{thread} && has_threadid($self)) {
+       if ($opts->{threads} && has_threadid($self)) {
                $enquire->set_collapse_key(THREADID);
        }
        $enquire->get_mset($opts->{offset} || 0, $opts->{limit} || 50);
@@ -364,7 +411,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 {
@@ -396,7 +443,7 @@ sub qparse_new ($) {
 
        # for IMAP, undocumented for WWW and may be split off go away
        $cb->($qp, $NVRP->new(BYTES, 'bytes:'));
-       $cb->($qp, $NVRP->new(TS, 'ts:'));
+       $cb->($qp, $NVRP->new(TS, 'rt:'));
        $cb->($qp, $NVRP->new(UID, 'uid:'));
 
        while (my ($name, $prefix) = each %bool_pfx_external) {
@@ -444,4 +491,30 @@ sub int_val ($$) {
        sortable_unserialise($val) + 0; # PV => IV conversion
 }
 
+sub get_pct ($) { # mset item
+       # Capped at "99%" since "100%" takes an extra column in the
+       # thread skeleton view.  <xapian/mset.h> says the value isn't
+       # very meaningful, anyways.
+       my $n = $_[0]->get_percent;
+       $n > 99 ? 99 : $n;
+}
+
+sub xap_terms ($$;@) {
+       my ($pfx, $xdb_or_doc, @docid) = @_; # @docid may be empty ()
+       my %ret;
+       eval {
+               my $end = $xdb_or_doc->termlist_end(@docid);
+               my $cur = $xdb_or_doc->termlist_begin(@docid);
+               for (; $cur != $end; $cur++) {
+                       $cur->skip_to($pfx);
+                       last if $cur == $end;
+                       my $tn = $cur->get_termname;
+                       if (index($tn, $pfx) == 0) {
+                               $ret{substr($tn, length($pfx))} = undef;
+                       }
+               }
+       };
+       \%ret;
+}
+
 1;