// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #pragma once #include #include #include #include // Model how boolean result decisions flow through intermediate nodes // of different types based on relative estimates for sub-expressions namespace search::queryeval { // Encapsulate information about strictness and in-flow in a structure // for convenient parameter passing. We do not need an explicit value // in the strict case since strict basically means the receiving end // will eventually decide the actual flow. We use a rate of 1.0 for // strict flow to indicate that the corpus is not reduced externally. class InFlow { private: double _value; public: constexpr InFlow(bool strict, double rate) noexcept : _value(strict ? -1.0 : std::max(rate, 0.0)) {} constexpr InFlow(bool strict) noexcept : InFlow(strict, 1.0) {} constexpr InFlow(double rate) noexcept : InFlow(false, rate) {} constexpr bool strict() noexcept { return _value < 0.0; } constexpr double rate() noexcept { return strict() ? 1.0 : _value; } }; struct FlowStats { double estimate; double cost; double strict_cost; constexpr FlowStats(double estimate_in, double cost_in, double strict_cost_in) noexcept : estimate(estimate_in), cost(cost_in), strict_cost(strict_cost_in) {} auto operator <=>(const FlowStats &rhs) const noexcept = default; }; namespace flow { // the default adapter expects the shape of std::unique_ptr // with respect to estimate, cost and strict_cost. struct DefaultAdapter { double estimate(const auto &child) const noexcept { return child->estimate(); } double cost(const auto &child) const noexcept { return child->cost(); } double strict_cost(const auto &child) const noexcept { return child->strict_cost(); } }; template concept DefaultAdaptable = requires(const T &t) { { t->estimate() } -> std::same_as; { t->cost() } -> std::same_as; { t->strict_cost() } -> std::same_as; }; // adapter making it possible to use FlowStats directly for testing struct DirectAdapter { double estimate(const auto &child) const noexcept { return child.estimate; } double cost(const auto &child) const noexcept { return child.cost; } double strict_cost(const auto &child) const noexcept { return child.strict_cost; } }; template concept DirectAdaptable = requires(const T &t) { { t.estimate } -> std::same_as; { t.cost } -> std::same_as; { t.strict_cost } -> std::same_as; }; auto make_adapter(const auto &children) { using type = std::remove_cvref_t::value_type; static_assert(DefaultAdaptable || DirectAdaptable, "unable to resolve children adapter"); if constexpr (DefaultAdaptable) { return DefaultAdapter(); } else { return DirectAdapter(); } } template struct IndirectAdapter { const T &data; [[no_unique_address]] ADAPTER adapter; IndirectAdapter(ADAPTER adapter_in, const T &data_in) noexcept : data(data_in), adapter(adapter_in) {} double estimate(size_t child) const noexcept { return adapter.estimate(data[child]); } double cost(size_t child) const noexcept { return adapter.cost(data[child]); } double strict_cost(size_t child) const noexcept { return adapter.strict_cost(data[child]); } }; inline auto make_index(size_t size) { vespalib::SmallVector index(size); for (size_t i = 0; i < size; ++i) { index[i] = i; } return index; } template struct MinAndCost { // sort children to minimize total cost of AND flow [[no_unique_address]] ADAPTER adapter; MinAndCost(ADAPTER adapter_in) noexcept : adapter(adapter_in) {} bool operator () (const auto &a, const auto &b) const noexcept { return (1.0 - adapter.estimate(a)) * adapter.cost(b) > (1.0 - adapter.estimate(b)) * adapter.cost(a); } }; template struct MinOrCost { // sort children to minimize total cost of OR flow [[no_unique_address]] ADAPTER adapter; MinOrCost(ADAPTER adapter_in) noexcept : adapter(adapter_in) {} bool operator () (const auto &a, const auto &b) const noexcept { return adapter.estimate(a) * adapter.cost(b) > adapter.estimate(b) * adapter.cost(a); } }; template double estimate_of_and(ADAPTER adapter, const T &children) { double flow = children.empty() ? 0.0 : adapter.estimate(children[0]); for (size_t i = 1; i < children.size(); ++i) { flow *= adapter.estimate(children[i]); } return flow; } template double estimate_of_or(ADAPTER adapter, const T &children) { double flow = 1.0; for (const auto &child: children) { flow *= (1.0 - adapter.estimate(child)); } return (1.0 - flow); } template double estimate_of_and_not(ADAPTER adapter, const T &children) { double flow = children.empty() ? 0.0 : adapter.estimate(children[0]); for (size_t i = 1; i < children.size(); ++i) { flow *= (1.0 - adapter.estimate(children[i])); } return flow; } template