LCOV - code coverage report
Current view: top level - queryparser - termgenerator_internal.cc (source / functions) Hit Total Coverage
Test: Test Coverage for xapian-core c2b6f1024d3a Lines: 402 428 93.9 %
Date: 2019-05-16 09:13:18 Functions: 27 27 100.0 %
Branches: 499 706 70.7 %

           Branch data     Line data    Source code
       1                 :            : /** @file termgenerator_internal.cc
       2                 :            :  * @brief TermGenerator class internals
       3                 :            :  */
       4                 :            : /* Copyright (C) 2007,2010,2011,2012,2015,2016,2017,2018,2019 Olly Betts
       5                 :            :  *
       6                 :            :  * This program is free software; you can redistribute it and/or modify
       7                 :            :  * it under the terms of the GNU General Public License as published by
       8                 :            :  * the Free Software Foundation; either version 2 of the License, or
       9                 :            :  * (at your option) any later version.
      10                 :            :  *
      11                 :            :  * This program is distributed in the hope that it will be useful,
      12                 :            :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      13                 :            :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      14                 :            :  * GNU General Public License for more details.
      15                 :            :  *
      16                 :            :  * You should have received a copy of the GNU General Public License
      17                 :            :  * along with this program; if not, write to the Free Software
      18                 :            :  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
      19                 :            :  */
      20                 :            : 
      21                 :            : #include <config.h>
      22                 :            : 
      23                 :            : #include "termgenerator_internal.h"
      24                 :            : 
      25                 :            : #include "api/msetinternal.h"
      26                 :            : #include "api/queryinternal.h"
      27                 :            : 
      28                 :            : #include <xapian/document.h>
      29                 :            : #include <xapian/queryparser.h>
      30                 :            : #include <xapian/stem.h>
      31                 :            : #include <xapian/unicode.h>
      32                 :            : 
      33                 :            : #include "stringutils.h"
      34                 :            : 
      35                 :            : #include <algorithm>
      36                 :            : #include <cmath>
      37                 :            : #include <deque>
      38                 :            : #include <limits>
      39                 :            : #include <list>
      40                 :            : #include <string>
      41                 :            : #include <unordered_map>
      42                 :            : #include <vector>
      43                 :            : 
      44                 :            : #include "cjk-tokenizer.h"
      45                 :            : 
      46                 :            : using namespace std;
      47                 :            : 
      48                 :            : namespace Xapian {
      49                 :            : 
      50                 :            : static inline bool
      51                 :       3245 : U_isupper(unsigned ch)
      52                 :            : {
      53 [ +  + ][ +  + ]:       3245 :     return (ch < 128 && C_isupper(static_cast<unsigned char>(ch)));
      54                 :            : }
      55                 :            : 
      56                 :            : static inline unsigned
      57                 :      21293 : check_wordchar(unsigned ch)
      58                 :            : {
      59         [ +  + ]:      21293 :     if (Unicode::is_wordchar(ch)) return Unicode::tolower(ch);
      60                 :       5992 :     return 0;
      61                 :            : }
      62                 :            : 
      63                 :            : static inline bool
      64                 :        576 : should_stem(const std::string & term)
      65                 :            : {
      66                 :            :     const unsigned int SHOULD_STEM_MASK =
      67                 :            :         (1 << Unicode::LOWERCASE_LETTER) |
      68                 :            :         (1 << Unicode::TITLECASE_LETTER) |
      69                 :            :         (1 << Unicode::MODIFIER_LETTER) |
      70                 :        576 :         (1 << Unicode::OTHER_LETTER);
      71                 :        576 :     Utf8Iterator u(term);
      72                 :        576 :     return ((SHOULD_STEM_MASK >> Unicode::get_category(*u)) & 1);
      73                 :            : }
      74                 :            : 
      75                 :            : /** Value representing "ignore this" when returned by check_infix() or
      76                 :            :  *  check_infix_digit().
      77                 :            :  */
      78                 :            : static const unsigned UNICODE_IGNORE = numeric_limits<unsigned>::max();
      79                 :            : 
      80                 :            : static inline unsigned
      81                 :       2217 : check_infix(unsigned ch)
      82                 :            : {
      83 [ +  + ][ +  - ]:       2217 :     if (ch == '\'' || ch == '&' || ch == 0xb7 || ch == 0x5f4 || ch == 0x2027) {
         [ +  - ][ +  - ]
                 [ -  + ]
      84                 :            :         // Unicode includes all these except '&' in its word boundary rules,
      85                 :            :         // as well as 0x2019 (which we handle below) and ':' (for Swedish
      86                 :            :         // apparently, but we ignore this for now as it's problematic in
      87                 :            :         // real world cases).
      88                 :         46 :         return ch;
      89                 :            :     }
      90                 :            :     // 0x2019 is Unicode apostrophe and single closing quote.
      91                 :            :     // 0x201b is Unicode single opening quote with the tail rising.
      92 [ +  + ][ -  + ]:       2171 :     if (ch == 0x2019 || ch == 0x201b) return '\'';
      93 [ +  + ][ -  + ]:       2144 :     if (ch >= 0x200b && (ch <= 0x200d || ch == 0x2060 || ch == 0xfeff))
         [ #  # ][ #  # ]
      94                 :          6 :         return UNICODE_IGNORE;
      95                 :       2138 :     return 0;
      96                 :            : }
      97                 :            : 
      98                 :            : static inline unsigned
      99                 :         24 : check_infix_digit(unsigned ch)
     100                 :            : {
     101                 :            :     // This list of characters comes from Unicode's word identifying algorithm.
     102         [ +  + ]:         24 :     switch (ch) {
     103                 :            :         case ',':
     104                 :            :         case '.':
     105                 :            :         case ';':
     106                 :            :         case 0x037e: // GREEK QUESTION MARK
     107                 :            :         case 0x0589: // ARMENIAN FULL STOP
     108                 :            :         case 0x060D: // ARABIC DATE SEPARATOR
     109                 :            :         case 0x07F8: // NKO COMMA
     110                 :            :         case 0x2044: // FRACTION SLASH
     111                 :            :         case 0xFE10: // PRESENTATION FORM FOR VERTICAL COMMA
     112                 :            :         case 0xFE13: // PRESENTATION FORM FOR VERTICAL COLON
     113                 :            :         case 0xFE14: // PRESENTATION FORM FOR VERTICAL SEMICOLON
     114                 :         20 :             return ch;
     115                 :            :     }
     116 [ -  + ][ #  # ]:          4 :     if (ch >= 0x200b && (ch <= 0x200d || ch == 0x2060 || ch == 0xfeff))
         [ #  # ][ #  # ]
     117                 :          0 :         return UNICODE_IGNORE;
     118                 :          4 :     return 0;
     119                 :            : }
     120                 :            : 
     121                 :            : static inline bool
     122                 :       2294 : is_digit(unsigned ch) {
     123                 :       2294 :     return (Unicode::get_category(ch) == Unicode::DECIMAL_DIGIT_NUMBER);
     124                 :            : }
     125                 :            : 
     126                 :            : static inline unsigned
     127                 :       2520 : check_suffix(unsigned ch)
     128                 :            : {
     129 [ +  + ][ -  + ]:       2520 :     if (ch == '+' || ch == '#') return ch;
     130                 :            :     // FIXME: what about '-'?
     131                 :       2516 :     return 0;
     132                 :            : }
     133                 :            : 
     134                 :            : template<typename ACTION>
     135                 :            : static bool
     136                 :         22 : parse_cjk(Utf8Iterator & itor, unsigned cjk_flags, bool with_positions,
     137                 :            :           ACTION action)
     138                 :            : {
     139                 :            :     static_assert(int(MSet::SNIPPET_CJK_WORDS) == TermGenerator::FLAG_CJK_WORDS,
     140                 :            :                   "CJK_WORDS flags have same value");
     141                 :            : #ifdef USE_ICU
     142                 :            :     if (cjk_flags & MSet::SNIPPET_CJK_WORDS) {
     143                 :            :         const char* cjk_start = itor.raw();
     144                 :            :         (void)CJK::get_cjk(itor);
     145                 :            :         size_t cjk_left = itor.raw() - cjk_start;
     146                 :            :         for (CJKWordIterator tk(cjk_start, cjk_left);
     147                 :            :              tk != CJKWordIterator();
     148                 :            :              ++tk) {
     149                 :            :             const string& cjk_token = *tk;
     150                 :            :             cjk_left -= cjk_token.length();
     151                 :            :             if (!action(cjk_token, with_positions, itor.left() + cjk_left))
     152                 :            :                 return false;
     153                 :            :         }
     154                 :            :         return true;
     155                 :            :     }
     156                 :            : #else
     157                 :            :     (void)cjk_flags;
     158                 :            : #endif
     159                 :            : 
     160 [ +  - ][ +  - ]:         22 :     CJKNgramIterator tk(itor);
     161 [ +  - ][ +  + ]:        298 :     while (tk != CJKNgramIterator()) {
         [ +  - ][ +  + ]
     162                 :        276 :         const string& cjk_token = *tk;
     163                 :            :         // FLAG_CJK_NGRAM only sets positions for tokens of length 1.
     164 [ +  - ][ +  + ]:        276 :         bool with_pos = with_positions && tk.unigram();
         [ +  - ][ +  + ]
     165 [ +  - ][ -  + ]:        276 :         if (!action(cjk_token, with_pos, tk.get_utf8iterator().left()))
         [ +  - ][ -  + ]
     166                 :          0 :             return false;
     167 [ +  - ][ +  - ]:        276 :         ++tk;
     168                 :            :     }
     169                 :            :     // Update itor to end of CJK text span.
     170                 :         22 :     itor = tk.get_utf8iterator();
     171                 :         22 :     return true;
     172                 :            : }
     173                 :            : 
     174                 :            : /** Templated framework for processing terms.
     175                 :            :  *
     176                 :            :  *  Calls action(term, positional) for each term to add, where term is a
     177                 :            :  *  std::string holding the term, and positional is a bool indicating
     178                 :            :  *  if this term carries positional information.
     179                 :            :  */
     180                 :            : template<typename ACTION>
     181                 :            : static void
     182                 :        609 : parse_terms(Utf8Iterator itor, unsigned cjk_flags, bool with_positions,
     183                 :            :             ACTION action)
     184                 :            : {
     185                 :       6118 :     while (true) {
     186                 :            :         // Advance to the start of the next term.
     187                 :            :         unsigned ch;
     188                 :            :         while (true) {
     189 [ +  + ][ +  + ]:       6742 :             if (itor == Utf8Iterator()) return;
     190                 :       6134 :             ch = check_wordchar(*itor);
     191 [ +  + ][ +  + ]:       6134 :             if (ch) break;
     192                 :       3104 :             ++itor;
     193                 :            :         }
     194                 :            : 
     195 [ +  - ][ +  - ]:       3030 :         string term;
     196                 :            :         // Look for initials separated by '.' (e.g. P.T.O., U.N.C.L.E).
     197                 :            :         // Don't worry if there's a trailing '.' or not.
     198 [ +  + ][ +  + ]:       3030 :         if (U_isupper(*itor)) {
     199                 :        520 :             const Utf8Iterator end;
     200                 :        520 :             Utf8Iterator p = itor;
     201   [ +  -  +  +  :       1846 :             do {
             +  -  +  + ]
           [ +  +  +  +  
          +  +  +  +  +  
              + ][ +  + ]
     202 [ +  - ][ +  - ]:        706 :                 Unicode::append_utf8(term, Unicode::tolower(*p++));
     203                 :       1846 :             } while (p != end && *p == '.' && ++p != end && U_isupper(*p));
     204                 :            :             // One letter does not make an acronym!  If we handled a single
     205                 :            :             // uppercase letter here, we wouldn't catch M&S below.
     206 [ +  + ][ +  + ]:        520 :             if (term.size() > 1) {
     207                 :            :                 // Check there's not a (lower case) letter or digit
     208                 :            :                 // immediately after it.
     209 [ +  - ][ +  - ]:         40 :                 if (p == end || !Unicode::is_wordchar(*p)) {
         [ +  - ][ +  + ]
         [ +  - ][ +  - ]
     210                 :         40 :                     itor = p;
     211                 :         40 :                     goto endofterm;
     212                 :            :                 }
     213                 :            :             }
     214 [ +  - ][ +  - ]:        480 :             term.resize(0);
     215                 :            :         }
     216                 :            : 
     217                 :            :         while (true) {
     218 [ +  + ][ +  - ]:       3098 :             if (cjk_flags && CJK::codepoint_is_cjk_wordchar(*itor)) {
         [ +  - ][ +  + ]
         [ +  + ][ +  - ]
         [ +  + ][ +  + ]
     219 [ +  - ][ -  + ]:         22 :                 if (!parse_cjk(itor, cjk_flags, with_positions, action))
         [ +  - ][ +  - ]
                 [ -  + ]
     220                 :         16 :                     return;
     221                 :            :                 while (true) {
     222 [ +  - ][ +  + ]:         25 :                     if (itor == Utf8Iterator()) return;
     223                 :          9 :                     ch = check_wordchar(*itor);
     224 [ #  # ][ +  + ]:          9 :                     if (ch) break;
     225                 :          3 :                     ++itor;
     226                 :            :                 }
     227                 :          6 :                 continue;
     228                 :            :             }
     229                 :            :             unsigned prevch;
     230   [ +  +  +  + ]:      12639 :             do {
     231 [ +  - ][ +  - ]:      13097 :                 Unicode::append_utf8(term, ch);
     232                 :      13097 :                 prevch = ch;
     233 [ +  + ][ -  + ]:      13125 :                 if (++itor == Utf8Iterator() ||
         [ #  # ][ +  - ]
           [ +  +  #  # ]
         [ +  + ][ +  + ]
         [ +  + ][ +  - ]
           [ +  +  #  # ]
     234 [ #  # ][ +  - ]:         28 :                     (cjk_flags && CJK::codepoint_is_cjk(*itor)))
     235                 :        458 :                     goto endofterm;
     236                 :      12639 :                 ch = check_wordchar(*itor);
     237                 :            :             } while (ch);
     238                 :            : 
     239                 :       2615 :             Utf8Iterator next(itor);
     240                 :       2615 :             ++next;
     241 [ +  + ][ +  + ]:       2615 :             if (next == Utf8Iterator()) break;
     242                 :       2511 :             unsigned nextch = check_wordchar(*next);
     243 [ +  + ][ +  + ]:       2511 :             if (!nextch) break;
     244                 :       2241 :             unsigned infix_ch = *itor;
     245 [ +  + ][ +  + ]:       2241 :             if (is_digit(prevch) && is_digit(*next)) {
           [ +  +  +  + ]
         [ +  + ][ +  + ]
     246                 :         24 :                 infix_ch = check_infix_digit(infix_ch);
     247                 :            :             } else {
     248                 :            :                 // Handle things like '&' in AT&T, apostrophes, etc.
     249                 :       2217 :                 infix_ch = check_infix(infix_ch);
     250                 :            :             }
     251 [ +  + ][ +  + ]:       2241 :             if (!infix_ch) break;
     252 [ +  - ][ +  + ]:         99 :             if (infix_ch != UNICODE_IGNORE)
     253 [ +  - ][ +  - ]:         93 :                 Unicode::append_utf8(term, infix_ch);
     254                 :         99 :             ch = nextch;
     255                 :         99 :             itor = next;
     256                 :            :         }
     257                 :            : 
     258                 :            :         {
     259                 :       2516 :             size_t len = term.size();
     260                 :       2516 :             unsigned count = 0;
     261 [ -  + ][ +  + ]:       2625 :             while ((ch = check_suffix(*itor))) {
     262 [ #  # ][ -  + ]:          4 :                 if (++count > 3) {
     263 [ #  # ][ #  # ]:          0 :                     term.resize(len);
     264                 :          0 :                     break;
     265                 :            :                 }
     266 [ #  # ][ +  - ]:          4 :                 Unicode::append_utf8(term, ch);
     267 [ #  # ][ -  + ]:          4 :                 if (++itor == Utf8Iterator()) goto endofterm;
     268                 :            :             }
     269                 :            :             // Don't index fish+chips as fish+ chips.
     270 [ -  + ][ +  + ]:       2516 :             if (Unicode::is_wordchar(*itor))
     271 [ #  # ][ +  - ]:          1 :                 term.resize(len);
     272                 :            :         }
     273                 :            : 
     274                 :            : endofterm:
     275 [ +  - ][ -  + ]:       3014 :         if (!action(term, with_positions, itor.left()))
         [ +  + ][ -  + ]
     276 [ +  + ][ +  + ]:       3029 :             return;
     277                 :       3013 :     }
     278                 :            : }
     279                 :            : 
     280                 :            : void
     281                 :        117 : TermGenerator::Internal::index_text(Utf8Iterator itor, termcount wdf_inc,
     282                 :            :                                     const string & prefix, bool with_positions)
     283                 :            : {
     284                 :            : #ifndef USE_ICU
     285         [ +  + ]:        117 :     if (flags & FLAG_CJK_WORDS) {
     286                 :            :         throw Xapian::FeatureUnavailableError("FLAG_CJK_WORDS requires "
     287 [ +  - ][ +  - ]:          9 :                                               "building Xapian to use ICU");
                 [ +  - ]
     288                 :            :     }
     289                 :            : #endif
     290                 :        108 :     unsigned cjk_flags = flags & (FLAG_CJK_NGRAM | FLAG_CJK_WORDS);
     291 [ +  + ][ -  + ]:        108 :     if (cjk_flags == 0 && CJK::is_cjk_enabled()) {
                 [ -  + ]
     292                 :          0 :         cjk_flags = FLAG_CJK_NGRAM;
     293                 :            :     }
     294                 :            : 
     295                 :            :     stop_strategy current_stop_mode;
     296         [ +  + ]:        108 :     if (!stopper.get()) {
     297                 :         98 :         current_stop_mode = TermGenerator::STOP_NONE;
     298                 :            :     } else {
     299                 :         10 :         current_stop_mode = stop_mode;
     300                 :            :     }
     301                 :            : 
     302                 :            :     parse_terms(itor, cjk_flags, with_positions,
     303                 :       1050 :         [=](const string & term, bool positional, size_t) {
     304         [ +  + ]:        770 :             if (term.size() > max_word_length) return true;
     305                 :            : 
     306 [ +  + ][ +  - ]:        768 :             if (current_stop_mode == TermGenerator::STOP_ALL && (*stopper)(term))
         [ +  + ][ +  + ]
     307                 :          3 :                 return true;
     308                 :            : 
     309 [ +  + ][ +  + ]:        765 :             if (strategy == TermGenerator::STEM_SOME ||
     310         [ +  + ]:         23 :                 strategy == TermGenerator::STEM_NONE ||
     311                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     312         [ +  + ]:        750 :                 if (positional) {
     313 [ +  - ][ +  - ]:        695 :                     doc.add_posting(prefix + term, ++cur_pos, wdf_inc);
     314                 :            :                 } else {
     315 [ +  - ][ +  - ]:         55 :                     doc.add_term(prefix + term, wdf_inc);
     316                 :            :                 }
     317                 :            :             }
     318                 :            : 
     319                 :            :             // MSVC seems to need "this->" on member variables in this
     320                 :            :             // situation.
     321 [ +  + ][ +  + ]:        765 :             if ((this->flags & FLAG_SPELLING) && prefix.empty())
                 [ +  + ]
     322         [ +  + ]:          7 :                 db.add_spelling(term);
     323                 :            : 
     324   [ +  +  +  + ]:       1521 :             if (strategy == TermGenerator::STEM_NONE ||
                 [ +  + ]
     325                 :        928 :                 !stemmer.internal.get()) return true;
     326                 :            : 
     327 [ +  + ][ +  + ]:        593 :             if (strategy == TermGenerator::STEM_SOME ||
     328                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     329 [ +  + ][ +  + ]:        584 :                 if (current_stop_mode == TermGenerator::STOP_STEMMED &&
                 [ +  + ]
     330         [ +  - ]:          6 :                     (*stopper)(term))
     331                 :          2 :                     return true;
     332                 :            : 
     333                 :            :                 // Note, this uses the lowercased term, but that's OK as we
     334                 :            :                 // only want to avoid stemming terms starting with a digit.
     335         [ +  + ]:        576 :                 if (!should_stem(term)) return true;
     336                 :            :             }
     337                 :            : 
     338                 :            :             // Add stemmed form without positional information.
     339         [ +  - ]:        583 :             const string& stem = stemmer(term);
     340         [ +  + ]:        583 :             if (rare(stem.empty())) return true;
     341         [ +  - ]:       1164 :             string stemmed_term;
     342         [ +  + ]:        582 :             if (strategy != TermGenerator::STEM_ALL) {
     343         [ +  - ]:        571 :                 stemmed_term += "Z";
     344                 :            :             }
     345         [ +  - ]:        582 :             stemmed_term += prefix;
     346         [ +  - ]:        582 :             stemmed_term += stem;
     347 [ +  + ][ +  - ]:        582 :             if (strategy != TermGenerator::STEM_SOME && with_positions) {
     348         [ +  + ]:         21 :                 if (strategy != TermGenerator::STEM_SOME_FULL_POS) ++cur_pos;
     349         [ +  - ]:         21 :                 doc.add_posting(stemmed_term, cur_pos, wdf_inc);
     350                 :            :             } else {
     351         [ +  - ]:        561 :                 doc.add_term(stemmed_term, wdf_inc);
     352                 :            :             }
     353                 :        582 :             return true;
     354         [ +  + ]:        877 :         });
     355                 :        107 : }
     356                 :            : 
     357                 :            : struct Sniplet {
     358                 :            :     double* relevance;
     359                 :            : 
     360                 :            :     size_t term_end;
     361                 :            : 
     362                 :            :     size_t highlight;
     363                 :            : 
     364                 :       2448 :     Sniplet(double* r, size_t t, size_t h)
     365                 :       2448 :         : relevance(r), term_end(t), highlight(h) { }
     366                 :            : };
     367                 :            : 
     368                 :       1002 : class SnipPipe {
     369                 :            :     deque<Sniplet> pipe;
     370                 :            :     deque<Sniplet> best_pipe;
     371                 :            : 
     372                 :            :     // Requested length for snippet.
     373                 :            :     size_t length;
     374                 :            : 
     375                 :            :     // Position in text of start of current pipe contents.
     376                 :            :     size_t begin = 0;
     377                 :            : 
     378                 :            :     // Rolling sum of the current pipe contents.
     379                 :            :     double sum = 0;
     380                 :            : 
     381                 :            :     size_t phrase_len = 0;
     382                 :            : 
     383                 :            :   public:
     384                 :            :     size_t best_begin = 0;
     385                 :            : 
     386                 :            :     size_t best_end = 0;
     387                 :            : 
     388                 :            :     double best_sum = 0;
     389                 :            : 
     390                 :            :     // Add one to length to allow for inter-word space.
     391                 :            :     // FIXME: We ought to correctly allow for multiple spaces.
     392         [ +  - ]:        501 :     explicit SnipPipe(size_t length_) : length(length_ + 1) { }
     393                 :            : 
     394                 :            :     bool pump(double* r, size_t t, size_t h, unsigned flags);
     395                 :            : 
     396                 :            :     void done();
     397                 :            : 
     398                 :            :     bool drain(const string & input,
     399                 :            :                const string & hi_start,
     400                 :            :                const string & hi_end,
     401                 :            :                const string & omit,
     402                 :            :                string & output);
     403                 :            : };
     404                 :            : 
     405                 :            : #define DECAY 2.0
     406                 :            : 
     407                 :            : inline bool
     408                 :       2448 : SnipPipe::pump(double* r, size_t t, size_t h, unsigned flags)
     409                 :            : {
     410         [ +  + ]:       2448 :     if (h > 1) {
     411         [ +  - ]:         51 :         if (pipe.size() >= h - 1) {
     412                 :            :             // The final term of a phrase is entering the window.  Peg the
     413                 :            :             // phrase's relevance onto the first term of the phrase, so it'll
     414                 :            :             // be removed from `sum` when the phrase starts to leave the
     415                 :            :             // window.
     416                 :         51 :             auto & phrase_start = pipe[pipe.size() - (h - 1)];
     417         [ +  - ]:         51 :             if (phrase_start.relevance) {
     418                 :         51 :                 *phrase_start.relevance *= DECAY;
     419                 :         51 :                 sum -= *phrase_start.relevance;
     420                 :            :             }
     421                 :         51 :             sum += *r;
     422                 :         51 :             phrase_start.relevance = r;
     423                 :         51 :             phrase_start.highlight = h;
     424                 :         51 :             *r /= DECAY;
     425                 :            :         }
     426                 :         51 :         r = NULL;
     427                 :         51 :         h = 0;
     428                 :            :     }
     429                 :       2448 :     pipe.emplace_back(r, t, h);
     430         [ +  + ]:       2448 :     if (r) {
     431                 :       2149 :         sum += *r;
     432                 :       2149 :         *r /= DECAY;
     433                 :            :     }
     434                 :            : 
     435                 :            :     // If necessary, discard words from the start of the pipe until it has the
     436                 :            :     // desired length.
     437                 :            :     // FIXME: Also shrink the window past words with relevance < 0?
     438         [ +  + ]:       3121 :     while (t - begin > length /* || pipe.front().relevance < 0.0 */) {
     439                 :        876 :         const Sniplet& word = pipe.front();
     440         [ +  + ]:        876 :         if (word.relevance) {
     441                 :        816 :             *word.relevance *= DECAY;
     442                 :        816 :             sum -= *word.relevance;
     443                 :            :         }
     444                 :        876 :         begin = word.term_end;
     445         [ +  + ]:        876 :         if (best_end >= begin)
     446                 :        574 :             best_pipe.push_back(word);
     447                 :        876 :         pipe.pop_front();
     448                 :            :         // E.g. can happen if the current term is longer than the requested
     449                 :            :         // length!
     450         [ +  + ]:        876 :         if (rare(pipe.empty())) break;
     451                 :            :     }
     452                 :            : 
     453                 :            :     // Using > here doesn't work well, as we don't extend a snippet over terms
     454                 :            :     // with 0 weight.
     455         [ +  + ]:       2448 :     if (sum >= best_sum) {
     456                 :            :         // Discard any part of `best_pipe` which is before `begin`.
     457         [ +  + ]:       2003 :         if (begin >= best_end) {
     458                 :        663 :             best_pipe.clear();
     459                 :            :         } else {
     460   [ +  +  +  - ]:       1930 :             while (!best_pipe.empty() &&
                 [ +  + ]
     461                 :        295 :                    best_pipe.front().term_end <= begin) {
     462                 :        295 :                 best_pipe.pop_front();
     463                 :            :             }
     464                 :            :         }
     465                 :       2003 :         best_sum = sum;
     466                 :       2003 :         best_begin = begin;
     467                 :       2003 :         best_end = t;
     468         [ +  + ]:        445 :     } else if ((flags & Xapian::MSet::SNIPPET_EXHAUSTIVE) == 0) {
     469 [ +  - ][ -  + ]:         12 :         if (best_sum > 0 && best_end < begin) {
     470                 :            :             // We found something, and we aren't still looking near it.
     471                 :            :             // FIXME: Benchmark this and adjust if necessary.
     472                 :          0 :             return false;
     473                 :            :         }
     474                 :            :     }
     475                 :       2448 :     return true;
     476                 :            : }
     477                 :            : 
     478                 :            : inline void
     479                 :        501 : SnipPipe::done()
     480                 :            : {
     481                 :            :     // Discard any part of `pipe` which is after `best_end`.
     482         [ +  + ]:        501 :     if (begin >= best_end) {
     483                 :        100 :         pipe.clear();
     484                 :            :     } else {
     485                 :            :         // We should never empty the pipe (as that case should be handled
     486                 :            :         // above).
     487   [ +  -  +  + ]:       1046 :         while (rare(!pipe.empty()) &&
                 [ +  + ]
     488                 :        523 :                pipe.back().term_end > best_end) {
     489                 :        122 :             pipe.pop_back();
     490                 :            :         }
     491                 :            :     }
     492                 :        501 : }
     493                 :            : 
     494                 :            : // Check if a non-word character is should be included at the start of the
     495                 :            : // snippet.  We want to include certain leading non-word characters, but not
     496                 :            : // others.
     497                 :            : static inline bool
     498                 :        414 : snippet_check_leading_nonwordchar(unsigned ch) {
     499 [ +  + ][ +  + ]:       1235 :     if (Unicode::is_currency(ch) ||
     500 [ +  + ][ +  + ]:        821 :         Unicode::get_category(ch) == Unicode::OPEN_PUNCTUATION ||
     501                 :        323 :         Unicode::get_category(ch) == Unicode::INITIAL_QUOTE_PUNCTUATION) {
     502                 :         98 :         return true;
     503                 :            :     }
     504         [ +  + ]:        316 :     switch (ch) {
     505                 :            :         case '"':
     506                 :            :         case '#':
     507                 :            :         case '%':
     508                 :            :         case '&':
     509                 :            :         case '\'':
     510                 :            :         case '+':
     511                 :            :         case '-':
     512                 :            :         case '/':
     513                 :            :         case '<':
     514                 :            :         case '@':
     515                 :            :         case '\\':
     516                 :            :         case '`':
     517                 :            :         case '~':
     518                 :            :         case 0x00A1: // INVERTED EXCLAMATION MARK
     519                 :            :         case 0x00A7: // SECTION SIGN
     520                 :            :         case 0x00BF: // INVERTED QUESTION MARK
     521                 :        125 :             return true;
     522                 :            :     }
     523                 :        191 :     return false;
     524                 :            : }
     525                 :            : 
     526                 :            : static inline void
     527                 :       2137 : append_escaping_xml(const char* p, const char* end, string& output)
     528                 :            : {
     529         [ +  + ]:      10281 :     while (p != end) {
     530                 :       8144 :         char ch = *p++;
     531   [ +  +  +  + ]:       8144 :         switch (ch) {
     532                 :            :             case '&':
     533                 :          7 :                 output += "&amp;";
     534                 :          7 :                 break;
     535                 :            :             case '<':
     536                 :          7 :                 output += "&lt;";
     537                 :          7 :                 break;
     538                 :            :             case '>':
     539                 :          7 :                 output += "&gt;";
     540                 :          7 :                 break;
     541                 :            :             default:
     542                 :       8123 :                 output += ch;
     543                 :            :         }
     544                 :            :     }
     545                 :       2137 : }
     546                 :            : 
     547                 :            : inline bool
     548                 :       2044 : SnipPipe::drain(const string & input,
     549                 :            :                 const string & hi_start,
     550                 :            :                 const string & hi_end,
     551                 :            :                 const string & omit,
     552                 :            :                 string & output)
     553                 :            : {
     554 [ +  + ][ +  + ]:       2044 :     if (best_pipe.empty() && !pipe.empty()) {
                 [ +  + ]
     555                 :        394 :         swap(best_pipe, pipe);
     556                 :            :     }
     557                 :            : 
     558         [ +  + ]:       2044 :     if (best_pipe.empty()) {
     559                 :        494 :         size_t tail_len = input.size() - best_end;
     560         [ +  + ]:        494 :         if (tail_len == 0) return false;
     561                 :            : 
     562                 :            :         // See if this is the end of a sentence.
     563                 :            :         // FIXME: This is quite simplistic - look at the Unicode rules:
     564                 :            :         // https://unicode.org/reports/tr29/#Sentence_Boundaries
     565                 :        162 :         bool punc = false;
     566                 :        162 :         Utf8Iterator i(input.data() + best_end, tail_len);
     567         [ +  + ]:        335 :         while (i != Utf8Iterator()) {
     568                 :        281 :             unsigned ch = *i;
     569 [ +  + ][ +  - ]:        281 :             if (punc && Unicode::is_whitespace(ch)) break;
                 [ +  + ]
     570                 :            : 
     571                 :            :             // Allow "...", "!!", "!?!", etc...
     572 [ +  + ][ +  + ]:        275 :             punc = (ch == '.' || ch == '?' || ch == '!');
                 [ +  + ]
     573                 :            : 
     574         [ +  + ]:        275 :             if (Unicode::is_wordchar(ch)) break;
     575                 :        173 :             ++i;
     576                 :            :         }
     577                 :            : 
     578         [ +  + ]:        162 :         if (punc) {
     579                 :            :             // Include end of sentence punctuation.
     580         [ +  - ]:         46 :             append_escaping_xml(input.data() + best_end, i.raw(), output);
     581                 :            :         } else {
     582                 :            :             // Append "..." or equivalent if this doesn't seem to be the start
     583                 :            :             // of a sentence.
     584         [ +  - ]:        116 :             output += omit;
     585                 :            :         }
     586                 :            : 
     587                 :        494 :         return false;
     588                 :            :     }
     589                 :            : 
     590                 :       1550 :     const Sniplet & word = best_pipe.front();
     591                 :            : 
     592         [ +  + ]:       1550 :     if (output.empty()) {
     593                 :            :         // Start of the snippet.
     594         [ +  + ]:        438 :         enum { NO, PUNC, YES } sentence_boundary = (best_begin == 0) ? YES : NO;
     595                 :            : 
     596                 :        438 :         Utf8Iterator i(input.data() + best_begin, word.term_end - best_begin);
     597         [ +  - ]:        852 :         while (i != Utf8Iterator()) {
     598                 :        852 :             unsigned ch = *i;
     599   [ +  -  +  - ]:        852 :             switch (sentence_boundary) {
     600                 :            :                 case NO:
     601 [ +  - ][ +  - ]:        334 :                     if (ch == '.' || ch == '?' || ch == '!') {
                 [ -  + ]
     602                 :          0 :                         sentence_boundary = PUNC;
     603                 :            :                     }
     604                 :        334 :                     break;
     605                 :            :                 case PUNC:
     606         [ #  # ]:          0 :                     if (Unicode::is_whitespace(ch)) {
     607                 :          0 :                         sentence_boundary = YES;
     608 [ #  # ][ #  # ]:          0 :                     } else if (ch == '.' || ch == '?' || ch == '!') {
                 [ #  # ]
     609                 :            :                         // Allow "...", "!!", "!?!", etc...
     610                 :            :                     } else {
     611                 :          0 :                         sentence_boundary = NO;
     612                 :            :                     }
     613                 :          0 :                     break;
     614                 :            :                 case YES:
     615                 :        518 :                     break;
     616                 :            :             }
     617                 :            : 
     618                 :            :             // Start the snippet at the start of the first word, but include
     619                 :            :             // certain punctuation too.
     620         [ +  + ]:        852 :             if (Unicode::is_wordchar(ch)) {
     621                 :            :                 // But limit how much leading punctuation we include.
     622                 :        438 :                 size_t word_begin = i.raw() - input.data();
     623         [ +  + ]:        438 :                 if (word_begin - best_begin > 4) {
     624                 :          7 :                     best_begin = word_begin;
     625                 :            :                 }
     626                 :        438 :                 break;
     627                 :            :             }
     628                 :        414 :             ++i;
     629         [ +  + ]:        414 :             if (!snippet_check_leading_nonwordchar(ch)) {
     630                 :        191 :                 best_begin = i.raw() - input.data();
     631                 :            :             }
     632                 :            :         }
     633                 :            : 
     634                 :            :         // Add "..." or equivalent if this doesn't seem to be the start of a
     635                 :            :         // sentence.
     636         [ +  + ]:        438 :         if (sentence_boundary != YES) {
     637         [ +  - ]:        438 :             output += omit;
     638                 :            :         }
     639                 :            :     }
     640                 :            : 
     641         [ +  + ]:       1550 :     if (word.highlight) {
     642                 :            :         // Don't include inter-word characters in the highlight.
     643                 :        541 :         Utf8Iterator i(input.data() + best_begin, input.size() - best_begin);
     644         [ +  - ]:       1502 :         while (i != Utf8Iterator()) {
     645                 :        961 :             unsigned ch = *i;
     646         [ +  + ]:        961 :             if (Unicode::is_wordchar(ch)) {
     647         [ +  - ]:        541 :                 append_escaping_xml(input.data() + best_begin, i.raw(), output);
     648                 :        541 :                 best_begin = i.raw() - input.data();
     649                 :        541 :                 break;
     650                 :            :             }
     651                 :        420 :             ++i;
     652                 :            :         }
     653                 :            :     }
     654                 :            : 
     655         [ +  + ]:       1550 :     if (!phrase_len) {
     656                 :       1490 :         phrase_len = word.highlight;
     657         [ +  + ]:       1490 :         if (phrase_len) output += hi_start;
     658                 :            :     }
     659                 :            : 
     660                 :       1550 :     const char* p = input.data();
     661                 :       1550 :     append_escaping_xml(p + best_begin, p + word.term_end, output);
     662                 :       1550 :     best_begin = word.term_end;
     663                 :            : 
     664 [ +  + ][ +  + ]:       1550 :     if (phrase_len && --phrase_len == 0) output += hi_end;
                 [ +  + ]
     665                 :            : 
     666                 :       1550 :     best_pipe.pop_front();
     667                 :       2044 :     return true;
     668                 :            : }
     669                 :            : 
     670                 :            : static void
     671                 :       1121 : check_query(const Xapian::Query & query,
     672                 :            :             list<vector<string>> & exact_phrases,
     673                 :            :             unordered_map<string, double> & loose_terms,
     674                 :            :             list<const Xapian::Internal::QueryWildcard*> & wildcards,
     675                 :            :             list<const Xapian::Internal::QueryEditDistance*> & fuzzies,
     676                 :            :             size_t & longest_phrase)
     677                 :            : {
     678                 :            :     // FIXME: OP_NEAR, non-tight OP_PHRASE, OP_PHRASE with non-term subqueries
     679                 :       1121 :     size_t n_subqs = query.get_num_subqueries();
     680                 :       1121 :     Xapian::Query::op op = query.get_type();
     681         [ +  + ]:       1121 :     if (op == query.LEAF_TERM) {
     682                 :            :         const Xapian::Internal::QueryTerm & qt =
     683                 :        760 :             *static_cast<const Xapian::Internal::QueryTerm *>(query.internal.get());
     684 [ +  - ][ +  - ]:        760 :         loose_terms.insert(make_pair(qt.get_term(), 0));
     685         [ +  + ]:        361 :     } else if (op == query.OP_WILDCARD) {
     686                 :            :         using Xapian::Internal::QueryWildcard;
     687                 :            :         const QueryWildcard* qw =
     688                 :         15 :             static_cast<const QueryWildcard*>(query.internal.get());
     689         [ +  - ]:         15 :         wildcards.push_back(qw);
     690         [ -  + ]:        346 :     } else if (op == query.OP_EDIT_DISTANCE) {
     691                 :            :         using Xapian::Internal::QueryEditDistance;
     692                 :            :         const QueryEditDistance* qed =
     693                 :          0 :             static_cast<const QueryEditDistance*>(query.internal.get());
     694         [ #  # ]:          0 :         fuzzies.push_back(qed);
     695         [ +  + ]:        346 :     } else if (op == query.OP_PHRASE) {
     696                 :            :         const Xapian::Internal::QueryPhrase & phrase =
     697                 :         51 :             *static_cast<const Xapian::Internal::QueryPhrase *>(query.internal.get());
     698         [ +  - ]:         51 :         if (phrase.get_window() == n_subqs) {
     699                 :            :             // Tight phrase.
     700         [ +  + ]:        162 :             for (size_t i = 0; i != n_subqs; ++i) {
     701         [ -  + ]:        111 :                 if (query.get_subquery(i).get_type() != query.LEAF_TERM)
     702                 :          0 :                     goto non_term_subquery;
     703                 :            :             }
     704                 :            : 
     705                 :            :             // Tight phrase of terms.
     706         [ +  - ]:         51 :             exact_phrases.push_back(vector<string>());
     707                 :         51 :             vector<string> & terms = exact_phrases.back();
     708                 :         51 :             terms.reserve(n_subqs);
     709         [ +  + ]:        162 :             for (size_t i = 0; i != n_subqs; ++i) {
     710         [ +  - ]:        111 :                 Xapian::Query q = query.get_subquery(i);
     711                 :            :                 const Xapian::Internal::QueryTerm & qt =
     712                 :        111 :                     *static_cast<const Xapian::Internal::QueryTerm *>(q.internal.get());
     713         [ +  - ]:        111 :                 terms.push_back(qt.get_term());
     714                 :        111 :             }
     715         [ +  - ]:         51 :             if (n_subqs > longest_phrase) longest_phrase = n_subqs;
     716                 :       1121 :             return;
     717                 :            :         }
     718                 :            :     }
     719                 :            : non_term_subquery:
     720         [ +  + ]:       1690 :     for (size_t i = 0; i != n_subqs; ++i)
     721                 :            :         check_query(query.get_subquery(i), exact_phrases, loose_terms,
     722         [ +  - ]:        620 :                     wildcards, fuzzies, longest_phrase);
     723                 :            : }
     724                 :            : 
     725                 :            : static double*
     726                 :       4243 : check_term(unordered_map<string, double> & loose_terms,
     727                 :            :            const Xapian::Weight::Internal * stats,
     728                 :            :            const string & term,
     729                 :            :            double max_tw)
     730                 :            : {
     731         [ +  - ]:       4243 :     auto it = loose_terms.find(term);
     732         [ +  + ]:       4243 :     if (it == loose_terms.end()) return NULL;
     733                 :            : 
     734         [ +  + ]:        633 :     if (it->second == 0.0) {
     735                 :            :         double relevance;
     736 [ +  - ][ -  + ]:        528 :         if (!stats->get_termweight(term, relevance)) {
     737                 :            :             // FIXME: Assert?
     738         [ #  # ]:          0 :             loose_terms.erase(it);
     739                 :          0 :             return NULL;
     740                 :            :         }
     741                 :            : 
     742                 :        528 :         it->second = relevance + max_tw;
     743                 :            :     }
     744                 :       4243 :     return &it->second;
     745                 :            : }
     746                 :            : 
     747                 :            : string
     748                 :        515 : MSet::Internal::snippet(const string & text,
     749                 :            :                         size_t length,
     750                 :            :                         const Xapian::Stem & stemmer,
     751                 :            :                         unsigned flags,
     752                 :            :                         const string & hi_start,
     753                 :            :                         const string & hi_end,
     754                 :            :                         const string & omit) const
     755                 :            : {
     756 [ -  + ][ #  # ]:        515 :     if (hi_start.empty() && hi_end.empty() && text.size() <= length) {
         [ #  # ][ -  + ]
     757                 :            :         // Too easy!
     758         [ #  # ]:          0 :         return text;
     759                 :            :     }
     760                 :            : 
     761                 :            : #ifndef USE_ICU
     762         [ +  + ]:        515 :     if (flags & MSet::SNIPPET_CJK_WORDS) {
     763                 :            :         throw Xapian::FeatureUnavailableError("SNIPPET_CJK_WORDS requires "
     764 [ +  - ][ +  - ]:         14 :                                               "building Xapian to use ICU");
                 [ +  - ]
     765                 :            :     }
     766                 :            : #endif
     767                 :        501 :     auto SNIPPET_CJK_MASK = MSet::SNIPPET_CJK_NGRAM | MSet::SNIPPET_CJK_WORDS;
     768                 :        501 :     unsigned cjk_flags = flags & SNIPPET_CJK_MASK;
     769 [ +  + ][ +  - ]:        501 :     if (cjk_flags == 0 && CJK::is_cjk_enabled()) {
         [ -  + ][ -  + ]
     770                 :          0 :         cjk_flags = MSet::SNIPPET_CJK_NGRAM;
     771                 :            :     }
     772                 :            : 
     773                 :        501 :     size_t term_start = 0;
     774                 :        501 :     double min_tw = 0, max_tw = 0;
     775         [ +  - ]:        501 :     if (stats) stats->get_max_termweight(min_tw, max_tw);
     776         [ +  + ]:        501 :     if (max_tw == 0.0) {
     777                 :        236 :         max_tw = 1.0;
     778                 :            :     } else {
     779                 :            :         // Scale up by (1 + 1/64) so that highlighting works better for terms
     780                 :            :         // with termweight 0 (which happens for terms not in the database, and
     781                 :            :         // also with some weighting schemes for terms which occur in almost all
     782                 :            :         // documents.
     783                 :        265 :         max_tw *= 1.015625;
     784                 :            :     }
     785                 :            : 
     786         [ +  - ]:        501 :     SnipPipe snip(length);
     787                 :            : 
     788                 :       1002 :     list<vector<string>> exact_phrases;
     789         [ +  - ]:       1002 :     unordered_map<string, double> loose_terms;
     790                 :       1002 :     list<const Xapian::Internal::QueryWildcard*> wildcards;
     791                 :       1002 :     list<const Xapian::Internal::QueryEditDistance*> fuzzies;
     792                 :        501 :     size_t longest_phrase = 0;
     793                 :        501 :     check_query(enquire->query, exact_phrases, loose_terms,
     794         [ +  - ]:        501 :                 wildcards, fuzzies, longest_phrase);
     795                 :            : 
     796                 :       1002 :     vector<double> exact_phrases_relevance;
     797         [ +  - ]:        501 :     exact_phrases_relevance.reserve(exact_phrases.size());
     798         [ +  + ]:        552 :     for (auto&& terms : exact_phrases) {
     799                 :            :         // FIXME: What relevance to use?
     800         [ +  - ]:         51 :         exact_phrases_relevance.push_back(max_tw * terms.size());
     801                 :            :     }
     802                 :            : 
     803                 :       1002 :     vector<double> wildcards_relevance;
     804         [ +  - ]:        501 :     wildcards_relevance.reserve(wildcards.size());
     805         [ +  + ]:        516 :     for (auto&& pattern : wildcards) {
     806                 :            :         // FIXME: What relevance to use?
     807                 :            :         (void)pattern;
     808         [ +  - ]:         15 :         wildcards_relevance.push_back(max_tw + min_tw);
     809                 :            :     }
     810                 :            : 
     811                 :       1002 :     vector<double> fuzzies_relevance;
     812         [ +  - ]:        501 :     fuzzies_relevance.reserve(fuzzies.size());
     813         [ -  + ]:        501 :     for (auto&& pattern : fuzzies) {
     814                 :            :         // FIXME: What relevance to use?
     815                 :            :         (void)pattern;
     816         [ #  # ]:          0 :         fuzzies_relevance.push_back(max_tw + min_tw);
     817                 :            :     }
     818                 :            : 
     819                 :            :     // Background relevance is the same for a given MSet, so cache it
     820                 :            :     // between calls to MSet::snippet() on the same object.
     821                 :        501 :     unordered_map<string, double>& background = snippet_bg_relevance;
     822                 :            : 
     823                 :       1002 :     vector<string> phrase;
     824 [ +  + ][ +  - ]:        501 :     if (longest_phrase) phrase.resize(longest_phrase - 1);
     825                 :        501 :     size_t phrase_next = 0;
     826                 :        501 :     bool matchfound = false;
     827                 :            :     parse_terms(Utf8Iterator(text), cjk_flags, true,
     828                 :       2520 :         [&](const string & term, bool positional, size_t left) {
     829                 :            :             // FIXME: Don't hardcode this here.
     830                 :       2520 :             const size_t max_word_length = 64;
     831                 :            : 
     832         [ +  + ]:       2520 :             if (!positional) return true;
     833         [ -  + ]:       2448 :             if (term.size() > max_word_length) return true;
     834                 :            : 
     835                 :            :             // We get segments with any "inter-word" characters in front of
     836                 :            :             // each word, e.g.:
     837                 :            :             // [The][ cat][ sat][ on][ the][ mat]
     838                 :       2448 :             size_t term_end = text.size() - left;
     839                 :            : 
     840                 :       2448 :             double* relevance = 0;
     841                 :       2448 :             size_t highlight = 0;
     842         [ +  - ]:       2448 :             if (stats) {
     843                 :       2448 :                 size_t i = 0;
     844         [ +  + ]:       2725 :                 for (auto&& terms : exact_phrases) {
     845         [ +  + ]:        328 :                     if (term == terms.back()) {
     846                 :         82 :                         size_t n = terms.size() - 1;
     847                 :         82 :                         bool match = true;
     848         [ +  + ]:        142 :                         while (n--) {
     849         [ +  + ]:         91 :                             if (terms[n] != phrase[(n + phrase_next) % (longest_phrase - 1)]) {
     850                 :         31 :                                 match = false;
     851                 :         31 :                                 break;
     852                 :            :                             }
     853                 :            :                         }
     854         [ +  + ]:         82 :                         if (match) {
     855                 :            :                             // FIXME: Sort phrases, highest score first!
     856                 :         51 :                             relevance = &exact_phrases_relevance[i];
     857                 :         82 :                             highlight = terms.size();
     858                 :         51 :                             goto relevance_done;
     859                 :            :                         }
     860                 :            :                     }
     861                 :        277 :                     ++i;
     862                 :            :                 }
     863                 :            : 
     864         [ +  - ]:       2397 :                 relevance = check_term(loose_terms, stats.get(), term, max_tw);
     865         [ +  + ]:       2397 :                 if (relevance) {
     866                 :            :                     // Matched unstemmed term.
     867                 :        551 :                     highlight = 1;
     868                 :        551 :                     goto relevance_done;
     869                 :            :                 }
     870                 :            : 
     871         [ +  - ]:       1846 :                 string stem = "Z";
     872 [ +  - ][ +  - ]:       1846 :                 stem += stemmer(term);
     873         [ +  - ]:       1846 :                 relevance = check_term(loose_terms, stats.get(), stem, max_tw);
     874         [ +  + ]:       1846 :                 if (relevance) {
     875                 :            :                     // Matched stemmed term.
     876                 :         82 :                     highlight = 1;
     877                 :         82 :                     goto relevance_done;
     878                 :            :                 }
     879                 :            : 
     880                 :            :                 // Check wildcards.
     881                 :            :                 // FIXME: Sort wildcards, cheapest to check first or something?
     882                 :       1764 :                 i = 0;
     883         [ +  + ]:       1941 :                 for (auto&& qw : wildcards) {
     884 [ +  - ][ +  + ]:        216 :                     if (qw->test(term)) {
     885                 :         39 :                         relevance = &wildcards_relevance[i];
     886                 :         39 :                         highlight = 1;
     887                 :         39 :                         goto relevance_done;
     888                 :            :                     }
     889                 :        177 :                     ++i;
     890                 :            :                 }
     891                 :            : 
     892                 :            :                 // Check fuzzies.
     893                 :            :                 // FIXME: Sort fuzzies, cheapest to check first or something?
     894                 :       1725 :                 i = 0;
     895         [ -  + ]:       1725 :                 for (auto&& qed : fuzzies) {
     896         [ #  # ]:          0 :                     int result = qed->test(term);
     897         [ #  # ]:          0 :                     if (result) {
     898                 :            :                         // FIXME: Reduce relevance the more edits there are?
     899                 :            :                         // We can't just divide by result here as this
     900                 :            :                         // relevance is used by any term matching this
     901                 :            :                         // subquery.
     902                 :          0 :                         relevance = &fuzzies_relevance[i];
     903                 :          0 :                         highlight = 1;
     904                 :          0 :                         goto relevance_done;
     905                 :            :                     }
     906                 :          0 :                     ++i;
     907                 :            :                 }
     908                 :            : 
     909         [ +  + ]:       1725 :                 if (flags & Xapian::MSet::SNIPPET_BACKGROUND_MODEL) {
     910                 :            :                     // Background document model.
     911         [ +  - ]:       1477 :                     auto bgit = background.find(term);
     912 [ +  + ][ +  - ]:       1477 :                     if (bgit == background.end()) bgit = background.find(stem);
     913         [ +  + ]:       1477 :                     if (bgit == background.end()) {
     914         [ +  - ]:        827 :                         Xapian::doccount tf = enquire->db.get_termfreq(term);
     915         [ +  + ]:        827 :                         if (!tf) {
     916         [ +  - ]:        336 :                             tf = enquire->db.get_termfreq(stem);
     917                 :            :                         } else {
     918         [ +  - ]:        491 :                             stem = term;
     919                 :            :                         }
     920                 :        827 :                         double r = 0.0;
     921         [ +  + ]:        827 :                         if (tf) {
     922                 :            :                             // Add one to avoid log(0) when a term indexes all
     923                 :            :                             // documents.
     924                 :        491 :                             Xapian::doccount num_docs = stats->collection_size + 1;
     925                 :        491 :                             r = max_tw * log((num_docs - tf) / double(tf));
     926                 :        491 :                             r /= (length + 1) * log(double(num_docs));
     927                 :            : #if 0
     928                 :            :                             if (r <= 0) {
     929                 :            :                                 Utf8Iterator i(text.data() + term_start, text.data() + term_end);
     930                 :            :                                 while (i != Utf8Iterator()) {
     931                 :            :                                     if (Unicode::get_category(*i++) == Unicode::UPPERCASE_LETTER) {
     932                 :            :                                         r = max_tw * 0.05;
     933                 :            :                                     }
     934                 :            :                                 }
     935                 :            :                             }
     936                 :            : #endif
     937                 :            :                         }
     938 [ +  - ][ +  - ]:        827 :                         bgit = background.emplace(make_pair(stem, r)).first;
     939                 :            :                     }
     940         [ +  + ]:       1846 :                     relevance = &bgit->second;
     941                 :       2448 :                 }
     942                 :            :             } else {
     943                 :            : #if 0
     944                 :            :                 // In the absence of weight information, assume longer terms
     945                 :            :                 // are more relevant, and that unstemmed matches are a bit more
     946                 :            :                 // relevant than stemmed matches.
     947                 :            :                 if (queryterms.find(term) != queryterms.end()) {
     948                 :            :                     relevance = term.size() * 3;
     949                 :            :                 } else {
     950                 :            :                     string stem = "Z";
     951                 :            :                     stem += stemmer(term);
     952                 :            :                     if (queryterms.find(stem) != queryterms.end()) {
     953                 :            :                         relevance = term.size() * 2;
     954                 :            :                     }
     955                 :            :                 }
     956                 :            : #endif
     957                 :            :             }
     958                 :            : 
     959                 :            :             // FIXME: Allow Enquire without a DB set or an empty MSet() to be
     960                 :            :             // used if you don't want the collection model?
     961                 :            : 
     962                 :            : #if 0
     963                 :            :             // FIXME: Punctuation should somehow be included in the model, but this
     964                 :            :             // approach is problematic - we don't want the first word of a sentence
     965                 :            :             // to be favoured when it's at the end of the window.
     966                 :            : 
     967                 :            :             // Give first word in each sentence a relevance boost.
     968                 :            :             if (term_start == 0) {
     969                 :            :                 relevance += 10;
     970                 :            :             } else {
     971                 :            :                 for (size_t i = term_start; i + term.size() < term_end; ++i) {
     972                 :            :                     if (text[i] == '.' && Unicode::is_whitespace(text[i + 1])) {
     973                 :            :                         relevance += 10;
     974                 :            :                         break;
     975                 :            :                     }
     976                 :            :                 }
     977                 :            :             }
     978                 :            : #endif
     979                 :            : 
     980                 :            : relevance_done:
     981         [ +  + ]:       2448 :             if (longest_phrase) {
     982                 :        328 :                 phrase[phrase_next] = term;
     983                 :        328 :                 phrase_next = (phrase_next + 1) % (longest_phrase - 1);
     984                 :            :             }
     985                 :            : 
     986         [ +  + ]:       2448 :             if (highlight) matchfound = true;
     987                 :            : 
     988         [ -  + ]:       2448 :             if (!snip.pump(relevance, term_end, highlight, flags)) return false;
     989                 :            : 
     990                 :       2448 :             term_start = term_end;
     991                 :       2520 :             return true;
     992         [ +  - ]:        501 :         });
     993                 :            : 
     994                 :        501 :     snip.done();
     995                 :            : 
     996                 :            :     // Put together the snippet.
     997         [ +  - ]:       1002 :     string result;
     998 [ +  + ][ +  + ]:        501 :     if (matchfound || (flags & SNIPPET_EMPTY_WITHOUT_MATCH) == 0) {
     999 [ +  - ][ +  + ]:       2044 :         while (snip.drain(text, hi_start, hi_end, omit, result)) { }
    1000                 :            :     }
    1001                 :            : 
    1002                 :       1002 :     return result;
    1003                 :            : }
    1004                 :            : 
    1005                 :            : }

Generated by: LCOV version 1.11