LCOV - code coverage report
Current view: top level - queryparser - termgenerator_internal.cc (source / functions) Hit Total Coverage
Test: Test Coverage for xapian-core 7028d852e609 Lines: 382 398 96.0 %
Date: 2019-02-17 14:59:59 Functions: 24 24 100.0 %
Branches: 461 676 68.2 %

           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 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                 :       3236 : U_isupper(unsigned ch)
      52                 :            : {
      53 [ +  + ][ +  + ]:       3236 :     return (ch < 128 && C_isupper(static_cast<unsigned char>(ch)));
      54                 :            : }
      55                 :            : 
      56                 :            : static inline unsigned
      57                 :      21284 : check_wordchar(unsigned ch)
      58                 :            : {
      59         [ +  + ]:      21284 :     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                 :            : /** Templated framework for processing terms.
     135                 :            :  *
     136                 :            :  *  Calls action(term, positional) for each term to add, where term is a
     137                 :            :  *  std::string holding the term, and positional is a bool indicating
     138                 :            :  *  if this term carries positional information.
     139                 :            :  */
     140                 :            : template<typename ACTION>
     141                 :            : static void
     142                 :        600 : parse_terms(Utf8Iterator itor, bool cjk_ngram, bool with_positions, ACTION action)
     143                 :            : {
     144                 :       6118 :     while (true) {
     145                 :            :         // Advance to the start of the next term.
     146                 :            :         unsigned ch;
     147                 :            :         while (true) {
     148 [ +  + ][ +  + ]:       6724 :             if (itor == Utf8Iterator()) return;
     149                 :       6125 :             ch = check_wordchar(*itor);
     150 [ +  + ][ +  + ]:       6125 :             if (ch) break;
     151                 :       3104 :             ++itor;
     152                 :            :         }
     153                 :            : 
     154 [ +  - ][ +  - ]:       3021 :         string term;
     155                 :            :         // Look for initials separated by '.' (e.g. P.T.O., U.N.C.L.E).
     156                 :            :         // Don't worry if there's a trailing '.' or not.
     157 [ +  + ][ +  + ]:       3021 :         if (U_isupper(*itor)) {
     158                 :        520 :             const Utf8Iterator end;
     159                 :        520 :             Utf8Iterator p = itor;
     160   [ +  -  +  +  :       1846 :             do {
             +  -  +  + ]
           [ +  +  +  +  
          +  +  +  +  +  
              + ][ +  + ]
     161 [ +  - ][ +  - ]:        706 :                 Unicode::append_utf8(term, Unicode::tolower(*p++));
     162                 :       1846 :             } while (p != end && *p == '.' && ++p != end && U_isupper(*p));
     163                 :            :             // One letter does not make an acronym!  If we handled a single
     164                 :            :             // uppercase letter here, we wouldn't catch M&S below.
     165 [ +  + ][ +  + ]:        520 :             if (term.size() > 1) {
     166                 :            :                 // Check there's not a (lower case) letter or digit
     167                 :            :                 // immediately after it.
     168 [ +  - ][ +  - ]:         40 :                 if (p == end || !Unicode::is_wordchar(*p)) {
         [ +  - ][ +  + ]
         [ +  - ][ +  - ]
     169                 :         40 :                     itor = p;
     170                 :         40 :                     goto endofterm;
     171                 :            :                 }
     172                 :            :             }
     173 [ +  - ][ +  - ]:        480 :             term.resize(0);
     174                 :            :         }
     175                 :            : 
     176                 :            :         while (true) {
     177         [ -  + ]:       3120 :             if (cjk_ngram &&
           [ #  #  #  # ]
         [ -  + ][ +  + ]
           [ +  +  +  - ]
                 [ +  + ]
     178 [ #  # ][ +  - ]:         21 :                 CJK::codepoint_is_cjk(*itor) &&
     179                 :         13 :                 Unicode::is_wordchar(*itor)) {
     180 [ #  # ][ +  - ]:         13 :                 const string & cjk = CJK::get_cjk(itor);
     181 [ #  # ][ #  # ]:         64 :                 for (CJKTokenIterator tk(cjk); tk != CJKTokenIterator(); ++tk) {
         [ #  # ][ #  # ]
         [ #  # ][ +  - ]
         [ +  - ][ +  - ]
         [ +  + ][ +  - ]
     182 [ #  # ][ +  - ]:         51 :                     const string & cjk_token = *tk;
     183 [ #  # ][ #  # ]:         51 :                     if (!action(cjk_token, with_positions && tk.get_length() == 1, itor))
         [ #  # ][ #  # ]
         [ +  - ][ +  + ]
         [ +  - ][ -  + ]
     184                 :          0 :                         return;
     185                 :            :                 }
     186                 :            :                 while (true) {
     187 [ #  # ][ +  + ]:         16 :                     if (itor == Utf8Iterator()) return;
     188                 :          9 :                     ch = check_wordchar(*itor);
     189 [ #  # ][ +  + ]:          9 :                     if (ch) break;
     190                 :          3 :                     ++itor;
     191                 :            :                 }
     192 [ #  # ][ +  + ]:         13 :                 continue;
     193                 :            :             }
     194                 :            :             unsigned prevch;
     195   [ +  +  +  + ]:      12639 :             do {
     196 [ +  - ][ +  - ]:      13097 :                 Unicode::append_utf8(term, ch);
     197                 :      13097 :                 prevch = ch;
     198 [ +  + ][ -  + ]:      13125 :                 if (++itor == Utf8Iterator() ||
         [ #  # ][ +  - ]
           [ +  +  #  # ]
         [ +  + ][ +  + ]
         [ +  + ][ +  - ]
           [ +  +  #  # ]
     199 [ #  # ][ +  - ]:         28 :                     (cjk_ngram && CJK::codepoint_is_cjk(*itor)))
     200                 :        458 :                     goto endofterm;
     201                 :      12639 :                 ch = check_wordchar(*itor);
     202                 :            :             } while (ch);
     203                 :            : 
     204                 :       2615 :             Utf8Iterator next(itor);
     205                 :       2615 :             ++next;
     206 [ +  + ][ +  + ]:       2615 :             if (next == Utf8Iterator()) break;
     207                 :       2511 :             unsigned nextch = check_wordchar(*next);
     208 [ +  + ][ +  + ]:       2511 :             if (!nextch) break;
     209                 :       2241 :             unsigned infix_ch = *itor;
     210 [ +  + ][ +  + ]:       2241 :             if (is_digit(prevch) && is_digit(*next)) {
           [ +  +  +  + ]
         [ +  + ][ +  + ]
     211                 :         24 :                 infix_ch = check_infix_digit(infix_ch);
     212                 :            :             } else {
     213                 :            :                 // Handle things like '&' in AT&T, apostrophes, etc.
     214                 :       2217 :                 infix_ch = check_infix(infix_ch);
     215                 :            :             }
     216 [ +  + ][ +  + ]:       2241 :             if (!infix_ch) break;
     217 [ +  - ][ +  + ]:         99 :             if (infix_ch != UNICODE_IGNORE)
     218 [ +  - ][ +  - ]:         93 :                 Unicode::append_utf8(term, infix_ch);
     219                 :         99 :             ch = nextch;
     220                 :        112 :             itor = next;
     221                 :            :         }
     222                 :            : 
     223                 :            :         {
     224                 :       2516 :             size_t len = term.size();
     225                 :       2516 :             unsigned count = 0;
     226 [ -  + ][ +  + ]:       2625 :             while ((ch = check_suffix(*itor))) {
     227 [ #  # ][ -  + ]:          4 :                 if (++count > 3) {
     228 [ #  # ][ #  # ]:          0 :                     term.resize(len);
     229                 :          0 :                     break;
     230                 :            :                 }
     231 [ #  # ][ +  - ]:          4 :                 Unicode::append_utf8(term, ch);
     232 [ #  # ][ -  + ]:          4 :                 if (++itor == Utf8Iterator()) goto endofterm;
     233                 :            :             }
     234                 :            :             // Don't index fish+chips as fish+ chips.
     235 [ -  + ][ +  + ]:       2516 :             if (Unicode::is_wordchar(*itor))
     236 [ #  # ][ +  - ]:          1 :                 term.resize(len);
     237                 :            :         }
     238                 :            : 
     239                 :            : endofterm:
     240 [ +  - ][ -  + ]:       3014 :         if (!action(term, with_positions, itor))
         [ +  + ][ -  + ]
     241 [ +  - ][ +  + ]:       3020 :             return;
     242                 :       3013 :     }
     243                 :            : }
     244                 :            : 
     245                 :            : void
     246                 :        105 : TermGenerator::Internal::index_text(Utf8Iterator itor, termcount wdf_inc,
     247                 :            :                                     const string & prefix, bool with_positions)
     248                 :            : {
     249 [ +  + ][ -  + ]:        105 :     bool cjk_ngram = (flags & FLAG_CJK_NGRAM) || CJK::is_cjk_enabled();
     250                 :            : 
     251                 :            :     stop_strategy current_stop_mode;
     252         [ +  + ]:        105 :     if (!stopper.get()) {
     253                 :         95 :         current_stop_mode = TermGenerator::STOP_NONE;
     254                 :            :     } else {
     255                 :         10 :         current_stop_mode = stop_mode;
     256                 :            :     }
     257                 :            : 
     258                 :            :     parse_terms(itor, cjk_ngram, with_positions,
     259                 :        905 :         [=](const string & term, bool positional, const Utf8Iterator &) {
     260         [ +  + ]:        695 :             if (term.size() > max_word_length) return true;
     261                 :            : 
     262 [ +  + ][ +  - ]:        693 :             if (current_stop_mode == TermGenerator::STOP_ALL && (*stopper)(term))
         [ +  + ][ +  + ]
     263                 :          3 :                 return true;
     264                 :            : 
     265 [ +  + ][ +  + ]:        690 :             if (strategy == TermGenerator::STEM_SOME ||
     266         [ +  + ]:         23 :                 strategy == TermGenerator::STEM_NONE ||
     267                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     268         [ +  + ]:        675 :                 if (positional) {
     269 [ +  - ][ +  - ]:        656 :                     doc.add_posting(prefix + term, ++cur_pos, wdf_inc);
     270                 :            :                 } else {
     271 [ +  - ][ +  - ]:         19 :                     doc.add_term(prefix + term, wdf_inc);
     272                 :            :                 }
     273                 :            :             }
     274                 :            : 
     275                 :            :             // MSVC seems to need "this->" on member variables in this
     276                 :            :             // situation.
     277 [ +  + ][ +  + ]:        690 :             if ((this->flags & FLAG_SPELLING) && prefix.empty())
                 [ +  + ]
     278         [ +  + ]:          7 :                 db.add_spelling(term);
     279                 :            : 
     280   [ +  +  +  + ]:       1371 :             if (strategy == TermGenerator::STEM_NONE ||
                 [ +  + ]
     281                 :        778 :                 !stemmer.internal.get()) return true;
     282                 :            : 
     283 [ +  + ][ +  + ]:        593 :             if (strategy == TermGenerator::STEM_SOME ||
     284                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     285 [ +  + ][ +  + ]:        584 :                 if (current_stop_mode == TermGenerator::STOP_STEMMED &&
                 [ +  + ]
     286         [ +  - ]:          6 :                     (*stopper)(term))
     287                 :          2 :                     return true;
     288                 :            : 
     289                 :            :                 // Note, this uses the lowercased term, but that's OK as we
     290                 :            :                 // only want to avoid stemming terms starting with a digit.
     291         [ +  + ]:        576 :                 if (!should_stem(term)) return true;
     292                 :            :             }
     293                 :            : 
     294                 :            :             // Add stemmed form without positional information.
     295         [ +  - ]:        583 :             const string& stem = stemmer(term);
     296         [ +  + ]:        583 :             if (rare(stem.empty())) return true;
     297         [ +  - ]:       1164 :             string stemmed_term;
     298         [ +  + ]:        582 :             if (strategy != TermGenerator::STEM_ALL) {
     299         [ +  - ]:        571 :                 stemmed_term += "Z";
     300                 :            :             }
     301         [ +  - ]:        582 :             stemmed_term += prefix;
     302         [ +  - ]:        582 :             stemmed_term += stem;
     303 [ +  + ][ +  - ]:        582 :             if (strategy != TermGenerator::STEM_SOME && with_positions) {
     304         [ +  + ]:         21 :                 if (strategy != TermGenerator::STEM_SOME_FULL_POS) ++cur_pos;
     305         [ +  - ]:         21 :                 doc.add_posting(stemmed_term, cur_pos, wdf_inc);
     306                 :            :             } else {
     307         [ +  - ]:        561 :                 doc.add_term(stemmed_term, wdf_inc);
     308                 :            :             }
     309                 :        582 :             return true;
     310         [ +  + ]:        799 :         });
     311                 :        104 : }
     312                 :            : 
     313                 :            : struct Sniplet {
     314                 :            :     double* relevance;
     315                 :            : 
     316                 :            :     size_t term_end;
     317                 :            : 
     318                 :            :     size_t highlight;
     319                 :            : 
     320                 :       2370 :     Sniplet(double* r, size_t t, size_t h)
     321                 :       2370 :         : relevance(r), term_end(t), highlight(h) { }
     322                 :            : };
     323                 :            : 
     324                 :        990 : class SnipPipe {
     325                 :            :     deque<Sniplet> pipe;
     326                 :            :     deque<Sniplet> best_pipe;
     327                 :            : 
     328                 :            :     // Requested length for snippet.
     329                 :            :     size_t length;
     330                 :            : 
     331                 :            :     // Position in text of start of current pipe contents.
     332                 :            :     size_t begin = 0;
     333                 :            : 
     334                 :            :     // Rolling sum of the current pipe contents.
     335                 :            :     double sum = 0;
     336                 :            : 
     337                 :            :     size_t phrase_len = 0;
     338                 :            : 
     339                 :            :   public:
     340                 :            :     size_t best_begin = 0;
     341                 :            : 
     342                 :            :     size_t best_end = 0;
     343                 :            : 
     344                 :            :     double best_sum = 0;
     345                 :            : 
     346                 :            :     // Add one to length to allow for inter-word space.
     347                 :            :     // FIXME: We ought to correctly allow for multiple spaces.
     348         [ +  - ]:        495 :     explicit SnipPipe(size_t length_) : length(length_ + 1) { }
     349                 :            : 
     350                 :            :     bool pump(double* r, size_t t, size_t h, unsigned flags);
     351                 :            : 
     352                 :            :     void done();
     353                 :            : 
     354                 :            :     bool drain(const string & input,
     355                 :            :                const string & hi_start,
     356                 :            :                const string & hi_end,
     357                 :            :                const string & omit,
     358                 :            :                string & output);
     359                 :            : };
     360                 :            : 
     361                 :            : #define DECAY 2.0
     362                 :            : 
     363                 :            : inline bool
     364                 :       2370 : SnipPipe::pump(double* r, size_t t, size_t h, unsigned flags)
     365                 :            : {
     366         [ +  + ]:       2370 :     if (h > 1) {
     367         [ +  - ]:         51 :         if (pipe.size() >= h - 1) {
     368                 :            :             // The final term of a phrase is entering the window.  Peg the
     369                 :            :             // phrase's relevance onto the first term of the phrase, so it'll
     370                 :            :             // be removed from `sum` when the phrase starts to leave the
     371                 :            :             // window.
     372                 :         51 :             auto & phrase_start = pipe[pipe.size() - (h - 1)];
     373         [ +  - ]:         51 :             if (phrase_start.relevance) {
     374                 :         51 :                 *phrase_start.relevance *= DECAY;
     375                 :         51 :                 sum -= *phrase_start.relevance;
     376                 :            :             }
     377                 :         51 :             sum += *r;
     378                 :         51 :             phrase_start.relevance = r;
     379                 :         51 :             phrase_start.highlight = h;
     380                 :         51 :             *r /= DECAY;
     381                 :            :         }
     382                 :         51 :         r = NULL;
     383                 :         51 :         h = 0;
     384                 :            :     }
     385                 :       2370 :     pipe.emplace_back(r, t, h);
     386         [ +  + ]:       2370 :     if (r) {
     387                 :       2137 :         sum += *r;
     388                 :       2137 :         *r /= DECAY;
     389                 :            :     }
     390                 :            : 
     391                 :            :     // If necessary, discard words from the start of the pipe until it has the
     392                 :            :     // desired length.
     393                 :            :     // FIXME: Also shrink the window past words with relevance < 0?
     394         [ +  + ]:       3022 :     while (t - begin > length /* || pipe.front().relevance < 0.0 */) {
     395                 :        855 :         const Sniplet& word = pipe.front();
     396         [ +  + ]:        855 :         if (word.relevance) {
     397                 :        810 :             *word.relevance *= DECAY;
     398                 :        810 :             sum -= *word.relevance;
     399                 :            :         }
     400                 :        855 :         begin = word.term_end;
     401         [ +  + ]:        855 :         if (best_end >= begin)
     402                 :        553 :             best_pipe.push_back(word);
     403                 :        855 :         pipe.pop_front();
     404                 :            :         // E.g. can happen if the current term is longer than the requested
     405                 :            :         // length!
     406         [ +  + ]:        855 :         if (rare(pipe.empty())) break;
     407                 :            :     }
     408                 :            : 
     409                 :            :     // Using > here doesn't work well, as we don't extend a snippet over terms
     410                 :            :     // with 0 weight.
     411         [ +  + ]:       2370 :     if (sum >= best_sum) {
     412                 :            :         // Discard any part of `best_pipe` which is before `begin`.
     413         [ +  + ]:       1937 :         if (begin >= best_end) {
     414                 :        657 :             best_pipe.clear();
     415                 :            :         } else {
     416   [ +  +  +  - ]:       1852 :             while (!best_pipe.empty() &&
                 [ +  + ]
     417                 :        286 :                    best_pipe.front().term_end <= begin) {
     418                 :        286 :                 best_pipe.pop_front();
     419                 :            :             }
     420                 :            :         }
     421                 :       1937 :         best_sum = sum;
     422                 :       1937 :         best_begin = begin;
     423                 :       1937 :         best_end = t;
     424         [ -  + ]:        433 :     } else if ((flags & Xapian::MSet::SNIPPET_EXHAUSTIVE) == 0) {
     425 [ #  # ][ #  # ]:          0 :         if (best_sum > 0 && best_end < begin) {
     426                 :            :             // We found something, and we aren't still looking near it.
     427                 :            :             // FIXME: Benchmark this and adjust if necessary.
     428                 :          0 :             return false;
     429                 :            :         }
     430                 :            :     }
     431                 :       2370 :     return true;
     432                 :            : }
     433                 :            : 
     434                 :            : inline void
     435                 :        495 : SnipPipe::done()
     436                 :            : {
     437                 :            :     // Discard any part of `pipe` which is after `best_end`.
     438         [ +  + ]:        495 :     if (begin >= best_end) {
     439                 :        100 :         pipe.clear();
     440                 :            :     } else {
     441                 :            :         // We should never empty the pipe (as that case should be handled
     442                 :            :         // above).
     443   [ +  -  +  + ]:       1010 :         while (rare(!pipe.empty()) &&
                 [ +  + ]
     444                 :        505 :                pipe.back().term_end > best_end) {
     445                 :        110 :             pipe.pop_back();
     446                 :            :         }
     447                 :            :     }
     448                 :        495 : }
     449                 :            : 
     450                 :            : // Check if a non-word character is should be included at the start of the
     451                 :            : // snippet.  We want to include certain leading non-word characters, but not
     452                 :            : // others.
     453                 :            : static inline bool
     454                 :        414 : snippet_check_leading_nonwordchar(unsigned ch) {
     455 [ +  + ][ +  + ]:       1235 :     if (Unicode::is_currency(ch) ||
     456 [ +  + ][ +  + ]:        821 :         Unicode::get_category(ch) == Unicode::OPEN_PUNCTUATION ||
     457                 :        323 :         Unicode::get_category(ch) == Unicode::INITIAL_QUOTE_PUNCTUATION) {
     458                 :         98 :         return true;
     459                 :            :     }
     460         [ +  + ]:        316 :     switch (ch) {
     461                 :            :         case '"':
     462                 :            :         case '#':
     463                 :            :         case '%':
     464                 :            :         case '&':
     465                 :            :         case '\'':
     466                 :            :         case '+':
     467                 :            :         case '-':
     468                 :            :         case '/':
     469                 :            :         case '<':
     470                 :            :         case '@':
     471                 :            :         case '\\':
     472                 :            :         case '`':
     473                 :            :         case '~':
     474                 :            :         case 0x00A1: // INVERTED EXCLAMATION MARK
     475                 :            :         case 0x00A7: // SECTION SIGN
     476                 :            :         case 0x00BF: // INVERTED QUESTION MARK
     477                 :        125 :             return true;
     478                 :            :     }
     479                 :        191 :     return false;
     480                 :            : }
     481                 :            : 
     482                 :            : static inline void
     483                 :       2068 : append_escaping_xml(const char* p, const char* end, string& output)
     484                 :            : {
     485         [ +  + ]:      10041 :     while (p != end) {
     486                 :       7973 :         char ch = *p++;
     487   [ +  +  +  + ]:       7973 :         switch (ch) {
     488                 :            :             case '&':
     489                 :          7 :                 output += "&amp;";
     490                 :          7 :                 break;
     491                 :            :             case '<':
     492                 :          7 :                 output += "&lt;";
     493                 :          7 :                 break;
     494                 :            :             case '>':
     495                 :          7 :                 output += "&gt;";
     496                 :          7 :                 break;
     497                 :            :             default:
     498                 :       7952 :                 output += ch;
     499                 :            :         }
     500                 :            :     }
     501                 :       2068 : }
     502                 :            : 
     503                 :            : inline bool
     504                 :       1981 : SnipPipe::drain(const string & input,
     505                 :            :                 const string & hi_start,
     506                 :            :                 const string & hi_end,
     507                 :            :                 const string & omit,
     508                 :            :                 string & output)
     509                 :            : {
     510 [ +  + ][ +  + ]:       1981 :     if (best_pipe.empty() && !pipe.empty()) {
                 [ +  + ]
     511                 :        388 :         swap(best_pipe, pipe);
     512                 :            :     }
     513                 :            : 
     514         [ +  + ]:       1981 :     if (best_pipe.empty()) {
     515                 :        488 :         size_t tail_len = input.size() - best_end;
     516         [ +  + ]:        488 :         if (tail_len == 0) return false;
     517                 :            : 
     518                 :            :         // See if this is the end of a sentence.
     519                 :            :         // FIXME: This is quite simplistic - look at the Unicode rules:
     520                 :            :         // https://unicode.org/reports/tr29/#Sentence_Boundaries
     521                 :        159 :         bool punc = false;
     522                 :        159 :         Utf8Iterator i(input.data() + best_end, tail_len);
     523         [ +  + ]:        332 :         while (i != Utf8Iterator()) {
     524                 :        278 :             unsigned ch = *i;
     525 [ +  + ][ +  - ]:        278 :             if (punc && Unicode::is_whitespace(ch)) break;
                 [ +  + ]
     526                 :            : 
     527                 :            :             // Allow "...", "!!", "!?!", etc...
     528 [ +  + ][ +  + ]:        272 :             punc = (ch == '.' || ch == '?' || ch == '!');
                 [ +  + ]
     529                 :            : 
     530         [ +  + ]:        272 :             if (Unicode::is_wordchar(ch)) break;
     531                 :        173 :             ++i;
     532                 :            :         }
     533                 :            : 
     534         [ +  + ]:        159 :         if (punc) {
     535                 :            :             // Include end of sentence punctuation.
     536         [ +  - ]:         46 :             append_escaping_xml(input.data() + best_end, i.raw(), output);
     537                 :            :         } else {
     538                 :            :             // Append "..." or equivalent if this doesn't seem to be the start
     539                 :            :             // of a sentence.
     540         [ +  - ]:        113 :             output += omit;
     541                 :            :         }
     542                 :            : 
     543                 :        488 :         return false;
     544                 :            :     }
     545                 :            : 
     546                 :       1493 :     const Sniplet & word = best_pipe.front();
     547                 :            : 
     548         [ +  + ]:       1493 :     if (output.empty()) {
     549                 :            :         // Start of the snippet.
     550         [ +  + ]:        432 :         enum { NO, PUNC, YES } sentence_boundary = (best_begin == 0) ? YES : NO;
     551                 :            : 
     552                 :        432 :         Utf8Iterator i(input.data() + best_begin, word.term_end - best_begin);
     553         [ +  - ]:        846 :         while (i != Utf8Iterator()) {
     554                 :        846 :             unsigned ch = *i;
     555   [ +  -  +  - ]:        846 :             switch (sentence_boundary) {
     556                 :            :                 case NO:
     557 [ +  - ][ +  - ]:        331 :                     if (ch == '.' || ch == '?' || ch == '!') {
                 [ -  + ]
     558                 :          0 :                         sentence_boundary = PUNC;
     559                 :            :                     }
     560                 :        331 :                     break;
     561                 :            :                 case PUNC:
     562         [ #  # ]:          0 :                     if (Unicode::is_whitespace(ch)) {
     563                 :          0 :                         sentence_boundary = YES;
     564 [ #  # ][ #  # ]:          0 :                     } else if (ch == '.' || ch == '?' || ch == '!') {
                 [ #  # ]
     565                 :            :                         // Allow "...", "!!", "!?!", etc...
     566                 :            :                     } else {
     567                 :          0 :                         sentence_boundary = NO;
     568                 :            :                     }
     569                 :          0 :                     break;
     570                 :            :                 case YES:
     571                 :        515 :                     break;
     572                 :            :             }
     573                 :            : 
     574                 :            :             // Start the snippet at the start of the first word, but include
     575                 :            :             // certain punctuation too.
     576         [ +  + ]:        846 :             if (Unicode::is_wordchar(ch)) {
     577                 :            :                 // But limit how much leading punctuation we include.
     578                 :        432 :                 size_t word_begin = i.raw() - input.data();
     579         [ +  + ]:        432 :                 if (word_begin - best_begin > 4) {
     580                 :          7 :                     best_begin = word_begin;
     581                 :            :                 }
     582                 :        432 :                 break;
     583                 :            :             }
     584                 :        414 :             ++i;
     585         [ +  + ]:        414 :             if (!snippet_check_leading_nonwordchar(ch)) {
     586                 :        191 :                 best_begin = i.raw() - input.data();
     587                 :            :             }
     588                 :            :         }
     589                 :            : 
     590                 :            :         // Add "..." or equivalent if this doesn't seem to be the start of a
     591                 :            :         // sentence.
     592         [ +  + ]:        432 :         if (sentence_boundary != YES) {
     593         [ +  - ]:        432 :             output += omit;
     594                 :            :         }
     595                 :            :     }
     596                 :            : 
     597         [ +  + ]:       1493 :     if (word.highlight) {
     598                 :            :         // Don't include inter-word characters in the highlight.
     599                 :        529 :         Utf8Iterator i(input.data() + best_begin, input.size() - best_begin);
     600         [ +  - ]:       1478 :         while (i != Utf8Iterator()) {
     601                 :        949 :             unsigned ch = *i;
     602         [ +  + ]:        949 :             if (Unicode::is_wordchar(ch)) {
     603         [ +  - ]:        529 :                 append_escaping_xml(input.data() + best_begin, i.raw(), output);
     604                 :        529 :                 best_begin = i.raw() - input.data();
     605                 :        529 :                 break;
     606                 :            :             }
     607                 :        420 :             ++i;
     608                 :            :         }
     609                 :            :     }
     610                 :            : 
     611         [ +  + ]:       1493 :     if (!phrase_len) {
     612                 :       1433 :         phrase_len = word.highlight;
     613         [ +  + ]:       1433 :         if (phrase_len) output += hi_start;
     614                 :            :     }
     615                 :            : 
     616                 :       1493 :     const char* p = input.data();
     617                 :       1493 :     append_escaping_xml(p + best_begin, p + word.term_end, output);
     618                 :       1493 :     best_begin = word.term_end;
     619                 :            : 
     620 [ +  + ][ +  + ]:       1493 :     if (phrase_len && --phrase_len == 0) output += hi_end;
                 [ +  + ]
     621                 :            : 
     622                 :       1493 :     best_pipe.pop_front();
     623                 :       1981 :     return true;
     624                 :            : }
     625                 :            : 
     626                 :            : static void
     627                 :       1073 : check_query(const Xapian::Query & query,
     628                 :            :             list<vector<string>> & exact_phrases,
     629                 :            :             unordered_map<string, double> & loose_terms,
     630                 :            :             list<const Xapian::Internal::QueryWildcard*> & wildcards,
     631                 :            :             size_t & longest_phrase)
     632                 :            : {
     633                 :            :     // FIXME: OP_NEAR, non-tight OP_PHRASE, OP_PHRASE with non-term subqueries
     634                 :       1073 :     size_t n_subqs = query.get_num_subqueries();
     635                 :       1073 :     Xapian::Query::op op = query.get_type();
     636         [ +  + ]:       1073 :     if (op == query.LEAF_TERM) {
     637                 :            :         const Xapian::Internal::QueryTerm & qt =
     638                 :        718 :             *static_cast<const Xapian::Internal::QueryTerm *>(query.internal.get());
     639 [ +  - ][ +  - ]:        718 :         loose_terms.insert(make_pair(qt.get_term(), 0));
     640         [ +  + ]:        355 :     } else if (op == query.OP_WILDCARD) {
     641                 :            :         using Xapian::Internal::QueryWildcard;
     642                 :            :         const QueryWildcard* qw =
     643                 :         15 :             static_cast<const QueryWildcard*>(query.internal.get());
     644         [ +  - ]:         15 :         wildcards.push_back(qw);
     645         [ +  + ]:        340 :     } else if (op == query.OP_PHRASE) {
     646                 :            :         const Xapian::Internal::QueryPhrase & phrase =
     647                 :         51 :             *static_cast<const Xapian::Internal::QueryPhrase *>(query.internal.get());
     648         [ +  - ]:         51 :         if (phrase.get_window() == n_subqs) {
     649                 :            :             // Tight phrase.
     650         [ +  + ]:        162 :             for (size_t i = 0; i != n_subqs; ++i) {
     651         [ -  + ]:        111 :                 if (query.get_subquery(i).get_type() != query.LEAF_TERM)
     652                 :          0 :                     goto non_term_subquery;
     653                 :            :             }
     654                 :            : 
     655                 :            :             // Tight phrase of terms.
     656         [ +  - ]:         51 :             exact_phrases.push_back(vector<string>());
     657                 :         51 :             vector<string> & terms = exact_phrases.back();
     658                 :         51 :             terms.reserve(n_subqs);
     659         [ +  + ]:        162 :             for (size_t i = 0; i != n_subqs; ++i) {
     660         [ +  - ]:        111 :                 Xapian::Query q = query.get_subquery(i);
     661                 :            :                 const Xapian::Internal::QueryTerm & qt =
     662                 :        111 :                     *static_cast<const Xapian::Internal::QueryTerm *>(q.internal.get());
     663         [ +  - ]:        111 :                 terms.push_back(qt.get_term());
     664                 :        111 :             }
     665         [ +  - ]:         51 :             if (n_subqs > longest_phrase) longest_phrase = n_subqs;
     666                 :       1073 :             return;
     667                 :            :         }
     668                 :            :     }
     669                 :            : non_term_subquery:
     670         [ +  + ]:       1600 :     for (size_t i = 0; i != n_subqs; ++i)
     671                 :            :         check_query(query.get_subquery(i), exact_phrases, loose_terms,
     672         [ +  - ]:        578 :                     wildcards, longest_phrase);
     673                 :            : }
     674                 :            : 
     675                 :            : static double*
     676                 :       4099 : check_term(unordered_map<string, double> & loose_terms,
     677                 :            :            const Xapian::Weight::Internal * stats,
     678                 :            :            const string & term,
     679                 :            :            double max_tw)
     680                 :            : {
     681         [ +  - ]:       4099 :     auto it = loose_terms.find(term);
     682         [ +  + ]:       4099 :     if (it == loose_terms.end()) return NULL;
     683                 :            : 
     684         [ +  + ]:        621 :     if (it->second == 0.0) {
     685                 :            :         double relevance;
     686 [ +  - ][ -  + ]:        516 :         if (!stats->get_termweight(term, relevance)) {
     687                 :            :             // FIXME: Assert?
     688         [ #  # ]:          0 :             loose_terms.erase(it);
     689                 :          0 :             return NULL;
     690                 :            :         }
     691                 :            : 
     692                 :        516 :         it->second = relevance + max_tw;
     693                 :            :     }
     694                 :       4099 :     return &it->second;
     695                 :            : }
     696                 :            : 
     697                 :            : string
     698                 :        495 : MSet::Internal::snippet(const string & text,
     699                 :            :                         size_t length,
     700                 :            :                         const Xapian::Stem & stemmer,
     701                 :            :                         unsigned flags,
     702                 :            :                         const string & hi_start,
     703                 :            :                         const string & hi_end,
     704                 :            :                         const string & omit) const
     705                 :            : {
     706 [ -  + ][ #  # ]:        495 :     if (hi_start.empty() && hi_end.empty() && text.size() <= length) {
         [ #  # ][ -  + ]
     707                 :            :         // Too easy!
     708         [ #  # ]:          0 :         return text;
     709                 :            :     }
     710                 :            : 
     711         [ +  - ]:        495 :     bool cjk_ngram = CJK::is_cjk_enabled();
     712                 :            : 
     713                 :        495 :     size_t term_start = 0;
     714                 :        495 :     double min_tw = 0, max_tw = 0;
     715         [ +  - ]:        495 :     if (stats) stats->get_max_termweight(min_tw, max_tw);
     716         [ +  + ]:        495 :     if (max_tw == 0.0) {
     717                 :        236 :         max_tw = 1.0;
     718                 :            :     } else {
     719                 :            :         // Scale up by (1 + 1/64) so that highlighting works better for terms
     720                 :            :         // with termweight 0 (which happens for terms not in the database, and
     721                 :            :         // also with some weighting schemes for terms which occur in almost all
     722                 :            :         // documents.
     723                 :        259 :         max_tw *= 1.015625;
     724                 :            :     }
     725                 :            : 
     726         [ +  - ]:        495 :     SnipPipe snip(length);
     727                 :            : 
     728                 :        990 :     list<vector<string>> exact_phrases;
     729         [ +  - ]:        990 :     unordered_map<string, double> loose_terms;
     730                 :        990 :     list<const Xapian::Internal::QueryWildcard*> wildcards;
     731                 :        495 :     size_t longest_phrase = 0;
     732                 :        495 :     check_query(enquire->query, exact_phrases, loose_terms,
     733         [ +  - ]:        495 :                 wildcards, longest_phrase);
     734                 :            : 
     735                 :        990 :     vector<double> exact_phrases_relevance;
     736         [ +  - ]:        495 :     exact_phrases_relevance.reserve(exact_phrases.size());
     737         [ +  + ]:        546 :     for (auto&& terms : exact_phrases) {
     738                 :            :         // FIXME: What relevance to use?
     739         [ +  - ]:         51 :         exact_phrases_relevance.push_back(max_tw * terms.size());
     740                 :            :     }
     741                 :            : 
     742                 :        990 :     vector<double> wildcards_relevance;
     743         [ +  - ]:        495 :     wildcards_relevance.reserve(exact_phrases.size());
     744         [ +  + ]:        510 :     for (auto&& pattern : wildcards) {
     745                 :            :         // FIXME: What relevance to use?
     746                 :            :         (void)pattern;
     747         [ +  - ]:         15 :         wildcards_relevance.push_back(max_tw + min_tw);
     748                 :            :     }
     749                 :            : 
     750                 :            :     // Background relevance is the same for a given MSet, so cache it
     751                 :            :     // between calls to MSet::snippet() on the same object.
     752                 :        495 :     unordered_map<string, double>& background = snippet_bg_relevance;
     753                 :            : 
     754                 :        990 :     vector<string> phrase;
     755 [ +  + ][ +  - ]:        495 :     if (longest_phrase) phrase.resize(longest_phrase - 1);
     756                 :        495 :     size_t phrase_next = 0;
     757                 :        495 :     bool matchfound = false;
     758                 :            :     parse_terms(Utf8Iterator(text), cjk_ngram, true,
     759                 :       2370 :         [&](const string & term, bool positional, const Utf8Iterator & it) {
     760                 :            :             // FIXME: Don't hardcode this here.
     761                 :       2370 :             const size_t max_word_length = 64;
     762                 :            : 
     763         [ -  + ]:       2370 :             if (!positional) return true;
     764         [ -  + ]:       2370 :             if (term.size() > max_word_length) return true;
     765                 :            : 
     766                 :            :             // We get segments with any "inter-word" characters in front of
     767                 :            :             // each word, e.g.:
     768                 :            :             // [The][ cat][ sat][ on][ the][ mat]
     769                 :       2370 :             size_t term_end = text.size() - it.left();
     770                 :            : 
     771                 :       2370 :             double* relevance = 0;
     772                 :       2370 :             size_t highlight = 0;
     773         [ +  - ]:       2370 :             if (stats) {
     774                 :       2370 :                 size_t i = 0;
     775         [ +  + ]:       2647 :                 for (auto&& terms : exact_phrases) {
     776         [ +  + ]:        328 :                     if (term == terms.back()) {
     777                 :         82 :                         size_t n = terms.size() - 1;
     778                 :         82 :                         bool match = true;
     779         [ +  + ]:        142 :                         while (n--) {
     780         [ +  + ]:         91 :                             if (terms[n] != phrase[(n + phrase_next) % (longest_phrase - 1)]) {
     781                 :         31 :                                 match = false;
     782                 :         31 :                                 break;
     783                 :            :                             }
     784                 :            :                         }
     785         [ +  + ]:         82 :                         if (match) {
     786                 :            :                             // FIXME: Sort phrases, highest score first!
     787                 :         51 :                             relevance = &exact_phrases_relevance[i];
     788                 :         82 :                             highlight = terms.size();
     789                 :         51 :                             goto relevance_done;
     790                 :            :                         }
     791                 :            :                     }
     792                 :        277 :                     ++i;
     793                 :            :                 }
     794                 :            : 
     795         [ +  - ]:       2319 :                 relevance = check_term(loose_terms, stats.get(), term, max_tw);
     796         [ +  + ]:       2319 :                 if (relevance) {
     797                 :            :                     // Matched unstemmed term.
     798                 :        539 :                     highlight = 1;
     799                 :        539 :                     goto relevance_done;
     800                 :            :                 }
     801                 :            : 
     802         [ +  - ]:       1780 :                 string stem = "Z";
     803 [ +  - ][ +  - ]:       1780 :                 stem += stemmer(term);
     804         [ +  - ]:       1780 :                 relevance = check_term(loose_terms, stats.get(), stem, max_tw);
     805         [ +  + ]:       1780 :                 if (relevance) {
     806                 :            :                     // Matched stemmed term.
     807                 :         82 :                     highlight = 1;
     808                 :         82 :                     goto relevance_done;
     809                 :            :                 }
     810                 :            : 
     811                 :            :                 // Check wildcards.
     812                 :            :                 // FIXME: Sort wildcards, cheapest to check first or something?
     813                 :       1698 :                 i = 0;
     814         [ +  + ]:       1875 :                 for (auto&& qw : wildcards) {
     815 [ +  - ][ +  + ]:        216 :                     if (qw->test(term)) {
     816                 :         39 :                         relevance = &wildcards_relevance[i];
     817                 :         39 :                         highlight = 1;
     818                 :         39 :                         goto relevance_done;
     819                 :            :                     }
     820                 :        177 :                     ++i;
     821                 :            :                 }
     822                 :            : 
     823         [ +  + ]:       1659 :                 if (flags & Xapian::MSet::SNIPPET_BACKGROUND_MODEL) {
     824                 :            :                     // Background document model.
     825         [ +  - ]:       1477 :                     auto bgit = background.find(term);
     826 [ +  + ][ +  - ]:       1477 :                     if (bgit == background.end()) bgit = background.find(stem);
     827         [ +  + ]:       1477 :                     if (bgit == background.end()) {
     828         [ +  - ]:        827 :                         Xapian::doccount tf = enquire->db.get_termfreq(term);
     829         [ +  + ]:        827 :                         if (!tf) {
     830         [ +  - ]:        336 :                             tf = enquire->db.get_termfreq(stem);
     831                 :            :                         } else {
     832         [ +  - ]:        491 :                             stem = term;
     833                 :            :                         }
     834                 :        827 :                         double r = 0.0;
     835         [ +  + ]:        827 :                         if (tf) {
     836                 :            :                             // Add one to avoid log(0) when a term indexes all
     837                 :            :                             // documents.
     838                 :        491 :                             Xapian::doccount num_docs = stats->collection_size + 1;
     839                 :        491 :                             r = max_tw * log((num_docs - tf) / double(tf));
     840                 :        491 :                             r /= (length + 1) * log(double(num_docs));
     841                 :            : #if 0
     842                 :            :                             if (r <= 0) {
     843                 :            :                                 Utf8Iterator i(text.data() + term_start, text.data() + term_end);
     844                 :            :                                 while (i != Utf8Iterator()) {
     845                 :            :                                     if (Unicode::get_category(*i++) == Unicode::UPPERCASE_LETTER) {
     846                 :            :                                         r = max_tw * 0.05;
     847                 :            :                                     }
     848                 :            :                                 }
     849                 :            :                             }
     850                 :            : #endif
     851                 :            :                         }
     852 [ +  - ][ +  - ]:        827 :                         bgit = background.emplace(make_pair(stem, r)).first;
     853                 :            :                     }
     854         [ +  + ]:       1780 :                     relevance = &bgit->second;
     855                 :       2370 :                 }
     856                 :            :             } else {
     857                 :            : #if 0
     858                 :            :                 // In the absence of weight information, assume longer terms
     859                 :            :                 // are more relevant, and that unstemmed matches are a bit more
     860                 :            :                 // relevant than stemmed matches.
     861                 :            :                 if (queryterms.find(term) != queryterms.end()) {
     862                 :            :                     relevance = term.size() * 3;
     863                 :            :                 } else {
     864                 :            :                     string stem = "Z";
     865                 :            :                     stem += stemmer(term);
     866                 :            :                     if (queryterms.find(stem) != queryterms.end()) {
     867                 :            :                         relevance = term.size() * 2;
     868                 :            :                     }
     869                 :            :                 }
     870                 :            : #endif
     871                 :            :             }
     872                 :            : 
     873                 :            :             // FIXME: Allow Enquire without a DB set or an empty MSet() to be
     874                 :            :             // used if you don't want the collection model?
     875                 :            : 
     876                 :            : #if 0
     877                 :            :             // FIXME: Punctuation should somehow be included in the model, but this
     878                 :            :             // approach is problematic - we don't want the first word of a sentence
     879                 :            :             // to be favoured when it's at the end of the window.
     880                 :            : 
     881                 :            :             // Give first word in each sentence a relevance boost.
     882                 :            :             if (term_start == 0) {
     883                 :            :                 relevance += 10;
     884                 :            :             } else {
     885                 :            :                 for (size_t i = term_start; i + term.size() < term_end; ++i) {
     886                 :            :                     if (text[i] == '.' && Unicode::is_whitespace(text[i + 1])) {
     887                 :            :                         relevance += 10;
     888                 :            :                         break;
     889                 :            :                     }
     890                 :            :                 }
     891                 :            :             }
     892                 :            : #endif
     893                 :            : 
     894                 :            : relevance_done:
     895         [ +  + ]:       2370 :             if (longest_phrase) {
     896                 :        328 :                 phrase[phrase_next] = term;
     897                 :        328 :                 phrase_next = (phrase_next + 1) % (longest_phrase - 1);
     898                 :            :             }
     899                 :            : 
     900         [ +  + ]:       2370 :             if (highlight) matchfound = true;
     901                 :            : 
     902         [ -  + ]:       2370 :             if (!snip.pump(relevance, term_end, highlight, flags)) return false;
     903                 :            : 
     904                 :       2370 :             term_start = term_end;
     905                 :       2370 :             return true;
     906         [ +  - ]:        495 :         });
     907                 :            : 
     908                 :        495 :     snip.done();
     909                 :            : 
     910                 :            :     // Put together the snippet.
     911         [ +  - ]:        990 :     string result;
     912 [ +  + ][ +  + ]:        495 :     if (matchfound || (flags & SNIPPET_EMPTY_WITHOUT_MATCH) == 0) {
     913 [ +  - ][ +  + ]:       1981 :         while (snip.drain(text, hi_start, hi_end, omit, result)) { }
     914                 :            :     }
     915                 :            : 
     916                 :        990 :     return result;
     917                 :            : }
     918                 :            : 
     919                 :            : }

Generated by: LCOV version 1.11