/* ** Copyright (C) 2008-2021 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation; either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software Foundation, ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** */ #include #include #include #include #include #include #include #include #include #include #include "mu-msg-fields.h" #include "mu-query-results.hh" #include "mu-query-match-deciders.hh" #include "mu-query-threads.hh" #include using namespace Mu; struct Query::Private { Private(const Store& store) : store_{store}, parser_{store_} {} // New // bool calculate_threads (Xapian::Enquire& enq, size maxnum); Xapian::Enquire make_enquire(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags) const; Xapian::Enquire make_related_enquire(const StringSet& thread_ids, MuMsgFieldId sortfieldid, QueryFlags qflags) const; Option run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags, size_t max_size) const; Option run_singular(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const; Option run_related(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const; Option run(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const; const Store& store_; const Parser parser_; }; Query::Query(const Store& store) : priv_{std::make_unique(store)} {} Query::Query(Query&& other) = default; Query::~Query() = default; static Xapian::Enquire& maybe_sort(Xapian::Enquire& enq, MuMsgFieldId sortfieldid, QueryFlags qflags) { if (sortfieldid != MU_MSG_FIELD_ID_NONE) enq.set_sort_by_value(static_cast(sortfieldid), any_of(qflags & QueryFlags::Descending)); return enq; } Xapian::Enquire Query::Private::make_enquire(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags) const { Xapian::Enquire enq{store_.database()}; if (expr.empty() || expr == R"("")") enq.set_query(Xapian::Query::MatchAll); else { WarningVec warns; const auto tree{parser_.parse(expr, warns)}; for (auto&& w : warns) g_warning("query warning: %s", to_string(w).c_str()); enq.set_query(xapian_query(tree)); g_debug("qtree: %s", to_string(tree).c_str()); } return maybe_sort(enq, sortfieldid, qflags); } Xapian::Enquire Query::Private::make_related_enquire(const StringSet& thread_ids, MuMsgFieldId sortfieldid, QueryFlags qflags) const { Xapian::Enquire enq{store_.database()}; static std::string pfx(1, mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_THREAD_ID)); std::vector qvec; for (auto&& t : thread_ids) qvec.emplace_back(pfx + t); Xapian::Query qr{Xapian::Query::OP_OR, qvec.begin(), qvec.end()}; enq.set_query(qr); return maybe_sort(enq, sortfieldid, qflags); } struct ThreadKeyMaker : public Xapian::KeyMaker { ThreadKeyMaker(const QueryMatches& matches) : match_info_(matches) {} std::string operator()(const Xapian::Document& doc) const override { const auto it{match_info_.find(doc.get_docid())}; return (it == match_info_.end()) ? "" : it->second.thread_path; } const QueryMatches& match_info_; }; Option Query::Private::run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags, size_t maxnum) const { const auto descending{any_of(qflags & QueryFlags::Descending)}; calculate_threads(qres, descending); ThreadKeyMaker key_maker{qres.query_matches()}; enq.set_sort_by_key(&key_maker, descending); DeciderInfo minfo; minfo.matches = qres.query_matches(); auto mset{enq.get_mset(0, maxnum, {}, make_thread_decider(qflags, minfo).get())}; mset.fetch(); return QueryResults{mset, std::move(qres.query_matches())}; } Option Query::Private::run_singular(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const { // i.e. a query _without_ related messages, but still possibly // with threading. // // In the threading case, the sortfield-id is ignored, we always sort by // date (since threading the threading results are always by date.) const auto singular_qflags{qflags | QueryFlags::Leader}; const auto threading{any_of(qflags & QueryFlags::Threading)}; DeciderInfo minfo{}; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wextra" auto enq{make_enquire(expr, threading ? MU_MSG_FIELD_ID_DATE : sortfieldid, qflags)}; #pragma GCC diagnostic ignored "-Wswitch-default" #pragma GCC diagnostic pop auto mset{enq.get_mset(0, maxnum, {}, make_leader_decider(singular_qflags, minfo).get())}; mset.fetch(); auto qres{QueryResults{mset, std::move(minfo.matches)}}; return threading ? run_threaded(std::move(qres), enq, qflags, maxnum) : qres; } static Option opt_string(const Xapian::Document& doc, MuMsgFieldId id) noexcept { std::string val = xapian_try([&] { return doc.get_value(id); }, std::string{""}); if (val.empty()) return Nothing; else return Some(std::move(val)); } Option Query::Private::run_related(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const { // i.e. a query _with_ related messages and possibly with threading. // // In the threading case, the sortfield-id is ignored, we always sort by // date (since threading the threading results are always by date.); // moreover, in either threaded or non-threaded case, we sort the first // ("leader") query by date, i.e, we prefer the newest or oldest // (descending) messages. const auto leader_qflags{QueryFlags::Leader | qflags}; const auto threading{any_of(qflags & QueryFlags::Threading)}; // Run our first, "leader" query DeciderInfo minfo{}; auto enq{make_enquire(expr, MU_MSG_FIELD_ID_DATE, leader_qflags)}; const auto mset{ enq.get_mset(0, maxnum, {}, make_leader_decider(leader_qflags, minfo).get())}; // Gather the thread-ids we found mset.fetch(); for (auto it = mset.begin(); it != mset.end(); ++it) { auto thread_id{opt_string(it.get_document(), MU_MSG_FIELD_ID_THREAD_ID)}; if (thread_id) minfo.thread_ids.emplace(std::move(*thread_id)); } // Now, determine the "related query". // // In the threaded-case, we search among _all_ messages, since complete // threads are preferred; no need to sort in that case since the search // is unlimited and the sorting happens during threading. auto r_enq{make_related_enquire(minfo.thread_ids, threading ? MU_MSG_FIELD_ID_NONE : sortfieldid, qflags)}; const auto r_mset{r_enq.get_mset(0, threading ? store_.size() : maxnum, {}, make_related_decider(qflags, minfo).get())}; auto qres{QueryResults{r_mset, std::move(minfo.matches)}}; return threading ? run_threaded(std::move(qres), r_enq, qflags, maxnum) : qres; } Option Query::Private::run(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const { const auto eff_maxnum{maxnum == 0 ? store_.size() : maxnum}; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wextra" const auto eff_sortfield{sortfieldid == MU_MSG_FIELD_ID_NONE ? MU_MSG_FIELD_ID_DATE : sortfieldid}; #pragma GCC diagnostic pop if (any_of(qflags & QueryFlags::IncludeRelated)) return run_related(expr, eff_sortfield, qflags, eff_maxnum); else return run_singular(expr, eff_sortfield, qflags, eff_maxnum); } Option Query::run(const std::string& expr, MuMsgFieldId sortfieldid, QueryFlags qflags, size_t maxnum) const try { // some flags are for internal use only. g_return_val_if_fail(none_of(qflags & QueryFlags::Leader), Nothing); StopWatch sw{format("ran query '%s'; related: %s; threads: %s; max-size: %zu", expr.c_str(), any_of(qflags & QueryFlags::IncludeRelated) ? "yes" : "no", any_of(qflags & QueryFlags::Threading) ? "yes" : "no", maxnum)}; return priv_->run(expr, sortfieldid, qflags, maxnum); } catch (...) { return Nothing; } size_t Query::count(const std::string& expr) const { return xapian_try( [&] { const auto enq{priv_->make_enquire(expr, MU_MSG_FIELD_ID_NONE, {})}; auto mset{enq.get_mset(0, priv_->store_.size())}; mset.fetch(); return mset.size(); }, 0); } std::string Query::parse(const std::string& expr, bool xapian) const { WarningVec warns; const auto tree{priv_->parser_.parse(expr, warns)}; for (auto&& w : warns) g_warning("query warning: %s", to_string(w).c_str()); if (xapian) return xapian_query(tree).get_description(); else return to_string(tree); }