This is an automated email from the ASF dual-hosted git repository.

HappenLee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 287a334b63c [feature](function) Support map arguments for 
inner_product (#67311)
287a334b63c is described below

commit 287a334b63ce4d4fa83ecde7cef96d73f4426490
Author: Pxl <[email protected]>
AuthorDate: Mon Sep 7 09:53:09 2026 +0800

    [feature](function) Support map arguments for inner_product (#67311)
    
    Related PR: apache/doris-website#4113
    
    Problem Summary:
    
    `inner_product` currently only accepts dense `ARRAY<FLOAT>` vectors.
    This PR extends it to sparse vectors represented as `MAP<K, FLOAT>`,
    where matching map keys identify dimensions.
    
    Map keys are intentionally limited to integral and string types. The BE
    dispatches to concrete native key types and hashes keys directly:
    numeric keys use their native column data, while string keys use
    zero-copy `StringRef` access. The implementation does not serialize keys
    or use runtime type erasure. For each row, it builds a flat hash map
    from the smaller input map and probes it with the larger map, using O(m
    + n) time and O(min(m, n)) temporary space. Existing dense array
    behavior remains unchanged.
    
    The implementation also validates unsupported key types in FE and BE,
    preserves NULL-key matching, and rejects NULL map values and NULL outer
    maps.
    
    ### Release note
    
    Support `inner_product(MAP<K, FLOAT>, MAP<K, FLOAT>)` for integral and
    string key types.
    
    ### Check List (For Author)
    
    - Test
        - [x] Regression test
            - `test_map_inner_product`
        - [x] Unit Test
            - `FunctionMapInnerProductTest.*` (11 tests under ASAN)
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    Additional validation:
    
    - `DISABLE_BE_CDC_CLIENT=ON ./build.sh --be`
    - `DISABLE_BUILD_UI=ON ./build.sh --fe`
    - `build-support/check-build-hygiene.sh`
    - `build-support/check-format.sh`
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. `inner_product` now accepts compatible MAP arguments in
    addition to ARRAY arguments.
    
    - Does this need documentation?
        - [ ] No.
        - [x] Yes. apache/doris-website#4113
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../function/array/function_array_distance.cpp     |   3 +-
 be/src/exprs/function/function_inner_product.h     | 479 +++++++++++++++++++++
 .../function/function_map_inner_product_test.cpp   | 429 ++++++++++++++++++
 .../expressions/functions/scalar/InnerProduct.java |  42 +-
 .../functions/scalar/InnerProductTest.java         |  82 ++++
 .../map_functions/test_map_inner_product.out       |  20 +
 .../map_functions/test_map_inner_product.groovy    | 124 ++++++
 7 files changed, 1177 insertions(+), 2 deletions(-)

diff --git a/be/src/exprs/function/array/function_array_distance.cpp 
b/be/src/exprs/function/array/function_array_distance.cpp
index 3f37775d6be..103f737edbf 100644
--- a/be/src/exprs/function/array/function_array_distance.cpp
+++ b/be/src/exprs/function/array/function_array_distance.cpp
@@ -19,6 +19,7 @@
 
 #include <algorithm>
 
+#include "exprs/function/function_inner_product.h"
 #include "exprs/function/simple_function_factory.h"
 
 namespace doris {
@@ -90,7 +91,7 @@ void register_function_array_distance(SimpleFunctionFactory& 
factory) {
     factory.register_function<FunctionArrayDistance<L2Distance>>();
     factory.register_function<FunctionArrayDistance<CosineDistance>>();
     factory.register_function<FunctionArrayDistance<CosineSimilarity>>();
-    factory.register_function<FunctionArrayDistance<InnerProduct>>();
+    factory.register_function<FunctionInnerProduct>();
     factory.register_function<FunctionArrayDistance<L2DistanceApproximate>>();
     
factory.register_function<FunctionArrayDistance<InnerProductApproximate>>();
 }
diff --git a/be/src/exprs/function/function_inner_product.h 
b/be/src/exprs/function/function_inner_product.h
new file mode 100644
index 00000000000..3485db95c6d
--- /dev/null
+++ b/be/src/exprs/function/function_inner_product.h
@@ -0,0 +1,479 @@
+// 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.
+
+#pragma once
+
+#include "core/assert_cast.h"
+#include "core/column/column_const.h"
+#include "core/column/column_map.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/data_type/data_type_map.h"
+#include "core/string_ref.h"
+#include "exec/common/hash_table/hash.h"
+#include "exec/common/hash_table/phmap_fwd_decl.h"
+#include "exec/common/util.hpp"
+#include "exprs/function/array/function_array_distance.h"
+
+namespace doris {
+
+namespace detail {
+
+template <PrimitiveType KeyType>
+struct InnerProductMapKeyTraits {
+    using ColumnType = PrimitiveTypeTraits<KeyType>::ColumnType;
+    using Key = PrimitiveTypeTraits<KeyType>::CppType;
+    using KeyAccessor = const Key*;
+    using Hash = HashCRC32<Key>;
+
+    static KeyAccessor get_key_accessor(const ColumnType& column) {
+        return column.get_data().data();
+    }
+
+    static Key get_key(KeyAccessor keys, size_t index) { return keys[index]; }
+};
+
+template <>
+struct InnerProductMapKeyTraits<TYPE_STRING> {
+    using ColumnType = ColumnString;
+    using Key = StringRef;
+    using KeyAccessor = const ColumnType*;
+    using Hash = StringRefHash;
+
+    static KeyAccessor get_key_accessor(const ColumnType& column) { return 
&column; }
+
+    static Key get_key(KeyAccessor keys, size_t index) { return 
keys->get_data_at(index); }
+};
+
+} // namespace detail
+
+class FunctionInnerProduct final : public FunctionArrayDistance<InnerProduct> {
+public:
+    static FunctionPtr create() { return 
std::make_shared<FunctionInnerProduct>(); }
+
+    DataTypePtr get_return_type_impl(const DataTypes& arguments) const 
override {
+        if (arguments.size() != 2) {
+            throw doris::Exception(ErrorCode::INVALID_ARGUMENT, "Invalid 
number of arguments");
+        }
+
+        const bool both_arrays = arguments[0]->get_primitive_type() == 
TYPE_ARRAY &&
+                                 arguments[1]->get_primitive_type() == 
TYPE_ARRAY;
+        if (both_arrays) {
+            return 
FunctionArrayDistance<InnerProduct>::get_return_type_impl(arguments);
+        }
+
+        const bool both_maps = arguments[0]->get_primitive_type() == TYPE_MAP 
&&
+                               arguments[1]->get_primitive_type() == TYPE_MAP;
+        if (!both_maps) {
+            throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                   "Arguments for function {} must be arrays 
or maps", get_name());
+        }
+
+        const auto& left_type = assert_cast<const 
DataTypeMap&>(*remove_nullable(arguments[0]));
+        const auto& right_type = assert_cast<const 
DataTypeMap&>(*remove_nullable(arguments[1]));
+        if (!left_type.get_key_type()->equals(*right_type.get_key_type())) {
+            throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                   "Map keys for function {} must have the 
same type", get_name());
+        }
+        const auto key_type = 
remove_nullable(left_type.get_key_type())->get_primitive_type();
+        if (!_is_supported_map_key_type(key_type)) {
+            throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                   "Function {} only supports integer or 
string map keys",
+                                   get_name());
+        }
+        if (remove_nullable(left_type.get_value_type())->get_primitive_type() 
!= TYPE_FLOAT ||
+            remove_nullable(right_type.get_value_type())->get_primitive_type() 
!= TYPE_FLOAT) {
+            throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                   "Map values for function {} must be FLOAT", 
get_name());
+        }
+        return std::make_shared<DataTypeFloat32>();
+    }
+
+    Status execute_impl(FunctionContext* context, Block& block, const 
ColumnNumbers& arguments,
+                        uint32_t result, size_t input_rows_count) const 
override {
+        if (block.get_by_position(arguments[0]).type->get_primitive_type() == 
TYPE_MAP) {
+            return _execute_map(block, arguments, result, input_rows_count);
+        }
+        return FunctionArrayDistance<InnerProduct>::execute_impl(context, 
block, arguments, result,
+                                                                 
input_rows_count);
+    }
+
+private:
+    using ColumnType = PrimitiveTypeTraits<TYPE_FLOAT>::ColumnType;
+
+    struct MapRange {
+        size_t begin;
+        size_t size;
+    };
+
+    static ALWAYS_INLINE MapRange _get_map_range(const ColumnMap& map, bool 
is_const, size_t row) {
+        const size_t actual_row = index_check_const(row, is_const);
+        return {map.offset_at(actual_row), map.size_at(actual_row)};
+    }
+
+    static bool _is_supported_map_key_type(PrimitiveType type) {
+        switch (type) {
+        case TYPE_TINYINT:
+        case TYPE_SMALLINT:
+        case TYPE_INT:
+        case TYPE_BIGINT:
+        case TYPE_LARGEINT:
+        case TYPE_CHAR:
+        case TYPE_VARCHAR:
+        case TYPE_STRING:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    static const ColumnMap& _get_map_column(const ColumnPtr& column, const 
char* argument_name,
+                                            bool& is_const) {
+        const IColumn* raw_column = column.get();
+        is_const = is_column_const(*raw_column);
+        if (is_const) {
+            raw_column = assert_cast<const 
ColumnConst*>(raw_column)->get_data_column_ptr().get();
+        }
+
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(raw_column)) {
+            if (raw_column->has_null()) {
+                throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                       "{} for function {} cannot be null", 
argument_name,
+                                       InnerProduct::name);
+            }
+            raw_column = nullable->get_nested_column_ptr().get();
+        }
+
+        return assert_cast<const ColumnMap&>(*raw_column);
+    }
+
+    static const IColumn& _get_key_column(const IColumn& column, const UInt8*& 
null_map) {
+        null_map = nullptr;
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(&column)) {
+            null_map = nullable->get_null_map_data().data();
+            return nullable->get_nested_column();
+        }
+        return column;
+    }
+
+    static const IColumn& _get_value_column(const IColumn& column,
+                                            const ColumnNullable*& 
nullable_with_null) {
+        nullable_with_null = nullptr;
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(&column)) {
+            if (nullable->has_null()) {
+                nullable_with_null = nullable;
+            }
+            return nullable->get_nested_column();
+        }
+        return column;
+    }
+
+    template <typename KeyTraits>
+    static void _validate_retained_values(typename KeyTraits::KeyAccessor keys,
+                                          const UInt8* key_null_map,
+                                          const ColumnNullable& 
nullable_values, MapRange range,
+                                          const char* argument_name) {
+        if (!nullable_values.has_null(range.begin, range.begin + range.size)) {
+            return;
+        }
+
+        using Key = typename KeyTraits::Key;
+        doris::flat_hash_set<Key, typename KeyTraits::Hash> seen_keys;
+        seen_keys.reserve(range.size);
+        const auto& value_null_map = nullable_values.get_null_map_data();
+        bool has_null_key = false;
+
+        // Only the last value for each key is visible. Ignore NULL values 
shadowed by a later
+        // duplicate, matching ColumnMap::deduplicate_keys() semantics.
+        for (size_t i = range.begin + range.size; i > range.begin; --i) {
+            const size_t index = i - 1;
+            if (key_null_map != nullptr && key_null_map[index]) {
+                if (!has_null_key) {
+                    has_null_key = true;
+                    if (value_null_map[index]) {
+                        throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                               "{} for function {} cannot have 
null", argument_name,
+                                               InnerProduct::name);
+                    }
+                }
+                continue;
+            }
+
+            if (seen_keys.emplace(KeyTraits::get_key(keys, index)).second &&
+                value_null_map[index]) {
+                throw doris::Exception(ErrorCode::INVALID_ARGUMENT,
+                                       "{} for function {} cannot have null", 
argument_name,
+                                       InnerProduct::name);
+            }
+        }
+    }
+
+    template <typename KeyTraits>
+    struct MapData {
+        typename KeyTraits::KeyAccessor keys;
+        const UInt8* key_null_map;
+        const float* values;
+        const ColumnNullable* nullable_values;
+    };
+
+    template <typename KeyTraits>
+    static void _execute_map_with_cached_constant(const MapData<KeyTraits>& 
constant,
+                                                  MapRange constant_range,
+                                                  const MapData<KeyTraits>& 
varying,
+                                                  const ColumnMap& varying_map,
+                                                  const char* 
varying_argument_name,
+                                                  ColumnType::Container& 
destination_data,
+                                                  size_t input_rows_count) {
+        using Key = typename KeyTraits::Key;
+        struct CachedValue {
+            float value;
+            size_t last_matched_row;
+        };
+
+        doris::flat_hash_map<Key, CachedValue, typename KeyTraits::Hash> 
constant_values_by_key;
+        constant_values_by_key.reserve(constant_range.size);
+        bool has_null_key = false;
+        float null_key_value = 0.0F;
+        size_t null_key_last_matched_row = input_rows_count;
+
+        for (size_t i = constant_range.begin + constant_range.size; i > 
constant_range.begin; --i) {
+            const size_t index = i - 1;
+            if (constant.key_null_map != nullptr && 
constant.key_null_map[index]) {
+                if (!has_null_key) {
+                    has_null_key = true;
+                    null_key_value = constant.values[index];
+                }
+            } else {
+                constant_values_by_key.emplace(
+                        KeyTraits::get_key(constant.keys, index),
+                        CachedValue {constant.values[index], 
input_rows_count});
+            }
+        }
+
+        for (size_t row = 0; row < input_rows_count; ++row) {
+            const MapRange varying_range = _get_map_range(varying_map, false, 
row);
+            if (varying.nullable_values != nullptr) {
+                _validate_retained_values<KeyTraits>(varying.keys, 
varying.key_null_map,
+                                                     *varying.nullable_values, 
varying_range,
+                                                     varying_argument_name);
+            }
+            if (constant_range.size == 0 || varying_range.size == 0) {
+                destination_data[row] = 0.0F;
+                continue;
+            }
+
+            float inner_product = 0.0F;
+            for (size_t i = varying_range.begin + varying_range.size; i > 
varying_range.begin;
+                 --i) {
+                const size_t index = i - 1;
+                if (varying.key_null_map != nullptr && 
varying.key_null_map[index]) {
+                    if (has_null_key && null_key_last_matched_row != row) {
+                        inner_product += null_key_value * 
varying.values[index];
+                        null_key_last_matched_row = row;
+                    }
+                    continue;
+                }
+
+                const auto it =
+                        
constant_values_by_key.find(KeyTraits::get_key(varying.keys, index));
+                if (it != constant_values_by_key.end() && 
it->second.last_matched_row != row) {
+                    inner_product += it->second.value * varying.values[index];
+                    it->second.last_matched_row = row;
+                }
+            }
+            destination_data[row] = inner_product;
+        }
+    }
+
+    template <PrimitiveType KeyType>
+    static void _execute_map_typed(const ColumnMap& left, bool left_is_const,
+                                   const ColumnMap& right, bool right_is_const,
+                                   ColumnType::Container& destination_data,
+                                   size_t input_rows_count) {
+        using KeyTraits = detail::InnerProductMapKeyTraits<KeyType>;
+        using Key = typename KeyTraits::Key;
+        using KeyColumn = typename KeyTraits::ColumnType;
+        using TypedMapData = MapData<KeyTraits>;
+
+        const UInt8* left_key_null_map = nullptr;
+        const UInt8* right_key_null_map = nullptr;
+        const auto& left_keys =
+                assert_cast<const KeyColumn&>(_get_key_column(left.get_keys(), 
left_key_null_map));
+        const auto& right_keys = assert_cast<const KeyColumn&>(
+                _get_key_column(right.get_keys(), right_key_null_map));
+        const ColumnNullable* left_nullable_values = nullptr;
+        const ColumnNullable* right_nullable_values = nullptr;
+        const auto& left_values =
+                assert_cast<const ColumnType&>(
+                        _get_value_column(left.get_values(), 
left_nullable_values))
+                        .get_data();
+        const auto& right_values =
+                assert_cast<const ColumnType&>(
+                        _get_value_column(right.get_values(), 
right_nullable_values))
+                        .get_data();
+
+        const TypedMapData left_data {KeyTraits::get_key_accessor(left_keys), 
left_key_null_map,
+                                      left_values.data(), 
left_nullable_values};
+        const TypedMapData right_data 
{KeyTraits::get_key_accessor(right_keys), right_key_null_map,
+                                       right_values.data(), 
right_nullable_values};
+
+        if (left_is_const && left_data.nullable_values != nullptr) {
+            _validate_retained_values<KeyTraits>(left_data.keys, 
left_data.key_null_map,
+                                                 *left_data.nullable_values,
+                                                 _get_map_range(left, true, 
0), "First argument");
+        }
+        if (right_is_const && right_data.nullable_values != nullptr) {
+            _validate_retained_values<KeyTraits>(right_data.keys, 
right_data.key_null_map,
+                                                 *right_data.nullable_values,
+                                                 _get_map_range(right, true, 
0), "Second argument");
+        }
+
+        if (left_is_const != right_is_const && input_rows_count > 1) {
+            const bool constant_is_left = left_is_const;
+            const TypedMapData constant = constant_is_left ? left_data : 
right_data;
+            const TypedMapData varying = constant_is_left ? right_data : 
left_data;
+            const auto& varying_map = constant_is_left ? right : left;
+            const MapRange constant_range =
+                    _get_map_range(constant_is_left ? left : right, true, 0);
+
+            // Reuse the constant side only when its scratch space does not 
exceed the total
+            // varying input. Otherwise the per-row path below keeps memory 
bounded by the smaller
+            // map in each row.
+            if (constant_range.size <= varying_map.get_keys().size()) {
+                _execute_map_with_cached_constant<KeyTraits>(
+                        constant, constant_range, varying, varying_map,
+                        constant_is_left ? "Second argument" : "First 
argument", destination_data,
+                        input_rows_count);
+                return;
+            }
+        }
+
+        // Build the hash table from the smaller map row to minimize temporary 
memory.
+        doris::flat_hash_map<Key, float, typename KeyTraits::Hash> 
values_by_key;
+        for (size_t row = 0; row < input_rows_count; ++row) {
+            const MapRange left_range = _get_map_range(left, left_is_const, 
row);
+            const MapRange right_range = _get_map_range(right, right_is_const, 
row);
+            if (!left_is_const && left_data.nullable_values != nullptr) {
+                _validate_retained_values<KeyTraits>(left_data.keys, 
left_data.key_null_map,
+                                                     
*left_data.nullable_values, left_range,
+                                                     "First argument");
+            }
+            if (!right_is_const && right_data.nullable_values != nullptr) {
+                _validate_retained_values<KeyTraits>(right_data.keys, 
right_data.key_null_map,
+                                                     
*right_data.nullable_values, right_range,
+                                                     "Second argument");
+            }
+            if (left_range.size == 0 || right_range.size == 0) {
+                destination_data[row] = 0.0F;
+                continue;
+            }
+            const bool build_left = left_range.size <= right_range.size;
+            const TypedMapData build = build_left ? left_data : right_data;
+            const TypedMapData probe = build_left ? right_data : left_data;
+            const MapRange build_range = build_left ? left_range : right_range;
+            const MapRange probe_range = build_left ? right_range : left_range;
+
+            values_by_key.clear();
+            values_by_key.reserve(build_range.size);
+            bool has_null_key = false;
+            float null_key_value = 0.0F;
+            // Scan backwards so emplace keeps the last value for duplicate 
keys.
+            for (size_t i = build_range.begin + build_range.size; i > 
build_range.begin; --i) {
+                const size_t index = i - 1;
+                if (build.key_null_map != nullptr && 
build.key_null_map[index]) {
+                    if (!has_null_key) {
+                        has_null_key = true;
+                        null_key_value = build.values[index];
+                    }
+                } else {
+                    values_by_key.emplace(KeyTraits::get_key(build.keys, 
index),
+                                          build.values[index]);
+                }
+            }
+
+            float inner_product = 0.0F;
+            // Erase matches while scanning backwards so probe duplicates also 
use the last value.
+            for (size_t i = probe_range.begin + probe_range.size; i > 
probe_range.begin; --i) {
+                const size_t index = i - 1;
+                if (probe.key_null_map != nullptr && 
probe.key_null_map[index]) {
+                    if (has_null_key) {
+                        inner_product += null_key_value * probe.values[index];
+                        has_null_key = false;
+                    }
+                    continue;
+                }
+                const auto it = 
values_by_key.find(KeyTraits::get_key(probe.keys, index));
+                if (it != values_by_key.end()) {
+                    inner_product += it->second * probe.values[index];
+                    values_by_key.erase(it);
+                }
+            }
+            destination_data[row] = inner_product;
+        }
+    }
+
+    Status _execute_map(Block& block, const ColumnNumbers& arguments, uint32_t 
result,
+                        size_t input_rows_count) const {
+        bool left_is_const = false;
+        bool right_is_const = false;
+        const auto& left = 
_get_map_column(block.get_by_position(arguments[0]).column,
+                                           "First argument", left_is_const);
+        const auto& right = 
_get_map_column(block.get_by_position(arguments[1]).column,
+                                            "Second argument", right_is_const);
+
+        auto destination = ColumnType::create(input_rows_count);
+        auto& destination_data = destination->get_data();
+        const auto& map_type = assert_cast<const DataTypeMap&>(
+                *remove_nullable(block.get_by_position(arguments[0]).type));
+        switch 
(remove_nullable(map_type.get_key_type())->get_primitive_type()) {
+        case TYPE_TINYINT:
+            _execute_map_typed<TYPE_TINYINT>(left, left_is_const, right, 
right_is_const,
+                                             destination_data, 
input_rows_count);
+            break;
+        case TYPE_SMALLINT:
+            _execute_map_typed<TYPE_SMALLINT>(left, left_is_const, right, 
right_is_const,
+                                              destination_data, 
input_rows_count);
+            break;
+        case TYPE_INT:
+            _execute_map_typed<TYPE_INT>(left, left_is_const, right, 
right_is_const,
+                                         destination_data, input_rows_count);
+            break;
+        case TYPE_BIGINT:
+            _execute_map_typed<TYPE_BIGINT>(left, left_is_const, right, 
right_is_const,
+                                            destination_data, 
input_rows_count);
+            break;
+        case TYPE_LARGEINT:
+            _execute_map_typed<TYPE_LARGEINT>(left, left_is_const, right, 
right_is_const,
+                                              destination_data, 
input_rows_count);
+            break;
+        case TYPE_CHAR:
+        case TYPE_VARCHAR:
+        case TYPE_STRING:
+            _execute_map_typed<TYPE_STRING>(left, left_is_const, right, 
right_is_const,
+                                            destination_data, 
input_rows_count);
+            break;
+        default:
+            return Status::InvalidArgument("Function {} only supports integer 
or string map keys",
+                                           get_name());
+        }
+
+        block.replace_by_position(result, std::move(destination));
+        return Status::OK();
+    }
+};
+
+} // namespace doris
diff --git a/be/test/exprs/function/function_map_inner_product_test.cpp 
b/be/test/exprs/function/function_map_inner_product_test.cpp
new file mode 100644
index 00000000000..d2f1c55e1ec
--- /dev/null
+++ b/be/test/exprs/function/function_map_inner_product_test.cpp
@@ -0,0 +1,429 @@
+// 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 <gtest/gtest.h>
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "common/exception.h"
+#include "common/status.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_array.h"
+#include "core/column/column_const.h"
+#include "core/column/column_map.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "exprs/function/function_inner_product.h"
+#include "exprs/function/simple_function_factory.h"
+
+namespace doris {
+namespace {
+
+MutableColumnPtr make_offsets(const std::vector<size_t>& offsets) {
+    auto result = ColumnArray::ColumnOffsets::create();
+    for (size_t offset : offsets) {
+        result->insert_value(offset);
+    }
+    return result;
+}
+
+template <typename ColumnType, typename ValueType>
+MutableColumnPtr make_nullable_vector(const 
std::vector<std::optional<ValueType>>& values) {
+    auto nested = ColumnType::create();
+    auto null_map = ColumnUInt8::create();
+    for (const auto& value : values) {
+        nested->insert_value(value.value_or(ValueType {}));
+        null_map->insert_value(value.has_value() ? 0 : 1);
+    }
+    return ColumnNullable::create(std::move(nested), std::move(null_map));
+}
+
+MutableColumnPtr make_nullable_string(const 
std::vector<std::optional<std::string>>& values) {
+    auto nested = ColumnString::create();
+    auto null_map = ColumnUInt8::create();
+    for (const auto& value : values) {
+        const std::string data = value.value_or("");
+        nested->insert_data(data.data(), data.size());
+        null_map->insert_value(value.has_value() ? 0 : 1);
+    }
+    return ColumnNullable::create(std::move(nested), std::move(null_map));
+}
+
+ColumnPtr make_int_float_map(const std::vector<std::optional<int32_t>>& keys,
+                             const std::vector<std::optional<float>>& values,
+                             const std::vector<size_t>& offsets) {
+    return ColumnMap::create(make_nullable_vector<ColumnInt32>(keys),
+                             make_nullable_vector<ColumnFloat32>(values), 
make_offsets(offsets));
+}
+
+ColumnPtr make_high_cardinality_map_with_shadowed_nulls(size_t key_count) {
+    std::vector<std::optional<int32_t>> keys;
+    std::vector<std::optional<float>> values;
+    keys.reserve(key_count + 3);
+    values.reserve(key_count + 3);
+    for (size_t key = 0; key < key_count; ++key) {
+        keys.emplace_back(static_cast<int32_t>(key));
+        if (key == 5) {
+            values.emplace_back(std::nullopt);
+        } else {
+            values.emplace_back(1.0F);
+        }
+    }
+    keys.emplace_back(5);
+    values.emplace_back(2.0F);
+    keys.emplace_back(std::nullopt);
+    values.emplace_back(std::nullopt);
+    keys.emplace_back(std::nullopt);
+    values.emplace_back(4.0F);
+    return make_int_float_map(keys, values, {key_count + 3});
+}
+
+ColumnPtr make_largeint_float_map(const std::vector<std::optional<Int128>>& 
keys,
+                                  const std::vector<std::optional<float>>& 
values,
+                                  const std::vector<size_t>& offsets) {
+    return ColumnMap::create(make_nullable_vector<ColumnInt128>(keys),
+                             make_nullable_vector<ColumnFloat32>(values), 
make_offsets(offsets));
+}
+
+ColumnPtr make_string_float_map(const std::vector<std::optional<std::string>>& 
keys,
+                                const std::vector<std::optional<float>>& 
values,
+                                const std::vector<size_t>& offsets) {
+    return ColumnMap::create(make_nullable_string(keys),
+                             make_nullable_vector<ColumnFloat32>(values), 
make_offsets(offsets));
+}
+
+Status execute_inner_product(Block& block, const DataTypePtr& return_type) {
+    ColumnsWithTypeAndName arguments {block.get_by_position(0), 
block.get_by_position(1)};
+    auto function =
+            SimpleFunctionFactory::instance().get_function("inner_product", 
arguments, return_type);
+    if (function == nullptr) {
+        return Status::InternalError("function inner_product is not 
registered");
+    }
+    return function->execute(nullptr, block, {0, 1}, 2, block.rows());
+}
+
+DataTypePtr nullable_int_type() {
+    return make_nullable(std::make_shared<DataTypeInt32>());
+}
+
+DataTypePtr nullable_float_type() {
+    return make_nullable(std::make_shared<DataTypeFloat32>());
+}
+
+} // namespace
+
+TEST(FunctionMapInnerProductTest, numeric_keys) {
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    block.insert({make_int_float_map({1, 2, 1, 3}, {1.0F, 2.0F, -2.0F, 0.5F}, 
{2, 4, 4}), map_type,
+                  "left"});
+    block.insert({make_int_float_map({2, 1, 3, 2, 1}, {3.0F, 4.0F, 8.0F, 
99.0F, 4.0F}, {2, 5, 5}),
+                  map_type, "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 3);
+    EXPECT_FLOAT_EQ(result[0], 10.0F);
+    EXPECT_FLOAT_EQ(result[1], -4.0F);
+    EXPECT_FLOAT_EQ(result[2], 0.0F);
+}
+
+TEST(FunctionMapInnerProductTest, const_map) {
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    block.insert({ColumnConst::create(make_int_float_map({1, 2}, {2.0F, 3.0F}, 
{2}), 2), map_type,
+                  "left"});
+    block.insert({make_int_float_map({2, 1}, {4.0F, 5.0F}, {1, 2}), map_type, 
"right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 2);
+    EXPECT_FLOAT_EQ(result[0], 12.0F);
+    EXPECT_FLOAT_EQ(result[1], 10.0F);
+}
+
+TEST(FunctionMapInnerProductTest, reuses_high_cardinality_const_map) {
+    constexpr size_t key_count = 1024;
+    constexpr size_t row_count = 3;
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    auto constant_map = 
make_high_cardinality_map_with_shadowed_nulls(key_count);
+
+    std::vector<std::optional<int32_t>> varying_keys;
+    std::vector<std::optional<float>> varying_values;
+    std::vector<size_t> varying_offsets;
+    varying_keys.reserve(row_count * (key_count + 2));
+    varying_values.reserve(row_count * (key_count + 2));
+    varying_offsets.reserve(row_count);
+    for (size_t row = 0; row < row_count; ++row) {
+        for (size_t key = 0; key < key_count; ++key) {
+            varying_keys.emplace_back(static_cast<int32_t>(key));
+            varying_values.emplace_back(1.0F);
+        }
+        varying_keys.emplace_back(5);
+        varying_values.emplace_back(3.0F);
+        varying_keys.emplace_back(std::nullopt);
+        varying_values.emplace_back(static_cast<float>(row + 1));
+        varying_offsets.emplace_back(varying_keys.size());
+    }
+    auto varying_map = make_int_float_map(varying_keys, varying_values, 
varying_offsets);
+
+    auto expect_results = [&](bool constant_is_left) {
+        Block block;
+        ColumnPtr constant = ColumnConst::create(constant_map, row_count);
+        block.insert({constant_is_left ? constant : varying_map, map_type, 
"left"});
+        block.insert({constant_is_left ? varying_map : constant, map_type, 
"right"});
+        block.insert({nullptr, return_type, "result"});
+
+        ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+        const auto& result =
+                assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+        ASSERT_EQ(result.size(), row_count);
+        EXPECT_FLOAT_EQ(result[0], 1033.0F);
+        EXPECT_FLOAT_EQ(result[1], 1037.0F);
+        EXPECT_FLOAT_EQ(result[2], 1041.0F);
+    };
+
+    expect_results(true);
+    expect_results(false);
+}
+
+TEST(FunctionMapInnerProductTest, avoids_caching_oversized_const_map) {
+    constexpr size_t key_count = 1024;
+    constexpr size_t row_count = 3;
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    std::vector<std::optional<int32_t>> constant_keys;
+    std::vector<std::optional<float>> constant_values;
+    constant_keys.reserve(key_count);
+    constant_values.reserve(key_count);
+    for (size_t key = 0; key < key_count; ++key) {
+        constant_keys.emplace_back(static_cast<int32_t>(key));
+        constant_values.emplace_back(1.0F);
+    }
+    auto constant_map = make_int_float_map(constant_keys, constant_values, 
{key_count});
+    auto varying_map = make_int_float_map({5, 7, -1}, {3.0F, 2.0F, 7.0F}, {1, 
2, 3});
+
+    auto expect_results = [&](bool constant_is_left) {
+        Block block;
+        ColumnPtr constant = ColumnConst::create(constant_map, row_count);
+        block.insert({constant_is_left ? constant : varying_map, map_type, 
"left"});
+        block.insert({constant_is_left ? varying_map : constant, map_type, 
"right"});
+        block.insert({nullptr, return_type, "result"});
+
+        ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+        const auto& result =
+                assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+        ASSERT_EQ(result.size(), row_count);
+        EXPECT_FLOAT_EQ(result[0], 3.0F);
+        EXPECT_FLOAT_EQ(result[1], 2.0F);
+        EXPECT_FLOAT_EQ(result[2], 0.0F);
+    };
+
+    expect_results(true);
+    expect_results(false);
+}
+
+TEST(FunctionMapInnerProductTest, 
empty_map_short_circuits_high_cardinality_row) {
+    constexpr size_t key_count = 4096;
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    std::vector<std::optional<int32_t>> keys;
+    std::vector<std::optional<float>> values;
+    keys.reserve(key_count);
+    values.reserve(key_count);
+    for (size_t key = 0; key < key_count; ++key) {
+        keys.emplace_back(static_cast<int32_t>(key));
+        values.emplace_back(1.0F);
+    }
+
+    Block block;
+    block.insert({make_int_float_map(keys, values, {0, key_count}), map_type, 
"left"});
+    block.insert({make_int_float_map(keys, values, {key_count, key_count}), 
map_type, "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 2);
+    EXPECT_FLOAT_EQ(result[0], 0.0F);
+    EXPECT_FLOAT_EQ(result[1], 0.0F);
+}
+
+TEST(FunctionMapInnerProductTest, largeint_keys) {
+    auto largeint_type = make_nullable(std::make_shared<DataTypeInt128>());
+    auto map_type = std::make_shared<DataTypeMap>(largeint_type, 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    block.insert({make_largeint_float_map({Int128 {1}, Int128 {2}}, {2.0F, 
3.0F}, {2}), map_type,
+                  "left"});
+    block.insert({make_largeint_float_map({Int128 {2}, Int128 {3}}, {4.0F, 
5.0F}, {2}), map_type,
+                  "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 1);
+    EXPECT_FLOAT_EQ(result[0], 12.0F);
+}
+
+TEST(FunctionMapInnerProductTest, string_and_null_keys) {
+    auto nullable_string = make_nullable(std::make_shared<DataTypeString>());
+    auto map_type = std::make_shared<DataTypeMap>(nullable_string, 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    block.insert({make_string_float_map({"a", std::nullopt}, {2.0F, 3.0F}, 
{2}), map_type, "left"});
+    block.insert(
+            {make_string_float_map({std::nullopt, "a"}, {4.0F, 5.0F}, {2}), 
map_type, "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 1);
+    EXPECT_FLOAT_EQ(result[0], 22.0F);
+}
+
+TEST(FunctionMapInnerProductTest, duplicate_keys_use_last_value) {
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    // Rows 0 and 2 build the left map; rows 1 and 3 build the right map.
+    // Rows 0 and 1 use ordinary duplicate keys; rows 2 and 3 use duplicate 
NULL keys.
+    block.insert(
+            {make_int_float_map(
+                     {1, 1, 1, 1, 2, std::nullopt, std::nullopt, std::nullopt, 
std::nullopt, 2},
+                     {2.0F, 3.0F, 4.0F, 5.0F, 7.0F, 2.0F, 3.0F, 4.0F, 5.0F, 
7.0F}, {2, 5, 7, 10}),
+             map_type, "left"});
+    block.insert(
+            {make_int_float_map(
+                     {1, 1, 2, 1, 1, std::nullopt, std::nullopt, 2, 
std::nullopt, std::nullopt},
+                     {4.0F, 5.0F, 7.0F, 2.0F, 3.0F, 4.0F, 5.0F, 7.0F, 2.0F, 
3.0F}, {3, 5, 8, 10}),
+             map_type, "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 4);
+    EXPECT_FLOAT_EQ(result[0], 15.0F);
+    EXPECT_FLOAT_EQ(result[1], 15.0F);
+    EXPECT_FLOAT_EQ(result[2], 15.0F);
+    EXPECT_FLOAT_EQ(result[3], 15.0F);
+}
+
+TEST(FunctionMapInnerProductTest, shadowed_null_values_are_ignored) {
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+    Block block;
+    // Cover a shadowed NULL value on either side and in both build/probe 
roles. Rows 1 and 3
+    // additionally exercise the separate NULL-key bucket.
+    block.insert(
+            {make_int_float_map(
+                     {1, 1, std::nullopt, std::nullopt, 2, 1, 2, std::nullopt, 
2, 3},
+                     {std::nullopt, 2.0F, std::nullopt, 2.0F, 5.0F, 3.0F, 
4.0F, 3.0F, 4.0F, 5.0F},
+                     {2, 5, 7, 10}),
+             map_type, "left"});
+    block.insert(
+            {make_int_float_map(
+                     {1, 2, 3, std::nullopt, 3, 1, 1, 3, std::nullopt, 
std::nullopt},
+                     {3.0F, 4.0F, 5.0F, 3.0F, 4.0F, std::nullopt, 2.0F, 5.0F, 
std::nullopt, 2.0F},
+                     {3, 5, 8, 10}),
+             map_type, "right"});
+    block.insert({nullptr, return_type, "result"});
+
+    ASSERT_TRUE(execute_inner_product(block, return_type).ok());
+    const auto& result =
+            assert_cast<const 
ColumnFloat32&>(*block.get_by_position(2).column).get_data();
+    ASSERT_EQ(result.size(), 4);
+    EXPECT_FLOAT_EQ(result[0], 6.0F);
+    EXPECT_FLOAT_EQ(result[1], 6.0F);
+    EXPECT_FLOAT_EQ(result[2], 6.0F);
+    EXPECT_FLOAT_EQ(result[3], 6.0F);
+}
+
+TEST(FunctionMapInnerProductTest, rejects_retained_null_values) {
+    auto map_type = std::make_shared<DataTypeMap>(nullable_int_type(), 
nullable_float_type());
+    auto return_type = std::make_shared<DataTypeFloat32>();
+
+    auto expect_rejected = [&](ColumnPtr left, ColumnPtr right, const 
std::string& message) {
+        Block block;
+        block.insert({std::move(left), map_type, "left"});
+        block.insert({std::move(right), map_type, "right"});
+        block.insert({nullptr, return_type, "result"});
+
+        const auto status = execute_inner_product(block, return_type);
+        ASSERT_FALSE(status.ok());
+        EXPECT_NE(status.to_string().find(message), std::string::npos);
+    };
+
+    const std::string first_argument_error =
+            "First argument for function inner_product cannot have null";
+    const std::string second_argument_error =
+            "Second argument for function inner_product cannot have null";
+
+    // Retained NULL on the left while the left and right maps are selected 
for build.
+    expect_rejected(make_int_float_map({1}, {std::nullopt}, {1}),
+                    make_int_float_map({2, 3}, {2.0F, 3.0F}, {2}), 
first_argument_error);
+    expect_rejected(make_int_float_map({std::nullopt, std::nullopt}, {1.0F, 
std::nullopt}, {2}),
+                    make_int_float_map({1}, {3.0F}, {1}), 
first_argument_error);
+
+    // Retained NULL on the right while the left and right maps are selected 
for build.
+    expect_rejected(make_int_float_map({1}, {3.0F}, {1}),
+                    make_int_float_map({std::nullopt, std::nullopt}, {1.0F, 
std::nullopt}, {2}),
+                    second_argument_error);
+    expect_rejected(make_int_float_map({2, 3}, {2.0F, 3.0F}, {2}),
+                    make_int_float_map({1}, {std::nullopt}, {1}), 
second_argument_error);
+
+    // Empty-map short-circuiting must not hide a retained NULL in the 
nonempty map.
+    expect_rejected(make_int_float_map({1}, {std::nullopt}, {1}), 
make_int_float_map({}, {}, {0}),
+                    first_argument_error);
+    expect_rejected(make_int_float_map({}, {}, {0}), make_int_float_map({1}, 
{std::nullopt}, {1}),
+                    second_argument_error);
+}
+
+TEST(FunctionMapInnerProductTest, rejects_unsupported_key_type) {
+    auto double_type = make_nullable(std::make_shared<DataTypeFloat64>());
+    auto map_type = std::make_shared<DataTypeMap>(double_type, 
nullable_float_type());
+    DataTypes arguments {map_type, map_type};
+
+    try {
+        FunctionInnerProduct::create()->get_return_type_impl(arguments);
+        FAIL() << "Expected unsupported map key type to be rejected";
+    } catch (const doris::Exception& exception) {
+        EXPECT_NE(std::string(exception.what())
+                          .find("inner_product only supports integer or string 
map keys"),
+                  std::string::npos);
+    }
+}
+
+} // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProduct.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProduct.java
index 78780bf931f..a5e73abb7db 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProduct.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProduct.java
@@ -18,13 +18,17 @@
 package org.apache.doris.nereids.trees.expressions.functions.scalar;
 
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
 import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.nereids.types.coercion.AnyDataType;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
@@ -39,7 +43,10 @@ public class InnerProduct extends ScalarFunction implements 
ExplicitlyCastableSi
 
     public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
             FunctionSignature.ret(FloatType.INSTANCE)
-                    .args(ArrayType.of(FloatType.INSTANCE), 
ArrayType.of(FloatType.INSTANCE))
+                    .args(ArrayType.of(FloatType.INSTANCE), 
ArrayType.of(FloatType.INSTANCE)),
+            FunctionSignature.ret(FloatType.INSTANCE)
+                    .args(MapType.of(new AnyDataType(0), FloatType.INSTANCE),
+                            MapType.of(new AnyDataType(0), FloatType.INSTANCE))
     );
 
     /**
@@ -54,6 +61,39 @@ public class InnerProduct extends ScalarFunction implements 
ExplicitlyCastableSi
         super(functionParams);
     }
 
+    @Override
+    public void checkLegalityBeforeTypeCoercion() {
+        checkMapKeyTypes(true);
+    }
+
+    @Override
+    public void checkLegalityAfterRewrite() {
+        checkMapKeyTypes(false);
+    }
+
+    private void checkMapKeyTypes(boolean allowNullKeyType) {
+        DataType firstKeyType = null;
+        for (Expression argument : getArguments()) {
+            DataType argumentType = argument.getDataType();
+            if (argumentType.isMapType()) {
+                DataType keyType = ((MapType) argumentType).getKeyType();
+                if (allowNullKeyType && keyType.isNullType()) {
+                    continue;
+                }
+                if (!keyType.isIntegralType() && !keyType.isStringLikeType()) {
+                    throw new AnalysisException("inner_product only supports 
integer or string map keys,"
+                            + " but got " + keyType.toSql() + " in expression 
" + toSql());
+                }
+                if (firstKeyType != null && firstKeyType.isIntegralType() != 
keyType.isIntegralType()) {
+                    throw new AnalysisException("inner_product requires map 
keys from the same type family,"
+                            + " but got " + firstKeyType.toSql() + " and " + 
keyType.toSql()
+                            + " in expression " + toSql());
+                }
+                firstKeyType = keyType;
+            }
+        }
+    }
+
     /**
      * withChildren.
      */
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProductTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProductTest.java
new file mode 100644
index 00000000000..e53c100b295
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/InnerProductTest.java
@@ -0,0 +1,82 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteTestHelper;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.qe.GlobalVariable;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class InnerProductTest {
+
+    private static final NereidsParser PARSER = new NereidsParser();
+
+    @Test
+    public void testInferNullMapKeyType() {
+        Expression expression = analyze(
+                "inner_product(map(null, cast(2 as float)),"
+                        + " map(cast(null as int), cast(3 as float)))");
+
+        Assertions.assertTrue(expression instanceof InnerProduct);
+        for (Expression argument : ((InnerProduct) expression).getArguments()) 
{
+            Assertions.assertTrue(argument.getDataType().isMapType());
+            Assertions.assertEquals(IntegerType.INSTANCE,
+                    ((MapType) argument.getDataType()).getKeyType());
+        }
+        Assertions.assertDoesNotThrow(expression::checkLegalityAfterRewrite);
+    }
+
+    @Test
+    public void testRejectMixedMapKeyFamiliesInBothCoercionModes() {
+        boolean originalBehavior = 
GlobalVariable.enableNewTypeCoercionBehavior;
+        try {
+            for (boolean enableNewBehavior : new boolean[] {true, false}) {
+                GlobalVariable.enableNewTypeCoercionBehavior = 
enableNewBehavior;
+                AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                        () -> analyze("inner_product(map(1, cast(2 as float)),"
+                                + " map('1', cast(3 as float)))"));
+                Assertions.assertTrue(exception.getMessage().contains("same 
type family"),
+                        exception::getMessage);
+            }
+        } finally {
+            GlobalVariable.enableNewTypeCoercionBehavior = originalBehavior;
+        }
+    }
+
+    @Test
+    public void testRejectAllNullMapKeyTypesAfterCoercion() {
+        Expression expression = analyze(
+                "inner_product(map(null, cast(1 as float)),"
+                        + " map(null, cast(2 as float)))");
+
+        AnalysisException exception = Assertions.assertThrows(
+                AnalysisException.class, 
expression::checkLegalityAfterRewrite);
+        Assertions.assertTrue(exception.getMessage().contains(
+                "only supports integer or string map keys"), 
exception::getMessage);
+    }
+
+    private Expression analyze(String sql) {
+        return 
ExpressionRewriteTestHelper.typeCoercion(PARSER.parseExpression(sql));
+    }
+}
diff --git 
a/regression-test/data/query_p0/sql_functions/map_functions/test_map_inner_product.out
 
b/regression-test/data/query_p0/sql_functions/map_functions/test_map_inner_product.out
new file mode 100644
index 00000000000..df73024c626
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/map_functions/test_map_inner_product.out
@@ -0,0 +1,20 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !map_inner_product_rows --
+1      10.0
+2      -4.0
+3      0.0
+
+-- !map_inner_product_string_keys --
+22.0
+
+-- !map_inner_product_null_key --
+38.0
+
+-- !map_inner_product_inferred_null_key --
+6.0
+
+-- !map_inner_product_disjoint --
+0.0
+
+-- !map_inner_product_dense_compatibility --
+11.0
diff --git 
a/regression-test/suites/query_p0/sql_functions/map_functions/test_map_inner_product.groovy
 
b/regression-test/suites/query_p0/sql_functions/map_functions/test_map_inner_product.groovy
new file mode 100644
index 00000000000..a7c11284b6c
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/map_functions/test_map_inner_product.groovy
@@ -0,0 +1,124 @@
+// 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.
+
+suite("test_map_inner_product", "p0,nonConcurrent") {
+    sql "drop table if exists test_map_inner_product"
+    sql """
+        create table test_map_inner_product (
+            id int,
+            lhs map<int, float>,
+            rhs map<int, float>
+        )
+        duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num" = "1")
+    """
+    sql """
+        insert into test_map_inner_product values
+            (1, map(1, 1.0, 2, 2.0), map(2, 3.0, 1, 4.0)),
+            (2, map(1, -2.0, 3, 0.5), map(1, 4.0, 2, 99.0, 3, 8.0)),
+            (3, cast(map() as map<int, float>), map(1, 10.0)),
+            (4, map(1, cast(null as float)), map(1, 2.0)),
+            (5, cast(null as map<int, float>), map(1, 2.0))
+    """
+
+    order_qt_map_inner_product_rows """
+        select id, inner_product(lhs, rhs)
+        from test_map_inner_product
+        where id <= 3
+        order by id
+    """
+
+    qt_map_inner_product_string_keys """
+        select inner_product(
+            map('a', 2.0, 'b', 3.0),
+            map('b', 4.0, 'c', 100.0, 'a', 5.0))
+    """
+
+    qt_map_inner_product_null_key """
+        select inner_product(
+            map(cast(null as int), 2.0, 1, 3.0),
+            map(1, 10.0, cast(null as int), 4.0))
+    """
+
+    qt_map_inner_product_inferred_null_key """
+        select inner_product(
+            map(null, cast(2 as float)),
+            map(cast(null as int), cast(3 as float)))
+    """
+
+    qt_map_inner_product_disjoint """
+        select inner_product(map(1, 2.0), map(2, 3.0))
+    """
+
+    qt_map_inner_product_dense_compatibility """
+        select inner_product([1.0, 2.0], [3.0, 4.0])
+    """
+
+    test {
+        sql """
+            select inner_product(lhs, rhs)
+            from test_map_inner_product
+            where id = 4
+        """
+        exception "First argument for function inner_product cannot have null"
+    }
+
+    test {
+        sql """
+            select inner_product(lhs, rhs)
+            from test_map_inner_product
+            where id = 5
+        """
+        exception "First argument for function inner_product cannot be null"
+    }
+
+    test {
+        sql """
+            select inner_product(
+                map(cast('2024-01-01' as date), cast(1 as float)),
+                map(cast('2024-01-01' as date), cast(2 as float)))
+        """
+        exception "inner_product only supports integer or string map keys"
+    }
+
+    test {
+        sql """
+            select inner_product(
+                map(null, cast(1 as float)),
+                map(null, cast(2 as float)))
+        """
+        exception "inner_product only supports integer or string map keys"
+    }
+
+    def originalTypeCoercionBehavior = sql """
+        show global variables like 'enable_new_type_coercion_behavior'
+    """
+    try {
+        sql "set global enable_new_type_coercion_behavior = false"
+        test {
+            sql """
+                select inner_product(
+                    map(1, cast(2 as float)),
+                    map('1', cast(3 as float)))
+            """
+            exception "inner_product requires map keys from the same type 
family"
+        }
+    } finally {
+        sql "set global enable_new_type_coercion_behavior = 
${originalTypeCoercionBehavior[0][1]}"
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to