LCOV - code coverage report
Current view: top level - core/search - SPSearchConfiguration.cc (source / functions) Hit Total Coverage
Test: coverage.info Lines: 604 646 93.5 %
Date: 2024-05-12 00:16:13 Functions: 60 61 98.4 %

          Line data    Source code
       1             : /**
       2             : Copyright (c) 2020-2022 Roman Katuntsev <sbkarr@stappler.org>
       3             : Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
       4             : 
       5             : Permission is hereby granted, free of charge, to any person obtaining a copy
       6             : of this software and associated documentation files (the "Software"), to deal
       7             : in the Software without restriction, including without limitation the rights
       8             : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       9             : copies of the Software, and to permit persons to whom the Software is
      10             : furnished to do so, subject to the following conditions:
      11             : 
      12             : The above copyright notice and this permission notice shall be included in
      13             : all copies or substantial portions of the Software.
      14             : 
      15             : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      16             : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      17             : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      18             : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      19             : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      20             : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
      21             : THE SOFTWARE.
      22             : **/
      23             : 
      24             : #include "SPSearchConfiguration.h"
      25             : #include "SPHtmlParser.h"
      26             : #include "SPValid.h"
      27             : #include <inttypes.h>
      28             : 
      29             : namespace STAPPLER_VERSIONIZED stappler::search {
      30             : 
      31             : static StemmerEnv *Configuration_makeLocalConfig(StemmerEnv *orig);
      32             : static void Stemmer_Reader_run(StringView origin, Function<void(const StringView &, const Callback<void()> &cancelCb)> &&cb);
      33             : static void *StemmerEnv_getUserData(StemmerEnv *);
      34             : 
      35      208425 : static bool stemWordDefault(Language lang, StemmerEnv *env, ParserToken tok, StringView word, const Callback<void(StringView)> &cb, const StringView *stopwords) {
      36      208425 :         switch (tok) {
      37       93600 :         case ParserToken::AsciiWord:
      38             :         case ParserToken::AsciiHyphenatedWord:
      39             :         case ParserToken::HyphenatedWord_AsciiPart:
      40             :         case ParserToken::Word:
      41             :         case ParserToken::HyphenatedWord:
      42             :         case ParserToken::HyphenatedWord_Part:
      43             :                 switch (lang) {
      44       11700 :                 case Language::Simple: {
      45       11700 :                         auto str = normalizeWord(word);
      46       11700 :                         if (stopwords && isStopword(str, stopwords)) {
      47           0 :                                 return false;
      48             :                         }
      49       11700 :                         cb(str);
      50       11700 :                         break;
      51       11700 :                 }
      52       81900 :                 default: {
      53       81900 :                         auto str = normalizeWord(word);
      54       81900 :                         if (stopwords && isStopword(str, stopwords)) {
      55           0 :                                 return false;
      56             :                         }
      57       81900 :                         return stemWord(str, cb, env);
      58             :                         break;
      59       81900 :                 }
      60             :                 }
      61       11700 :                 break;
      62             : 
      63       14725 :         case ParserToken::NumWord:
      64             :         case ParserToken::NumHyphenatedWord:
      65             :         case ParserToken::HyphenatedWord_NumPart: {
      66       14725 :                 auto str = normalizeWord(word);
      67       14725 :                 if (stopwords && isStopword(str, stopwords)) {
      68           0 :                         return false;
      69             :                 }
      70       14725 :                 cb(str);
      71       14725 :                 break;
      72       14725 :         }
      73             : 
      74          25 :         case ParserToken::Email: {
      75          25 :                 auto str = normalizeWord(word);
      76          25 :                 valid::validateEmail(str);
      77          25 :                 if (stopwords && isStopword(str, stopwords)) {
      78           0 :                         return false;
      79             :                 }
      80          25 :                 cb(str);
      81          25 :                 break;
      82          25 :         }
      83             : 
      84         575 :         case ParserToken::Url: {
      85         575 :                 auto str = normalizeWord(word);
      86         575 :                 valid::validateUrl(str);
      87         575 :                 if (stopwords && isStopword(str, stopwords)) {
      88           0 :                         return false;
      89             :                 }
      90         575 :                 cb(str);
      91         575 :                 break;
      92         575 :         }
      93             : 
      94         900 :         case ParserToken::Version:
      95             :         case ParserToken::Path:
      96             :         case ParserToken::ScientificFloat: {
      97         900 :                 auto str = normalizeWord(word);
      98         900 :                 if (stopwords && isStopword(str, stopwords)) {
      99           0 :                         return false;
     100             :                 }
     101         900 :                 cb(str);
     102         900 :                 break;
     103         900 :         }
     104             : 
     105        8050 :         case ParserToken::Float:
     106             :         case ParserToken::Integer: {
     107        8050 :                 cb(word);
     108        8050 :                 break;
     109             :         }
     110             : 
     111         500 :         case ParserToken::Custom: {
     112         500 :                 StringViewUtf8 tmp(word);
     113         500 :                 auto num = tmp.readChars<StringViewUtf8::CharGroup<CharGroupId::Numbers>>();
     114         500 :                 if (num.size() == 2) {
     115         500 :                         if (tmp.is(':') || tmp.is('-') || tmp.is(u'–')) {
     116         500 :                                 bool cond = tmp.is('-') || tmp.is(u'–');
     117         500 :                                 String str; str.reserve(word.size());
     118         500 :                                 if (cond) {
     119         400 :                                         StringViewUtf8 word2(word);
     120        1600 :                                         while (!word2.empty()) {
     121        1200 :                                                 auto r = word2.readUntil<StringViewUtf8::CharGroup<CharGroupId::WhiteSpace>, StringViewUtf8::Chars<u'–'>, StringViewUtf8::Chars<':'>>();
     122        1200 :                                                 if (!r.empty()) {
     123        1200 :                                                         str.append(r.data(), r.size());
     124             :                                                 }
     125        1200 :                                                 if (word2.is(u'–')) {
     126         200 :                                                         str.emplace_back('-');
     127         200 :                                                         ++ word2;
     128        1000 :                                                 } else if (word2.is(':')) {
     129         600 :                                                         str.emplace_back(':');
     130         600 :                                                         ++ word2;
     131             :                                                 } else {
     132         400 :                                                         auto space = word2.readChars<StringViewUtf8::CharGroup<CharGroupId::WhiteSpace>>();
     133         400 :                                                         if (cond && !space.empty() && !r.empty()) {
     134           0 :                                                                 str.emplace_back('/');
     135             :                                                         }
     136             :                                                 }
     137             :                                         }
     138             :                                 } else {
     139         200 :                                         while (!word.empty()) {
     140         100 :                                                 auto r = word.readUntil<StringViewUtf8::CharGroup<CharGroupId::WhiteSpace>>();
     141         100 :                                                 if (!r.empty()) {
     142         100 :                                                         str.append(r.data(), r.size());
     143             :                                                 }
     144             :                                         }
     145             :                                 }
     146         500 :                                 cb(string::tolower<Interface>(StringView(str)));
     147         500 :                                 return true;
     148         500 :                         }
     149             :                 }
     150           0 :                 auto str = normalizeWord(word);
     151           0 :                 if (stopwords && isStopword(str, stopwords)) {
     152           0 :                         return false;
     153             :                 }
     154           0 :                 cb(str);
     155           0 :                 break;
     156           0 :         }
     157       90000 :         case ParserToken::XMLEntity:
     158             :         case ParserToken::Blank:
     159       90000 :                 return false;
     160             :                 break;
     161             :         }
     162       36025 :         return true;
     163             : }
     164             : 
     165             : struct Configuration::Data : AllocBase {
     166             :         pool_t *pool = nullptr;
     167             :         std::atomic<uint32_t> refCount = 1;
     168             :         Language language = Language::Simple;
     169             :         StemmerEnv *primary = nullptr;
     170             :         StemmerEnv *secondary = nullptr;
     171             : 
     172             :         Map<ParserToken, StemmerCallback> stemmers;
     173             : 
     174             :         PreStemCallback preStem;
     175             :         const StringView *customStopwords = nullptr;
     176             : 
     177         225 :         Data(pool_t *p, Language lang)
     178         225 :         : pool(p), language(lang), primary(search::getStemmer(language))
     179         450 :         , secondary(search::getStemmer((lang == Language::Simple) ? Language::Simple : Language::English)) { }
     180             : };
     181             : 
     182          25 : Configuration::Configuration() : Configuration(Language::English) { }
     183             : 
     184         225 : Configuration::Configuration(Language lang) {
     185         225 :         pool::initialize();
     186         225 :         auto p = pool::create(pool::acquire());
     187         225 :         perform([&, this] {
     188         225 :                 data = new (p) Data(p, lang);
     189         225 :         }, p);
     190         225 : }
     191             : 
     192         200 : Configuration::~Configuration() {
     193         400 :         if (data->refCount.fetch_sub(1) == 1) {
     194         200 :                 data->~Data();
     195         200 :                 pool::destroy(data->pool);
     196         200 :                 pool::terminate();
     197             :         }
     198         200 : }
     199             : 
     200          50 : void Configuration::setLanguage(Language lang) {
     201         225 :         perform([&, this] {
     202          50 :                 auto prev = data->language;
     203          50 :                 auto prevSec = (prev == Language::Simple) ? Language::Simple : Language::English;
     204          50 :                 auto newSec = (lang == Language::Simple) ? Language::Simple : Language::English;
     205          50 :                 data->language = lang;
     206          50 :                 data->primary = search::getStemmer(data->language);
     207          50 :                 if (prevSec != newSec) {
     208          25 :                         data->secondary = search::getStemmer(newSec);
     209             :                 }
     210          50 :         }, data->pool);
     211          50 : }
     212             : 
     213          25 : Language Configuration::getLanguage() const {
     214          25 :         return data->language;
     215             : }
     216             : 
     217         125 : void Configuration::setStemmer(ParserToken tok, StemmerCallback &&cb) {
     218         375 :         perform([&, this] {
     219         125 :                 data->stemmers.emplace(tok, move(cb));
     220         125 :         }, data->pool);
     221         125 : }
     222             : 
     223          75 : Configuration::StemmerCallback Configuration::getStemmer(ParserToken tok) const {
     224          75 :         auto it = data->stemmers.find(tok);
     225          75 :         if (it != data->stemmers.end()) {
     226          25 :                 return it->second;
     227             :         }
     228             : 
     229         150 :         return StemmerCallback([&, lang = data->language, env = getEnvForToken(tok), stopwords = data->customStopwords]
     230          50 :                                                         (StringView word, const Callback<void(StringView)> &cb) -> bool {
     231          50 :                 return stemWordDefault(lang, env, tok, word, cb, stopwords);
     232          50 :         });
     233             : }
     234             : 
     235          25 : void Configuration::setCustomStopwords(const StringView *w) {
     236          25 :         data->customStopwords = w;
     237          25 : }
     238             : 
     239          25 : const StringView *Configuration::getCustomStopwords() const {
     240          25 :         return data->customStopwords;
     241             : }
     242             : 
     243          25 : void Configuration::setPreStem(PreStemCallback &&cb) {
     244         100 :         perform([&, this] {
     245          25 :                 data->preStem = move(cb);
     246          25 :         }, data->pool);
     247             : 
     248          25 : }
     249          25 : const Configuration::PreStemCallback &Configuration::getPreStem() const {
     250          25 :         return data->preStem;
     251             : }
     252             : 
     253         550 : void Configuration::stemPhrase(const StringView &str, const StemWordCallback &cb) const {
     254         550 :         parsePhrase(str, [&, this] (StringView word, ParserToken tok) {
     255       99425 :                 if (data->preStem != nullptr && !isWordPart(tok)) {
     256       39575 :                         auto ret = data->preStem(word, tok);
     257       39575 :                         if (!ret.empty()) {
     258        1250 :                                 for (auto &it : ret) {
     259         750 :                                         auto str = normalizeWord(it);
     260         750 :                                         cb(word, str, tok);
     261         750 :                                 }
     262         500 :                                 return isComplexWord(tok) ? ParserStatus::PreventSubdivide : ParserStatus::Continue;
     263             :                         }
     264       39575 :                 }
     265       98925 :                 stemWord(word, tok, cb);
     266       98925 :                 return ParserStatus::Continue;
     267             :         });
     268         550 : }
     269             : 
     270       10100 : size_t Configuration::makeSearchVector(SearchVector &vec, StringView str, SearchData::Rank rank, size_t counter,
     271             :                 const Callback<void(StringView, StringView, ParserToken)> &cb) const {
     272       10100 :         if (str.empty()) {
     273          50 :                 return counter;
     274             :         }
     275             : 
     276       41975 :         auto pushWord = [&] (StringView s) -> const StringView * {
     277      121600 :                 ++ vec.documentLength;
     278       41975 :                 auto it = vec.words.find(s);
     279       41975 :                 if (it == vec.words.end()) {
     280       18825 :                         return &vec.words.emplace(s.pdup(vec.words.get_allocator()), SearchVector::MatchVector({pair(counter, rank)})).first->first;
     281             :                 } else {
     282       23150 :                         auto value = pair(counter, rank);
     283       23150 :                         auto iit = std::lower_bound(it->second.begin(), it->second.end(), value,
     284       40900 :                                         [&] (const Pair<size_t, SearchData::Rank> &l, const Pair<size_t, SearchData::Rank> &r) {
     285       40900 :                                 if (l.first != r.first) {
     286       40900 :                                         return l.first < r.first;
     287             :                                 } else {
     288           0 :                                         return toInt(l.second) < toInt(r.second);
     289             :                                 }
     290       23150 :                         });
     291       23150 :                         if (iit == it->second.end()) {
     292       23150 :                                 it->second.emplace_back(value);
     293           0 :                         } else if (*iit != value) {
     294           0 :                                 it->second.emplace(iit, value);
     295             :                         }
     296       23150 :                         return &it->first;
     297             :                 }
     298       10050 :         };
     299             : 
     300       10050 :         parsePhrase(str, [&, this] (StringView word, ParserToken tok) {
     301       83850 :                 if (tok != ParserToken::Blank && !isWordPart(tok)) {
     302       45125 :                         ++ counter;
     303             :                 }
     304             : 
     305       83850 :                 if (data->preStem != nullptr && !isWordPart(tok)) {
     306       29300 :                         auto ret = data->preStem(word, tok);
     307       29300 :                         if (ret.size() == 1) {
     308         375 :                                 auto str = normalizeWord(ret.back());
     309         375 :                                 if (auto sPtr = pushWord(str)) {
     310         375 :                                         if (cb != nullptr) { cb(*sPtr, word, tok); }
     311         375 :                                         return isComplexWord(tok) ? ParserStatus::PreventSubdivide : ParserStatus::Continue;
     312             :                                 }
     313       29300 :                         } else if (!ret.empty()) {
     314         500 :                                 for (auto &it : ret) {
     315         375 :                                         auto str = normalizeWord(it);
     316         375 :                                         pushWord(str);
     317         375 :                                 }
     318         125 :                                 return isComplexWord(tok) ? ParserStatus::PreventSubdivide : ParserStatus::Continue;
     319             :                         }
     320       29300 :                 }
     321             : 
     322       83350 :                 stemWord(word, tok, [&] (StringView w, StringView s, ParserToken tok) {
     323       41225 :                         if (!s.empty()) {
     324       41225 :                                 if (auto sPtr = pushWord(s)) {
     325       41225 :                                         if (cb != nullptr) { cb(*sPtr, word, tok); }
     326             :                                 }
     327             :                         }
     328       41225 :                 });
     329       83350 :                 return ParserStatus::Continue;
     330             :         });
     331             : 
     332       10050 :         return counter;
     333             : }
     334             : 
     335         775 : String Configuration::encodeSearchVectorPostgres(const SearchVector &vec, SearchData::Rank rank) const {
     336         775 :         StringStream ret;
     337        6200 :         for (auto &it : vec.words) {
     338        5425 :                 if (!ret.empty()) {
     339        4650 :                         ret << " ";
     340             :                 }
     341             : 
     342        5425 :                 StringView r(it.first);
     343        5425 :                 ret << "'";
     344       10850 :                 while (!r.empty()) {
     345        5425 :                         auto v = r.readUntil<StringView::Chars<'\''>>();
     346        5425 :                         if (!v.empty()) {
     347        5425 :                                 ret << v;
     348             :                         }
     349        5425 :                         if (r.is('\'')) {
     350          25 :                                 ret << "''";
     351          25 :                                 ++ r;
     352             :                         }
     353             :                 }
     354        5425 :                 ret << "':";
     355       23175 :                 for (auto &v : it.second) {
     356       17750 :                         if (ret.weak().back() != ':') { ret << ","; }
     357       17750 :                         ret << v.first;
     358       17750 :                         auto r = v.second;
     359       17750 :                         if (r == SearchRank::Unknown) {
     360           0 :                                 r = rank;
     361             :                         }
     362       17750 :                         switch (r) {
     363       14750 :                         case SearchRank::A: ret << 'A'; break;
     364        3000 :                         case SearchRank::B: ret << 'B'; break;
     365           0 :                         case SearchRank::C: ret << 'C'; break;
     366           0 :                         case SearchRank::D:
     367             :                         case SearchRank::Unknown:
     368           0 :                                 break;
     369             :                         }
     370             :                 }
     371             :         }
     372        1550 :         return ret.str();
     373         775 : }
     374             : 
     375        1825 : Bytes Configuration::encodeSearchVectorData(const SearchVector &data, SearchData::Rank rank) const {
     376        1825 :         data::cbor::Encoder<Interface> enc(true);
     377        1825 :         data::cbor::_writeArrayStart(enc, 3);
     378        1825 :         data::cbor::_writeInt(enc, 1); // version
     379        1825 :         data::cbor::_writeInt(enc, data.documentLength); // version
     380        1825 :         data::cbor::_writeMapStart(enc, data.words.size());
     381       14925 :         for (auto &it : data.words) {
     382       13100 :                 enc.write(it.first);
     383       13100 :                 _writeArrayStart(enc, it.second.size() * 2);
     384       36875 :                 for (auto &iit : it.second) {
     385       23775 :                         data::cbor::_writeInt(enc, iit.first);
     386       23775 :                         data::cbor::_writeInt(enc, toInt( (iit.second == SearchData::Rank::Unknown) ? rank : iit.second ));
     387             :                 }
     388             :         }
     389        1825 :         auto result = enc.data();
     390        1825 :         auto r = data::compress<Interface>(result.data(), result.size(), EncodeFormat::Compression::LZ4HCCompression, true);
     391        1825 :         if (r.empty()) {
     392        1775 :                 return result;
     393             :         }
     394          50 :         return r;
     395        1825 : }
     396             : 
     397          25 : void Configuration::stemHtml(const StringView &str, const StemWordCallback &cb) const {
     398          25 :         parseHtml(str, [&, this] (StringView str) {
     399         175 :                 stemPhrase(str, cb);
     400         175 :         });
     401          25 : }
     402             : 
     403      208425 : bool Configuration::stemWord(const StringView &word, ParserToken tok, const StemWordCallback &cb) const {
     404      208425 :         auto it = data->stemmers.find(tok);
     405      208425 :         if (it != data->stemmers.end()) {
     406         100 :                 return it->second(word, [&] (StringView stem) {
     407          50 :                         cb(word, stem, tok);
     408          50 :                 });
     409             :         } else {
     410      416750 :                 return stemWordDefault(data->language, getEnvForToken(tok), tok, word, [&] (StringView stem) {
     411      110625 :                         cb(word, stem, tok);
     412      416750 :                 }, data->customStopwords);
     413             :         }
     414             : }
     415             : 
     416      208425 : StemmerEnv *Configuration::getEnvForToken(ParserToken tok) const {
     417      208425 :         switch (tok) {
     418       58250 :         case ParserToken::AsciiWord:
     419             :         case ParserToken::AsciiHyphenatedWord:
     420             :         case ParserToken::HyphenatedWord_AsciiPart:
     421       58250 :                 if (data->secondary) {
     422       47100 :                         if (memory::pool::acquire() == StemmerEnv_getUserData(data->secondary)) {
     423           0 :                                 return data->secondary;
     424             :                         } else {
     425       47100 :                                 return Configuration_makeLocalConfig(data->secondary);
     426             :                         }
     427             :                 }
     428       11150 :                 break;
     429       35375 :         case ParserToken::Word:
     430             :         case ParserToken::HyphenatedWord:
     431             :         case ParserToken::HyphenatedWord_Part:
     432       35375 :                 if (data->primary) {
     433       34825 :                         if (memory::pool::acquire() == StemmerEnv_getUserData(data->primary)) {
     434           0 :                                 return data->primary;
     435             :                         } else {
     436       34825 :                                 return Configuration_makeLocalConfig(data->primary);
     437             :                         }
     438             :                 }
     439         550 :                 break;
     440             : 
     441      114800 :         case ParserToken::NumWord:
     442             :         case ParserToken::NumHyphenatedWord:
     443             :         case ParserToken::HyphenatedWord_NumPart:
     444             :         case ParserToken::Email:
     445             :         case ParserToken::Url:
     446             :         case ParserToken::Version:
     447             :         case ParserToken::Path:
     448             :         case ParserToken::Integer:
     449             :         case ParserToken::Float:
     450             :         case ParserToken::ScientificFloat:
     451             :         case ParserToken::XMLEntity:
     452             :         case ParserToken::Custom:
     453             :         case ParserToken::Blank:
     454      114800 :                 return nullptr;
     455             :         }
     456       11700 :         return nullptr;
     457             : }
     458             : 
     459         100 : String Configuration::makeHeadline(const HeadlineConfig &cfg, const StringView &origin, const Vector<String> &stemList) const {
     460         100 :         memory::PoolInterface::StringStreamType result;
     461         100 :         result.reserve(origin.size() + (cfg.startToken.size() + cfg.stopToken.size()) * stemList.size());
     462             : 
     463         100 :         bool isOpen = false;
     464         100 :         StringViewUtf8 r(origin);
     465         100 :         StringView dropSep;
     466             : 
     467         100 :         parsePhrase(origin, [&, this] (StringView word, ParserToken tok) {
     468         900 :                 auto status = ParserStatus::Continue;
     469        1400 :                 if (tok == ParserToken::Blank || !stemWord(word, tok, [&, this] (StringView word, StringView stem, ParserToken tok) {
     470         500 :                         auto it = std::lower_bound(stemList.begin(), stemList.end(), stem);
     471         500 :                         if (it != stemList.end() && *it == stem) {
     472         100 :                                 if (!isOpen) {
     473         100 :                                         result << cfg.startToken;
     474         100 :                                         isOpen = true;
     475         500 :                                 } else if (!dropSep.empty()) {
     476           0 :                                         result << dropSep;
     477           0 :                                         dropSep.clear();
     478             :                                 }
     479         100 :                                 if (isComplexWord(tok)) {
     480           0 :                                         status = ParserStatus::PreventSubdivide;
     481             :                                 }
     482             :                         } else {
     483         400 :                                 if (isOpen) {
     484         100 :                                         result << cfg.stopToken;
     485         100 :                                         isOpen = false;
     486         100 :                                         if (!dropSep.empty()) {
     487         100 :                                                 result << dropSep;
     488         100 :                                                 dropSep.clear();
     489             :                                         }
     490             :                                 }
     491         400 :                                 if (isComplexWord(tok)) {
     492           0 :                                         return;
     493             :                                 }
     494             :                         }
     495         500 :                         result << word;
     496             :                 })) {
     497         400 :                         if (isOpen) {
     498         100 :                                 if (!dropSep.empty()) {
     499           0 :                                         dropSep = StringView(dropSep.data(), word.data() + word.size() - dropSep.data());
     500             :                                 } else {
     501         100 :                                         dropSep = word;
     502             :                                 }
     503             :                         } else {
     504         300 :                                 result << word;
     505             :                         }
     506             :                 }
     507         900 :                 return status;
     508             :         });
     509             : 
     510         100 :         if (isOpen) {
     511           0 :                 result << cfg.stopToken;
     512           0 :                 isOpen = false;
     513             :         }
     514             : 
     515         200 :         return result.str();
     516         100 : }
     517             : 
     518         225 : String Configuration::makeHtmlHeadlines(const HeadlineConfig &cfg, const StringView &origin, const Vector<String> &stemList, size_t count) const {
     519         225 :         return makeHeadlines(cfg, [&] (const Function<bool(const StringView &, const StringView &)> &cb) {
     520         225 :                 Stemmer_Reader_run(origin, [&] (const StringView &str, const Callback<void()> &cancelCb) {
     521        1300 :                         if (!cb(str, StringView())) {
     522          50 :                                 cancelCb();
     523             :                         }
     524        1300 :                 });
     525         450 :         }, stemList, count);
     526             : }
     527             : 
     528         225 : String Configuration::makeHeadlines(const HeadlineConfig &cfg, const Callback<void(const Function<bool(const StringView &frag, const StringView &tag)>)> &cb,
     529             :                 const Vector<String> &stemList, size_t count) const {
     530             : 
     531             :         using SplitTokens = StringViewUtf8::MatchCompose<
     532             :                 StringViewUtf8::MatchCharGroup<CharGroupId::WhiteSpace>,
     533             :                 StringViewUtf8::MatchChars<'-', u'—', u'\'', u'«', u'»', u'’', u'“', '(', ')', '"', ',', '*', ':', ';', '/', '\\'>>;
     534             : 
     535             :         using TrimToken = StringView::Chars<'.'>;
     536             : 
     537             :         struct WordIndex {
     538             :                 StringView word;
     539             :                 uint16_t index;
     540             :                 uint16_t selectedCount; // selected words in block
     541             :                 uint16_t allWordsCount; // all words in block;
     542             :                 const WordIndex *end;
     543             :         };
     544             : 
     545         225 :         WordIndex *topIndex = nullptr;
     546         225 :         StringStream ret; ret.reserve(1_KiB);
     547             : 
     548         625 :         auto rateWord = [&] (WordIndex &index, WordIndex *list, size_t listCount) {
     549         625 :                 index.end = &index;
     550         625 :                 index.selectedCount = 1;
     551         625 :                 index.allWordsCount = 1;
     552         950 :                 while (listCount > 0) {
     553         350 :                         uint16_t offset = list->index - index.index;
     554         350 :                         if (offset < cfg.maxWords) {
     555         325 :                                 ++ index.selectedCount;
     556         325 :                                 index.allWordsCount = offset;
     557         325 :                                 index.end = list;
     558             :                         } else {
     559          25 :                                 break;
     560             :                         }
     561             : 
     562         325 :                         ++ list;
     563         325 :                         -- listCount;
     564             :                 }
     565             : 
     566         625 :                 if (!topIndex || index.selectedCount > topIndex->selectedCount
     567         275 :                                 || (index.selectedCount == topIndex->selectedCount && index.allWordsCount < topIndex->allWordsCount)) {
     568         350 :                         topIndex = &index;
     569             :                 }
     570         850 :         };
     571             : 
     572         350 :         auto writeFragmentWords = [&] (StringStream &out, const StringView &str, const std::array<WordIndex, 32> &words, const WordIndex *word) {
     573         350 :                 bool isOpen = false;
     574         600 :                 for (auto it = word; it < word->end; ++ it) {
     575         250 :                         if (!isOpen) {
     576         225 :                                 out << cfg.startToken;
     577             :                         }
     578         250 :                         out << it->word;
     579         250 :                         auto next = it + 1;
     580         250 :                         if (next <= word->end && it->index + 1 == next->index) {
     581         150 :                                 isOpen = true;
     582             :                         } else {
     583         100 :                                 isOpen = false;
     584         100 :                                 out << cfg.stopToken;
     585             :                         }
     586             : 
     587         250 :                         if (next <= word->end) {
     588         250 :                                 out << StringView(it->word.data() + it->word.size(), next->word.data() - (it->word.data() + it->word.size()));
     589             :                         }
     590             :                 }
     591             : 
     592         350 :                 if (!isOpen) {
     593         225 :                         out << cfg.startToken;
     594             :                 }
     595         350 :                 out << word->end->word;
     596         350 :                 out << cfg.stopToken;
     597         350 :                 isOpen = false;
     598         350 :         };
     599             : 
     600         125 :         auto makeFragmentPrefix = [&] (StringStream &out, const StringView &str, size_t numWords, size_t allWords) {
     601         125 :                 if (numWords == allWords) {
     602           0 :                         out << str;
     603           0 :                         return;
     604         125 :                 } else if (numWords == 0) {
     605           0 :                         return;
     606             :                 }
     607             : 
     608         125 :                 StringViewUtf8 r(str);
     609         800 :                 while (!r.empty() && numWords > 0) {
     610         675 :                         r.backwardSkipChars<SplitTokens>();
     611         675 :                         auto tmp = StringViewUtf8(r.backwardReadUntil<SplitTokens>());
     612             : 
     613         675 :                         auto tmpR = tmp;
     614         675 :                         tmpR.trimChars<TrimToken>();
     615             : 
     616         675 :                         if (string::getUtf16Length(tmpR) > cfg.shortWord) {
     617         500 :                                 -- numWords;
     618             :                         }
     619             :                 }
     620             : 
     621         125 :                 if (!r.empty()) {
     622         125 :                         out << cfg.separator << " ";
     623             :                 }
     624             : 
     625         125 :                 out << StringView(r.data() + r.size(), (str.data() + str.size()) - (r.data() + r.size()));
     626         225 :         };
     627             : 
     628         175 :         auto makeFragmentSuffix = [&] (StringStream &out, const StringView &str, size_t numWords, size_t allWords) {
     629         175 :                 if (numWords == allWords) {
     630           0 :                         out << str;
     631           0 :                         return;
     632         175 :                 } else if (numWords == 0) {
     633           0 :                         return;
     634             :                 }
     635             : 
     636         175 :                 StringViewUtf8 r(str);
     637        1250 :                 while (!r.empty() && numWords > 0) {
     638        1075 :                         auto sep = StringViewUtf8(r.readChars<SplitTokens>());
     639        1075 :                         auto tmp = StringViewUtf8(r.readUntil<SplitTokens>());
     640             : 
     641        1075 :                         auto tmpR = tmp;
     642        1075 :                         tmpR.trimChars<TrimToken>();
     643             : 
     644        1075 :                         out << sep << tmp;
     645             : 
     646        1075 :                         if (string::getUtf16Length(tmpR) > cfg.shortWord) {
     647         825 :                                 -- numWords;
     648             :                         }
     649             :                 }
     650             : 
     651         175 :                 if (!r.empty()) {
     652         175 :                         out << " " << cfg.separator;
     653             :                 }
     654         225 :         };
     655             : 
     656         350 :         auto makeFragment = [&] (StringStream &out, const StringView &str, const StringView &tagId, const std::array<WordIndex, 32> &words, const WordIndex *word, size_t idx) {
     657         350 :                 out << cfg.startFragment;
     658         350 :                 auto prefixView = StringView(str.data(), word->word.data() - str.data());
     659         350 :                 auto suffixView = StringView(word->end->word.data() + word->end->word.size(), str.data() + str.size() - (word->end->word.data() + word->end->word.size()));
     660         350 :                 if (idx < cfg.maxWords) {
     661             :                         // write whole block
     662         125 :                         out << prefixView;
     663         125 :                         writeFragmentWords(out, str, words, word);
     664         125 :                         out << suffixView;
     665         225 :                 } else if (word->allWordsCount < cfg.minWords) {
     666             :                         // make offsets
     667         175 :                         size_t availStart = word->index;
     668         175 :                         size_t availEnd = idx - word->end->index - 1;
     669         175 :                         size_t diff = (cfg.minWords - word->allWordsCount) + 1;
     670             : 
     671         175 :                         if (availStart >= diff / 2 && availEnd >= diff / 2) {
     672          50 :                                 makeFragmentPrefix(out, prefixView, diff / 2, word->index);
     673          50 :                                 writeFragmentWords(out, str, words, word);
     674          50 :                                 makeFragmentSuffix(out, suffixView, diff / 2, idx - word->end->index - 1);
     675         125 :                         } else if (availStart < diff / 2 && availEnd < diff / 2) {
     676          25 :                                 out << prefixView;
     677          25 :                                 writeFragmentWords(out, str, words, word);
     678          25 :                                 out << suffixView;
     679         100 :                         } else if (availStart < diff / 2) {
     680          75 :                                 out << prefixView;
     681          75 :                                 writeFragmentWords(out, str, words, word);
     682          75 :                                 makeFragmentSuffix(out, suffixView, diff - availStart - 1, idx - word->end->index - 1);
     683          25 :                         } else if (availEnd < diff / 2) {
     684          25 :                                 makeFragmentPrefix(out, prefixView, diff - availEnd - 1, word->index);
     685          25 :                                 writeFragmentWords(out, str, words, word);
     686          25 :                                 out << suffixView;
     687             :                         }
     688             :                 } else {
     689             :                         // try minimal offsets
     690          50 :                         makeFragmentPrefix(out, prefixView, 1, word->index);
     691          50 :                         writeFragmentWords(out, str, words, word);
     692          50 :                         makeFragmentSuffix(out, suffixView, 1, idx - word->end->index - 1);
     693             :                 }
     694         350 :                 out << cfg.stopFragment;
     695             : 
     696         350 :                 if (cfg.fragmentCallback) {
     697          75 :                         cfg.fragmentCallback(out.weak(), tagId);
     698             :                 }
     699         350 :         };
     700             : 
     701         225 :         cb([&, this] (const StringView &str, const StringView &fragmentTag) -> bool {
     702        1300 :                 std::array<WordIndex, 32> wordsMatch;
     703        1300 :                 uint16_t wordCount = 0;
     704        1300 :                 uint16_t idx = 0;
     705             : 
     706        1300 :                 bool enabledComplex = false;
     707        1300 :                 parsePhrase(str, [&, this] (StringView word, ParserToken tok) {
     708       63300 :                         auto status = ParserStatus::Continue;
     709       63300 :                         if (tok != ParserToken::Blank && string::getUtf16Length(word) > cfg.shortWord && wordCount < 32) {
     710       22475 :                                 if (enabledComplex) {
     711         100 :                                         if (isWordPart(tok)) {
     712          75 :                                                 wordsMatch[wordCount] = WordIndex{word, idx};
     713          75 :                                                 ++ wordCount;
     714          75 :                                                 ++ idx;
     715          75 :                                                 return status;
     716             :                                         } else {
     717          25 :                                                 enabledComplex = false;
     718             :                                         }
     719             :                                 }
     720       22400 :                                 stemWord(word, tok, [&] (StringView word, StringView stem, ParserToken tok) {
     721       44625 :                                         auto it = std::lower_bound(stemList.begin(), stemList.end(), stem);
     722       20925 :                                         if (it != stemList.end() && string::caseCompare_u(*it, stem) == 0) {
     723         575 :                                                 if (isComplexWord(tok)) {
     724          25 :                                                         enabledComplex = true;
     725             :                                                 } else {
     726         550 :                                                         wordsMatch[wordCount] = WordIndex{word, idx};
     727         550 :                                                         ++ wordCount;
     728             :                                                 }
     729             :                                         }
     730       20925 :                                 });
     731       22400 :                                 ++ idx;
     732             :                         }
     733       63225 :                         return status;
     734             :                 });
     735             : 
     736        1300 :                 if (wordCount == 0) {
     737         950 :                         return true;
     738             :                 }
     739             : 
     740         975 :                 for (size_t i = 0; i < wordCount; ++ i) {
     741         625 :                         rateWord(wordsMatch[i], &wordsMatch[i + 1], wordCount - 1 - i);
     742             :                 }
     743             : 
     744         350 :                 if (topIndex && count > 0) {
     745         350 :                         if (cfg.fragmentCallback) {
     746          75 :                                 StringStream out;
     747          75 :                                 makeFragment(out, str, fragmentTag, wordsMatch, topIndex, idx);
     748          75 :                                 ret << out.weak();
     749          75 :                         } else {
     750         275 :                                 makeFragment(ret, str, fragmentTag, wordsMatch, topIndex, idx);
     751         275 :                                 -- count;
     752             :                         }
     753             :                 }
     754             : 
     755         350 :                 if (count == 0) {
     756          50 :                         return false;
     757             :                 }
     758             : 
     759         300 :                 topIndex = nullptr;
     760         300 :                 return true;
     761             :         });
     762         450 :         return ret.str();
     763         225 : }
     764             : 
     765         325 : Vector<String> Configuration::stemQuery(const SearchQuery &query) const {
     766         325 :         Vector<String> queryList;
     767         325 :         doStemQuery(queryList, query);
     768         325 :         return queryList;
     769           0 : }
     770             : 
     771         800 : void Configuration::doStemQuery(Vector<String> &queryList, const SearchQuery &query) const {
     772         800 :         if (!query.value.empty()) {
     773         475 :                 emplace_ordered(queryList, query.value);
     774             :         }
     775        1275 :         for (auto &it : query.args) {
     776         475 :                 doStemQuery(queryList, it);
     777             :         }
     778         800 : }
     779             : 
     780             : struct Configuration_ParserControl {
     781             :         Vector<SearchQuery *> stack;
     782             :         StringView error;
     783             :         bool neg = false;
     784             :         bool success = true;
     785             :         bool strict = false;
     786             : 
     787        3925 :         bool popNeg() {
     788        3925 :                 auto tmp = neg;
     789        3925 :                 neg = false;
     790        3925 :                 return tmp;
     791             :         }
     792             : 
     793         325 :         void pushNeg() {
     794         325 :                 neg = !neg;
     795         325 :         }
     796             : };
     797             : 
     798        3100 : static bool Configuration_parseQueryBlank(Configuration_ParserControl &control, StringView r) {
     799         275 :         auto makeShift = [&] (SearchQuery *q, SearchOp op) {
     800         275 :                 SearchQuery tmp = move(*q);
     801         275 :                 q->clear();
     802         275 :                 q->op = op;
     803         275 :                 q->args.emplace_back(move(tmp));
     804         275 :         };
     805             : 
     806        7625 :         while (!r.empty()) {
     807        4600 :                 auto q = control.stack.back();
     808        4600 :                 r.skipUntil<StringView::Chars<'"', '|', '!', '(', ')'>>();
     809        4600 :                 if (q->block == SearchQuery::Quoted) {
     810             :                         // ignore any punctuation within quotes
     811        1100 :                         if (r[0] != '"') {
     812         725 :                                 ++ r;
     813         725 :                                 continue;
     814             :                         }
     815             :                 }
     816        3875 :                 switch (r[0]) {
     817         775 :                 case '"':
     818         775 :                         if (q->block != SearchQuery::Quoted) {
     819         400 :                                 if (q->op == SearchOp::None) {
     820         100 :                                         q->op = SearchOp::Follow;
     821         100 :                                         q->block = SearchQuery::Quoted;
     822             :                                 } else {
     823         300 :                                         if (q->op == SearchOp::Or) {
     824          25 :                                                 makeShift(q, SearchOp::And);
     825             :                                         }
     826         300 :                                         auto &top = q->args.emplace_back();
     827         300 :                                         top.op = SearchOp::Follow;
     828         300 :                                         top.block = SearchQuery::Quoted;
     829         300 :                                         top.neg = control.popNeg();
     830         300 :                                         control.stack.emplace_back(&top);
     831             :                                 }
     832             :                         } else {
     833         375 :                                 control.stack.pop_back();
     834             :                         }
     835         775 :                         break;
     836         625 :                 case '|':
     837         625 :                         if (q->op == SearchOp::Or) {
     838         100 :                                 auto &top = q->args.emplace_back();
     839         100 :                                 control.stack.emplace_back(&top);
     840         525 :                         } else if (q->op == SearchOp::And && q->args.size() <= 1) {
     841         325 :                                 q->op = SearchOp::Or;
     842         325 :                                 auto &top = q->args.emplace_back();
     843         325 :                                 control.stack.emplace_back(&top);
     844         200 :                         } else if (q->op != SearchOp::None || (q->op == SearchOp::None && !q->value.empty()) ) {
     845         200 :                                 makeShift(q, SearchOp::Or);
     846         200 :                                 auto &top = q->args.emplace_back();
     847         200 :                                 control.stack.emplace_back(&top);
     848             :                         }
     849         625 :                         break;
     850         325 :                 case '!': {
     851         325 :                         control.pushNeg();
     852         325 :                         break;
     853             :                 }
     854         550 :                 case '(':
     855         550 :                         if (q->block == SearchQuery::None) {
     856         500 :                                 if (q->op == SearchOp::None) {
     857         150 :                                         q->op = SearchOp::And;
     858         150 :                                         q->block = SearchQuery::Parentesis;
     859             :                                 } else {
     860         350 :                                         if (q->op == SearchOp::Or) {
     861          50 :                                                 makeShift(q, SearchOp::And);
     862             :                                         }
     863         350 :                                         auto &top = q->args.emplace_back();
     864         350 :                                         top.op = SearchOp::And;
     865         350 :                                         top.block = SearchQuery::Parentesis;
     866         350 :                                         top.neg = control.popNeg();
     867         350 :                                         control.stack.emplace_back(&top);
     868             :                                 }
     869             :                         } else {
     870          50 :                                 control.error = StringView("Invalid '(' token within block");
     871          50 :                                 if (control.strict) {
     872          25 :                                         return false;
     873             :                                 }
     874             :                         }
     875         525 :                         break;
     876         550 :                 case ')':
     877         550 :                         if (q->block == SearchQuery::Parentesis) {
     878         400 :                                 control.stack.pop_back();
     879             :                         } else {
     880         150 :                                 control.error = StringView("Invalid ')' outside of parenthesis");
     881         150 :                                 if (control.strict) {
     882          50 :                                         return false;
     883             :                                 }
     884             :                         }
     885         500 :                         break;
     886        1050 :                 default: break;
     887             :                 }
     888             : 
     889        3800 :                 ++ r;
     890             :         }
     891        3025 :         return true;
     892             : }
     893             : 
     894        3325 : static bool Configuration_parseQueryWord(Configuration_ParserControl &control, StringView word, size_t offset = 0, StringView source = StringView()) {
     895        3325 :         auto q = control.stack.back();
     896             : 
     897        3325 :         if (q->op == SearchOp::None) {
     898         375 :                 if (q->value.empty()) {
     899         375 :                         q->value = word.str<memory::PoolInterface>();
     900         375 :                         q->source = source;
     901         375 :                         q->offset = offset;
     902         375 :                         q->neg = control.popNeg();
     903         375 :                         if (control.stack.size() > 1) {
     904         375 :                                 control.stack.pop_back();
     905             :                         }
     906             :                 } else {
     907           0 :                         control.error = StringView("Invalid element");
     908           0 :                         if (control.strict) {
     909           0 :                                 return false;
     910             :                         }
     911             :                 }
     912        2950 :         } else if (q->op == SearchOp::And || q->op == SearchOp::Follow) {
     913        2900 :                 auto &v = q->args.emplace_back(SearchQuery(word, offset, source));
     914        2900 :                 v.neg = control.popNeg();
     915        2900 :         } else {
     916          50 :                 SearchQuery tmp = move(*q);
     917          50 :                 q->clear();
     918          50 :                 q->op = SearchOp::And;
     919          50 :                 q->args.emplace_back(move(tmp));
     920          50 :                 q->args.emplace_back(SearchQuery(word, offset, source));
     921          50 :         }
     922        3325 :         return true;
     923             : }
     924             : 
     925        1150 : SearchQuery Configuration::parseQuery(StringView str, bool strict, StringView *err) const {
     926        1150 :         SearchQuery query;
     927        1150 :         query.op = SearchOp::And;
     928             : 
     929        1150 :         Configuration_ParserControl control;
     930        1150 :         control.stack.emplace_back(&query);
     931        1150 :         control.strict = strict;
     932             : 
     933        1150 :         size_t prev = 0;
     934        1150 :         size_t counter = 0;
     935        1150 :         auto ret = search::parsePhrase(str, [&, this] (StringView word, ParserToken tok) {
     936        6425 :                 auto status = isComplexWord(tok) ? ParserStatus::PreventSubdivide : ParserStatus::Continue;
     937        6425 :                 if (tok == ParserToken::Blank) {
     938        3100 :                         if (!Configuration_parseQueryBlank(control, word)) {
     939          75 :                                 return ParserStatus::Stop;
     940             :                         }
     941             :                 } else {
     942        6650 :                         ++ counter;
     943        3325 :                         if (data->preStem && !isWordPart(tok)) {
     944         200 :                                 auto ret = data->preStem(word, tok);
     945         200 :                                 if (!ret.empty()) {
     946        3325 :                                         auto offset = counter - prev;
     947          75 :                                         prev = counter;
     948         300 :                                         for (auto &it : ret) {
     949         225 :                                                 auto str = normalizeWord(it);
     950         225 :                                                 if (!Configuration_parseQueryWord(control, str, offset, word)) {
     951           0 :                                                         return ParserStatus::Stop;
     952             :                                                 }
     953         225 :                                         }
     954          75 :                                         return isComplexWord(tok) ? ParserStatus::PreventSubdivide : ParserStatus::Continue;
     955             :                                 }
     956         200 :                         }
     957        3250 :                         stemWord(word, tok, [&] (StringView w, StringView s, ParserToken tok) {
     958        3100 :                                 if (!s.empty()) {
     959        3100 :                                         if (!Configuration_parseQueryWord(control, s, counter - prev, w)) {
     960           0 :                                                 status = ParserStatus::Stop;
     961             :                                         }
     962        3100 :                                         prev = counter;
     963             :                                 }
     964        3100 :                         });
     965             :                 }
     966        6275 :                 return status;
     967             :         });
     968             : 
     969        1150 :         if (!ret) {
     970          75 :                 if (err) {
     971           0 :                         *err = control.error;
     972             :                 }
     973          75 :                 return SearchQuery();
     974             :         }
     975             : 
     976        1075 :         if (control.stack.back()->block == SearchQuery::Block::Quoted
     977          25 :                         && control.stack.back()->op == SearchOp::Follow
     978        1100 :                         && control.stack.back()->args.empty()) {
     979          25 :                 auto v = control.stack.back();
     980          25 :                 control.stack.pop_back();
     981          25 :                 if (!control.stack.empty()) {
     982          25 :                         if (&control.stack.back()->args.back() == v) {
     983          25 :                                 control.stack.back()->args.pop_back();
     984             :                         }
     985             :                 }
     986             :         }
     987             : 
     988        1075 :         query.normalize();
     989        1075 :         return query;
     990        1150 : }
     991             : 
     992          50 : bool Configuration::isMatch(const SearchVector &vec, StringView q) const {
     993          50 :         auto query = parseQuery(q);
     994         100 :         return query.isMatch(vec);
     995          50 : }
     996             : 
     997          50 : bool Configuration::isMatch(const SearchVector &vec, const SearchQuery &query) const {
     998          50 :         return query.isMatch(vec);
     999             : }
    1000             : }

Generated by: LCOV version 1.14