X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FSearch.pm;h=273cc57cf94ba8b12d18acfcefd4aaa13e8a2c7a;hb=refs%2Fheads%2Fmaster;hp=c5a1bd69cdaa7076c8c4445457146717dd16c6a8;hpb=c471b946ef629cf3db9043081a1aeaa189436f6b;p=public-inbox.git
diff --git a/lib/PublicInbox/Search.pm b/lib/PublicInbox/Search.pm
index c5a1bd69..273cc57c 100644
--- a/lib/PublicInbox/Search.pm
+++ b/lib/PublicInbox/Search.pm
@@ -1,21 +1,23 @@
-# Copyright (C) 2015-2021 all contributors
+# Copyright (C) all contributors
# License: AGPL-3.0+
# based on notmuch, but with no concept of folders, files or flags
#
# Read-only search interface for use by the web and NNTP interfaces
package PublicInbox::Search;
use strict;
+use v5.10.1;
use parent qw(Exporter);
our @EXPORT_OK = qw(retry_reopen int_val get_pct xap_terms);
use List::Util qw(max);
use POSIX qw(strftime);
+use Carp ();
# values for searching, changing the numeric value breaks
# compatibility with old indices (so don't change them it)
use constant {
- TS => 0, # Received: header in Unix time (IMAP INTERNALDATE)
- YYYYMMDD => 1, # Date: header for searching in the WWW UI
- DT => 2, # Date: YYYYMMDDHHMMSS
+ TS => 0, # Received: in Unix time (IMAP INTERNALDATE, JMAP receivedAt)
+ YYYYMMDD => 1, # redundant with DT below
+ DT => 2, # Date: YYYYMMDDHHMMSS (IMAP SENT*, JMAP sentAt)
# added for public-inbox 1.6.0+
BYTES => 3, # IMAP RFC822.SIZE
@@ -116,9 +118,10 @@ my %bool_pfx_external = (
dfpre => 'XDFPRE',
dfpost => 'XDFPOST',
dfblob => 'XDFPRE XDFPOST',
+ patchid => 'XDFID',
);
-my $non_quoted_body = 'XNQ XDFN XDFA XDFB XDFHH XDFCTX XDFPRE XDFPOST';
+my $non_quoted_body = 'XNQ XDFN XDFA XDFB XDFHH XDFCTX XDFPRE XDFPOST XDFID';
my %prob_prefix = (
# for mairix compatibility
s => 'S',
@@ -153,12 +156,9 @@ my %prob_prefix = (
our @HELP = (
's:' => 'match within Subject e.g. s:"a quick brown fox"',
'd:' => < < 'match within message body, including text attachments',
'nq:' => 'match non-quoted text within message body',
@@ -179,6 +179,10 @@ EOF
'dfpre:' => 'match pre-image git blob ID',
'dfpost:' => 'match post-image git blob ID',
'dfblob:' => 'match either pre or post-image git blob ID',
+ 'patchid:' => "match `git patch-id --stable' output",
+ 'rt:' => <{shard})) {
$self->{xpfx};
- } else { # v2 only:
+ } else { # v2 + extindex only:
"$self->{xpfx}/$self->{shard}";
}
}
@@ -198,7 +202,7 @@ sub xdb_shards_flat ($) {
my (@xdb, $slow_phrase);
load_xapian();
$self->{qp_flags} //= $QP_FLAGS;
- if ($xpfx =~ m/xapian${\SCHEMA_VERSION}\z/) {
+ if ($xpfx =~ m!/xapian[0-9]+\z!) {
@xdb = ($X{Database}->new($xpfx));
$self->{qp_flags} |= FLAG_PHRASE() if !-f "$xpfx/iamchert";
} else {
@@ -233,12 +237,12 @@ sub mset_to_artnums {
sub xdb ($) {
my ($self) = @_;
- $self->{xdb} //= do {
+ $self->{xdb} // do {
my @xdb = $self->xdb_shards_flat or return;
$self->{nshard} = scalar(@xdb);
my $xdb = shift @xdb;
$xdb->add_database($_) for @xdb;
- $xdb;
+ $self->{xdb} = $xdb;
};
}
@@ -246,10 +250,10 @@ sub new {
my ($class, $ibx) = @_;
ref $ibx or die "BUG: expected PublicInbox::Inbox object: $ibx";
my $xap = $ibx->version > 1 ? 'xap' : 'public-inbox/xapian';
- bless {
- xpfx => "$ibx->{inboxdir}/$xap" . SCHEMA_VERSION,
- altid => $ibx->{altid},
- }, $class;
+ my $xpfx = "$ibx->{inboxdir}/$xap".SCHEMA_VERSION;
+ my $self = bless { xpfx => $xpfx }, $class;
+ $self->{altid} = $ibx->{altid} if defined($ibx->{altid});
+ $self;
}
sub reopen {
@@ -275,7 +279,9 @@ sub date_parse_prepare {
# expand "d:20101002" => "d:20101002..20101003" and like
# n.b. git doesn't do YYYYMMDD w/o '-', it needs YYYY-MM-DD
+ # We upgrade "d:" to "dt:" to iff using approxidate
if ($pfx eq 'd') {
+ my $fmt = "\0%Y%m%d";
if (!defined($r[1])) {
if ($r[0] =~ /\A([0-9]{4})([0-9]{2})([0-9]{2})\z/) {
push @$to_parse, "$1-$2-$3";
@@ -283,14 +289,27 @@ sub date_parse_prepare {
# to parse anyways for "d+" below
} else {
push @$to_parse, $r[0];
+ if ($r[0] !~ /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/) {
+ $pfx = 'dt';
+ $fmt = "\0%Y%m%d%H%M%S";
+ }
}
- $r[0] = "\0%Y%m%d$#$to_parse\0";
- $r[1] = "\0%Y%m%d+\0";
+ $r[0] = "$fmt+$#$to_parse\0";
+ $r[1] = "$fmt+\0";
} else {
for my $x (@r) {
next if $x eq '' || $x =~ /\A[0-9]{8}\z/;
push @$to_parse, $x;
- $x = "\0%Y%m%d$#$to_parse\0";
+ if ($x !~ /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/) {
+ $pfx = 'dt';
+ }
+ $x = "$fmt$#$to_parse\0";
+ }
+ if ($pfx eq 'dt') {
+ for (@r) {
+ s/\0%Y%m%d/\0%Y%m%d%H%M%S/;
+ s/\A([0-9]{8})\z/${1}000000/;
+ }
}
}
} elsif ($pfx eq 'dt') {
@@ -316,7 +335,7 @@ sub date_parse_prepare {
push @$to_parse, $x;
$x = "\0%s$#$to_parse\0";
}
- $r[1] //= "\0%s+\0";
+ $r[1] //= "\0%s+\0"; # add 1 day
}
"$pfx:".join('..', @r).$end;
}
@@ -326,9 +345,12 @@ sub date_parse_finalize {
# git-rev-parse can handle any number of args up to system
# limits (around (4096*32) bytes on Linux).
my @r = $git->date_parse(@$to_parse);
- my $i;
- $_[2] =~ s/\0(%[%YmdHMSs]+)([0-9\+]+)\0/strftime($1,
- gmtime($2 eq '+' ? ($r[$i]+86400) : $r[$i=$2+0]))/sge;
+ # n.b. git respects TZ, times stored in SQLite/Xapian are always UTC,
+ # and gmtime doesn't seem to do the right thing when TZ!=UTC
+ my ($i, $t);
+ $_[2] =~ s/\0(%[%YmdHMSs]+)([0-9\+]+)\0/
+ $t = $2 eq '+' ? ($r[$i]+86400) : $r[$i=$2+0];
+ $1 eq '%s' ? $t : strftime($1, gmtime($t))/sge;
}
# n.b. argv never has NUL, though we'll need to filter it out
@@ -370,7 +392,7 @@ sub query_approxidate {
sub mset {
my ($self, $query_string, $opts) = @_;
$opts ||= {};
- my $qp = $self->{qp} //= qparse_new($self);
+ my $qp = $self->{qp} //= $self->qparse_new;
my $query = $qp->parse_query($query_string, $self->{qp_flags});
_do_enquire($self, $query, $opts);
}
@@ -379,27 +401,24 @@ sub retry_reopen {
my ($self, $cb, @arg) = @_;
for my $i (1..10) {
if (wantarray) {
- my @ret;
- eval { @ret = $cb->($self, @arg) };
+ my @ret = eval { $cb->($self, @arg) };
return @ret unless $@;
} else {
- my $ret;
- eval { $ret = $cb->($self, @arg) };
+ my $ret = eval { $cb->($self, @arg) };
return $ret unless $@;
}
# Exception: The revision being read has been discarded -
# you should call Xapian::Database::reopen()
if (ref($@) =~ /\bDatabaseModifiedError\b/) {
- warn "reopen try #$i on $@\n";
reopen($self);
} else {
# let caller decide how to spew, because ExtMsg queries
# get wonky and trigger:
# "something terrible happened at .../Xapian/Enquire.pm"
- die;
+ Carp::croak($@);
}
}
- die "Too many Xapian database modifications in progress\n";
+ Carp::croak("Too many Xapian database modifications in progress\n");
}
sub _do_enquire {
@@ -441,8 +460,9 @@ sub _enquire_once { # retry_reopen callback
$enquire->set_sort_by_relevance_then_value(TS, !$opts->{asc});
}
- # `mairix -t / --threads' or JMAP collapseThreads
- if ($opts->{threads} && has_threadid($self)) {
+ # `lei q -t / --threads' or JMAP collapseThreads; but don't collapse
+ # on `-tt' ({threads} > 1) which sets the Flagged|Important keyword
+ if (($opts->{threads} // 0) == 1 && has_threadid($self)) {
$enquire->set_collapse_key(THREADID);
}
$enquire->get_mset($opts->{offset} || 0, $opts->{limit} || 50);
@@ -463,7 +483,7 @@ sub mset_to_smsg {
sub stemmer { $X{Stem}->new($LANG) }
# read-only
-sub qparse_new ($) {
+sub qparse_new {
my ($self) = @_;
my $xdb = xdb($self);
@@ -481,7 +501,7 @@ sub qparse_new ($) {
$cb->($qp, $NVRP->new(DT, 'dt:'));
# for IMAP, undocumented for WWW and may be split off go away
- $cb->($qp, $NVRP->new(BYTES, 'bytes:'));
+ $cb->($qp, $NVRP->new(BYTES, 'z:'));
$cb->($qp, $NVRP->new(TS, 'rt:'));
$cb->($qp, $NVRP->new(UID, 'uid:'));
@@ -516,7 +536,7 @@ EOF
sub help {
my ($self) = @_;
- $self->{qp} //= qparse_new($self); # parse altids
+ $self->{qp} //= $self->qparse_new; # parse altids
my @ret = @HELP;
if (my $user_pfx = $self->{-user_pfx}) {
push @ret, @$user_pfx;
@@ -524,9 +544,10 @@ sub help {
\@ret;
}
+# always returns a scalar value
sub int_val ($$) {
my ($doc, $col) = @_;
- my $val = $doc->get_value($col) or return; # undefined is '' in Xapian
+ my $val = $doc->get_value($col) or return undef; # undef is '' in Xapian
sortable_unserialise($val) + 0; # PV => IV conversion
}
@@ -541,19 +562,23 @@ sub get_pct ($) { # mset item
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;
+ 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;
+ $ret{substr($tn, length($pfx))} = undef if !index($tn, $pfx);
+ }
+ wantarray ? sort(keys(%ret)) : \%ret;
+}
+
+# get combined docid from over.num:
+# (not generic Xapian, only works with our sharding scheme)
+sub num2docid ($$) {
+ my ($self, $num) = @_;
+ my $nshard = $self->{nshard};
+ ($num - 1) * $nshard + $num % $nshard + 1;
}
1;