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]

Reply via email to