LCOV - code coverage report
Current view: top level - queryparser - termgenerator_internal.cc (source / functions) Hit Total Coverage
Test: Test Coverage for xapian-core 954b5873a738 Lines: 402 428 93.9 %
Date: 2019-06-30 05:20:33 Functions: 27 27 100.0 %
Branches: 501 710 70.6 %

           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                 :       3867 : U_isupper(unsigned ch)
      52                 :            : {
      53 [ +  + ][ +  + ]:       3867 :     return (ch < 128 && C_isupper(static_cast<unsigned char>(ch)));
      54                 :            : }
      55                 :            : 
      56                 :            : static inline unsigned
      57                 :      25810 : check_wordchar(unsigned ch)
      58                 :            : {
      59         [ +  + ]:      25810 :     if (Unicode::is_wordchar(ch)) return Unicode::tolower(ch);
      60                 :       7498 :     return 0;
      61                 :            : }
      62                 :            : 
      63                 :            : static inline bool
      64                 :        732 : 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                 :        732 :         (1 << Unicode::OTHER_LETTER);
      71                 :        732 :     Utf8Iterator u(term);
      72                 :        732 :     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                 :       2762 : check_infix(unsigned ch)
      82                 :            : {
      83 [ +  + ][ +  - ]:       2762 :     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                 :         81 :         return ch;
      89                 :            :     }
      90                 :            :     // 0x2019 is Unicode apostrophe and single closing quote.
      91                 :            :     // 0x201b is Unicode single opening quote with the tail rising.
      92 [ +  + ][ -  + ]:       2681 :     if (ch == 0x2019 || ch == 0x201b) return '\'';
      93 [ +  + ][ -  + ]:       2633 :     if (ch >= 0x200b && (ch <= 0x200d || ch == 0x2060 || ch == 0xfeff))
         [ #  # ][ #  # ]
      94                 :          6 :         return UNICODE_IGNORE;
      95                 :       2627 :     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                 :       2839 : is_digit(unsigned ch) {
     123                 :       2839 :     return (Unicode::get_category(ch) == Unicode::DECIMAL_DIGIT_NUMBER);
     124                 :            : }
     125                 :            : 
     126                 :            : static inline unsigned
     127                 :       3131 : check_suffix(unsigned ch)
     128                 :            : {
     129 [ +  + ][ -  + ]:       3131 :     if (ch == '+' || ch == '#') return ch;
     130                 :            :     // FIXME: what about '-'?
     131                 :       3127 :     return 0;
     132                 :            : }
     133                 :            : 
     134                 :            : template<typename ACTION>
     135                 :            : static bool
     136                 :         29 : 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 [ +  - ][ +  - ]:         29 :     CJKNgramIterator tk(itor);
     161 [ +  - ][ +  + ]:        480 :     while (tk != CJKNgramIterator()) {
         [ +  - ][ +  + ]
     162                 :        451 :         const string& cjk_token = *tk;
     163                 :            :         // FLAG_CJK_NGRAM only sets positions for tokens of length 1.
     164 [ +  - ][ +  + ]:        451 :         bool with_pos = with_positions && tk.unigram();
         [ +  - ][ +  + ]
     165 [ +  - ][ -  + ]:        451 :         if (!action(cjk_token, with_pos, tk.get_utf8iterator().left()))
         [ +  - ][ -  + ]
     166                 :          0 :             return false;
     167 [ +  - ][ +  - ]:        451 :         ++tk;
     168                 :            :     }
     169                 :            :     // Update itor to end of CJK text span.
     170                 :         29 :     itor = tk.get_utf8iterator();
     171                 :         29 :     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                 :        663 : parse_terms(Utf8Iterator itor, unsigned cjk_flags, bool with_positions,
     183                 :            :             ACTION action)
     184                 :            : {
     185                 :       7486 :     while (true) {
     186                 :            :         // Advance to the start of the next term.
     187                 :            :         unsigned ch;
     188                 :            :         while (true) {
     189 [ +  + ][ +  + ]:       8171 :             if (itor == Utf8Iterator()) return;
     190                 :       7509 :             ch = check_wordchar(*itor);
     191 [ +  + ][ +  + ]:       7509 :             if (ch) break;
     192                 :       3857 :             ++itor;
     193                 :            :         }
     194                 :            : 
     195 [ +  - ][ +  - ]:       3652 :         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 [ +  + ][ +  + ]:       3652 :         if (U_isupper(*itor)) {
     199                 :        653 :             const Utf8Iterator end;
     200                 :        653 :             Utf8Iterator p = itor;
     201   [ +  -  +  +  :       2112 :             do {
             +  -  +  + ]
           [ +  +  +  +  
          +  +  +  +  +  
              + ][ +  + ]
     202 [ +  - ][ +  - ]:        839 :                 Unicode::append_utf8(term, Unicode::tolower(*p++));
     203                 :       2112 :             } 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 [ +  + ][ +  + ]:        653 :             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 [ +  - ][ +  - ]:        613 :             term.resize(0);
     215                 :            :         }
     216                 :            : 
     217                 :            :         while (true) {
     218 [ +  + ][ +  - ]:       3776 :             if (cjk_flags && CJK::codepoint_is_cjk_wordchar(*itor)) {
         [ +  - ][ +  + ]
         [ +  + ][ +  - ]
         [ +  + ][ +  + ]
     219 [ +  - ][ -  + ]:         29 :                 if (!parse_cjk(itor, cjk_flags, with_positions, action))
         [ +  - ][ +  - ]
                 [ -  + ]
     220                 :         23 :                     return;
     221                 :            :                 while (true) {
     222 [ +  - ][ +  + ]:         32 :                     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   [ +  +  +  + ]:      15150 :             do {
     231 [ +  - ][ +  - ]:      15612 :                 Unicode::append_utf8(term, ch);
     232                 :      15612 :                 prevch = ch;
     233 [ +  + ][ -  + ]:      15640 :                 if (++itor == Utf8Iterator() ||
         [ #  # ][ +  - ]
           [ +  +  #  # ]
         [ +  + ][ +  + ]
         [ +  + ][ +  - ]
           [ +  +  #  # ]
     234 [ #  # ][ +  - ]:         28 :                     (cjk_flags && CJK::codepoint_is_cjk(*itor)))
     235                 :        462 :                     goto endofterm;
     236                 :      15150 :                 ch = check_wordchar(*itor);
     237                 :            :             } while (ch);
     238                 :            : 
     239                 :       3282 :             Utf8Iterator next(itor);
     240                 :       3282 :             ++next;
     241 [ +  + ][ +  + ]:       3282 :             if (next == Utf8Iterator()) break;
     242                 :       3142 :             unsigned nextch = check_wordchar(*next);
     243 [ +  + ][ +  + ]:       3142 :             if (!nextch) break;
     244                 :       2786 :             unsigned infix_ch = *itor;
     245 [ +  + ][ +  + ]:       2786 :             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                 :       2762 :                 infix_ch = check_infix(infix_ch);
     250                 :            :             }
     251 [ +  + ][ +  + ]:       2786 :             if (!infix_ch) break;
     252 [ +  - ][ +  + ]:        155 :             if (infix_ch != UNICODE_IGNORE)
     253 [ +  - ][ +  - ]:        149 :                 Unicode::append_utf8(term, infix_ch);
     254                 :        155 :             ch = nextch;
     255                 :        155 :             itor = next;
     256                 :            :         }
     257                 :            : 
     258                 :            :         {
     259                 :       3127 :             size_t len = term.size();
     260                 :       3127 :             unsigned count = 0;
     261 [ -  + ][ +  + ]:       3292 :             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 [ -  + ][ +  + ]:       3127 :             if (Unicode::is_wordchar(*itor))
     271 [ #  # ][ +  - ]:          1 :                 term.resize(len);
     272                 :            :         }
     273                 :            : 
     274                 :            : endofterm:
     275 [ +  - ][ -  + ]:       3629 :         if (!action(term, with_positions, itor.left()))
         [ +  + ][ -  + ]
     276 [ +  + ][ +  + ]:       3651 :             return;
     277                 :       3628 :     }
     278                 :            : }
     279                 :            : 
     280                 :            : void
     281                 :        132 : TermGenerator::Internal::index_text(Utf8Iterator itor, termcount wdf_inc,
     282                 :            :                                     const string & prefix, bool with_positions)
     283                 :            : {
     284                 :            : #ifndef USE_ICU
     285         [ +  + ]:        132 :     if (flags & FLAG_CJK_WORDS) {
     286                 :            :         throw Xapian::FeatureUnavailableError("FLAG_CJK_WORDS requires "
     287 [ +  - ][ +  - ]:          9 :                                               "building Xapian to use ICU");
                 [ +  - ]
     288                 :            :     }
     289                 :            : #endif
     290                 :        123 :     unsigned cjk_flags = flags & (FLAG_CJK_NGRAM | FLAG_CJK_WORDS);
     291 [ +  + ][ -  + ]:        123 :     if (cjk_flags == 0 && CJK::is_cjk_enabled()) {
                 [ -  + ]
     292                 :          0 :         cjk_flags = FLAG_CJK_NGRAM;
     293                 :            :     }
     294                 :            : 
     295                 :            :     stop_strategy current_stop_mode;
     296         [ +  + ]:        123 :     if (!stopper.get()) {
     297                 :        113 :         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                 :       1265 :         [=](const string & term, bool positional, size_t) {
     304         [ +  + ]:        951 :             if (term.size() > max_word_length) return true;
     305                 :            : 
     306 [ +  + ][ +  - ]:        949 :             if (current_stop_mode == TermGenerator::STOP_ALL && (*stopper)(term))
         [ +  + ][ +  + ]
     307                 :          3 :                 return true;
     308                 :            : 
     309 [ +  + ][ +  + ]:        946 :             if (strategy == TermGenerator::STEM_SOME ||
     310         [ +  + ]:         23 :                 strategy == TermGenerator::STEM_NONE ||
     311                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     312         [ +  + ]:        931 :                 if (positional) {
     313 [ +  - ][ +  - ]:        864 :                     doc.add_posting(prefix + term, ++cur_pos, wdf_inc);
     314                 :            :                 } else {
     315 [ +  - ][ +  - ]:         67 :                     doc.add_term(prefix + term, wdf_inc);
     316                 :            :                 }
     317                 :            :             }
     318                 :            : 
     319                 :            :             // MSVC seems to need "this->" on member variables in this
     320                 :            :             // situation.
     321 [ +  + ][ +  + ]:        946 :             if ((this->flags & FLAG_SPELLING) && prefix.empty())
                 [ +  + ]
     322         [ +  + ]:          7 :                 db.add_spelling(term);
     323                 :            : 
     324   [ +  +  +  + ]:       1883 :             if (strategy == TermGenerator::STEM_NONE ||
                 [ +  + ]
     325                 :       1134 :                 !stemmer.internal.get()) return true;
     326                 :            : 
     327 [ +  + ][ +  + ]:        749 :             if (strategy == TermGenerator::STEM_SOME ||
     328                 :         23 :                 strategy == TermGenerator::STEM_SOME_FULL_POS) {
     329 [ +  + ][ +  + ]:        740 :                 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         [ +  + ]:        732 :                 if (!should_stem(term)) return true;
     336                 :            :             }
     337                 :            : 
     338                 :            :             // Add stemmed form without positional information.
     339         [ +  - ]:        739 :             const string& stem = stemmer(term);
     340         [ +  + ]:        739 :             if (rare(stem.empty())) return true;
     341         [ +  - ]:       1476 :             string stemmed_term;
     342         [ +  + ]:        738 :             if (strategy != TermGenerator::STEM_ALL) {
     343         [ +  - ]:        727 :                 stemmed_term += "Z";
     344                 :            :             }
     345         [ +  - ]:        738 :             stemmed_term += prefix;
     346         [ +  - ]:        738 :             stemmed_term += stem;
     347 [ +  + ][ +  - ]:        738 :             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         [ +  - ]:        717 :                 doc.add_term(stemmed_term, wdf_inc);
     352                 :            :             }
     353                 :        738 :             return true;
     354         [ +  + ]:       1073 :         });
     355                 :        122 : }
     356                 :            : 
     357                 :            : struct Sniplet {
     358                 :            :     double* relevance;
     359                 :            : 
     360                 :            :     size_t term_end;
     361                 :            : 
     362                 :            :     size_t highlight;
     363                 :            : 
     364                 :       2985 :     Sniplet(double* r, size_t t, size_t h)
     365                 :       2985 :         : relevance(r), term_end(t), highlight(h) { }
     366                 :            : };
     367                 :            : 
     368                 :       1080 : 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         [ +  - ]:        540 :     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                 :       2985 : SnipPipe::pump(double* r, size_t t, size_t h, unsigned flags)
     409                 :            : {
     410         [ +  + ]:       2985 :     if (h > 1) {
     411         [ +  - ]:         60 :         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                 :         60 :             auto & phrase_start = pipe[pipe.size() - (h - 1)];
     417         [ +  - ]:         60 :             if (phrase_start.relevance) {
     418                 :         60 :                 *phrase_start.relevance *= DECAY;
     419                 :         60 :                 sum -= *phrase_start.relevance;
     420                 :            :             }
     421                 :         60 :             sum += *r;
     422                 :         60 :             phrase_start.relevance = r;
     423                 :         60 :             phrase_start.highlight = h;
     424                 :         60 :             *r /= DECAY;
     425                 :            :         }
     426                 :         60 :         r = NULL;
     427                 :         60 :         h = 0;
     428                 :            :     }
     429                 :       2985 :     pipe.emplace_back(r, t, h);
     430         [ +  + ]:       2985 :     if (r) {
     431                 :       2611 :         sum += *r;
     432                 :       2611 :         *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         [ +  + ]:       3862 :     while (t - begin > length /* || pipe.front().relevance < 0.0 */) {
     439                 :       1101 :         const Sniplet& word = pipe.front();
     440         [ +  + ]:       1101 :         if (word.relevance) {
     441                 :       1023 :             *word.relevance *= DECAY;
     442                 :       1023 :             sum -= *word.relevance;
     443                 :            :         }
     444                 :       1101 :         begin = word.term_end;
     445         [ +  + ]:       1101 :         if (best_end >= begin)
     446                 :        742 :             best_pipe.push_back(word);
     447                 :       1101 :         pipe.pop_front();
     448                 :            :         // E.g. can happen if the current term is longer than the requested
     449                 :            :         // length!
     450         [ +  + ]:       1101 :         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         [ +  + ]:       2985 :     if (sum >= best_sum) {
     456                 :            :         // Discard any part of `best_pipe` which is before `begin`.
     457         [ +  + ]:       2375 :         if (begin >= best_end) {
     458                 :        717 :             best_pipe.clear();
     459                 :            :         } else {
     460   [ +  +  +  - ]:       2362 :             while (!best_pipe.empty() &&
                 [ +  + ]
     461                 :        352 :                    best_pipe.front().term_end <= begin) {
     462                 :        352 :                 best_pipe.pop_front();
     463                 :            :             }
     464                 :            :         }
     465                 :       2375 :         best_sum = sum;
     466                 :       2375 :         best_begin = begin;
     467                 :       2375 :         best_end = t;
     468         [ +  + ]:        610 :     } else if ((flags & Xapian::MSet::SNIPPET_EXHAUSTIVE) == 0) {
     469 [ +  - ][ -  + ]:         24 :         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                 :       2985 :     return true;
     476                 :            : }
     477                 :            : 
     478                 :            : inline void
     479                 :        540 : SnipPipe::done()
     480                 :            : {
     481                 :            :     // Discard any part of `pipe` which is after `best_end`.
     482         [ +  + ]:        540 :     if (begin >= best_end) {
     483                 :        109 :         pipe.clear();
     484                 :            :     } else {
     485                 :            :         // We should never empty the pipe (as that case should be handled
     486                 :            :         // above).
     487   [ +  -  +  + ]:       1196 :         while (rare(!pipe.empty()) &&
                 [ +  + ]
     488                 :        598 :                pipe.back().term_end > best_end) {
     489                 :        167 :             pipe.pop_back();
     490                 :            :         }
     491                 :            :     }
     492                 :        540 : }
     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                 :        450 : snippet_check_leading_nonwordchar(unsigned ch) {
     499 [ +  + ][ +  + ]:       1343 :     if (Unicode::is_currency(ch) ||
     500 [ +  + ][ +  + ]:        893 :         Unicode::get_category(ch) == Unicode::OPEN_PUNCTUATION ||
     501                 :        359 :         Unicode::get_category(ch) == Unicode::INITIAL_QUOTE_PUNCTUATION) {
     502                 :         98 :         return true;
     503                 :            :     }
     504         [ +  + ]:        352 :     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                 :        131 :             return true;
     522                 :            :     }
     523                 :        221 :     return false;
     524                 :            : }
     525                 :            : 
     526                 :            : static inline void
     527                 :       2545 : append_escaping_xml(const char* p, const char* end, string& output)
     528                 :            : {
     529         [ +  + ]:      12225 :     while (p != end) {
     530                 :       9680 :         char ch = *p++;
     531   [ +  +  +  + ]:       9680 :         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                 :       9659 :                 output += ch;
     543                 :            :         }
     544                 :            :     }
     545                 :       2545 : }
     546                 :            : 
     547                 :            : inline bool
     548                 :       2401 : SnipPipe::drain(const string & input,
     549                 :            :                 const string & hi_start,
     550                 :            :                 const string & hi_end,
     551                 :            :                 const string & omit,
     552                 :            :                 string & output)
     553                 :            : {
     554 [ +  + ][ +  + ]:       2401 :     if (best_pipe.empty() && !pipe.empty()) {
                 [ +  + ]
     555                 :        424 :         swap(best_pipe, pipe);
     556                 :            :     }
     557                 :            : 
     558         [ +  + ]:       2401 :     if (best_pipe.empty()) {
     559                 :        533 :         size_t tail_len = input.size() - best_end;
     560         [ +  + ]:        533 :         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                 :        198 :         bool punc = false;
     566                 :        198 :         Utf8Iterator i(input.data() + best_end, tail_len);
     567         [ +  + ]:        404 :         while (i != Utf8Iterator()) {
     568                 :        338 :             unsigned ch = *i;
     569 [ +  + ][ +  - ]:        338 :             if (punc && Unicode::is_whitespace(ch)) break;
                 [ +  + ]
     570                 :            : 
     571                 :            :             // Allow "...", "!!", "!?!", etc...
     572 [ +  + ][ +  + ]:        326 :             punc = (ch == '.' || ch == '?' || ch == '!');
                 [ +  + ]
     573                 :            : 
     574         [ +  + ]:        326 :             if (Unicode::is_wordchar(ch)) break;
     575                 :        206 :             ++i;
     576                 :            :         }
     577                 :            : 
     578         [ +  + ]:        198 :         if (punc) {
     579                 :            :             // Include end of sentence punctuation.
     580         [ +  - ]:         64 :             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         [ +  - ]:        134 :             output += omit;
     585                 :            :         }
     586                 :            : 
     587                 :        533 :         return false;
     588                 :            :     }
     589                 :            : 
     590                 :       1868 :     const Sniplet & word = best_pipe.front();
     591                 :            : 
     592         [ +  + ]:       1868 :     if (output.empty()) {
     593                 :            :         // Start of the snippet.
     594         [ +  + ]:        477 :         enum { NO, PUNC, YES } sentence_boundary = (best_begin == 0) ? YES : NO;
     595                 :            : 
     596                 :        477 :         Utf8Iterator i(input.data() + best_begin, word.term_end - best_begin);
     597         [ +  - ]:        927 :         while (i != Utf8Iterator()) {
     598                 :        927 :             unsigned ch = *i;
     599   [ +  -  +  - ]:        927 :             switch (sentence_boundary) {
     600                 :            :                 case NO:
     601 [ +  - ][ +  - ]:        367 :                     if (ch == '.' || ch == '?' || ch == '!') {
                 [ -  + ]
     602                 :          0 :                         sentence_boundary = PUNC;
     603                 :            :                     }
     604                 :        367 :                     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                 :        560 :                     break;
     616                 :            :             }
     617                 :            : 
     618                 :            :             // Start the snippet at the start of the first word, but include
     619                 :            :             // certain punctuation too.
     620         [ +  + ]:        927 :             if (Unicode::is_wordchar(ch)) {
     621                 :            :                 // But limit how much leading punctuation we include.
     622                 :        477 :                 size_t word_begin = i.raw() - input.data();
     623         [ +  + ]:        477 :                 if (word_begin - best_begin > 4) {
     624                 :          7 :                     best_begin = word_begin;
     625                 :            :                 }
     626                 :        477 :                 break;
     627                 :            :             }
     628                 :        450 :             ++i;
     629         [ +  + ]:        450 :             if (!snippet_check_leading_nonwordchar(ch)) {
     630                 :        221 :                 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         [ +  + ]:        477 :         if (sentence_boundary != YES) {
     637         [ +  - ]:        477 :             output += omit;
     638                 :            :         }
     639                 :            :     }
     640                 :            : 
     641         [ +  + ]:       1868 :     if (word.highlight) {
     642                 :            :         // Don't include inter-word characters in the highlight.
     643                 :        613 :         Utf8Iterator i(input.data() + best_begin, input.size() - best_begin);
     644         [ +  - ]:       1709 :         while (i != Utf8Iterator()) {
     645                 :       1096 :             unsigned ch = *i;
     646         [ +  + ]:       1096 :             if (Unicode::is_wordchar(ch)) {
     647         [ +  - ]:        613 :                 append_escaping_xml(input.data() + best_begin, i.raw(), output);
     648                 :        613 :                 best_begin = i.raw() - input.data();
     649                 :        613 :                 break;
     650                 :            :             }
     651                 :        483 :             ++i;
     652                 :            :         }
     653                 :            :     }
     654                 :            : 
     655         [ +  + ]:       1868 :     if (!phrase_len) {
     656                 :       1790 :         phrase_len = word.highlight;
     657         [ +  + ]:       1790 :         if (phrase_len) output += hi_start;
     658                 :            :     }
     659                 :            : 
     660                 :       1868 :     const char* p = input.data();
     661                 :       1868 :     append_escaping_xml(p + best_begin, p + word.term_end, output);
     662                 :       1868 :     best_begin = word.term_end;
     663                 :            : 
     664 [ +  + ][ +  + ]:       1868 :     if (phrase_len && --phrase_len == 0) output += hi_end;
                 [ +  + ]
     665                 :            : 
     666                 :       1868 :     best_pipe.pop_front();
     667                 :       2401 :     return true;
     668                 :            : }
     669                 :            : 
     670                 :            : static void
     671                 :       1220 : 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                 :       1220 :     size_t n_subqs = query.get_num_subqueries();
     680                 :       1220 :     Xapian::Query::op op = query.get_type();
     681         [ +  + ]:       1220 :     if (op == query.LEAF_TERM) {
     682                 :            :         const Xapian::Internal::QueryTerm & qt =
     683                 :        820 :             *static_cast<const Xapian::Internal::QueryTerm *>(query.internal.get());
     684 [ +  - ][ +  - ]:        820 :         loose_terms.insert(make_pair(qt.get_term(), 0));
     685         [ +  + ]:        400 :     } else if (op == query.OP_WILDCARD) {
     686                 :            :         using Xapian::Internal::QueryWildcard;
     687                 :            :         const QueryWildcard* qw =
     688                 :         30 :             static_cast<const QueryWildcard*>(query.internal.get());
     689         [ +  - ]:         30 :         wildcards.push_back(qw);
     690         [ -  + ]:        370 :     } 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         [ +  + ]:        370 :     } else if (op == query.OP_PHRASE) {
     696                 :            :         const Xapian::Internal::QueryPhrase & phrase =
     697                 :         60 :             *static_cast<const Xapian::Internal::QueryPhrase *>(query.internal.get());
     698         [ +  - ]:         60 :         if (phrase.get_window() == n_subqs) {
     699                 :            :             // Tight phrase.
     700         [ +  + ]:        198 :             for (size_t i = 0; i != n_subqs; ++i) {
     701         [ -  + ]:        138 :                 if (query.get_subquery(i).get_type() != query.LEAF_TERM)
     702                 :          0 :                     goto non_term_subquery;
     703                 :            :             }
     704                 :            : 
     705                 :            :             // Tight phrase of terms.
     706         [ +  - ]:         60 :             exact_phrases.push_back(vector<string>());
     707                 :         60 :             vector<string> & terms = exact_phrases.back();
     708                 :         60 :             terms.reserve(n_subqs);
     709         [ +  + ]:        198 :             for (size_t i = 0; i != n_subqs; ++i) {
     710         [ +  - ]:        138 :                 Xapian::Query q = query.get_subquery(i);
     711                 :            :                 const Xapian::Internal::QueryTerm & qt =
     712                 :        138 :                     *static_cast<const Xapian::Internal::QueryTerm *>(q.internal.get());
     713         [ +  - ]:        138 :                 terms.push_back(qt.get_term());
     714                 :        138 :             }
     715         [ +  - ]:         60 :             if (n_subqs > longest_phrase) longest_phrase = n_subqs;
     716                 :       1220 :             return;
     717                 :            :         }
     718                 :            :     }
     719                 :            : non_term_subquery:
     720         [ +  + ]:       1840 :     for (size_t i = 0; i != n_subqs; ++i)
     721                 :            :         check_query(query.get_subquery(i), exact_phrases, loose_terms,
     722         [ +  - ]:        680 :                     wildcards, fuzzies, longest_phrase);
     723                 :            : }
     724                 :            : 
     725                 :            : static double*
     726                 :       5287 : check_term(unordered_map<string, double> & loose_terms,
     727                 :            :            const Xapian::Weight::Internal * stats,
     728                 :            :            const string & term,
     729                 :            :            double max_tw)
     730                 :            : {
     731         [ +  - ]:       5287 :     auto it = loose_terms.find(term);
     732         [ +  + ]:       5287 :     if (it == loose_terms.end()) return NULL;
     733                 :            : 
     734         [ +  + ]:        657 :     if (it->second == 0.0) {
     735                 :            :         double relevance;
     736 [ +  - ][ -  + ]:        552 :         if (!stats->get_termweight(term, relevance)) {
     737                 :            :             // FIXME: Assert?
     738         [ #  # ]:          0 :             loose_terms.erase(it);
     739                 :          0 :             return NULL;
     740                 :            :         }
     741                 :            : 
     742                 :        552 :         it->second = relevance + max_tw;
     743                 :            :     }
     744                 :       5287 :     return &it->second;
     745                 :            : }
     746                 :            : 
     747                 :            : string
     748                 :        554 : 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 [ -  + ][ #  # ]:        554 :     if (hi_start.empty() && hi_end.empty() && text.size() <= length) {
         [ #  # ][ -  + ]
     757                 :            :         // Too easy!
     758         [ #  # ]:          0 :         return text;
     759                 :            :     }
     760                 :            : 
     761                 :            : #ifndef USE_ICU
     762         [ +  + ]:        554 :     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                 :        540 :     auto SNIPPET_CJK_MASK = MSet::SNIPPET_CJK_NGRAM | MSet::SNIPPET_CJK_WORDS;
     768                 :        540 :     unsigned cjk_flags = flags & SNIPPET_CJK_MASK;
     769 [ +  + ][ +  - ]:        540 :     if (cjk_flags == 0 && CJK::is_cjk_enabled()) {
         [ -  + ][ -  + ]
     770                 :          0 :         cjk_flags = MSet::SNIPPET_CJK_NGRAM;
     771                 :            :     }
     772                 :            : 
     773                 :        540 :     size_t term_start = 0;
     774                 :        540 :     double min_tw = 0, max_tw = 0;
     775         [ +  - ]:        540 :     if (stats) stats->get_max_termweight(min_tw, max_tw);
     776         [ +  + ]:        540 :     if (max_tw == 0.0) {
     777                 :        269 :         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                 :        271 :         max_tw *= 1.015625;
     784                 :            :     }
     785                 :            : 
     786         [ +  - ]:        540 :     SnipPipe snip(length);
     787                 :            : 
     788                 :       1080 :     list<vector<string>> exact_phrases;
     789         [ +  - ]:       1080 :     unordered_map<string, double> loose_terms;
     790                 :       1080 :     list<const Xapian::Internal::QueryWildcard*> wildcards;
     791                 :       1080 :     list<const Xapian::Internal::QueryEditDistance*> fuzzies;
     792                 :        540 :     size_t longest_phrase = 0;
     793                 :        540 :     check_query(enquire->query, exact_phrases, loose_terms,
     794         [ +  - ]:        540 :                 wildcards, fuzzies, longest_phrase);
     795                 :            : 
     796                 :       1080 :     vector<double> exact_phrases_relevance;
     797         [ +  - ]:        540 :     exact_phrases_relevance.reserve(exact_phrases.size());
     798         [ +  + ]:        600 :     for (auto&& terms : exact_phrases) {
     799                 :            :         // FIXME: What relevance to use?
     800         [ +  - ]:         60 :         exact_phrases_relevance.push_back(max_tw * terms.size());
     801                 :            :     }
     802                 :            : 
     803                 :       1080 :     vector<double> wildcards_relevance;
     804         [ +  - ]:        540 :     wildcards_relevance.reserve(wildcards.size());
     805         [ +  + ]:        570 :     for (auto&& pattern : wildcards) {
     806                 :            :         // FIXME: What relevance to use?
     807                 :            :         (void)pattern;
     808         [ +  - ]:         30 :         wildcards_relevance.push_back(max_tw + min_tw);
     809                 :            :     }
     810                 :            : 
     811                 :       1080 :     vector<double> fuzzies_relevance;
     812         [ +  - ]:        540 :     fuzzies_relevance.reserve(fuzzies.size());
     813         [ -  + ]:        540 :     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                 :        540 :     unordered_map<string, double>& background = snippet_bg_relevance;
     822                 :            : 
     823                 :       1080 :     vector<string> phrase;
     824 [ +  + ][ +  - ]:        540 :     if (longest_phrase) phrase.resize(longest_phrase - 1);
     825                 :        540 :     size_t phrase_next = 0;
     826                 :        540 :     bool matchfound = false;
     827                 :            :     parse_terms(Utf8Iterator(text), cjk_flags, true,
     828                 :       3129 :         [&](const string & term, bool positional, size_t left) {
     829                 :            :             // FIXME: Don't hardcode this here.
     830                 :       3129 :             const size_t max_word_length = 64;
     831                 :            : 
     832         [ +  + ]:       3129 :             if (!positional) return true;
     833         [ -  + ]:       2985 :             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                 :       2985 :             size_t term_end = text.size() - left;
     839                 :            : 
     840                 :       2985 :             double* relevance = 0;
     841                 :       2985 :             size_t highlight = 0;
     842         [ +  - ]:       2985 :             if (stats) {
     843                 :       2985 :                 size_t i = 0;
     844         [ +  + ]:       3364 :                 for (auto&& terms : exact_phrases) {
     845 [ +  - ][ +  + ]:        439 :                     if (term == terms.back()) {
     846                 :         94 :                         size_t n = terms.size() - 1;
     847                 :         94 :                         bool match = true;
     848         [ +  + ]:        172 :                         while (n--) {
     849 [ +  - ][ +  + ]:        112 :                             if (terms[n] != phrase[(n + phrase_next) % (longest_phrase - 1)]) {
     850                 :         34 :                                 match = false;
     851                 :         34 :                                 break;
     852                 :            :                             }
     853                 :            :                         }
     854         [ +  + ]:         94 :                         if (match) {
     855                 :            :                             // FIXME: Sort phrases, highest score first!
     856                 :         60 :                             relevance = &exact_phrases_relevance[i];
     857                 :         94 :                             highlight = terms.size();
     858                 :         60 :                             goto relevance_done;
     859                 :            :                         }
     860                 :            :                     }
     861                 :        379 :                     ++i;
     862                 :            :                 }
     863                 :            : 
     864         [ +  - ]:       2925 :                 relevance = check_term(loose_terms, stats.get(), term, max_tw);
     865         [ +  + ]:       2925 :                 if (relevance) {
     866                 :            :                     // Matched unstemmed term.
     867                 :        563 :                     highlight = 1;
     868                 :        563 :                     goto relevance_done;
     869                 :            :                 }
     870                 :            : 
     871         [ +  - ]:       2362 :                 string stem = "Z";
     872 [ +  - ][ +  - ]:       2362 :                 stem += stemmer(term);
     873         [ +  - ]:       2362 :                 relevance = check_term(loose_terms, stats.get(), stem, max_tw);
     874         [ +  + ]:       2362 :                 if (relevance) {
     875                 :            :                     // Matched stemmed term.
     876                 :         94 :                     highlight = 1;
     877                 :         94 :                     goto relevance_done;
     878                 :            :                 }
     879                 :            : 
     880                 :            :                 // Check wildcards.
     881                 :            :                 // FIXME: Sort wildcards, cheapest to check first or something?
     882                 :       2268 :                 i = 0;
     883         [ +  + ]:       2622 :                 for (auto&& qw : wildcards) {
     884 [ +  - ][ +  + ]:        432 :                     if (qw->test(term)) {
     885                 :         78 :                         relevance = &wildcards_relevance[i];
     886                 :         78 :                         highlight = 1;
     887                 :         78 :                         goto relevance_done;
     888                 :            :                     }
     889                 :        354 :                     ++i;
     890                 :            :                 }
     891                 :            : 
     892                 :            :                 // Check fuzzies.
     893                 :            :                 // FIXME: Sort fuzzies, cheapest to check first or something?
     894                 :       2190 :                 i = 0;
     895         [ -  + ]:       2190 :                 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         [ +  + ]:       2190 :                 if (flags & Xapian::MSet::SNIPPET_BACKGROUND_MODEL) {
     910                 :            :                     // Background document model.
     911         [ +  - ]:       1876 :                     auto bgit = background.find(term);
     912 [ +  + ][ +  - ]:       1876 :                     if (bgit == background.end()) bgit = background.find(stem);
     913         [ +  + ]:       1876 :                     if (bgit == background.end()) {
     914         [ +  - ]:       1094 :                         Xapian::doccount tf = enquire->db.get_termfreq(term);
     915         [ +  + ]:       1094 :                         if (!tf) {
     916         [ +  - ]:        336 :                             tf = enquire->db.get_termfreq(stem);
     917                 :            :                         } else {
     918         [ +  - ]:        758 :                             stem = term;
     919                 :            :                         }
     920                 :       1094 :                         double r = 0.0;
     921         [ +  + ]:       1094 :                         if (tf) {
     922                 :            :                             // Add one to avoid log(0) when a term indexes all
     923                 :            :                             // documents.
     924                 :        758 :                             Xapian::doccount num_docs = stats->collection_size + 1;
     925                 :        758 :                             r = max_tw * log((num_docs - tf) / double(tf));
     926                 :        758 :                             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 [ +  - ][ +  - ]:       1094 :                         bgit = background.emplace(make_pair(stem, r)).first;
     939                 :            :                     }
     940         [ +  + ]:       2362 :                     relevance = &bgit->second;
     941                 :       2985 :                 }
     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         [ +  + ]:       2985 :             if (longest_phrase) {
     982                 :        439 :                 phrase[phrase_next] = term;
     983                 :        439 :                 phrase_next = (phrase_next + 1) % (longest_phrase - 1);
     984                 :            :             }
     985                 :            : 
     986         [ +  + ]:       2985 :             if (highlight) matchfound = true;
     987                 :            : 
     988         [ -  + ]:       2985 :             if (!snip.pump(relevance, term_end, highlight, flags)) return false;
     989                 :            : 
     990                 :       2985 :             term_start = term_end;
     991                 :       3129 :             return true;
     992         [ +  - ]:        540 :         });
     993                 :            : 
     994                 :        540 :     snip.done();
     995                 :            : 
     996                 :            :     // Put together the snippet.
     997         [ +  - ]:       1080 :     string result;
     998 [ +  + ][ +  + ]:        540 :     if (matchfound || (flags & SNIPPET_EMPTY_WITHOUT_MATCH) == 0) {
     999 [ +  - ][ +  + ]:       2401 :         while (snip.drain(text, hi_start, hi_end, omit, result)) { }
    1000                 :            :     }
    1001                 :            : 
    1002                 :       1080 :     return result;
    1003                 :            : }
    1004                 :            : 
    1005                 :            : }

Generated by: LCOV version 1.11