Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@


### AI inside + SQL inside
1. **AI Inside:** Run embedding, reranking, LLM inference and prompt management inside the database, supporting a complete document-in/data-out RAG workflow.
1. **AI Inside:** Run embedding, reranking, LLM inference and prompt management inside the database, supporting a complete document-in/data-out RAG workflow. Supported AI providers include OpenAI, DeepSeek, Aliyun (DashScope/OpenAI-compatible), SiliconFlow, Hunyuan, and [MiniMax](https://www.minimaxi.com).
2. **SQL Inside:** Powered by the proven OceanBase engine, delivering real-time writes and queries with full ACID compliance, and seamless MySQL ecosystem compatibility.


Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
seekdb 提供了 Semantic Index 功能,只需写入文本数据,系统即可自动进行 Embedding 并生成向量索引,查询时仅需指定文本搜索条件即可进行语义搜索。该功能对用户屏蔽了向量嵌入和查询结果 Rerank 的复杂流程,显著简化 AI 应用开发对数据库的使用方式。

### 无缝对接各类模型,内置 AI Function 实现库内实时推理
seekdb 支持大语言模型和向量嵌入模型接入,通过 DBMS_AI_SERVICE 系统包实现模型注册和管理。内置 AI_COMPLETE、AI_PROMPT、AI_EMBED、AI_RERANK 等 AI Function,支持在标准 SQL 语法下进行数据嵌入和库内实时推理。
seekdb 支持大语言模型和向量嵌入模型接入,通过 DBMS_AI_SERVICE 系统包实现模型注册和管理。内置 AI_COMPLETE、AI_PROMPT、AI_EMBED、AI_RERANK 等 AI Function,支持在标准 SQL 语法下进行数据嵌入和库内实时推理。支持的 AI 服务提供商包括 OpenAI、DeepSeek、阿里云(DashScope/OpenAI 兼容)、SiliconFlow、混元、以及 [MiniMax](https://www.minimaxi.com)。

### 基于 JSON 的动态 Schema,支持文档元数据动态存储和高效访问
seekdb 支持 JSON 数据类型,具备动态 Schema 能力。支持 JSON 的部分更新以降低数据更新成本,提供 JSON 函数索引、多值索引来优化查询性能。实现半结构化编码降低存储成本。在 AI 应用中,JSON 可作为文档元信息的存储类型,并支持与全文、向量的混合搜索。
Expand Down
87 changes: 87 additions & 0 deletions mittest/simple_server/test_ai_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,93 @@ TEST_F(TestAiService, test_get_increment_ai_model_keys_reversely)
}
}

TEST_F(TestAiService, test_minimax_ai_model_endpoint)
{
share::ObTenantSwitchGuard tenant_guard;
ASSERT_EQ(OB_SUCCESS, tenant_guard.switch_to(OB_SYS_TENANT_ID));
ObTenantAiService *ai_service = MTL(ObTenantAiService*);
ObAiServiceGuard ai_service_guard;
const ObAiModelEndpointInfo *endpoint_info = nullptr;

ObString endpoint_name = "minimax_endpoint";
ObString ai_model_name = "minimax_ai_model";
ObString url = "https://api.minimax.io/v1/chat/completions";
ObString access_key = "minimax-test-key-1234567890";
ObString provider = "minimax";
ObString request_model_name = "MiniMax-M2.7";
ObString parameters = "";
ObString request_transform_fn = "";
ObString response_transform_fn = "";
common::ObArenaAllocator allocator;
ObSqlString sql;

// 1. create MiniMax ai model endpoint
std::string json_str = R"({"url": ")";
json_str += url.ptr();
json_str += R"(", "access_key": ")";
json_str += access_key.ptr();
json_str += R"(", "ai_model_name": ")";
json_str += ai_model_name.ptr();
json_str += R"(", "provider": ")";
json_str += provider.ptr();
json_str += R"(", "request_model_name": ")";
json_str += request_model_name.ptr();
json_str += R"(", "parameters": ")";
json_str += parameters.ptr();
json_str += R"(", "request_transform_fn": ")";
json_str += request_transform_fn.ptr();
json_str += R"(", "response_transform_fn": ")";
json_str += response_transform_fn.ptr();
json_str += R"("})";
sql.assign_fmt("call DBMS_AI_SERVICE.CREATE_AI_MODEL_ENDPOINT ('%s', '%s')", endpoint_name.ptr(), json_str.c_str());
int64_t affected_rows = 0;
common::ObMySQLProxy &sql_proxy = get_curr_simple_server().get_sql_proxy();
ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows));

// 2. get MiniMax ai model endpoint by endpoint name
ASSERT_EQ(OB_SUCCESS, ai_service->get_ai_service_guard(ai_service_guard));
ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info));
ASSERT_TRUE(endpoint_info != nullptr);
check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key,
provider, request_model_name, parameters, request_transform_fn, response_transform_fn);

// 3. get MiniMax ai model endpoint by ai model name
endpoint_info = nullptr;
ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint_by_ai_model_name(ai_model_name, endpoint_info));
ASSERT_TRUE(endpoint_info != nullptr);
check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key,
provider, request_model_name, parameters, request_transform_fn, response_transform_fn);

// 4. alter MiniMax endpoint to use embedding URL
url = "https://api.minimax.io/v1/embeddings";
request_model_name = "embo-01";

json_str = R"({"url": ")";
json_str += url.ptr();
json_str += R"(", "request_model_name": ")";
json_str += request_model_name.ptr();
json_str += R"(", "request_transform_fn": ")";
json_str += request_transform_fn.ptr();
json_str += R"(", "response_transform_fn": ")";
json_str += response_transform_fn.ptr();
json_str += R"("})";

sql.assign_fmt("call DBMS_AI_SERVICE.ALTER_AI_MODEL_ENDPOINT ('%s', '%s')", endpoint_name.ptr(), json_str.c_str());
ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows));

// 5. verify altered endpoint
endpoint_info = nullptr;
ASSERT_EQ(OB_SUCCESS, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info));
ASSERT_TRUE(endpoint_info != nullptr);
check_ai_model_endpoint(*endpoint_info, allocator, endpoint_name, ai_model_name, url, access_key,
provider, request_model_name, parameters, request_transform_fn, response_transform_fn);

// 6. drop MiniMax ai model endpoint
sql.assign_fmt("call DBMS_AI_SERVICE.DROP_AI_MODEL_ENDPOINT ('%s')", endpoint_name.ptr());
ASSERT_EQ(OB_SUCCESS, sql_proxy.write(sql.ptr(), affected_rows));
ASSERT_EQ(OB_AI_FUNC_ENDPOINT_NOT_FOUND, ai_service_guard.get_ai_endpoint(endpoint_name, endpoint_info));
}

TEST_F(TestAiService, end)
{
RunCtx.time_sec_ = 0;
Expand Down
3 changes: 2 additions & 1 deletion src/share/ai_service/ob_ai_service_struct.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const char *VALID_PROVIDERS[] = {
"DEEPSEEK",
"SILICONFLOW",
"HUNYUAN-OPENAI",
"OPENAI"
"OPENAI",
"MINIMAX"
};

#define EXTRACT_JSON_ELEM_STR(json_key, member) \
Expand Down
101 changes: 100 additions & 1 deletion src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,102 @@ int ObSiliconflowUtils::ObSiliconflowRerank::parse_output(common::ObIAllocator &
return ret;
}

// MiniMax provider implementation
int ObMiniMaxUtils::get_header(common::ObIAllocator &allocator,
common::ObString &api_key,
common::ObArray<ObString> &headers)
{
// MiniMax uses the same Bearer token authentication as OpenAI
return ObOpenAIUtils::get_header(allocator, api_key, headers);
}

int ObMiniMaxUtils::ObMiniMaxEmbed::get_header(common::ObIAllocator &allocator,
ObString &api_key,
common::ObArray<ObString> &headers)
{
int ret = OB_SUCCESS;
if (OB_FAIL(ObMiniMaxUtils::get_header(allocator, api_key, headers))) {
LOG_WARN("Failed to get header", K(ret));
}
return ret;
}

int ObMiniMaxUtils::ObMiniMaxEmbed::get_body(common::ObIAllocator &allocator,
common::ObString &model,
common::ObArray<ObString> &contents,
common::ObJsonObject *config,
common::ObJsonObject *&body)
{
int ret = OB_SUCCESS;
if (model.empty() || contents.empty()) {
ret = OB_INVALID_ARGUMENT;
LOG_WARN("Model name or contents is empty", K(ret));
} else {
ObJsonObject *body_obj = nullptr;
ObJsonString *model_str = nullptr;
ObJsonArray *texts_array = nullptr;
ObJsonString *type_str = nullptr;
ObString type_val = ObString::make_string("db");
if (OB_FAIL(ObAIFuncJsonUtils::get_json_object(allocator, body_obj))) {
LOG_WARN("Failed to get json object", K(ret));
} else if (OB_FAIL(ObAIFuncJsonUtils::get_json_string(allocator, model, model_str))) {
LOG_WARN("Failed to get json string", K(ret));
} else if (OB_FAIL(body_obj->add("model", model_str))) {
LOG_WARN("Failed to add model", K(ret));
} else if (OB_FAIL(ObAIFuncJsonUtils::transform_array_to_json_array(allocator, contents, texts_array))) {
LOG_WARN("Failed to get json array", K(ret));
} else if (OB_FAIL(body_obj->add("texts", texts_array))) {
LOG_WARN("Failed to add texts", K(ret));
} else if (OB_FAIL(ObAIFuncJsonUtils::get_json_string(allocator, type_val, type_str))) {
LOG_WARN("Failed to get type string", K(ret));
} else if (OB_FAIL(body_obj->add("type", type_str))) {
LOG_WARN("Failed to add type", K(ret));
} else if (OB_FAIL(ObAIFuncJsonUtils::compact_json_object(allocator, config, body_obj))) {
LOG_WARN("Failed to compact json object", K(ret));
} else {
body = body_obj;
}
}
return ret;
}

int ObMiniMaxUtils::ObMiniMaxEmbed::parse_output(common::ObIAllocator &allocator,
common::ObJsonObject *http_response,
common::ObIJsonBase *&result)
{
int ret = OB_SUCCESS;
if (OB_ISNULL(http_response)) {
ret = OB_INVALID_ARGUMENT;
LOG_WARN("http_response is null", K(ret));
} else {
ObJsonArray *result_array = nullptr;
ObJsonNode *vectors_node = nullptr;
if (OB_FAIL(ObAIFuncJsonUtils::get_json_array(allocator, result_array))) {
LOG_WARN("Failed to get json array", K(ret));
} else if (OB_ISNULL(vectors_node = http_response->get_value("vectors"))) {
ret = OB_INVALID_DATA;
LOG_WARN("Failed to get vectors from MiniMax response", K(ret));
} else {
// MiniMax returns {"vectors": [[0.1, 0.2, ...], [0.3, 0.4, ...]], ...}
// Each element in vectors is already an embedding array
ObJsonArray *vectors_array = static_cast<ObJsonArray *>(vectors_node);
for (int64_t i = 0; OB_SUCC(ret) && i < vectors_array->element_count(); i++) {
ObJsonNode *embedding = vectors_array->get_value(i);
if (OB_ISNULL(embedding)) {
ret = OB_INVALID_DATA;
LOG_WARN("Failed to get embedding vector", K(ret), K(i));
} else if (OB_FAIL(result_array->append(embedding))) {
LOG_WARN("Failed to append embedding", K(ret));
}
}
if (OB_SUCC(ret)) {
result = result_array;
}
}
}
return ret;
}


int ObAIFuncUtils::get_header(ObIAllocator &allocator,
const ObAIFuncExprInfo &info,
Expand Down Expand Up @@ -1241,7 +1337,8 @@ int ObAIFuncUtils::get_complete_provider(ObIAllocator &allocator, const ObString
|| ob_provider_check(provider, ObAIFuncProviderUtils::ALIYUN)
|| ob_provider_check(provider, ObAIFuncProviderUtils::DEEPSEEK)
|| ob_provider_check(provider, ObAIFuncProviderUtils::SILICONFLOW)
|| ob_provider_check(provider, ObAIFuncProviderUtils::HUNYUAN)) {
|| ob_provider_check(provider, ObAIFuncProviderUtils::HUNYUAN)
|| ob_provider_check(provider, ObAIFuncProviderUtils::MINIMAX)) {
complete_provider = OB_NEWx(ObOpenAIUtils::ObOpenAIComplete, &allocator);
} else if (ob_provider_check(provider, ObAIFuncProviderUtils::DASHSCOPE)) {
complete_provider = OB_NEWx(ObDashscopeUtils::ObDashscopeComplete, &allocator);
Expand Down Expand Up @@ -1271,6 +1368,8 @@ int ObAIFuncUtils::get_embed_provider(ObIAllocator &allocator, const ObString &p
embed_provider = OB_NEWx(ObOpenAIUtils::ObOpenAIEmbed, &allocator);
} else if (ob_provider_check(provider, ObAIFuncProviderUtils::DASHSCOPE)) {
embed_provider = OB_NEWx(ObDashscopeUtils::ObDashscopeEmbed, &allocator);
} else if (ob_provider_check(provider, ObAIFuncProviderUtils::MINIMAX)) {
embed_provider = OB_NEWx(ObMiniMaxUtils::ObMiniMaxEmbed, &allocator);
} else {
ret = OB_NOT_SUPPORTED;
LOG_WARN("this provider current not support", K(ret));
Expand Down
30 changes: 30 additions & 0 deletions src/sql/engine/expr/ob_expr_ai/ob_ai_func_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,35 @@ class ObSiliconflowUtils
private:
DISALLOW_COPY_AND_ASSIGN(ObSiliconflowUtils);
};
class ObMiniMaxUtils
{
public:
class ObMiniMaxEmbed : public ObAIFuncIEmbed
{
public:
ObMiniMaxEmbed() {}
virtual ~ObMiniMaxEmbed() {}
virtual int get_header(common::ObIAllocator &allocator,
common::ObString &api_key,
common::ObArray<ObString> &headers) override;
virtual int get_body(common::ObIAllocator &allocator,
common::ObString &model,
common::ObArray<ObString> &contents,
common::ObJsonObject *config,
common::ObJsonObject *&body) override;
virtual int parse_output(common::ObIAllocator &allocator,
common::ObJsonObject *http_response,
common::ObIJsonBase *&result) override;
private:
DISALLOW_COPY_AND_ASSIGN(ObMiniMaxEmbed);
};
static int get_header(common::ObIAllocator &allocator,
common::ObString &api_key,
common::ObArray<ObString> &headers);
private:
DISALLOW_COPY_AND_ASSIGN(ObMiniMaxUtils);
};

class ObAIFuncJsonUtils
{
public:
Expand Down Expand Up @@ -337,6 +366,7 @@ class ObAIFuncProviderUtils
static constexpr char SILICONFLOW[20] = "SILICONFLOW";
static constexpr char HUNYUAN[20] = "HUNYUAN-OPENAI";
static constexpr char DEEPSEEK[20] = "DEEPSEEK";
static constexpr char MINIMAX[20] = "MINIMAX";

private:
DISALLOW_COPY_AND_ASSIGN(ObAIFuncProviderUtils);
Expand Down
1 change: 1 addition & 0 deletions unittest/sql/engine/expr/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sql_unittest(test_expr_relation_map)
ob_unittest(test_ob_openai_utils)
ob_unittest(test_ob_dashscope_utils)
ob_unittest(test_ob_siliconflow_utils)
ob_unittest(test_ob_minimax_utils)
ob_unittest(ob_expr_ai_prompt_test)

# engine_expr_test_lrpad_SOURCES=engine/expr/ob_expr_lrpad_test.cpp
Expand Down
Loading
Loading