X-Git-Url: http://www.git.stargrave.org/?a=blobdiff_plain;f=lib%2FPublicInbox%2FSearch.pm;h=523003b3c269ae42e9ee210e91415b2846735daa;hb=1f1b1f0e22f7b4cbe5c8e843c1932222c0461393;hp=76a270bca0c01e9c6121c530ff405594f04d6114;hpb=c1a2b1f49ef4fd6e1f0033cbe3e3bcd8f65aca37;p=public-inbox.git diff --git a/lib/PublicInbox/Search.pm b/lib/PublicInbox/Search.pm index 76a270bc..523003b3 100644 --- a/lib/PublicInbox/Search.pm +++ b/lib/PublicInbox/Search.pm @@ -5,17 +5,19 @@ # 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 @@ -153,12 +155,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 +178,9 @@ 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', + 'rt:' => <{shard})) { $self->{xpfx}; - } else { # v2 only: + } else { # v2 + extindex only: "$self->{xpfx}/$self->{shard}"; } } @@ -198,7 +200,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 +235,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 +248,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 +277,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 +287,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,40 +333,64 @@ 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; } +sub date_parse_finalize { + my ($git, $to_parse) = @_; + # 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); + # 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 # if this $argv isn't from a command execution sub query_argv_to_string { my (undef, $git, $argv) = @_; my $to_parse; my $tmp = join(' ', map {; - if (s!\b(d|rt|dt):([[:print:]]+)\z!date_parse_prepare( + if (s!\b(d|rt|dt):(\S+)\z!date_parse_prepare( $to_parse //= [], $1, $2)!sge) { $_; + } elsif (/\s/) { + s/(.*?)\b(\w+:)// ? qq{$1$2"$_"} : qq{"$_"}; } else { - /\s/ ? (s/\A(\w+:)// ? qq{$1"$_"} : qq{"$_}) : $_ + $_ } } @$argv); - # git-rev-parse can handle any number of args up to system - # limits (around (4096*32) bytes on Linux). - if ($to_parse) { - my @r = $git->date_parse(@$to_parse); - my $i; - $tmp =~ s/\0(%[%YmdHMSs]+)([0-9\+]+)\0/strftime($1, - gmtime($2 eq '+' ? ($r[$i]+86400) : $r[$i=$2+0]))/sge; - } + date_parse_finalize($git, $to_parse, $tmp) if $to_parse; $tmp } +# this is for the WWW "q=" query parameter and "lei q --stdin" +# it can't do d:"5 days ago", but it will do d:5.days.ago +sub query_approxidate { + my (undef, $git) = @_; # $_[2] = $query_string (modified in-place) + my $DQ = qq<"\x{201c}\x{201d}>; # Xapian can use curly quotes + $_[2] =~ tr/\x00/ /; # Xapian doesn't do NUL, we use it as a placeholder + my ($terms, $phrase, $to_parse); + $_[2] =~ s{([^$DQ]*)([$DQ][^$DQ]*[$DQ])?}{ + ($terms, $phrase) = ($1, $2); + $terms =~ s!\b(d|rt|dt):(\S+)! + date_parse_prepare($to_parse //= [], $1, $2)!sge; + $terms.($phrase // ''); + }sge; + date_parse_finalize($git, $to_parse, $_[2]) if $to_parse; +} + # read-only 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); } @@ -358,27 +399,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 { @@ -442,7 +480,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); @@ -460,7 +498,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:')); @@ -495,7 +533,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; @@ -520,19 +558,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;