airborne12 commented on code in PR #68758: URL: https://github.com/apache/doris/pull/68758#discussion_r4265799316
########## be/src/storage/index/inverted/query/engine/complete_null_bitmap.cpp: ########## @@ -0,0 +1,367 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "storage/index/inverted/query_v2/complete_null_bitmap.h" + +#include <algorithm> +#include <array> +#include <memory> +#include <optional> +#include <span> +#include <utility> + +#include "core/custom_allocator.h" +#include "storage/index/collection_similarity.h" +#include "storage/index/inverted/query_v2/phrase_query/phrase_scorer.h" +#include "storage/index/inverted/query_v2/scored_rows_scorer.h" +#include "storage/index/inverted/query_v2/term_query/term_scorer.h" +#include "storage/index/query/boolean/truth_set.h" +#include "storage/index/query/exec/collect_postings.h" +#include "storage/index/query/roaring_docid_sink.h" + +namespace doris::segment_v2::inverted_index::query_v2 { +namespace { + +template <typename Next, typename Visitor> +roaring::Roaring consume_true_rows(uint32_t doc, Next&& next, Visitor&& visit) { + roaring::Roaring result; + std::array<uint32_t, 256> batch {}; + size_t count = 0; + while (doc != TERMINATED) { + batch[count++] = doc; + visit(doc); + if (count == batch.size()) { + result.addMany(count, batch.data()); + count = 0; + } + doc = next(); + } + if (count != 0) { + result.addMany(count, batch.data()); + } + return result; +} + +template <typename ScorerType, typename Visitor> +roaring::Roaring scan_candidate_rows(ScorerType& scorer, uint32_t doc, + const roaring::Roaring& candidates, Visitor&& visit) { + const uint32_t last = candidates.maximum(); + if (doc < candidates.minimum()) { + doc = scorer.seek(candidates.minimum()); + } + const auto find_match = [&]() { + while (doc != TERMINATED && doc <= last && !candidates.contains(doc)) { + doc = scorer.advance(); + } + return doc <= last ? doc : TERMINATED; + }; + const auto first = find_match(); + return consume_true_rows( + first, + [&]() { + if (doc == last) { + return TERMINATED; + } + doc = scorer.advance(); + return find_match(); + }, + std::forward<Visitor>(visit)); +} + +template <typename ScorerType, typename Visitor> +roaring::Roaring seek_candidate_rows(ScorerType& scorer, uint32_t doc, + const roaring::Roaring& candidates, Visitor&& visit) { + auto candidate = candidates.begin(); + const auto find_match = [&]() { + while (candidate.i.has_value && doc != TERMINATED) { + if (doc < *candidate) { + doc = scorer.seek(*candidate); + } + if (doc == *candidate) { + return doc; + } + candidate.equalorlarger(doc); + } + return TERMINATED; + }; + const auto first = find_match(); + return consume_true_rows( + first, + [&]() { + ++candidate; + return find_match(); + }, + std::forward<Visitor>(visit)); +} + +template <typename ScorerType, typename Visitor> +roaring::Roaring consume_typed_true_rows(ScorerType& scorer, uint32_t doc, + const roaring::Roaring* candidates, Visitor&& visit) { + const auto visit_doc = [&](uint32_t row) { visit(row, [&]() { return scorer.score(); }); }; + if (candidates == nullptr) { + return consume_true_rows( + doc, [&]() { return scorer.advance(); }, visit_doc); + } + if (candidates->isEmpty()) { + return {}; + } + // Sequential reads avoid repeated cursor searches when candidates are dense. + constexpr uint64_t seek_cost_multiplier = 4; + if (candidates->cardinality() * seek_cost_multiplier >= scorer.cost()) { + return scan_candidate_rows(scorer, doc, *candidates, visit_doc); + } + return seek_candidate_rows(scorer, doc, *candidates, visit_doc); +} + +template <bool VisitRows, typename Visitor> +roaring::Roaring consume_true_rows(Scorer& scorer, uint32_t doc, const roaring::Roaring* candidates, + Visitor&& visit) { + if (auto* term = dynamic_cast<TermScorer*>(&scorer); term != nullptr) { + roaring::Roaring result; + index_query::RoaringDocIdSink sink(result); + THROW_IF_ERROR(index_query::collect_postings<VisitRows>( + term->postings_doc_set(), candidates, sink, + [&](uint32_t row, uint32_t frequency, uint32_t norm) { + visit(row, [&]() { return term->score_for_posting(frequency, norm); }); + })); + return result; + } + if (auto* phrase = dynamic_cast<PhraseScorer<SegmentPostingsPtr>*>(&scorer); + phrase != nullptr) { + return consume_typed_true_rows(*phrase, doc, candidates, std::forward<Visitor>(visit)); + } + return consume_typed_true_rows(scorer, doc, candidates, std::forward<Visitor>(visit)); +} + +struct ScoredRow { + uint32_t doc; + float score; +}; + +class MaterializedScorer final : public Scorer { +public: + MaterializedScorer(const ScorerPtr& source, bool enable_scoring, + const NullBitmapResolver* resolver, const roaring::Roaring* candidates) + : _iterator(_true_rows.begin()) { + if (const auto* nulls = source->get_null_bitmap(resolver); nulls != nullptr) { + _null_rows = candidates == nullptr ? *nulls : *nulls & *candidates; + } + std::optional<roaring::Roaring> true_candidates; + if (candidates != nullptr && !_null_rows.isEmpty()) { + true_candidates = *candidates - _null_rows; + candidates = &*true_candidates; + } + if (enable_scoring) { + _true_rows = consume_true_rows<true>( + *source, source->doc(), candidates, [&](uint32_t doc, auto&& score) { + _scores.push_back({.doc = doc, .score = score()}); + }); + } else { + _true_rows = consume_true_rows<false>(*source, source->doc(), candidates, + [](uint32_t, auto&&) {}); + } + _iterator = _true_rows.begin(); + } + + uint32_t doc() const override { + if (!_scores.empty()) { + return _ordinal < _scores.size() ? _scores[_ordinal].doc : TERMINATED; + } + return _iterator.i.has_value ? *_iterator : TERMINATED; + } + uint32_t size_hint() const override { return static_cast<uint32_t>(_true_rows.cardinality()); } + + uint32_t advance() override { + if (!_scores.empty()) { + if (_ordinal < _scores.size()) { + ++_ordinal; + } + } else if (_iterator.i.has_value) { + ++_iterator; + } + return doc(); + } + + uint32_t seek(uint32_t target) override { + const uint32_t current = doc(); + if (target <= current) { + return current; + } + if (!_scores.empty()) { + // Unique sorted document IDs bound the skipped row count by the document gap. + const size_t limit = std::min(_scores.size(), _ordinal + (target - current)); + const auto next = std::lower_bound( + _scores.begin() + _ordinal, _scores.begin() + limit, target, + [](const ScoredRow& row, uint32_t value) { return row.doc < value; }); + _ordinal = next - _scores.begin(); + } else { + _iterator.equalorlarger(target); + } + return doc(); + } + + float score() override { return _scores.empty() ? 0.0F : _scores[_ordinal].score; } + bool has_null_bitmap(const NullBitmapResolver* /*resolver*/ = nullptr) override { + return !_null_rows.isEmpty(); + } + const roaring::Roaring* get_null_bitmap( + const NullBitmapResolver* /*resolver*/ = nullptr) override { + return &_null_rows; + } + const roaring::Roaring* get_true_bitmap() const override { return &_true_rows; } + +private: + roaring::Roaring _true_rows; + roaring::Roaring _null_rows; + roaring::Roaring::const_iterator _iterator; + DorisVector<ScoredRow> _scores; + size_t _ordinal = 0; +}; + +// Equal UNKNOWN sets stay UNKNOWN under AND and OR. +std::optional<roaring::Roaring> shared_null_bitmap(std::span<const ScorerPtr> children, + const NullBitmapResolver* resolver) { + const roaring::Roaring* shared = nullptr; + bool first = true; + for (const auto& child : children) { + const auto* nulls = child == nullptr ? nullptr : child->get_null_bitmap(resolver); + if (nulls != nullptr && nulls->isEmpty()) { + nulls = nullptr; + } + if (first) { + shared = nulls; + first = false; + } else if (shared != nulls && + (shared == nullptr || nulls == nullptr || *shared != *nulls)) { + return std::nullopt; + } + } + return shared == nullptr ? roaring::Roaring() : *shared; +} + +} // namespace + +index_query::TruthSet collect_truth_set(const ScorerPtr& scorer, const NullBitmapResolver* resolver, + const roaring::Roaring* candidates) { + index_query::TruthSet result; + if (scorer == nullptr) { + return result; + } + // Candidates dense enough to scan cost less to intersect once than to test row by row. + constexpr uint64_t dense_candidate_factor = 4; + if (candidates != nullptr && + candidates->cardinality() * dense_candidate_factor >= scorer->cost()) { + result = collect_truth_set(scorer, resolver); + result.true_rows &= *candidates; + result.null_rows &= *candidates; + return result; + } + if (const auto* nulls = scorer->get_null_bitmap(resolver); nulls != nullptr) { + result.null_rows = candidates == nullptr ? *nulls : *nulls & *candidates; + } + if (const auto* truths = scorer->get_true_bitmap(); truths != nullptr) { + result.true_rows = candidates == nullptr ? *truths : *truths & *candidates; + } else { + uint32_t doc = scorer->doc(); + if (doc == TERMINATED) { + doc = scorer->advance(); + } + std::optional<roaring::Roaring> true_candidates; + if (candidates != nullptr && !result.null_rows.isEmpty()) { + true_candidates = *candidates - result.null_rows; + candidates = &*true_candidates; + } + result.true_rows = + consume_true_rows<false>(*scorer, doc, candidates, [](uint32_t, auto&&) {}); + } + return result; +} + +void collect_true_rows(const ScorerPtr& scorer, roaring::Roaring* rows) { + if (const auto* truths = scorer->get_true_bitmap(); truths != nullptr) { + *rows |= *truths; + return; + } + auto collected = + consume_true_rows<false>(*scorer, scorer->doc(), nullptr, [](uint32_t, auto&&) {}); + if (rows->isEmpty()) { + rows->swap(collected); + } else { + *rows |= collected; + } +} + +roaring::Roaring collect_scored_rows(const ScorerPtr& scorer, uint32_t doc_base, + CollectionSimilarity& similarity) { + if (const auto* listed = dynamic_cast<const ScoredRowsScorer*>(scorer.get()); + listed != nullptr) { + const std::span<const uint32_t> rows = listed->rows(); + const std::span<const float> scores = listed->scores(); + for (size_t i = 0; i < rows.size(); ++i) { + similarity.collect(rows[i] + doc_base, scores[i]); + } + roaring::Roaring result; + result.addMany(rows.size(), rows.data()); + return result; + } + return consume_true_rows<true>( + *scorer, scorer->doc(), nullptr, + [&](uint32_t row, auto&& score) { similarity.collect(row + doc_base, score()); }); +} + +ScorerPtr materialize_scorer(ScorerPtr source, bool enable_scoring, + const NullBitmapResolver* resolver, + const roaring::Roaring* candidates) { + if (source->get_true_bitmap() != nullptr) { + return source; + } + return std::make_shared<MaterializedScorer>(source, enable_scoring, resolver, candidates); +} + +roaring::Roaring complete_null_bitmap(std::vector<ScorerPtr>& children, bool intersection, + bool enable_scoring, const NullBitmapResolver* resolver) { + if (auto shared = shared_null_bitmap(children, resolver); shared.has_value()) { + return std::move(*shared); + } + const bool nullable = std::ranges::any_of(children, [resolver](const ScorerPtr& child) { + return child != nullptr && child->has_null_bitmap(resolver); + }); + if (!nullable) { + return {}; + } + + index_query::TruthSet result; + bool first = true; + for (auto& child : children) { + index_query::TruthSet value; + if (child != nullptr) { + child = materialize_scorer(std::move(child), enable_scoring, resolver); Review Comment: By design, SEARCH is going to stop handling NULL and ignore it: it will not track NULL or UNKNOWN rows for nullable fields. We are therefore leaving its current NULL handling as is in this PR instead of fixing it. Here, the second scorer build after TopK exists only to fetch the NULL rows of a scored SEARCH. Once SEARCH ignores NULL it no longer needs them, so the repeated scan goes away with that change instead of being restructured now. ########## be/src/exprs/function/search/function_search.cpp: ########## @@ -355,12 +402,8 @@ Status FunctionSearch::evaluate_inverted_index_with_search_param_unguarded( // so score() queries must execute scorers to populate CollectionSimilarity. const bool enable_scoring = index_query_context != nullptr && index_query_context->collection_similarity != nullptr; - // Also bypass the DSL cache when any referenced field is served by an SNII reader. auto* dsl_cache = - enable_cache && !enable_scoring && - !referenced_fields_contain_snii_reader(search_param.root, iterators) - ? InvertedIndexQueryCache::instance() - : nullptr; + enable_cache && !enable_scoring ? InvertedIndexQueryCache::instance() : nullptr; Review Comment: By design, SEARCH is going to stop handling NULL and ignore it: it will not track NULL or UNKNOWN rows for nullable fields. We are therefore leaving its current NULL handling as is in this PR instead of fixing it. The NULL entry of the DSL cache holds only those NULL rows. When SEARCH ignores NULL it stops writing and reading that entry, so the data bitmap is the whole cached result and there is no pair left to publish together. ########## be/src/storage/index/inverted/query/engine/boolean_query/listed_terms.cpp: ########## @@ -0,0 +1,304 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "storage/index/inverted/query/engine/boolean_query/listed_terms.h" + +#include <parallel_hashmap/phmap.h> + +#include <algorithm> +#include <bit> +#include <optional> +#include <span> +#include <utility> + +#include "common/check.h" +#include "common/exception.h" +#include "storage/index/inverted/query/engine/postings/listed_walk.h" +#include "storage/index/inverted/query/engine/scored_rows_scorer.h" +#include "storage/index/inverted/query/exec/block_doc_set.h" +#include "storage/index/inverted/query/exec/cursor_chained_postings.h" +#include "storage/index/inverted/query/exec/roaring_docid_sink.h" +#include "storage/index/inverted/query/exec/term_waves.h" +#include "storage/index/inverted/query/phrase/position_span.h" + +namespace doris::segment_v2::inverted_index::query_v2 { + +ListedTerms::ListedTerms(index_query::IndexSourcePtr source, + std::shared_ptr<roaring::Roaring> nulls) + : _source(std::move(source)), _nulls(std::move(nulls)) {} + +void ListedTerms::add(size_t clause, std::string term, + index_query::ScoringContextPtr<float> similarity) { + DCHECK(_clauses.empty() || _clauses.back() < clause); + _clauses.push_back(clause); + _terms.push_back(std::move(term)); + _similarities.push_back(std::move(similarity)); +} + +bool ListedTerms::holds(size_t clause) const { + return std::ranges::binary_search(_clauses, clause); +} + +void ListedTerms::open(bool conjunctive, bool scoring) { + if (!conjunctive) { + return; + } + for (const std::string& term : _terms) { + bool held = false; + THROW_IF_ERROR(_source->may_hold(term, &held)); + if (!held) { + _cursors.resize(_terms.size()); + return; + } + } + // A scored conjunction reads the positions of the rows it lists. + THROW_IF_ERROR( + _source->open_terms(_terms, /*positions=*/scoring, /*scoring=*/false, &_cursors)); +} + +bool ListedTerms::has_absent_term() const { + return std::ranges::any_of(_cursors, [](const auto& cursor) { return cursor == nullptr; }); +} + +uint64_t ListedTerms::cheapest_doc_freq() const { + uint64_t cheapest = UINT64_MAX; + for (const auto& cursor : _cursors) { + cheapest = std::min<uint64_t>(cheapest, cursor == nullptr ? 0 : cursor->doc_freq()); + } + return cheapest; +} + +index_query::TruthSet ListedTerms::conjunction(const std::vector<uint32_t>* candidates) { + index_query::TruthSet result; + if (has_absent_term()) { + return result; + } + std::vector<uint32_t> docs; + THROW_IF_ERROR(index_query::chain_cursors(_cursors, candidates, &docs)); + result.true_rows.addMany(docs.size(), docs.data()); + if (_nulls != nullptr) { + result.null_rows = *_nulls; + } + return result; +} + +index_query::TruthSet ListedTerms::disjunction() { + index_query::TruthSet result; + bool any_present = false; + index_query::RoaringDocIdSink sink(result.true_rows); + THROW_IF_ERROR(_source->collect_terms(_terms, sink, &any_present)); + if (any_present && _nulls != nullptr) { + result.null_rows = *_nulls; + } + return result; +} + +namespace { + +// Merges a term's postings into the rows scored so far, both ascending: a row both hold sums +// the scores, a row either holds alone keeps its own. +void merge_term(index_query::BlockDocSet& docs, index_query::ScoringContext<float>& similarity, + std::vector<uint32_t>* rows, std::vector<float>* scores) { + // Reading the rows so far through spans lets the loop keep their bounds in registers, which + // it cannot do through vectors a caller may hold by reference. + const std::span<const uint32_t> old_rows(*rows); + const std::span<const float> old_scores(*scores); + std::vector<uint32_t> merged_rows; + std::vector<float> merged_scores; + merged_rows.reserve(old_rows.size() + docs.size_hint()); + merged_scores.reserve(old_rows.size() + docs.size_hint()); + size_t i = 0; + for (; !docs.exhausted(); docs.advance()) { + const uint32_t doc = docs.doc(); + for (; i < old_rows.size() && old_rows[i] < doc; ++i) { + merged_rows.push_back(old_rows[i]); + merged_scores.push_back(old_scores[i]); + } + float score = similarity.score(static_cast<float>(docs.freq()), docs.norm()); + if (i < old_rows.size() && old_rows[i] == doc) { + score += old_scores[i]; + ++i; + } + merged_rows.push_back(doc); + merged_scores.push_back(score); + } + merged_rows.insert(merged_rows.end(), old_rows.begin() + static_cast<std::ptrdiff_t>(i), + old_rows.end()); + merged_scores.insert(merged_scores.end(), old_scores.begin() + static_cast<std::ptrdiff_t>(i), + old_scores.end()); + rows->swap(merged_rows); + scores->swap(merged_scores); +} + +// The rows a disjunction holds and their scores, one slot per row of the segment: a term adds +// into the slots in one pass where merging would copy every row scored so far. +class RowSlots { +public: + RowSlots(uint32_t doc_count, std::span<const uint32_t> rows, std::span<const float> scores) + : _scores(doc_count, 0.0F), _held((doc_count + 63) / 64, 0) { + for (size_t i = 0; i < rows.size(); ++i) { + _scores[rows[i]] = scores[i]; + hold(rows[i]); + } + } + + void add_term(index_query::BlockDocSet& docs, index_query::ScoringContext<float>& similarity) { + for (; !docs.exhausted(); docs.advance()) { + const uint32_t doc = docs.doc(); + DCHECK_LT(doc, _scores.size()); + _scores[doc] += similarity.score(static_cast<float>(docs.freq()), docs.norm()); + hold(doc); + } + } + + // The rows held, ascending, and their scores. + void list(std::vector<uint32_t>* rows, std::vector<float>* scores) const { + size_t count = 0; + for (const uint64_t word : _held) { + count += std::popcount(word); + } + rows->clear(); + scores->clear(); + rows->reserve(count); + scores->reserve(count); + for (size_t word = 0; word < _held.size(); ++word) { + for (uint64_t bits = _held[word]; bits != 0; bits &= bits - 1) { + const auto row = static_cast<uint32_t>(word * 64 + std::countr_zero(bits)); + rows->push_back(row); + scores->push_back(_scores[row]); + } + } + } + +private: + void hold(uint32_t row) { _held[row / 64] |= uint64_t {1} << (row % 64); } + + std::vector<float> _scores; + std::vector<uint64_t> _held; +}; + +} // namespace + +// The chain lists the rows, then the terms read their positions a wave at a time, one round +// each, and score each row on the positions it holds there and the source's norm. +ScorerPtr ListedTerms::scored_conjunction() { + if (has_absent_term()) { + return std::make_shared<EmptyScorer>(); + } + std::vector<uint32_t> rows; + THROW_IF_ERROR(index_query::chain_cursors(_cursors, nullptr, &rows)); + if (rows.empty()) { Review Comment: By design, SEARCH is going to stop handling NULL and ignore it: it will not track NULL or UNKNOWN rows for nullable fields. We are therefore leaving its current NULL handling as is in this PR instead of fixing it. Without NULL tracking, an empty conjunction is simply an empty result, and `NOT` treats a row whose field is NULL like any other row the clause does not match. That is what this query returns now, so the missing `_nulls` is not something we plan to restore. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
