Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #pragma once |
19 | | |
20 | | #include <glog/logging.h> |
21 | | #include <rapidjson/document.h> |
22 | | |
23 | | #include <string_view> |
24 | | |
25 | | #include "core/data_type/data_type_nullable.h" |
26 | | #include "core/data_type/primitive_type.h" |
27 | | #include "exprs/function/ai/ai_functions.h" |
28 | | #include "util/jsonb_utils.h" |
29 | | #include "util/s3_uri.h" |
30 | | #include "util/s3_util.h" |
31 | | |
32 | | namespace doris { |
33 | | class FunctionEmbed : public AIFunction<FunctionEmbed> { |
34 | | public: |
35 | | static constexpr auto name = "embed"; |
36 | | |
37 | | static constexpr size_t number_of_arguments = 2; |
38 | | |
39 | | static constexpr auto system_prompt = ""; |
40 | | |
41 | 5 | DataTypePtr get_nested_return_type_impl(const DataTypes& /*arguments*/) const { |
42 | 5 | return std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeFloat32>())); |
43 | 5 | } |
44 | | |
45 | | using PreparedFunctionImpl::execute; |
46 | | |
47 | 23 | AIResource select_ai_resource(const TAIResource& resource, PrimitiveType input_type) const { |
48 | 23 | bool has_complete_multimodal_embed_properties = _has_complete_resource_properties( |
49 | 23 | resource.embed_mm_endpoint, resource.embed_mm_provider_type, |
50 | 23 | resource.embed_mm_model_name, resource.embed_mm_api_key); |
51 | 23 | if (input_type == PrimitiveType::TYPE_JSONB && has_complete_multimodal_embed_properties) { |
52 | 1 | return AIResource::from_multimodal_embed(resource); |
53 | 1 | } |
54 | 22 | bool has_complete_embed_properties = _has_complete_resource_properties( |
55 | 22 | resource.embed_endpoint, resource.embed_provider_type, resource.embed_model_name, |
56 | 22 | resource.embed_api_key); |
57 | 22 | return has_complete_embed_properties ? AIResource::from_embed(resource) |
58 | 22 | : AIResource(resource); |
59 | 23 | } |
60 | | |
61 | | Status execute(FunctionContext* context, Block& block, const ColumnNumbers& arguments, |
62 | | uint32_t result, size_t input_rows_count, const AIResource& config, |
63 | 18 | std::shared_ptr<AIAdapter>& adapter) const { |
64 | 18 | if (arguments.size() != 2) { |
65 | 1 | return Status::InvalidArgument("Function EMBED expects 2 arguments, but got {}", |
66 | 1 | arguments.size()); |
67 | 1 | } |
68 | | |
69 | 17 | const auto& input = block.get_by_position(arguments[1]); |
70 | 17 | ColumnUInt8::MutablePtr result_null_map; |
71 | 17 | if (input.type->is_nullable()) { |
72 | 4 | const auto& [column, is_const] = unpack_if_const(input.column); |
73 | 4 | const auto& nullable = |
74 | 4 | assert_cast<const ColumnNullable&, TypeCheckOnRelease::DISABLE>(*column); |
75 | 4 | result_null_map = ColumnUInt8::create(input_rows_count, 0); |
76 | 4 | VectorizedUtils::update_null_map(result_null_map->get_data(), |
77 | 4 | nullable.get_null_map_data(), is_const); |
78 | 4 | } |
79 | | |
80 | 17 | if (result_null_map && |
81 | 17 | !simd::contain_zero(result_null_map->get_data().data(), input_rows_count)) { |
82 | 1 | block.get_by_position(result).column = |
83 | 1 | block.get_by_position(result).type->create_column_const(input_rows_count, |
84 | 1 | Field()); |
85 | 1 | return Status::OK(); |
86 | 1 | } |
87 | | |
88 | 16 | ColumnPtr input_column = |
89 | 16 | input.unnest_nullable(input.type->is_nullable() ? input.get_nullable_column_info() |
90 | 16 | : NullableColumnInfo {}, |
91 | 16 | false) |
92 | 16 | .column; |
93 | 16 | PrimitiveType input_type = remove_nullable(input.type)->get_primitive_type(); |
94 | 16 | if (input_type == PrimitiveType::TYPE_JSONB) { |
95 | 9 | return _execute_multimodal_embed(context, block, result, input_rows_count, config, |
96 | 9 | adapter, input_column, std::move(result_null_map)); |
97 | 9 | } |
98 | 7 | if (input_type == PrimitiveType::TYPE_STRING || input_type == PrimitiveType::TYPE_VARCHAR || |
99 | 7 | input_type == PrimitiveType::TYPE_CHAR) { |
100 | 6 | return _execute_text_embed(context, block, result, input_rows_count, config, adapter, |
101 | 6 | input_column, std::move(result_null_map)); |
102 | 6 | } |
103 | 1 | return Status::InvalidArgument( |
104 | 1 | "Function EMBED expects the second argument to be STRING or JSON, but got type {}", |
105 | 1 | input.type->get_name()); |
106 | 7 | } |
107 | | |
108 | 24 | static FunctionPtr create() { return std::make_shared<FunctionEmbed>(); } |
109 | | |
110 | | private: |
111 | | static bool _has_complete_resource_properties(std::string_view endpoint, |
112 | | std::string_view provider_type, |
113 | | std::string_view model_name, |
114 | 45 | std::string_view api_key) { |
115 | 45 | return !endpoint.empty() && !provider_type.empty() && !model_name.empty() && |
116 | 45 | (provider_type == "LOCAL" || !api_key.empty()); |
117 | 45 | } |
118 | | |
119 | 15 | static int32_t _get_embed_max_batch_size(FunctionContext* context) { |
120 | 15 | QueryContext* query_ctx = context->state()->get_query_ctx(); |
121 | 15 | DORIS_CHECK(query_ctx != nullptr); |
122 | | |
123 | 15 | return query_ctx->query_options().embed_max_batch_size; |
124 | 15 | } |
125 | | |
126 | | Status _execute_text_embed(FunctionContext* context, Block& block, uint32_t result, |
127 | | size_t input_rows_count, const AIResource& config, |
128 | | std::shared_ptr<AIAdapter>& adapter, const ColumnPtr& input_column, |
129 | 6 | ColumnUInt8::MutablePtr result_null_map) const { |
130 | 6 | auto col_result = ColumnArray::create( |
131 | 6 | ColumnNullable::create(ColumnFloat32::create(), ColumnUInt8::create())); |
132 | 6 | std::vector<std::string> batch_prompts; |
133 | 6 | size_t current_batch_size = 0; |
134 | 6 | const int32_t max_batch_size = _get_embed_max_batch_size(context); |
135 | 6 | const size_t max_context_window_size = |
136 | 6 | static_cast<size_t>(get_ai_context_window_size(context)); |
137 | 6 | const NullMap* null_map = result_null_map ? &result_null_map->get_data() : nullptr; |
138 | 6 | const Columns prompt_columns {input_column}; |
139 | | |
140 | 29 | for (size_t i = 0; i < input_rows_count; ++i) { |
141 | 23 | if (null_map && (*null_map)[i]) { |
142 | 9 | continue; |
143 | 9 | } |
144 | | |
145 | 14 | std::string prompt; |
146 | 14 | RETURN_IF_ERROR(build_prompt(prompt_columns, i, prompt)); |
147 | | |
148 | 14 | const size_t prompt_size = prompt.size(); |
149 | | |
150 | 14 | if (prompt_size > max_context_window_size) { |
151 | | // flush history batch |
152 | 0 | RETURN_IF_ERROR(_flush_text_embedding_batch(batch_prompts, *col_result, config, |
153 | 0 | adapter, context)); |
154 | 0 | current_batch_size = 0; |
155 | |
|
156 | 0 | batch_prompts.emplace_back(std::move(prompt)); |
157 | 0 | RETURN_IF_ERROR(_flush_text_embedding_batch(batch_prompts, *col_result, config, |
158 | 0 | adapter, context)); |
159 | 0 | continue; |
160 | 0 | } |
161 | | |
162 | 14 | if (!batch_prompts.empty() && |
163 | 14 | (current_batch_size + prompt_size > max_context_window_size || |
164 | 8 | batch_prompts.size() >= static_cast<size_t>(max_batch_size))) { |
165 | 3 | RETURN_IF_ERROR(_flush_text_embedding_batch(batch_prompts, *col_result, config, |
166 | 3 | adapter, context)); |
167 | 3 | current_batch_size = 0; |
168 | 3 | } |
169 | | |
170 | 14 | batch_prompts.emplace_back(std::move(prompt)); |
171 | 14 | current_batch_size += prompt_size; |
172 | 14 | } |
173 | | |
174 | 6 | RETURN_IF_ERROR( |
175 | 6 | _flush_text_embedding_batch(batch_prompts, *col_result, config, adapter, context)); |
176 | | |
177 | 6 | block.replace_by_position(result, _expand_and_wrap_nullable_result( |
178 | 6 | std::move(col_result), std::move(result_null_map), |
179 | 6 | input_rows_count)); |
180 | 6 | return Status::OK(); |
181 | 6 | } |
182 | | |
183 | | Status _execute_multimodal_embed(FunctionContext* context, Block& block, uint32_t result, |
184 | | size_t input_rows_count, const AIResource& config, |
185 | | std::shared_ptr<AIAdapter>& adapter, |
186 | | const ColumnPtr& input_column, |
187 | 9 | ColumnUInt8::MutablePtr result_null_map) const { |
188 | 9 | auto col_result = ColumnArray::create( |
189 | 9 | ColumnNullable::create(ColumnFloat32::create(), ColumnUInt8::create())); |
190 | 9 | std::vector<MultimodalType> batch_media_types; |
191 | 9 | std::vector<std::string> batch_media_content_types; |
192 | 9 | std::vector<std::string> batch_media_urls; |
193 | 9 | const NullMap* null_map = result_null_map ? &result_null_map->get_data() : nullptr; |
194 | | |
195 | 9 | int64_t ttl_seconds = 3600; |
196 | 9 | QueryContext* query_ctx = context->state()->get_query_ctx(); |
197 | 9 | if (query_ctx && query_ctx->query_options().__isset.file_presigned_url_ttl_seconds) { |
198 | 9 | ttl_seconds = query_ctx->query_options().file_presigned_url_ttl_seconds; |
199 | 9 | if (ttl_seconds <= 0) { |
200 | 1 | ttl_seconds = 3600; |
201 | 1 | } |
202 | 9 | } |
203 | | |
204 | 9 | const int32_t max_batch_size = _get_embed_max_batch_size(context); |
205 | | |
206 | 24 | for (size_t i = 0; i < input_rows_count; ++i) { |
207 | 19 | if (null_map && (*null_map)[i]) { |
208 | 3 | continue; |
209 | 3 | } |
210 | | |
211 | 16 | rapidjson::Document file_input; |
212 | 16 | RETURN_IF_ERROR(_parse_file_input(*input_column, i, file_input)); |
213 | | |
214 | 16 | std::string content_type; |
215 | 16 | MultimodalType media_type; |
216 | 16 | RETURN_IF_ERROR(_infer_media_type(file_input, content_type, media_type)); |
217 | | |
218 | 14 | std::string media_url; |
219 | 14 | RETURN_IF_ERROR(_resolve_media_url(file_input, ttl_seconds, media_url)); |
220 | | |
221 | 12 | if (!batch_media_urls.empty() && |
222 | 12 | batch_media_urls.size() >= static_cast<size_t>(max_batch_size)) { |
223 | 1 | RETURN_IF_ERROR(_flush_multimodal_embedding_batch( |
224 | 1 | batch_media_types, batch_media_content_types, batch_media_urls, *col_result, |
225 | 1 | config, adapter, context)); |
226 | 1 | } |
227 | | |
228 | 12 | batch_media_types.emplace_back(media_type); |
229 | 12 | batch_media_content_types.emplace_back(std::move(content_type)); |
230 | 12 | batch_media_urls.emplace_back(std::move(media_url)); |
231 | 12 | } |
232 | | |
233 | 5 | RETURN_IF_ERROR(_flush_multimodal_embedding_batch( |
234 | 5 | batch_media_types, batch_media_content_types, batch_media_urls, *col_result, config, |
235 | 5 | adapter, context)); |
236 | | |
237 | 5 | block.replace_by_position(result, _expand_and_wrap_nullable_result( |
238 | 5 | std::move(col_result), std::move(result_null_map), |
239 | 5 | input_rows_count)); |
240 | 5 | return Status::OK(); |
241 | 5 | } |
242 | | |
243 | | // EMBED-private helper. |
244 | | // Sends one embedding request with a prebuilt request body and validates returned row count. |
245 | | Status _execute_prebuilt_embedding_request(const std::string& request_body, |
246 | | std::vector<std::vector<float>>& results, |
247 | | size_t expected_size, const AIResource& config, |
248 | | std::shared_ptr<AIAdapter>& adapter, |
249 | 0 | FunctionContext* context) const { |
250 | 0 | std::string response; |
251 | | #ifdef BE_TEST |
252 | | if (config.provider_type == "MOCK") { |
253 | | results.clear(); |
254 | | results.reserve(expected_size); |
255 | | for (size_t i = 0; i < expected_size; ++i) { |
256 | | results.emplace_back(std::initializer_list<float> {0, 1, 2, 3, 4}); |
257 | | } |
258 | | return Status::OK(); |
259 | | } |
260 | | #endif |
261 | |
|
262 | 0 | RETURN_IF_ERROR( |
263 | 0 | this->send_request_to_llm(request_body, response, config, adapter, context)); |
264 | | |
265 | 0 | RETURN_IF_ERROR(adapter->parse_embedding_response(response, results)); |
266 | 0 | if (results.empty()) { |
267 | 0 | return Status::InternalError("AI returned empty result"); |
268 | 0 | } |
269 | 0 | if (results.size() != expected_size) [[unlikely]] { |
270 | 0 | return Status::InternalError( |
271 | 0 | "AI embedding returned {} results, but {} inputs were sent", results.size(), |
272 | 0 | expected_size); |
273 | 0 | } |
274 | 0 | return Status::OK(); |
275 | 0 | } |
276 | | |
277 | | // EMBED-private helper. |
278 | | // Flushes one accumulated text embedding batch into the output array column. |
279 | | Status _flush_text_embedding_batch(std::vector<std::string>& batch_prompts, |
280 | | ColumnArray& col_result, const AIResource& config, |
281 | | std::shared_ptr<AIAdapter>& adapter, |
282 | 9 | FunctionContext* context) const { |
283 | 9 | if (batch_prompts.empty()) { |
284 | 0 | return Status::OK(); |
285 | 0 | } |
286 | | |
287 | 9 | std::string request_body; |
288 | 9 | RETURN_IF_ERROR(adapter->build_embedding_request(batch_prompts, request_body)); |
289 | 9 | std::vector<std::vector<float>> batch_results; |
290 | 9 | RETURN_IF_ERROR(_execute_prebuilt_embedding_request( |
291 | 9 | request_body, batch_results, batch_prompts.size(), config, adapter, context)); |
292 | 14 | for (const auto& batch_result : batch_results) { |
293 | 14 | _insert_embedding_result(col_result, batch_result); |
294 | 14 | } |
295 | 9 | batch_prompts.clear(); |
296 | 9 | return Status::OK(); |
297 | 9 | } |
298 | | |
299 | | // EMBED-private helper. |
300 | | // Flushes one accumulated multimodal embedding batch into the output array column. |
301 | | Status _flush_multimodal_embedding_batch(std::vector<MultimodalType>& batch_media_types, |
302 | | std::vector<std::string>& batch_media_content_types, |
303 | | std::vector<std::string>& batch_media_urls, |
304 | | ColumnArray& col_result, const AIResource& config, |
305 | | std::shared_ptr<AIAdapter>& adapter, |
306 | 6 | FunctionContext* context) const { |
307 | 6 | if (batch_media_urls.empty()) { |
308 | 0 | return Status::OK(); |
309 | 0 | } |
310 | | |
311 | 6 | std::string request_body; |
312 | 6 | RETURN_IF_ERROR(adapter->build_multimodal_embedding_request( |
313 | 6 | batch_media_types, batch_media_urls, batch_media_content_types, request_body)); |
314 | | |
315 | 6 | std::vector<std::vector<float>> batch_results; |
316 | 6 | RETURN_IF_ERROR(_execute_prebuilt_embedding_request( |
317 | 6 | request_body, batch_results, batch_media_urls.size(), config, adapter, context)); |
318 | 12 | for (const auto& batch_result : batch_results) { |
319 | 12 | _insert_embedding_result(col_result, batch_result); |
320 | 12 | } |
321 | 6 | batch_media_types.clear(); |
322 | 6 | batch_media_content_types.clear(); |
323 | 6 | batch_media_urls.clear(); |
324 | 6 | return Status::OK(); |
325 | 6 | } |
326 | | |
327 | | static void _insert_embedding_result(ColumnArray& col_array, |
328 | 26 | const std::vector<float>& float_result) { |
329 | 26 | auto& offsets = col_array.get_offsets(); |
330 | 26 | auto& nested_nullable_col = assert_cast<ColumnNullable&>(col_array.get_data()); |
331 | 26 | auto& nested_col = |
332 | 26 | assert_cast<ColumnFloat32&>(*(nested_nullable_col.get_nested_column_ptr())); |
333 | 26 | nested_col.reserve(nested_col.size() + float_result.size()); |
334 | | |
335 | 26 | size_t current_offset = nested_col.size(); |
336 | 26 | nested_col.insert_many_raw_data(reinterpret_cast<const char*>(float_result.data()), |
337 | 26 | float_result.size()); |
338 | 26 | offsets.push_back(current_offset + float_result.size()); |
339 | 26 | auto& null_map = nested_nullable_col.get_null_map_column(); |
340 | 26 | null_map.insert_many_vals(0, float_result.size()); |
341 | 26 | } |
342 | | |
343 | | static ColumnPtr _expand_and_wrap_nullable_result(ColumnArray::MutablePtr result, |
344 | | ColumnUInt8::MutablePtr result_null_map, |
345 | 11 | size_t input_rows_count) { |
346 | 11 | if (!result_null_map) { |
347 | 8 | return result; |
348 | 8 | } |
349 | | |
350 | 3 | auto& offsets = result->get_offsets(); |
351 | 3 | size_t compact_row = offsets.size(); |
352 | 3 | offsets.resize(input_rows_count); |
353 | | // For example, embedding rows 1 and 3 produces compact offsets [5, 10]. Given |
354 | | // result_null_map [1, 0, 1, 0, 1], expand them to [0, 5, 5, 10, 10], where NULL rows |
355 | | // reuse the previous offset. Fill backwards to avoid overwriting unread compact offsets. |
356 | 24 | for (size_t row = input_rows_count; row-- > 0;) { |
357 | 21 | if (result_null_map->get_data()[row]) { |
358 | 12 | offsets[row] = compact_row == 0 ? 0 : offsets[compact_row - 1]; |
359 | 12 | } else { |
360 | 9 | offsets[row] = offsets[--compact_row]; |
361 | 9 | } |
362 | 21 | } |
363 | 3 | return ColumnNullable::create(std::move(result), std::move(result_null_map)); |
364 | 11 | } |
365 | | |
366 | 52 | static bool _starts_with_ignore_case(std::string_view s, std::string_view prefix) { |
367 | 52 | if (s.size() < prefix.size()) { |
368 | 0 | return false; |
369 | 0 | } |
370 | 243 | return std::equal(prefix.begin(), prefix.end(), s.begin(), [](char a, char b) { |
371 | 243 | return std::tolower(static_cast<unsigned char>(a)) == |
372 | 243 | std::tolower(static_cast<unsigned char>(b)); |
373 | 243 | }); |
374 | 52 | } |
375 | | |
376 | | static Status _infer_media_type(const rapidjson::Value& file_input, std::string& content_type, |
377 | 16 | MultimodalType& media_type) { |
378 | 16 | RETURN_IF_ERROR(_get_required_string_field(file_input, "content_type", content_type)); |
379 | | |
380 | 15 | if (_starts_with_ignore_case(content_type, "image/")) { |
381 | 9 | media_type = MultimodalType::IMAGE; |
382 | 9 | return Status::OK(); |
383 | 9 | } else if (_starts_with_ignore_case(content_type, "video/")) { |
384 | 3 | media_type = MultimodalType::VIDEO; |
385 | 3 | return Status::OK(); |
386 | 3 | } else if (_starts_with_ignore_case(content_type, "audio/")) { |
387 | 2 | media_type = MultimodalType::AUDIO; |
388 | 2 | return Status::OK(); |
389 | 2 | } |
390 | | |
391 | 1 | return Status::InvalidArgument("Unsupported content_type for EMBED: {}", content_type); |
392 | 15 | } |
393 | | |
394 | | // Parse the FILE-like JSONB argument into a JSON object for downstream field reads. |
395 | | static Status _parse_file_input(const IColumn& file_column, size_t row_num, |
396 | 16 | rapidjson::Document& file_input) { |
397 | 16 | StringRef file_ref = file_column.get_data_at(row_num); |
398 | 16 | std::string file_json = JsonbToJson::jsonb_to_json_string(file_ref.data, file_ref.size); |
399 | 16 | file_input.Parse(file_json.c_str()); |
400 | 16 | DORIS_CHECK(!file_input.HasParseError() && file_input.IsObject()); |
401 | 16 | return Status::OK(); |
402 | 16 | } |
403 | | |
404 | | // TODO(lzq): After support FILE type, We should use the interface provided by FILE to get the fields |
405 | | // replacing this function |
406 | | static Status _get_required_string_field(const rapidjson::Value& obj, const char* field_name, |
407 | 35 | std::string& value) { |
408 | 35 | auto iter = obj.FindMember(field_name); |
409 | 35 | if (iter == obj.MemberEnd() || !iter->value.IsString()) { |
410 | 3 | return Status::InvalidArgument( |
411 | 3 | "EMBED file json field '{}' is required and must be a string", field_name); |
412 | 3 | } |
413 | 32 | value = iter->value.GetString(); |
414 | 32 | if (value.empty()) { |
415 | 0 | return Status::InvalidArgument("EMBED file json field '{}' can not be empty", |
416 | 0 | field_name); |
417 | 0 | } |
418 | 32 | return Status::OK(); |
419 | 32 | } |
420 | | |
421 | | static Status init_s3_client_conf_from_json(const rapidjson::Value& file_input, |
422 | 3 | S3ClientConf& s3_client_conf) { |
423 | 3 | std::string endpoint; |
424 | 3 | RETURN_IF_ERROR(_get_required_string_field(file_input, "endpoint", endpoint)); |
425 | 2 | std::string region; |
426 | 2 | RETURN_IF_ERROR(_get_required_string_field(file_input, "region", region)); |
427 | | |
428 | 4 | auto get_optional_string_field = [&](const char* field_name, std::string& value) { |
429 | 4 | auto iter = file_input.FindMember(field_name); |
430 | 4 | if (iter == file_input.MemberEnd() || iter->value.IsNull()) { |
431 | 0 | return; |
432 | 0 | } |
433 | 4 | DORIS_CHECK(iter->value.IsString()); |
434 | 4 | value = iter->value.GetString(); |
435 | 4 | }; |
436 | | |
437 | 1 | get_optional_string_field("ak", s3_client_conf.ak); |
438 | 1 | get_optional_string_field("sk", s3_client_conf.sk); |
439 | 1 | get_optional_string_field("role_arn", s3_client_conf.role_arn); |
440 | 1 | get_optional_string_field("external_id", s3_client_conf.external_id); |
441 | 1 | s3_client_conf.endpoint = endpoint; |
442 | 1 | s3_client_conf.region = region; |
443 | | |
444 | 1 | return Status::OK(); |
445 | 2 | } |
446 | | |
447 | | Status _resolve_media_url(const rapidjson::Value& file_input, int64_t ttl_seconds, |
448 | 14 | std::string& media_url) const { |
449 | 14 | std::string uri; |
450 | 14 | RETURN_IF_ERROR(_get_required_string_field(file_input, "uri", uri)); |
451 | | |
452 | | // If it's a direct http/https URL, use it as-is |
453 | 14 | if (_starts_with_ignore_case(uri, "http://") || _starts_with_ignore_case(uri, "https://")) { |
454 | 11 | media_url = uri; |
455 | 11 | return Status::OK(); |
456 | 11 | } |
457 | | |
458 | 3 | S3ClientConf s3_client_conf; |
459 | 3 | RETURN_IF_ERROR(init_s3_client_conf_from_json(file_input, s3_client_conf)); |
460 | 1 | auto s3_client = DORIS_TRY(S3ClientFactory::instance().create(s3_client_conf)); |
461 | | |
462 | 1 | S3URI s3_uri(uri); |
463 | 1 | RETURN_IF_ERROR(s3_uri.parse()); |
464 | 1 | std::string bucket = s3_uri.get_bucket(); |
465 | 1 | std::string key = s3_uri.get_key(); |
466 | 1 | DORIS_CHECK(!bucket.empty() && !key.empty()); |
467 | 1 | media_url = s3_client->generate_presigned_url({.bucket = bucket, .key = key, .prefix = ""}, |
468 | 1 | ttl_seconds); |
469 | 1 | return Status::OK(); |
470 | 1 | } |
471 | | }; |
472 | | |
473 | | }; // namespace doris |