Commit fce865d46b450ce838d63cd34dbe2871bd6f967c
Merge remote-tracking branch 'origin/master'
Showing
39 changed files
with
4368 additions
and
234 deletions
pom.xml
| ... | ... | @@ -39,7 +39,7 @@ |
| 39 | 39 | <jackson.version>2.17.2</jackson.version> |
| 40 | 40 | <json-schema.version>1.17.2</json-schema.version> |
| 41 | 41 | <!-- 向量数据 --> |
| 42 | - <weaviate.version>4.4.0</weaviate.version> | |
| 42 | + <milvus.version>2.6.15</milvus.version> | |
| 43 | 43 | </properties> |
| 44 | 44 | |
| 45 | 45 | <dependencies> |
| ... | ... | @@ -49,6 +49,30 @@ |
| 49 | 49 | <artifactId>spring-boot-starter-web</artifactId> |
| 50 | 50 | </dependency> |
| 51 | 51 | |
| 52 | + <!-- Milvus Java SDK 核心依赖 --> | |
| 53 | + <dependency> | |
| 54 | + <groupId>io.milvus</groupId> | |
| 55 | + <artifactId>milvus-sdk-java</artifactId> | |
| 56 | + <version>${milvus.version}</version> | |
| 57 | + </dependency> | |
| 58 | + | |
| 59 | + <dependency> | |
| 60 | + <groupId>dev.langchain4j</groupId> | |
| 61 | + <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> | |
| 62 | + <version>${langchain4j.version}</version> | |
| 63 | + </dependency> | |
| 64 | + | |
| 65 | +<!-- <dependency>--> | |
| 66 | +<!-- <groupId>com.baomidou</groupId>--> | |
| 67 | +<!-- <artifactId>mybatis-plus-boot-starter</artifactId>--> | |
| 68 | +<!-- <version>3.5.3.1</version>--> | |
| 69 | +<!-- </dependency>--> | |
| 70 | + | |
| 71 | + <dependency> | |
| 72 | + <groupId>org.springframework.boot</groupId> | |
| 73 | + <artifactId>spring-boot-starter-actuator</artifactId> | |
| 74 | + </dependency> | |
| 75 | + | |
| 52 | 76 | <dependency> |
| 53 | 77 | <groupId>org.springframework.boot</groupId> |
| 54 | 78 | <artifactId>spring-boot-starter-validation</artifactId> |
| ... | ... | @@ -63,6 +87,12 @@ |
| 63 | 87 | <artifactId>spring-boot-starter-aop</artifactId> |
| 64 | 88 | </dependency> |
| 65 | 89 | |
| 90 | + <dependency> | |
| 91 | + <groupId>com.google.code.gson</groupId> | |
| 92 | + <artifactId>gson</artifactId> | |
| 93 | + <version>2.10.1</version> | |
| 94 | + </dependency> | |
| 95 | + | |
| 66 | 96 | <!-- Spring Cloud Context 依赖 --> |
| 67 | 97 | <dependency> |
| 68 | 98 | <groupId>org.springframework.cloud</groupId> |
| ... | ... | @@ -270,69 +300,12 @@ |
| 270 | 300 | <version>1.3.2</version> |
| 271 | 301 | </dependency> |
| 272 | 302 | |
| 273 | - <!-- H2 Database --> | |
| 274 | - <!-- 最新稳定版本 --> | |
| 275 | - <!-- <dependency>--> | |
| 276 | - <!-- <groupId>com.h2database</groupId>--> | |
| 277 | - <!-- <artifactId>h2</artifactId>--> | |
| 278 | - <!-- <version>2.2.224</version>--> | |
| 279 | - <!-- </dependency>--> | |
| 280 | - | |
| 281 | - <!-- 数据验证 --> | |
| 282 | - <!-- <dependency>--> | |
| 283 | - <!-- <groupId>org.hibernate.orm</groupId>--> | |
| 284 | - <!-- <artifactId>hibernate-core</artifactId>--> | |
| 285 | - <!-- <version>6.4.4.Final</version>--> | |
| 286 | - <!-- </dependency>--> | |
| 287 | - <!-- <dependency>--> | |
| 288 | - <!-- <groupId>org.hibernate.validator</groupId>--> | |
| 289 | - <!-- <artifactId>hibernate-validator</artifactId>--> | |
| 290 | - <!-- <version>8.0.1.Final</version>--> | |
| 291 | - <!-- </dependency>--> | |
| 292 | 303 | <dependency> |
| 293 | 304 | <groupId>jakarta.persistence</groupId> |
| 294 | 305 | <artifactId>jakarta.persistence-api</artifactId> |
| 295 | 306 | <version>3.1.0</version> |
| 296 | 307 | </dependency> |
| 297 | 308 | |
| 298 | - | |
| 299 | - <!-- <dependency>--> | |
| 300 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 301 | - <!-- <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>--> | |
| 302 | - <!-- <version>${langchain4jpdf.version}</version>--> | |
| 303 | - <!-- </dependency>--> | |
| 304 | - | |
| 305 | - | |
| 306 | - <!-- 工具调用 --> | |
| 307 | - <!-- <dependency>--> | |
| 308 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 309 | - <!-- <artifactId>langchain4j</artifactId>--> | |
| 310 | - <!-- <version>${langchain4-10.version}</version>--> | |
| 311 | - <!-- </dependency>--> | |
| 312 | - | |
| 313 | - <!-- ========== 官方 SDK(可选) ========== --> | |
| 314 | - <!-- OpenAI 官方 SDK --> | |
| 315 | - <!-- <dependency>--> | |
| 316 | - <!-- <groupId>com.theokanning.openai-gpt3-java</groupId>--> | |
| 317 | - <!-- <artifactId>service</artifactId>--> | |
| 318 | - <!-- <version>${openai-java.version}</version>--> | |
| 319 | - <!-- </dependency>--> | |
| 320 | - | |
| 321 | - <!-- <!– Anthropic 官方 SDK –>--> | |
| 322 | - <!-- <dependency>--> | |
| 323 | - <!-- <groupId>com.anthropics</groupId>--> | |
| 324 | - <!-- <artifactId>anthropic-sdk-java</artifactId>--> | |
| 325 | - <!-- <version>${anthropic-sdk.version}</version>--> | |
| 326 | - <!-- </dependency>--> | |
| 327 | - | |
| 328 | - <!-- 嵌入向量 --> | |
| 329 | - <!-- <dependency>--> | |
| 330 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 331 | - <!-- <artifactId>langchain4j-embeddings</artifactId>--> | |
| 332 | - <!-- <version>${langchain4jbeta.version}</version>--> | |
| 333 | - <!-- </dependency>--> | |
| 334 | - | |
| 335 | - | |
| 336 | 309 | <!-- ========== LangChain4j 1.10.0 ========== --> |
| 337 | 310 | <!-- 核心库 --> |
| 338 | 311 | <dependency> |
| ... | ... | @@ -341,23 +314,13 @@ |
| 341 | 314 | <version>${langchain4j.version}</version> |
| 342 | 315 | </dependency> |
| 343 | 316 | |
| 344 | - <!-- OpenAI 集成(支持 Function Calling) --> | |
| 345 | -<!-- <dependency>--> | |
| 346 | -<!-- <groupId>dev.langchain4j</groupId>--> | |
| 347 | -<!-- <artifactId>langchain4j-open-ai</artifactId>--> | |
| 348 | -<!-- <version>${langchain4j.version}</version>--> | |
| 349 | -<!-- </dependency>--> | |
| 350 | 317 | |
| 351 | 318 | <dependency> |
| 352 | 319 | <groupId>dev.langchain4j</groupId> |
| 353 | 320 | <artifactId>langchain4j</artifactId> |
| 354 | 321 | <version>${langchain4j.version}</version> |
| 355 | 322 | </dependency> |
| 356 | - <!-- <dependency>--> | |
| 357 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 358 | - <!-- <artifactId>langchain4j-embeddings</artifactId>--> | |
| 359 | - <!-- <version>${langchain4j.version}</version>--> | |
| 360 | - <!-- </dependency>--> | |
| 323 | + | |
| 361 | 324 | <dependency> |
| 362 | 325 | <groupId>dev.langchain4j</groupId> |
| 363 | 326 | <artifactId>langchain4j-ollama</artifactId> |
| ... | ... | @@ -371,13 +334,6 @@ |
| 371 | 334 | <version>1.17.0</version> |
| 372 | 335 | </dependency> |
| 373 | 336 | |
| 374 | - <!-- 文档加载器 --> | |
| 375 | - <!-- Tika 文档解析器(支持多种格式) --> | |
| 376 | - <!-- <dependency>--> | |
| 377 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 378 | - <!-- <artifactId>langchain4j-document-parser-tika</artifactId>--> | |
| 379 | - <!-- <version>${langchain4j.version}</version>--> | |
| 380 | - <!-- </dependency>--> | |
| 381 | 337 | |
| 382 | 338 | <!-- 或者使用 Apache Tika 直接 --> |
| 383 | 339 | <dependency> |
| ... | ... | @@ -392,12 +348,6 @@ |
| 392 | 348 | <version>2.9.1</version> |
| 393 | 349 | </dependency> |
| 394 | 350 | |
| 395 | - <!-- 嵌入向量 --> | |
| 396 | - <!-- <dependency>--> | |
| 397 | - <!-- <groupId>dev.langchain4j</groupId>--> | |
| 398 | - <!-- <artifactId>langchain4j-embeddings</artifactId>--> | |
| 399 | - <!-- <version>${langchain4jbeta.version}</version>--> | |
| 400 | - <!-- </dependency>--> | |
| 401 | 351 | |
| 402 | 352 | <!-- Spring Retry --> |
| 403 | 353 | <dependency> | ... | ... |
src/main/java/com/xly/agent/ErpAiAgent.java
| ... | ... | @@ -27,10 +27,13 @@ public interface ErpAiAgent { |
| 27 | 27 | * 入参:用户问题、执行的SQL、表结构、JSON格式结果 |
| 28 | 28 | */ |
| 29 | 29 | @SystemMessage(""" |
| 30 | - 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景: | |
| 30 | + 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景: | |
| 31 | 31 | 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; |
| 32 | 32 | 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; |
| 33 | - 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”; | |
| 33 | + 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据” | |
| 34 | + 3.1. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 1000000,错误写法是 1,000,000,即使数字很大,也请保持连续的数字串,不要打断。 | |
| 35 | + 3.2 所有日期请转换为 YYYY-MM-DD 格式(例如:2026-03-15),严禁包含时间部分(如小时、分钟、秒)(例如:2026-03-15 00:00:00),也不要包含时区信息。” | |
| 36 | + 3.3. 金额,单价,数量 严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 2400056,错误写法是 2,400,056 即使数字很大,也请保持连续的数字串,不要打断。 | |
| 34 | 37 | 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; |
| 35 | 38 | 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 |
| 36 | 39 | """) |
| ... | ... | @@ -48,4 +51,132 @@ public interface ErpAiAgent { |
| 48 | 51 | @V("sql") String sql, |
| 49 | 52 | @V("tableStruct") String tableStruct, |
| 50 | 53 | @V("result") String result); |
| 54 | + | |
| 55 | + /** | |
| 56 | + * 动态表结构:自然语言解释SQL执行结果 | |
| 57 | + * 入参:用户问题、执行的SQL、表结构、JSON格式结果 | |
| 58 | + */ | |
| 59 | + @SystemMessage(""" | |
| 60 | + 你是专业的业务数据分析师,请分析以下查询结果: | |
| 61 | + 【用户问题】 | |
| 62 | + {{userInput}} | |
| 63 | + 【数据字段说明】 | |
| 64 | + {{sMilvusFiledDescription}} | |
| 65 | + 【查询结果数据(JSON格式)】 | |
| 66 | + {{result}} | |
| 67 | + 【分析要求】 | |
| 68 | + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; | |
| 69 | + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; | |
| 70 | + 3. 输出格式: | |
| 71 | + 3.1. 如果用户要求"表格形式展示",先输出简短文字说明,然后输出Markdown格式的表格 | |
| 72 | + 3.2. 如果用户未要求表格,仅返回解释内容,不要列出ID,无多余标题、换行、符号 | |
| 73 | + 3.3. 结果为空时直接返回"未查询到相关数据" | |
| 74 | + 3.4. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",") | |
| 75 | + 3.5. 所有日期请转换为 YYYY-MM-DD 格式,严禁包含时间部分 | |
| 76 | + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; | |
| 77 | + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 | |
| 78 | + """) | |
| 79 | + @UserMessage(""" | |
| 80 | + 【用户查询】 | |
| 81 | + {{userInput}} | |
| 82 | + 【字段说明】 | |
| 83 | + {{sMilvusFiledDescription}} | |
| 84 | + 【查询结果】 | |
| 85 | + 用户原始查询:{{userInput}} | |
| 86 | + 执行查询向量库后结果(JSON格式):{{result}} | |
| 87 | + 请根据上述信息+通用规则,对查询结果做业务解释: | |
| 88 | + """) | |
| 89 | + String explainMilvusResult(@MemoryId String userId, | |
| 90 | + @V("userInput") String userInput, | |
| 91 | + @V("sMilvusFiledDescription") String sMilvusFiledDescription, | |
| 92 | + @V("result") String result); | |
| 93 | + | |
| 94 | + /** | |
| 95 | + * AI路由判断接口 | |
| 96 | + * true: 走聚合查询(MySQL) | |
| 97 | + * false: 走向量检索(Milvus) | |
| 98 | + */ | |
| 99 | +// @SystemMessage(""" | |
| 100 | +// 你是一个智能查询路由专家,请根据用户需求判断应该使用哪种查询方式。 | |
| 101 | +// | |
| 102 | +// 判断标准: | |
| 103 | +// 1. 返回 true(聚合查询/MySQL)的场景: | |
| 104 | +// - 需要计算统计指标:总数、总和、平均值、最大/最小值、占比 | |
| 105 | +// - 需要数据汇总:分组统计、排行榜、TopN | |
| 106 | +// - 包含关键词:统计、求和、汇总、排名、平均、数量、总额、最高、最低、占比、分组、分析、趋势 | |
| 107 | +// - 示例:统计本月销售总额、查询销量前10的商品、各品类占比分析 | |
| 108 | +// | |
| 109 | +// 2. 返回 false(向量检索/Milvus)的场景: | |
| 110 | +// - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情 | |
| 111 | +// - 查找相似内容:根据语义查找相关文档、推荐相似商品 | |
| 112 | +// - 模糊匹配:不确定具体关键词,需要语义理解 | |
| 113 | +// - 内容检索:查找包含特定概念的文档 | |
| 114 | +// - 包含关键词:明细、详情、查询明细、查找、搜索、匹配、推荐、相似、相关、类似 | |
| 115 | +// - 示例:李留记的销售订单明细、查询关于人工智能的文档、找相似的图片 | |
| 116 | +// | |
| 117 | +// 重要规则: | |
| 118 | +// - 只返回 true 或 false,不要返回其他内容 | |
| 119 | +// - 不要解释,不要添加额外文字 | |
| 120 | +// - 如果用户要求"表格形式展示",返回 false(明细查询) | |
| 121 | +// - 如果用户指定具体人名、具体对象,返回 false(明细查询) | |
| 122 | +// """) | |
| 123 | +// @UserMessage("用户需求:{{userInput}}") | |
| 124 | + @SystemMessage(""" | |
| 125 | + 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false | |
| 126 | + - 如果用户需求包含以下关键词:统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组,则返回true | |
| 127 | + - 如果用户需求属于模糊匹配、普通语义检索,查询明细,(例如:查询报价单明细,查询客户信息),则返回false | |
| 128 | + - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情,则返回false | |
| 129 | + - 模糊匹配:不确定具体关键词,需要语义理解,则返回false | |
| 130 | + """) | |
| 131 | + @UserMessage(""" | |
| 132 | + 【用户需求】 | |
| 133 | + {{userInput}} | |
| 134 | + """) | |
| 135 | + Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput); | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 生成 Milvus 过滤条件 | |
| 139 | + */ | |
| 140 | + @SystemMessage(""" | |
| 141 | + MILVUS 标量过滤条件生成规则(严格遵守): | |
| 142 | + 1. 语法规范: | |
| 143 | + - 允许的操作符:==, !=, like | |
| 144 | + - 逻辑组合:&& (AND), || (OR) | |
| 145 | + - 所有字段都是字符串类型,值必须使用单引号包裹 | |
| 146 | + - 字符串中的单引号需要转义:'O''Reilly' | |
| 147 | + 2. 可用字段(只能使用这些字段): | |
| 148 | + - {{sMilvusFiled}} | |
| 149 | + 字段说明: | |
| 150 | + - {{sMilvusFiledDescription}} | |
| 151 | + 3. 重要规则: | |
| 152 | + - 只使用上述可用字段,不要创建新字段 | |
| 153 | + - 如果用户提到了文档类型(如"报价单"、"订单"等),但可用字段中没有类型字段,则忽略该条件 | |
| 154 | + - 只提取有明确值的字段条件 | |
| 155 | + 4. 生成规则: | |
| 156 | + - 如果没有提取到任何具体条件,返回空字符串 | |
| 157 | + - 从用户输入中提取明确的字段条件 | |
| 158 | + - 识别模式:字段名 + 操作符 + 值 | |
| 159 | + - 示例: | |
| 160 | + * "单据号 INV001" → sBillNo == 'INV001' | |
| 161 | + * "客户编号 C001" → sCustomerNo == 'C001' | |
| 162 | + * "销售人员张三" → sSalesManName == '张三' | |
| 163 | + * "产品包含手机" → sProductStyle like '%手机%' | |
| 164 | + 5. 输出格式: | |
| 165 | + - 仅返回纯过滤条件,无任何解释、换行、备注 | |
| 166 | + - 单条件:sBillNo == 'INV001' | |
| 167 | + - 多条件:(sBillNo == 'INV001' && sCustomerNo == 'C001') | |
| 168 | + - 无条件:直接返回空字符串 | |
| 169 | + """) | |
| 170 | + @UserMessage(""" | |
| 171 | + 【用户查询】 | |
| 172 | + - {{userInput}} | |
| 173 | + 【可用字段】 | |
| 174 | + - {{sMilvusFiled}} | |
| 175 | + 【字段说明】 | |
| 176 | + - {{sMilvusFiledDescription}} | |
| 177 | + """) | |
| 178 | + String getMilvusFilter(@MemoryId String userId, | |
| 179 | + @V("userInput") String userInput, | |
| 180 | + @V("sMilvusFiled") String sMilvusFiled, | |
| 181 | + @V("sMilvusFiledDescription") String sMilvusFiledDescription); | |
| 51 | 182 | } | ... | ... |
src/main/java/com/xly/config/OperableChatMemoryProvider.java
| ... | ... | @@ -121,6 +121,30 @@ public class OperableChatMemoryProvider implements ChatMemoryProvider { |
| 121 | 121 | // 步骤4: 完全重新设置消息列表 |
| 122 | 122 | return rebuildMemoryWithMessages(memoryId, currentMessages); |
| 123 | 123 | } |
| 124 | + | |
| 125 | + public List<ChatMessage> deleteUserLasterMessageBySize(Object memoryId,Integer size) { | |
| 126 | + if (Objects.isNull(memoryId) || size==0) { | |
| 127 | + return getCurrentChatMessages(memoryId); | |
| 128 | + } | |
| 129 | + // 步骤1: 获取当前所有消息 | |
| 130 | + ChatMemory currentMemory = this.get(memoryId); | |
| 131 | + List<ChatMessage> currentMessages = new ArrayList<>(currentMemory.messages()); | |
| 132 | + // 从后往前查找内容匹配的最后一条消息 | |
| 133 | + int indexToDelete = currentMessages.size(); | |
| 134 | + // 如果找到匹配的消息 | |
| 135 | + if (indexToDelete >= 0) { | |
| 136 | + List<ChatMessage> filteredMessages = new ArrayList<>(currentMessages); | |
| 137 | + for(int i=0;i<size;i++){ | |
| 138 | + indexToDelete = indexToDelete -1; | |
| 139 | + if(indexToDelete>1){ | |
| 140 | + filteredMessages.remove(indexToDelete); | |
| 141 | + } | |
| 142 | + } | |
| 143 | + return rebuildMemoryWithMessages(memoryId, filteredMessages); | |
| 144 | + } | |
| 145 | + // 步骤4: 完全重新设置消息列表 | |
| 146 | + return rebuildMemoryWithMessages(memoryId, currentMessages); | |
| 147 | + } | |
| 124 | 148 | /** |
| 125 | 149 | * 批量删除多条消息 |
| 126 | 150 | * @param memoryId 会话ID | ... | ... |
src/main/java/com/xly/entity/AiResponseAccumulator.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import com.xly.entity.AiResponseDTO; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * AI响应累积器 | |
| 9 | + */ | |
| 10 | +@Slf4j | |
| 11 | +public class AiResponseAccumulator { | |
| 12 | + | |
| 13 | + private final String requestId; | |
| 14 | + private final StringBuilder aiTextBuilder = new StringBuilder(); | |
| 15 | + private final StringBuilder systemTextBuilder = new StringBuilder(); | |
| 16 | + private String sSceneName; | |
| 17 | + private String sMethodName; | |
| 18 | + private String sReturnType; | |
| 19 | + private int totalChunks = 0; | |
| 20 | + private int processedChunks = 0; | |
| 21 | + private final long startTime; | |
| 22 | + | |
| 23 | + public AiResponseAccumulator(String requestId) { | |
| 24 | + this.requestId = requestId; | |
| 25 | + this.startTime = System.currentTimeMillis(); | |
| 26 | + } | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 累积单个AI响应 | |
| 30 | + */ | |
| 31 | + public void accumulate(AiResponseDTO response) { | |
| 32 | + processedChunks++; | |
| 33 | + | |
| 34 | + // 更新总块数 | |
| 35 | + if (response.getTotalChunks() != null && response.getTotalChunks() > 0) { | |
| 36 | + this.totalChunks = response.getTotalChunks(); | |
| 37 | + } | |
| 38 | + | |
| 39 | + // 累积AI文本片段 | |
| 40 | + if (StrUtil.isNotBlank(response.getTextFragment())) { | |
| 41 | + aiTextBuilder.append(response.getTextFragment()); | |
| 42 | + } | |
| 43 | + | |
| 44 | + // 累积系统文本片段 | |
| 45 | + if (StrUtil.isNotBlank(response.getSystemTextFragment())) { | |
| 46 | + systemTextBuilder.append(response.getSystemTextFragment()); | |
| 47 | + } | |
| 48 | + | |
| 49 | + // 更新元数据(取最后一次非空值) | |
| 50 | + if (StrUtil.isNotBlank(response.getSSceneName())) { | |
| 51 | + this.sSceneName = response.getSSceneName(); | |
| 52 | + } | |
| 53 | + if (StrUtil.isNotBlank(response.getSMethodName())) { | |
| 54 | + this.sMethodName = response.getSMethodName(); | |
| 55 | + } | |
| 56 | + if (StrUtil.isNotBlank(response.getSReturnType())) { | |
| 57 | + this.sReturnType = response.getSReturnType(); | |
| 58 | + } | |
| 59 | + | |
| 60 | + log.debug("累积进度: requestId={}, 已处理={}/{}块, AI文本长度={}", | |
| 61 | + requestId, processedChunks, totalChunks, aiTextBuilder.length()); | |
| 62 | + } | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * 获取完整的响应 | |
| 66 | + */ | |
| 67 | + public AiResponseDTO getCompleteResponse() { | |
| 68 | + AiResponseDTO response = new AiResponseDTO(); | |
| 69 | + response.setRequestId(requestId); | |
| 70 | + response.setAiText(aiTextBuilder.toString()); | |
| 71 | + response.setSystemText(systemTextBuilder.toString()); | |
| 72 | + response.setFullAiText(aiTextBuilder.toString()); | |
| 73 | + response.setFullSystemText(systemTextBuilder.toString()); | |
| 74 | + response.setSSceneName(sSceneName); | |
| 75 | + response.setSMethodName(sMethodName); | |
| 76 | + response.setSReturnType(sReturnType != null ? sReturnType : "MARKDOWN"); | |
| 77 | + response.setTotalChunks(totalChunks); | |
| 78 | + response.setChunkIndex(processedChunks - 1); | |
| 79 | + response.setIsLastChunk(true); | |
| 80 | + response.setElapsedTime(System.currentTimeMillis() - startTime); | |
| 81 | + | |
| 82 | + log.info("ERP累积完成: requestId={}, 总块数={}, AI文本长度={}, 系统文本长度={}, 耗时={}ms", | |
| 83 | + requestId, totalChunks, | |
| 84 | + aiTextBuilder.length(), systemTextBuilder.length(), | |
| 85 | + response.getElapsedTime()); | |
| 86 | + | |
| 87 | + return response; | |
| 88 | + } | |
| 89 | +} | |
| 0 | 90 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/AiResponseDTO.java
| ... | ... | @@ -9,9 +9,12 @@ import lombok.Data; |
| 9 | 9 | import lombok.NoArgsConstructor; |
| 10 | 10 | |
| 11 | 11 | import java.io.Serializable; |
| 12 | +import java.util.List; | |
| 13 | +import java.util.Map; | |
| 12 | 14 | |
| 13 | 15 | /** |
| 14 | 16 | * TTS响应数据传输对象 |
| 17 | + * 增强版:支持流式处理 | |
| 15 | 18 | */ |
| 16 | 19 | @Data |
| 17 | 20 | @Builder |
| ... | ... | @@ -20,14 +23,230 @@ import java.io.Serializable; |
| 20 | 23 | public class AiResponseDTO implements Serializable { |
| 21 | 24 | |
| 22 | 25 | private static final long serialVersionUID = 1L; |
| 23 | - // AI文字部分 | |
| 26 | + | |
| 27 | + // ============ 原有字段 ============ | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * AI文字部分 | |
| 31 | + */ | |
| 24 | 32 | private String aiText; |
| 25 | - //系统拼接返回的文字部分 | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 系统拼接返回的文字部分 | |
| 36 | + */ | |
| 26 | 37 | private String systemText; |
| 27 | - //业务场景名称 | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * 业务场景名称 | |
| 41 | + */ | |
| 28 | 42 | private String sSceneName; |
| 29 | - //业务方法名称 | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 业务方法名称 | |
| 46 | + */ | |
| 30 | 47 | private String sMethodName; |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 返回类型,默认MARKDOWN | |
| 51 | + */ | |
| 31 | 52 | private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode(); |
| 32 | 53 | |
| 54 | + // ============ 新增字段:流式处理支持 ============ | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * 请求ID,用于追踪整个流式请求 | |
| 58 | + */ | |
| 59 | + private String requestId; | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 响应码 | |
| 63 | + */ | |
| 64 | + private Integer code; | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * 响应消息 | |
| 68 | + */ | |
| 69 | + private String message; | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * 处理状态:PROCESSING, COMPLETED, FAILED | |
| 73 | + */ | |
| 74 | + private String status; | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * 当前处理的块编号(从0开始) | |
| 78 | + */ | |
| 79 | + private Integer chunkIndex; | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 总块数 | |
| 83 | + */ | |
| 84 | + private Integer totalChunks; | |
| 85 | + | |
| 86 | + /** | |
| 87 | + * 是否是最后一块 | |
| 88 | + */ | |
| 89 | + private Boolean isLastChunk; | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * 文本片段(用于流式传输) | |
| 93 | + * 当aiText过大时,可以分段传输 | |
| 94 | + */ | |
| 95 | + private String textFragment; | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 系统文本片段(用于流式传输) | |
| 99 | + */ | |
| 100 | + private String systemTextFragment; | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * 累积的完整AI文本(仅在最后一块时返回) | |
| 104 | + */ | |
| 105 | + private String fullAiText; | |
| 106 | + | |
| 107 | + /** | |
| 108 | + * 累积的完整系统文本(仅在最后一块时返回) | |
| 109 | + */ | |
| 110 | + private String fullSystemText; | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 处理进度(0-100) | |
| 114 | + */ | |
| 115 | + private Integer progress; | |
| 116 | + | |
| 117 | + /** | |
| 118 | + * 时间戳 | |
| 119 | + */ | |
| 120 | + private Long timestamp; | |
| 121 | + | |
| 122 | + /** | |
| 123 | + * 处理耗时(毫秒) | |
| 124 | + */ | |
| 125 | + private Long elapsedTime; | |
| 126 | + | |
| 127 | + /** | |
| 128 | + * 扩展字段,用于存储额外的元数据 | |
| 129 | + */ | |
| 130 | + private Map<String, Object> metadata; | |
| 131 | + | |
| 132 | + /** | |
| 133 | + * 错误信息(当status=FAILED时) | |
| 134 | + */ | |
| 135 | + private String errorMessage; | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 错误码(当status=FAILED时) | |
| 139 | + */ | |
| 140 | + private String errorCode; | |
| 141 | + | |
| 142 | + // ============ 便捷方法 ============ | |
| 143 | + | |
| 144 | + /** | |
| 145 | + * 判断是否处理成功 | |
| 146 | + */ | |
| 147 | + public boolean isSuccess() { | |
| 148 | + return code != null && code == 200; | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 判断是否处理中 | |
| 153 | + */ | |
| 154 | + public boolean isProcessing() { | |
| 155 | + return "PROCESSING".equals(status); | |
| 156 | + } | |
| 157 | + | |
| 158 | + /** | |
| 159 | + * 判断是否已完成 | |
| 160 | + */ | |
| 161 | + public boolean isCompleted() { | |
| 162 | + return "COMPLETED".equals(status); | |
| 163 | + } | |
| 164 | + | |
| 165 | + /** | |
| 166 | + * 判断是否失败 | |
| 167 | + */ | |
| 168 | + public boolean isFailed() { | |
| 169 | + return "FAILED".equals(status); | |
| 170 | + } | |
| 171 | + | |
| 172 | + /** | |
| 173 | + * 获取完整的文本(AI文本 + 系统文本) | |
| 174 | + */ | |
| 175 | + public String getFullText() { | |
| 176 | + StringBuilder sb = new StringBuilder(); | |
| 177 | + if (StrUtil.isNotBlank(aiText)) { | |
| 178 | + sb.append(aiText); | |
| 179 | + } | |
| 180 | + if (StrUtil.isNotBlank(systemText)) { | |
| 181 | + sb.append(systemText); | |
| 182 | + } | |
| 183 | + return sb.toString(); | |
| 184 | + } | |
| 185 | + | |
| 186 | + /** | |
| 187 | + * 创建处理中的响应 | |
| 188 | + */ | |
| 189 | + public static AiResponseDTO processing(String requestId, String textFragment, | |
| 190 | + Integer chunkIndex, Integer totalChunks) { | |
| 191 | + return AiResponseDTO.builder() | |
| 192 | + .requestId(requestId) | |
| 193 | + .code(200) | |
| 194 | + .message("Processing") | |
| 195 | + .status("PROCESSING") | |
| 196 | + .textFragment(textFragment) | |
| 197 | + .chunkIndex(chunkIndex) | |
| 198 | + .totalChunks(totalChunks) | |
| 199 | + .timestamp(System.currentTimeMillis()) | |
| 200 | + .progress(calculateProgress(chunkIndex, totalChunks)) | |
| 201 | + .build(); | |
| 202 | + } | |
| 203 | + | |
| 204 | + /** | |
| 205 | + * 创建完成响应 | |
| 206 | + */ | |
| 207 | + public static AiResponseDTO completed(String requestId, String fullAiText, | |
| 208 | + String fullSystemText, String sSceneName, | |
| 209 | + String sMethodName, String sReturnType) { | |
| 210 | + return AiResponseDTO.builder() | |
| 211 | + .requestId(requestId) | |
| 212 | + .code(200) | |
| 213 | + .message("Completed") | |
| 214 | + .status("COMPLETED") | |
| 215 | + .fullAiText(fullAiText) | |
| 216 | + .fullSystemText(fullSystemText) | |
| 217 | + .aiText(fullAiText) | |
| 218 | + .systemText(fullSystemText) | |
| 219 | + .sSceneName(sSceneName) | |
| 220 | + .sMethodName(sMethodName) | |
| 221 | + .sReturnType(sReturnType) | |
| 222 | + .progress(100) | |
| 223 | + .timestamp(System.currentTimeMillis()) | |
| 224 | + .isLastChunk(true) | |
| 225 | + .build(); | |
| 226 | + } | |
| 227 | + | |
| 228 | + /** | |
| 229 | + * 创建失败响应 | |
| 230 | + */ | |
| 231 | + public static AiResponseDTO failed(String requestId, String errorMessage, String errorCode) { | |
| 232 | + return AiResponseDTO.builder() | |
| 233 | + .requestId(requestId) | |
| 234 | + .code(500) | |
| 235 | + .message("Failed") | |
| 236 | + .status("FAILED") | |
| 237 | + .errorMessage(errorMessage) | |
| 238 | + .errorCode(errorCode) | |
| 239 | + .timestamp(System.currentTimeMillis()) | |
| 240 | + .build(); | |
| 241 | + } | |
| 242 | + | |
| 243 | + /** | |
| 244 | + * 计算进度 | |
| 245 | + */ | |
| 246 | + private static Integer calculateProgress(Integer chunkIndex, Integer totalChunks) { | |
| 247 | + if (chunkIndex == null || totalChunks == null || totalChunks == 0) { | |
| 248 | + return 0; | |
| 249 | + } | |
| 250 | + return (int) ((chunkIndex + 1) * 100.0 / totalChunks); | |
| 251 | + } | |
| 33 | 252 | } |
| 34 | 253 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/ToolMeta.java
src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java
0 → 100644
| 1 | +package com.xly.milvus.bean; | |
| 2 | + | |
| 3 | +import io.milvus.grpc.SearchResultData; | |
| 4 | +import io.milvus.response.SearchResultsWrapper; | |
| 5 | +import java.util.List; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 自定义SearchResultsWrapper,用于访问protected方法 | |
| 9 | + */ | |
| 10 | +public class CustomSearchResultsWrapper extends SearchResultsWrapper { | |
| 11 | + | |
| 12 | + public CustomSearchResultsWrapper(SearchResultData results) { | |
| 13 | + super(results); | |
| 14 | + } | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 公开访问getOutputFields方法 | |
| 18 | + */ | |
| 19 | + public List<String> getOutputFieldsPublic() { | |
| 20 | + return super.getOutputFields(); | |
| 21 | + } | |
| 22 | +} | |
| 0 | 23 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java
0 → 100644
| 1 | +package com.xly.milvus.bean; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Builder; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | +import java.util.Map; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * 相似度查询请求实体 | |
| 13 | + */ | |
| 14 | +@Data | |
| 15 | +@Builder | |
| 16 | +@NoArgsConstructor | |
| 17 | +@AllArgsConstructor | |
| 18 | +public class SimilaritySearchRequest { | |
| 19 | + private List<Float> queryVector; // 查询向量 | |
| 20 | + private String queryText; // 查询文本(如果有文本转向量服务) | |
| 21 | + private Integer topK = 10; // 返回数量 | |
| 22 | + private Double minScore; // 最小相似度得分 | |
| 23 | + private Double maxScore; // 最大相似度得分 | |
| 24 | + private String metricType = "IP"; // 距离类型: IP(内积), L2(欧氏距离), COSINE(余弦) | |
| 25 | + private List<String> outputFields; // 输出字段 | |
| 26 | + private Map<String, Object> filter; // 过滤条件 | |
| 27 | + private String partitionName; // 分区名称 | |
| 28 | + private Boolean withScore = true; // 是否返回得分 | |
| 29 | + private Boolean withDistance = false; // 是否返回距离 | |
| 30 | +} | |
| 0 | 31 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java
0 → 100644
| 1 | +package com.xly.milvus.bean; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Builder; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 相似度查询结果实体 - 增强版 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@Builder | |
| 15 | +@NoArgsConstructor | |
| 16 | +@AllArgsConstructor | |
| 17 | +public class SimilaritySearchResult { | |
| 18 | + private Long id; // 整数ID | |
| 19 | + private String strId; // 字符串ID | |
| 20 | + private Float score; // 相似度得分 | |
| 21 | + private Double normalizedScore; // 归一化后的得分(0-1之间) | |
| 22 | + private Map<String, Object> fields; // 字段数据 | |
| 23 | + private String collectionName; // 集合名称 | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 获取ID(优先返回字符串ID,如果没有则返回整数ID) | |
| 27 | + */ | |
| 28 | + public String getId() { | |
| 29 | + if (strId != null && !strId.isEmpty()) { | |
| 30 | + return strId; | |
| 31 | + } | |
| 32 | + return id != null ? String.valueOf(id) : null; | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 获取字段值 | |
| 37 | + */ | |
| 38 | + public Object getField(String fieldName) { | |
| 39 | + if (fields != null) { | |
| 40 | + return fields.get(fieldName); | |
| 41 | + } | |
| 42 | + return null; | |
| 43 | + } | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * 获取字符串字段值 | |
| 47 | + */ | |
| 48 | + public String getStringField(String fieldName) { | |
| 49 | + Object value = getField(fieldName); | |
| 50 | + return value != null ? value.toString() : null; | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 获取整数字段值 | |
| 55 | + */ | |
| 56 | + public Integer getIntField(String fieldName) { | |
| 57 | + Object value = getField(fieldName); | |
| 58 | + if (value instanceof Number) { | |
| 59 | + return ((Number) value).intValue(); | |
| 60 | + } | |
| 61 | + return null; | |
| 62 | + } | |
| 63 | +} | |
| 0 | 64 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/EmbeddingConfig.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import dev.langchain4j.model.embedding.EmbeddingModel; | |
| 4 | +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; | |
| 5 | +import org.springframework.context.annotation.Bean; | |
| 6 | +import org.springframework.context.annotation.Configuration; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * LangChain4j 嵌入模型配置类 | |
| 10 | + */ | |
| 11 | +@Configuration | |
| 12 | +public class EmbeddingConfig { | |
| 13 | + | |
| 14 | + @Bean | |
| 15 | + public EmbeddingModel embeddingModel() { | |
| 16 | + // 使用 All-MiniLM-L6-v2 嵌入模型 | |
| 17 | + // 这是一个轻量级的模型,维度为384,适合在本地运行 | |
| 18 | + return new AllMiniLmL6V2EmbeddingModel(); | |
| 19 | + } | |
| 20 | +} | |
| 0 | 21 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/MilvusConfig.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import io.milvus.v2.client.ConnectConfig; | |
| 4 | +import io.milvus.v2.client.MilvusClientV2; | |
| 5 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | +import org.springframework.beans.factory.annotation.Value; | |
| 7 | +import org.springframework.context.annotation.Bean; | |
| 8 | +import org.springframework.context.annotation.Configuration; | |
| 9 | +import org.springframework.retry.annotation.EnableRetry; | |
| 10 | +import org.springframework.retry.support.RetryTemplate; | |
| 11 | + | |
| 12 | +@Slf4j | |
| 13 | +@Configuration | |
| 14 | +@EnableRetry | |
| 15 | +public class MilvusConfig { | |
| 16 | + | |
| 17 | + @Value("${milvus.host:localhost}") | |
| 18 | + private String host; | |
| 19 | + | |
| 20 | + @Value("${milvus.port:19530}") | |
| 21 | + private Integer port; | |
| 22 | + | |
| 23 | + @Value("${milvus.database:default}") | |
| 24 | + private String database; | |
| 25 | + | |
| 26 | + @Value("${milvus.username:}") | |
| 27 | + private String username; | |
| 28 | + | |
| 29 | + @Value("${milvus.password:}") | |
| 30 | + private String password; | |
| 31 | + | |
| 32 | + @Value("${milvus.connect-timeout:10000}") | |
| 33 | + private Long connectTimeout; | |
| 34 | + | |
| 35 | + @Value("${milvus.rpc-deadline:10000}") | |
| 36 | + private Long rpcDeadline; | |
| 37 | + | |
| 38 | + @Value("${milvus.keep-alive-time:300000}") | |
| 39 | + private Long keepAliveTime; | |
| 40 | + | |
| 41 | + @Value("${milvus.keep-alive-timeout:5000}") | |
| 42 | + private Long keepAliveTimeout; | |
| 43 | + | |
| 44 | + @Value("${milvus.secure:false}") | |
| 45 | + private Boolean secure; | |
| 46 | + | |
| 47 | + @Value("${milvus.validate-on-startup:true}") | |
| 48 | + private boolean validateOnStartup; | |
| 49 | + | |
| 50 | + @Value("${milvus.fail-on-connect-error:false}") | |
| 51 | + private boolean failOnConnectError; | |
| 52 | + | |
| 53 | + @Value("${milvus.enable-precheck:true}") | |
| 54 | + private boolean enablePrecheck; | |
| 55 | + | |
| 56 | + @Bean(destroyMethod = "close") | |
| 57 | + public MilvusClientV2 milvusClient() { | |
| 58 | + try { | |
| 59 | + String uri = String.format("http://%s:%d", host, port); | |
| 60 | + // 构建连接配置 | |
| 61 | + ConnectConfig.ConnectConfigBuilder configBuilder = ConnectConfig.builder() | |
| 62 | + .uri(uri) | |
| 63 | + .dbName("default") | |
| 64 | + .connectTimeoutMs(connectTimeout) | |
| 65 | + .rpcDeadlineMs(rpcDeadline) | |
| 66 | + .keepAliveTimeMs(keepAliveTime) | |
| 67 | + .keepAliveTimeoutMs(keepAliveTimeout) | |
| 68 | + .keepAliveWithoutCalls(true) | |
| 69 | + .secure(secure) | |
| 70 | + .enablePrecheck(enablePrecheck); | |
| 71 | + | |
| 72 | + // 添加认证信息(如果有) | |
| 73 | + if (username != null && !username.isEmpty()) { | |
| 74 | + configBuilder.username(username); | |
| 75 | + if (password != null) { | |
| 76 | + configBuilder.password(password); | |
| 77 | + } | |
| 78 | + } | |
| 79 | + | |
| 80 | + ConnectConfig connectConfig = configBuilder.build(); | |
| 81 | + | |
| 82 | + // 创建客户端 | |
| 83 | + MilvusClientV2 client = new MilvusClientV2(connectConfig); | |
| 84 | + // 检查xlyerp数据库是否存在,如果不存在则创建 | |
| 85 | + var databases = client.listDatabases(); | |
| 86 | + if (!databases.getDatabaseNames().contains(database)) { | |
| 87 | + log.info("数据库 xlyerp 不存在,正在创建..."); | |
| 88 | + client.createDatabase( | |
| 89 | + io.milvus.v2.service.database.request.CreateDatabaseReq.builder() | |
| 90 | + .databaseName(database) | |
| 91 | + .build() | |
| 92 | + ); | |
| 93 | + log.info("数据库 xlyerp 创建成功"); | |
| 94 | + } | |
| 95 | + | |
| 96 | + // 切换到xlyerp数据库 | |
| 97 | + client.useDatabase (database); | |
| 98 | + log.info("切换到数据库: {}",database); | |
| 99 | + | |
| 100 | + // 启动验证 | |
| 101 | + if (validateOnStartup) { | |
| 102 | + validateConnection(client); | |
| 103 | + } | |
| 104 | + return client; | |
| 105 | + } catch (Exception e) { | |
| 106 | + log.error("创建Milvus客户端时发生异常: {}", e.getMessage(), e); | |
| 107 | + throw new RuntimeException("无法创建Milvus客户端", e); | |
| 108 | + } | |
| 109 | + | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 验证Milvus连接 - 基于源码中可用的方法 | |
| 114 | + */ | |
| 115 | + private void validateConnection(MilvusClientV2 client) { | |
| 116 | + try { | |
| 117 | + // 方法1: 检查服务器版本(源码中存在getServerVersion()) | |
| 118 | + String serverVersion = client.getServerVersion(); | |
| 119 | + | |
| 120 | + // 方法2: 列出数据库(源码中存在listDatabases()) | |
| 121 | + var databases = client.listDatabases(); | |
| 122 | + | |
| 123 | + // 方法3: 检查健康状态(源码中存在checkHealth()) | |
| 124 | + var health = client.checkHealth(); | |
| 125 | + | |
| 126 | + System.out.println("✅ Milvus连接成功"); | |
| 127 | + System.out.println(" - 服务器版本: " + serverVersion); | |
| 128 | + System.out.println(" - 数据库: " + databases); | |
| 129 | + System.out.println(" - 健康状态: " + health); | |
| 130 | + | |
| 131 | + // 验证指定数据库是否存在 | |
| 132 | + if (database != null && !database.isEmpty()) { | |
| 133 | + boolean dbExists = databases.getDatabaseNames().contains(database); | |
| 134 | + if (!dbExists && !"default".equals(database)) { | |
| 135 | + System.err.println("⚠️ 警告: 指定数据库 '" + database + "' 不存在"); | |
| 136 | + } | |
| 137 | + } | |
| 138 | + | |
| 139 | + } catch (Exception e) { | |
| 140 | + String errorMsg = String.format("❌ Milvus连接失败: %s:%d - %s",host, port, e.getMessage()); | |
| 141 | + | |
| 142 | + if (failOnConnectError) { | |
| 143 | + throw new IllegalStateException(errorMsg, e); | |
| 144 | + } else { | |
| 145 | + System.err.println(errorMsg); | |
| 146 | + System.err.println(" ⚠️ 应用将继续启动,但Milvus功能可能不可用"); | |
| 147 | + } | |
| 148 | + } | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 配置重试模板(可选) | |
| 153 | + */ | |
| 154 | + @Bean | |
| 155 | + public RetryTemplate milvusRetryTemplate() { | |
| 156 | + return RetryTemplate.builder() | |
| 157 | + .maxAttempts(3) | |
| 158 | + .exponentialBackoff(1000, 2, 10000) | |
| 159 | + .retryOn(Exception.class) | |
| 160 | + .build(); | |
| 161 | + } | |
| 162 | +} | |
| 0 | 163 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import io.milvus.v2.client.MilvusClientV2; | |
| 4 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 5 | +import org.springframework.boot.actuate.health.Health; | |
| 6 | +import org.springframework.boot.actuate.health.HealthIndicator; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import java.util.HashMap; | |
| 10 | +import java.util.Map; | |
| 11 | + | |
| 12 | +@Component | |
| 13 | +public class MilvusHealthIndicator implements HealthIndicator { | |
| 14 | + | |
| 15 | + @Autowired(required = false) | |
| 16 | + private MilvusClientV2 milvusClient; | |
| 17 | + | |
| 18 | + @Override | |
| 19 | + public Health health() { | |
| 20 | + if (milvusClient == null) { | |
| 21 | + return Health.down() | |
| 22 | + .withDetail("error", "Milvus客户端未初始化") | |
| 23 | + .build(); | |
| 24 | + } | |
| 25 | + | |
| 26 | + try { | |
| 27 | + // 检查客户端是否就绪(源码中存在clientIsReady()) | |
| 28 | + boolean isReady = milvusClient.clientIsReady(); | |
| 29 | + | |
| 30 | + if (!isReady) { | |
| 31 | + return Health.down() | |
| 32 | + .withDetail("error", "客户端未就绪") | |
| 33 | + .build(); | |
| 34 | + } | |
| 35 | + | |
| 36 | + long startTime = System.currentTimeMillis(); | |
| 37 | + | |
| 38 | + // 执行健康检查 - 使用源码中存在的方法 | |
| 39 | + String serverVersion = milvusClient.getServerVersion(); | |
| 40 | + var databases = milvusClient.listDatabases(); | |
| 41 | + var healthCheck = milvusClient.checkHealth(); | |
| 42 | + | |
| 43 | + long responseTime = System.currentTimeMillis() - startTime; | |
| 44 | + | |
| 45 | + Map<String, Object> details = new HashMap<>(); | |
| 46 | + details.put("serverVersion", serverVersion); | |
| 47 | + details.put("database", milvusClient.currentUsedDatabase()); | |
| 48 | + details.put("databases", databases.getDatabaseNames()); | |
| 49 | + details.put("healthStatus", healthCheck.getIsHealthy()); | |
| 50 | + details.put("responseTime", responseTime + "ms"); | |
| 51 | + details.put("clientReady", isReady); | |
| 52 | + | |
| 53 | + if (healthCheck.getQuotaStates() != null) { | |
| 54 | + details.put("quotaStates", healthCheck.getQuotaStates()); | |
| 55 | + } | |
| 56 | + | |
| 57 | + return Health.up() | |
| 58 | + .withDetails(details) | |
| 59 | + .build(); | |
| 60 | + | |
| 61 | + } catch (Exception e) { | |
| 62 | + return Health.down() | |
| 63 | + .withDetail("error", e.getMessage()) | |
| 64 | + .withDetail("clientReady", milvusClient.clientIsReady()) | |
| 65 | + .build(); | |
| 66 | + } | |
| 67 | + } | |
| 68 | +} | |
| 0 | 69 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/MilvusRetryConfig.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import io.milvus.v2.client.RetryConfig; | |
| 4 | +import org.springframework.beans.factory.annotation.Value; | |
| 5 | +import org.springframework.context.annotation.Bean; | |
| 6 | +import org.springframework.context.annotation.Configuration; | |
| 7 | + | |
| 8 | +@Configuration | |
| 9 | +public class MilvusRetryConfig { | |
| 10 | + | |
| 11 | + @Value("${milvus.retry.max-retry-times:75}") | |
| 12 | + private int maxRetryTimes; | |
| 13 | + | |
| 14 | + @Value("${milvus.retry.initial-backoff-ms:10}") | |
| 15 | + private long initialBackOffMs; | |
| 16 | + | |
| 17 | + @Value("${milvus.retry.max-backoff-ms:3000}") | |
| 18 | + private long maxBackOffMs; | |
| 19 | + | |
| 20 | + @Value("${milvus.retry.backoff-multiplier:3}") | |
| 21 | + private int backOffMultiplier; | |
| 22 | + | |
| 23 | + @Value("${milvus.retry.retry-on-rate-limit:true}") | |
| 24 | + private boolean retryOnRateLimit; | |
| 25 | + | |
| 26 | + @Value("${milvus.retry.max-retry-timeout-ms:0}") | |
| 27 | + private long maxRetryTimeoutMs; | |
| 28 | + | |
| 29 | + @Value("${milvus.retry.enabled:true}") | |
| 30 | + private boolean retryEnabled; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 方法名改为 createRetryConfig,避免与类名冲突 | |
| 34 | + */ | |
| 35 | + @Bean | |
| 36 | + public RetryConfig createRetryConfig() { | |
| 37 | + if (!retryEnabled) { | |
| 38 | + return RetryConfig.builder() | |
| 39 | + .maxRetryTimes(1) | |
| 40 | + .initialBackOffMs(0) | |
| 41 | + .maxBackOffMs(0) | |
| 42 | + .backOffMultiplier(1) | |
| 43 | + .retryOnRateLimit(false) | |
| 44 | + .maxRetryTimeoutMs(0) | |
| 45 | + .build(); | |
| 46 | + } | |
| 47 | + | |
| 48 | + return RetryConfig.builder() | |
| 49 | + .maxRetryTimes(maxRetryTimes) | |
| 50 | + .initialBackOffMs(initialBackOffMs) | |
| 51 | + .maxBackOffMs(maxBackOffMs) | |
| 52 | + .backOffMultiplier(backOffMultiplier) | |
| 53 | + .retryOnRateLimit(retryOnRateLimit) | |
| 54 | + .maxRetryTimeoutMs(maxRetryTimeoutMs) | |
| 55 | + .build(); | |
| 56 | + } | |
| 57 | +} | |
| 0 | 58 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/MilvusStartupValidator.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import io.milvus.v2.client.MilvusClientV2; | |
| 4 | +import io.milvus.v2.service.collection.request.HasCollectionReq; | |
| 5 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | +import org.slf4j.Logger; | |
| 7 | +import org.slf4j.LoggerFactory; | |
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 9 | +import org.springframework.beans.factory.annotation.Value; | |
| 10 | +import org.springframework.boot.ApplicationArguments; | |
| 11 | +import org.springframework.boot.ApplicationRunner; | |
| 12 | +import org.springframework.core.annotation.Order; | |
| 13 | +import org.springframework.stereotype.Component; | |
| 14 | + | |
| 15 | +import java.util.List; | |
| 16 | + | |
| 17 | +@Slf4j | |
| 18 | +@Component | |
| 19 | +@Order(1) | |
| 20 | +public class MilvusStartupValidator implements ApplicationRunner { | |
| 21 | + | |
| 22 | + @Autowired | |
| 23 | + private MilvusClientV2 milvusClient; | |
| 24 | + | |
| 25 | + @Value("${milvus.validation.collections:}") | |
| 26 | + private List<String> validationCollections; | |
| 27 | + | |
| 28 | + @Value("${milvus.validation.timeout:5000}") | |
| 29 | + private long validationTimeout; | |
| 30 | + | |
| 31 | + @Override | |
| 32 | + public void run(ApplicationArguments args) throws Exception { | |
| 33 | + log.info("开始Milvus启动验证..."); | |
| 34 | + | |
| 35 | + try { | |
| 36 | + // 1. 验证客户端就绪状态 | |
| 37 | + boolean isReady = milvusClient.clientIsReady(); | |
| 38 | + log.info("客户端就绪状态: {}", isReady); | |
| 39 | + | |
| 40 | + // 2. 获取服务器版本 | |
| 41 | + String serverVersion = milvusClient.getServerVersion(); | |
| 42 | + log.info("Milvus服务器版本: {}", serverVersion); | |
| 43 | + | |
| 44 | + // 3. 列出所有数据库 | |
| 45 | + var databases = milvusClient.listDatabases(); | |
| 46 | + log.info("可用数据库: {}", databases.getDatabaseNames()); | |
| 47 | + | |
| 48 | + // 4. 检查当前使用的数据库 | |
| 49 | + String currentDb = milvusClient.currentUsedDatabase(); | |
| 50 | + log.info("当前数据库: {}", currentDb); | |
| 51 | + | |
| 52 | + // 5. 健康检查 | |
| 53 | + var healthCheck = milvusClient.checkHealth(); | |
| 54 | + log.info("健康状态: {}", healthCheck.getIsHealthy()); | |
| 55 | + if (healthCheck.getQuotaStates() != null && !healthCheck.getQuotaStates().isEmpty()) { | |
| 56 | + log.info("配额状态: {}", healthCheck.getQuotaStates()); | |
| 57 | + } | |
| 58 | + | |
| 59 | + // 6. 验证指定的集合(可选) | |
| 60 | + if (validationCollections != null && !validationCollections.isEmpty()) { | |
| 61 | + for (String collectionName : validationCollections) { | |
| 62 | + boolean exists = milvusClient.hasCollection( | |
| 63 | + HasCollectionReq.builder() | |
| 64 | + .collectionName(collectionName) | |
| 65 | + .build() | |
| 66 | + ); | |
| 67 | + log.info("集合 '{}' 存在: {}", collectionName, exists); | |
| 68 | + } | |
| 69 | + } | |
| 70 | + | |
| 71 | + log.info("Milvus启动验证完成"); | |
| 72 | + | |
| 73 | + } catch (Exception e) { | |
| 74 | + log.error("Milvus启动验证失败: {}", e.getMessage(), e); | |
| 75 | + // 可以根据配置决定是否抛出异常 | |
| 76 | + // throw new RuntimeException("Milvus验证失败", e); | |
| 77 | + } | |
| 78 | + } | |
| 79 | +} | |
| 0 | 80 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/config/SearchResult.java
0 → 100644
| 1 | +package com.xly.milvus.config; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Builder; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 搜索结果实体 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@Builder | |
| 15 | +@NoArgsConstructor | |
| 16 | +@AllArgsConstructor | |
| 17 | +public class SearchResult { | |
| 18 | + private Long id; | |
| 19 | + private Float score; | |
| 20 | + private Map<String, Object> fields; | |
| 21 | + private String collectionName; | |
| 22 | +} | |
| 0 | 23 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/AiGlobalAgentQuestionSqlEmitterService.java
0 → 100644
| 1 | +package com.xly.milvus.service; | |
| 2 | + | |
| 3 | + | |
| 4 | +import java.util.Map; | |
| 5 | + | |
| 6 | +public interface AiGlobalAgentQuestionSqlEmitterService { | |
| 7 | + | |
| 8 | + /*** | |
| 9 | + * @Author 钱豹 | |
| 10 | + * @Date 15:27 2026/3/19 | |
| 11 | + * @Param [data, sQuestion, sSqlContent, collectionName] | |
| 12 | + * @return void | |
| 13 | + * @Description 插入向量库 | |
| 14 | + **/ | |
| 15 | + void addAiGlobalAgentQuestionSqlEmitter(String sKey,Map<String,Object> data, String sQuestion, String sSqlContent, String collectionName); | |
| 16 | + | |
| 17 | + | |
| 18 | + Map<String, Object> queryAiGlobalAgentQuestionSqlEmitter(String searchText, String collectionName); | |
| 19 | + | |
| 20 | +} | |
| 0 | 21 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/EmbeddingService.java
0 → 100644
| 1 | +package com.xly.milvus.service; | |
| 2 | + | |
| 3 | +import java.util.List; | |
| 4 | + | |
| 5 | +public interface EmbeddingService { | |
| 6 | + | |
| 7 | + /** | |
| 8 | + * 生成单个文本的向量 | |
| 9 | + */ | |
| 10 | + public List<Float> generateEmbedding(String text); | |
| 11 | + | |
| 12 | + /** | |
| 13 | + * 批量生成向量(高效版) | |
| 14 | + * 利用 LangChain4j 内置的并行化能力,显著提升性能 | |
| 15 | + */ | |
| 16 | + public List<List<Float>> generateEmbeddings(List<String> texts); | |
| 17 | +} | |
| 0 | 18 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/MilvusService.java
0 → 100644
| 1 | +package com.xly.milvus.service; | |
| 2 | + | |
| 3 | +import com.xly.tts.bean.TTSResponseDTO; | |
| 4 | + | |
| 5 | +import java.util.ArrayList; | |
| 6 | +import java.util.HashMap; | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 向量化服务接口 | |
| 12 | + */ | |
| 13 | +public interface MilvusService { | |
| 14 | + | |
| 15 | + /*** | |
| 16 | + * @Author 钱豹 | |
| 17 | + * @Date 22:17 2026/3/24 | |
| 18 | + * @Param | |
| 19 | + * @return | |
| 20 | + * @Description 初始化数据 | |
| 21 | + **/ | |
| 22 | + | |
| 23 | + TTSResponseDTO initDataToMilvus(Map<String, Object> reqMap); | |
| 24 | + | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 创建集合(如果不存在) | |
| 28 | + */ | |
| 29 | + void createCollectionIfNotExists(String collectionName, String sVectorfiled, String sVectorjson, Boolean bRset); | |
| 30 | + | |
| 31 | + | |
| 32 | + /*** | |
| 33 | + * @Author 钱豹 | |
| 34 | + * @Date 21:39 2026/3/24 | |
| 35 | + * @Param [collectionName, sVectorfiled, sVectorjson, data] | |
| 36 | + * @return long | |
| 37 | + * @Description 批量插入数据 | |
| 38 | + **/ | |
| 39 | + long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson, List<Map<String, Object>> data); | |
| 40 | + | |
| 41 | + /*** | |
| 42 | + * @Author 钱豹 | |
| 43 | + * @Date 10:39 2026/3/25 | |
| 44 | + * @Param | |
| 45 | + * @return | |
| 46 | + * @Description 向量库查询 | |
| 47 | + **/ | |
| 48 | + List<Map<String, Object>> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List<String> fields); | |
| 49 | + | |
| 50 | + /*** | |
| 51 | + * @Author 钱豹 | |
| 52 | + * @Date 10:56 2026/3/25 | |
| 53 | + * @Param [sVectorfiled] | |
| 54 | + * @return java.util.Map<java.lang.String,java.lang.Object> | |
| 55 | + * @Description 获取配置 | |
| 56 | + **/ | |
| 57 | + Map<String,Object> getMilvusFiled(String sVectorfiled); | |
| 58 | +} | |
| 0 | 59 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/VectorizationService.java
0 → 100644
| 1 | +package com.xly.milvus.service; | |
| 2 | + | |
| 3 | +import java.util.List; | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * 向量化服务接口 | |
| 7 | + */ | |
| 8 | +public interface VectorizationService { | |
| 9 | + | |
| 10 | + /** | |
| 11 | + * 将文本向量化 | |
| 12 | + * @param text 文本内容 | |
| 13 | + * @return 向量数组 | |
| 14 | + */ | |
| 15 | + List<Float> textToVector(String text); | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 批量向量化 | |
| 19 | + * @param texts 文本列表 | |
| 20 | + * @return 向量列表 | |
| 21 | + */ | |
| 22 | + List<List<Float>> batchTextToVector(List<String> texts); | |
| 23 | +} | |
| 0 | 24 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/impl/AiGlobalAgentQuestionSqlEmitterServiceImpl.java
0 → 100644
| 1 | +package com.xly.milvus.service.impl; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.ConcurrentHashSet; | |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 5 | +import com.google.gson.JsonArray; | |
| 6 | +import com.google.gson.JsonObject; | |
| 7 | +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService; | |
| 8 | +import com.xly.milvus.service.VectorizationService; | |
| 9 | +import com.xly.milvus.util.MapToJsonConverter; | |
| 10 | +import io.milvus.v2.client.MilvusClientV2; | |
| 11 | +import io.milvus.v2.common.ConsistencyLevel; | |
| 12 | +import io.milvus.v2.common.DataType; | |
| 13 | +import io.milvus.v2.common.IndexBuildState; | |
| 14 | +import io.milvus.v2.common.IndexParam; | |
| 15 | +import io.milvus.v2.service.collection.request.*; | |
| 16 | +import io.milvus.v2.service.index.request.CreateIndexReq; | |
| 17 | +import io.milvus.v2.service.index.request.DescribeIndexReq; | |
| 18 | +import io.milvus.v2.service.index.request.DropIndexReq; | |
| 19 | +import io.milvus.v2.service.index.response.DescribeIndexResp; | |
| 20 | +import io.milvus.v2.service.vector.request.InsertReq; | |
| 21 | +import io.milvus.v2.service.vector.request.SearchReq; | |
| 22 | +import io.milvus.v2.service.vector.request.data.FloatVec; | |
| 23 | +import io.milvus.v2.service.vector.response.InsertResp; | |
| 24 | +import io.milvus.v2.service.vector.response.SearchResp; | |
| 25 | +import lombok.RequiredArgsConstructor; | |
| 26 | +import lombok.extern.slf4j.Slf4j; | |
| 27 | +import org.springframework.beans.factory.annotation.Value; | |
| 28 | +import org.springframework.stereotype.Service; | |
| 29 | + | |
| 30 | +import java.util.*; | |
| 31 | + | |
| 32 | +@Slf4j | |
| 33 | +@Service("aiGlobalAgentQuestionSqlEmitterService") | |
| 34 | +@RequiredArgsConstructor | |
| 35 | +public class AiGlobalAgentQuestionSqlEmitterServiceImpl implements AiGlobalAgentQuestionSqlEmitterService { | |
| 36 | + | |
| 37 | + private final MilvusClientV2 milvusClient; | |
| 38 | + private final VectorizationService vectorizationService; | |
| 39 | + | |
| 40 | + // 或者从配置文件读取 | |
| 41 | + @Value("${milvus.vector.dimension:384}") | |
| 42 | + private int VECTOR_DIM; | |
| 43 | + | |
| 44 | + // 缓存已加载的集合 | |
| 45 | + private final Set<String> loadedCollections = new ConcurrentHashSet<>(); | |
| 46 | + | |
| 47 | + /*** | |
| 48 | + * @Author 钱豹 | |
| 49 | + * @Date 13:06 2026/3/19 | |
| 50 | + * @Param [] | |
| 51 | + * @return void | |
| 52 | + * @Description 插入数据 | |
| 53 | + **/ | |
| 54 | + @Override | |
| 55 | + public void addAiGlobalAgentQuestionSqlEmitter(String sKey,Map<String,Object> data,String sQuestion,String sSqlContent,String collectionName) { | |
| 56 | + // 向量化 | |
| 57 | + List<Float> vector = vectorizationService.textToVector(sKey); | |
| 58 | + | |
| 59 | + if (vector == null || vector.isEmpty()) { | |
| 60 | + throw new RuntimeException("向量化失败"); | |
| 61 | + } | |
| 62 | + | |
| 63 | + // 2. 转换为Milvus格式 | |
| 64 | + JsonObject row = convertToMilvusRow(data, vector,sQuestion,sSqlContent,sKey); | |
| 65 | + | |
| 66 | + //创建集合 | |
| 67 | +// createCollection(collectionName); | |
| 68 | + createCollectionIfNotExists(collectionName); | |
| 69 | + | |
| 70 | + // 3. 插入到Milvus | |
| 71 | + InsertReq insertReq = InsertReq.builder() | |
| 72 | + .collectionName(collectionName) | |
| 73 | + .data(List.of(row)) | |
| 74 | + .build(); | |
| 75 | + | |
| 76 | + InsertResp insertResp = milvusClient.insert(insertReq); | |
| 77 | + System.out.println("成功插入 " + insertResp.getInsertCnt() + " 条数据"); | |
| 78 | + System.out.println(" - 数据预览:"); | |
| 79 | + } | |
| 80 | + | |
| 81 | + @Override | |
| 82 | + public Map<String, Object> queryAiGlobalAgentQuestionSqlEmitter(String searchText, String collectionName) { | |
| 83 | + Map<String, Object> result = new HashMap<>(); | |
| 84 | + log.info("开始相似度查询: collection={}, searchText={}", collectionName, searchText); | |
| 85 | + // 2. 设置范围搜索参数 | |
| 86 | + Map<String, Object> searchParams = new HashMap<>(); | |
| 87 | + searchParams.put("nprobe", 10); | |
| 88 | + // 对于 IP 度量,相似度范围在 [minScore, maxScore] | |
| 89 | + searchParams.put("radius", 0.9); // 最小相似度 | |
| 90 | + searchParams.put("range_filter", 1); // 最大相似度 | |
| 91 | + // 1. 确保集合已加载 | |
| 92 | + ensureCollectionLoaded(collectionName); | |
| 93 | + | |
| 94 | + // 1. 向量化搜索文本 | |
| 95 | + List<Float> vectorList = vectorizationService.textToVector(searchText); | |
| 96 | + if (vectorList == null || vectorList.isEmpty()) { | |
| 97 | + throw new RuntimeException("向量化失败"); | |
| 98 | + } | |
| 99 | + // 2. 转换为 float[] | |
| 100 | + float[] floatArray = new float[vectorList.size()]; | |
| 101 | + for (int i = 0; i < vectorList.size(); i++) { | |
| 102 | + floatArray[i] = vectorList.get(i); | |
| 103 | + } | |
| 104 | + // 查询最近插入的数据(按时间倒序) | |
| 105 | +// QueryReq queryReq = QueryReq.builder() | |
| 106 | +// .collectionName(collectionName) | |
| 107 | +// .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id", "create_time","metadata")) | |
| 108 | +// .limit(100) | |
| 109 | +// .build(); | |
| 110 | +// QueryResp queryResp = milvusClient.query(queryReq); | |
| 111 | + | |
| 112 | + // 3. 创建 Milvus FloatVec 对象 | |
| 113 | + FloatVec floatVec = new FloatVec(floatArray); | |
| 114 | + // 4. 构建搜索请求 | |
| 115 | + SearchReq searchReq = SearchReq.builder() | |
| 116 | + .collectionName(collectionName) | |
| 117 | + .data(Collections.singletonList(floatVec)) | |
| 118 | + .annsField("vector") // 向量字段名 | |
| 119 | + .topK(10) // 返回最相似的10条 | |
| 120 | + .metricType(IndexParam.MetricType.IP) // 内积相似度 | |
| 121 | + .outputFields(Arrays.asList("sQuestion", "sSqlContent", "data_id", "create_time","metadata")) | |
| 122 | + .searchParams(searchParams) | |
| 123 | + .build(); | |
| 124 | + // 5. 执行搜索 | |
| 125 | + SearchResp searchResp = milvusClient.search(searchReq); | |
| 126 | + // 6. 处理结果 | |
| 127 | + List<List<SearchResp.SearchResult>> searchResults = searchResp.getSearchResults(); | |
| 128 | + if(ObjectUtil.isEmpty(searchResults)){ | |
| 129 | + return result; | |
| 130 | + } | |
| 131 | + List<SearchResp.SearchResult> firstResultList = searchResults.get(0); | |
| 132 | + if(ObjectUtil.isEmpty(firstResultList)){ | |
| 133 | + return result; | |
| 134 | + } | |
| 135 | + firstResultList.sort((a, b) -> Float.compare(b.getScore(), a.getScore())); | |
| 136 | + SearchResp.SearchResult item = firstResultList.get(0); | |
| 137 | + Map<String, Object> itemMap = new HashMap<>(); | |
| 138 | + itemMap.put("score", item.getScore()); | |
| 139 | + itemMap.put("id", item.getId()); | |
| 140 | + itemMap.putAll(item.getEntity()); | |
| 141 | + return itemMap; | |
| 142 | + } | |
| 143 | + | |
| 144 | + /** | |
| 145 | + * 确保集合已加载 | |
| 146 | + */ | |
| 147 | + private void ensureCollectionLoaded(String collectionName) { | |
| 148 | + try { | |
| 149 | + // 如果已经加载过,直接返回 | |
| 150 | + if (loadedCollections.contains(collectionName)) { | |
| 151 | + return; | |
| 152 | + } | |
| 153 | + log.info("检查集合加载状态: {}", collectionName); | |
| 154 | + // 检查集合是否存在 | |
| 155 | + HasCollectionReq hasCollectionReq = HasCollectionReq.builder() | |
| 156 | + .collectionName(collectionName) | |
| 157 | + .build(); | |
| 158 | + | |
| 159 | + boolean exists = milvusClient.hasCollection(hasCollectionReq); | |
| 160 | + | |
| 161 | + if (!exists) { | |
| 162 | + log.error("集合不存在: {}", collectionName); | |
| 163 | + throw new RuntimeException("集合不存在: " + collectionName); | |
| 164 | + } | |
| 165 | + | |
| 166 | + // 获取加载状态 | |
| 167 | + GetLoadStateReq getLoadStateReq = GetLoadStateReq.builder() | |
| 168 | + .collectionName(collectionName) | |
| 169 | + .build(); | |
| 170 | + | |
| 171 | + boolean isLoaded = milvusClient.getLoadState(getLoadStateReq); | |
| 172 | + | |
| 173 | + if (!isLoaded) { | |
| 174 | + log.info("加载集合到内存: {}", collectionName); | |
| 175 | + | |
| 176 | + // 加载集合 | |
| 177 | + LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder() | |
| 178 | + .collectionName(collectionName) | |
| 179 | + .build(); | |
| 180 | + | |
| 181 | + milvusClient.loadCollection(loadCollectionReq); | |
| 182 | + | |
| 183 | + // 等待加载完成 | |
| 184 | + waitForCollectionLoaded(collectionName); | |
| 185 | + | |
| 186 | + loadedCollections.add(collectionName); | |
| 187 | + log.info("集合加载完成: {}", collectionName); | |
| 188 | + } else { | |
| 189 | + loadedCollections.add(collectionName); | |
| 190 | + log.info("集合已加载: {}", collectionName); | |
| 191 | + } | |
| 192 | + | |
| 193 | + } catch (Exception e) { | |
| 194 | + log.error("确保集合加载失败", e); | |
| 195 | + throw new RuntimeException("集合加载失败: " + collectionName, e); | |
| 196 | + } | |
| 197 | + } | |
| 198 | + | |
| 199 | + /** | |
| 200 | + * 等待集合加载完成 | |
| 201 | + */ | |
| 202 | + private void waitForCollectionLoaded(String collectionName) { | |
| 203 | + int maxRetries = 30; | |
| 204 | + int retryInterval = 1000; // 1秒 | |
| 205 | + | |
| 206 | + for (int i = 0; i < maxRetries; i++) { | |
| 207 | + try { | |
| 208 | + GetLoadStateReq getLoadStateReq = GetLoadStateReq.builder() | |
| 209 | + .collectionName(collectionName) | |
| 210 | + .build(); | |
| 211 | + | |
| 212 | + boolean isLoaded = milvusClient.getLoadState(getLoadStateReq); | |
| 213 | + | |
| 214 | + if (isLoaded) { | |
| 215 | + log.info("集合加载状态确认: {}", collectionName); | |
| 216 | + return; | |
| 217 | + } | |
| 218 | + | |
| 219 | + Thread.sleep(retryInterval); | |
| 220 | + | |
| 221 | + } catch (Exception e) { | |
| 222 | + log.warn("检查加载状态失败,重试 {}/{}", i + 1, maxRetries); | |
| 223 | + } | |
| 224 | + } | |
| 225 | + | |
| 226 | + throw new RuntimeException("集合加载超时: " + collectionName); | |
| 227 | + } | |
| 228 | + | |
| 229 | + | |
| 230 | + /** | |
| 231 | + * 从实体对象构建Milvus插入数据 | |
| 232 | + */ | |
| 233 | + public JsonObject convertToMilvusRow(Map<String,Object> data, List<Float> vector,String sQuestion,String sSqlContent,String sKey) { | |
| 234 | + JsonObject row = new JsonObject(); | |
| 235 | + | |
| 236 | + // 添加向量 | |
| 237 | + JsonArray vectorArray = new JsonArray(); | |
| 238 | + vector.forEach(vectorArray::add); | |
| 239 | + row.add("vector", vectorArray); | |
| 240 | + // 添加文本字段 | |
| 241 | + row.addProperty("sKey", sKey); | |
| 242 | + row.addProperty("data_id", data.get("sId").toString()); | |
| 243 | + row.addProperty("sQuestion", sQuestion); | |
| 244 | + row.addProperty("sSqlContent", sSqlContent); | |
| 245 | + // 创建时间字段 - 必须提供! | |
| 246 | + row.addProperty("create_time", System.currentTimeMillis()); | |
| 247 | + // 创建时间字段 - 必须提供! | |
| 248 | +// row.add("create_time", JsonValue.from(System.currentTimeMillis())); | |
| 249 | + // 添加业务字段到metadata | |
| 250 | + JsonObject metadata = MapToJsonConverter.convert(data); | |
| 251 | + row.add("metadata", metadata); | |
| 252 | + return row; | |
| 253 | + } | |
| 254 | + | |
| 255 | + /** | |
| 256 | + * 创建集合(如果不存在) | |
| 257 | + */ | |
| 258 | + public void createCollectionIfNotExists(String collectionName) { | |
| 259 | + try { | |
| 260 | + // 检查集合是否存在 | |
| 261 | + HasCollectionReq hasCollectionReq = HasCollectionReq.builder() | |
| 262 | + .collectionName(collectionName) | |
| 263 | + .build(); | |
| 264 | + boolean exists = milvusClient.hasCollection(hasCollectionReq); | |
| 265 | + if (!exists) { | |
| 266 | + createCollection(collectionName); | |
| 267 | + log.info("集合 {} 创建成功", collectionName); | |
| 268 | + } | |
| 269 | + } catch (Exception e) { | |
| 270 | + log.error("检查/创建集合失败: {}", collectionName, e); | |
| 271 | + throw new RuntimeException("初始化Milvus集合失败", e); | |
| 272 | + } | |
| 273 | + } | |
| 274 | + | |
| 275 | + /** | |
| 276 | + * 创建集合(定义字段结构) | |
| 277 | + */ | |
| 278 | + private void createCollection(String collectionName) { | |
| 279 | + //删除现有集合 | |
| 280 | +// DropCollectionReq dropCollectionReq = DropCollectionReq.builder() | |
| 281 | +// .collectionName(collectionName) | |
| 282 | +// .build(); | |
| 283 | +// milvusClient.dropCollection(dropCollectionReq); | |
| 284 | + | |
| 285 | + // 定义字段列表 | |
| 286 | + List<CreateCollectionReq.FieldSchema> fieldSchemas = Arrays.asList( | |
| 287 | + // 1. 主键字段 | |
| 288 | + CreateCollectionReq.FieldSchema.builder() | |
| 289 | + .name("id") | |
| 290 | + .dataType(DataType.Int64) | |
| 291 | + .isPrimaryKey(true) | |
| 292 | + .autoID(true) // 使用自动ID | |
| 293 | + .description("主键ID") | |
| 294 | + .build(), | |
| 295 | + | |
| 296 | + // 2. 向量字段 | |
| 297 | + CreateCollectionReq.FieldSchema.builder() | |
| 298 | + .name("vector") | |
| 299 | + .dataType(DataType.FloatVector) | |
| 300 | + .dimension(VECTOR_DIM) | |
| 301 | + .description("向量字段,用于相似性搜索") | |
| 302 | + .build(), | |
| 303 | + | |
| 304 | + // 3. 问题字段 | |
| 305 | + CreateCollectionReq.FieldSchema.builder() | |
| 306 | + .name("sQuestion") | |
| 307 | + .dataType(DataType.VarChar) | |
| 308 | + .maxLength(1000) | |
| 309 | + .description("用户问题") | |
| 310 | + .build(), | |
| 311 | + | |
| 312 | + // 4. SQL内容字段 | |
| 313 | + CreateCollectionReq.FieldSchema.builder() | |
| 314 | + .name("sSqlContent") | |
| 315 | + .dataType(DataType.VarChar) | |
| 316 | + .maxLength(5000) // SQL可能较长 | |
| 317 | + .description("SQL语句") | |
| 318 | + .build(), | |
| 319 | + | |
| 320 | + // 5. 数据ID字段 | |
| 321 | + CreateCollectionReq.FieldSchema.builder() | |
| 322 | + .name("data_id") | |
| 323 | + .dataType(DataType.VarChar) | |
| 324 | + .maxLength(100) | |
| 325 | + .description("原始数据ID") | |
| 326 | + .build(), | |
| 327 | + | |
| 328 | + // 6. 创建时间字段 | |
| 329 | + CreateCollectionReq.FieldSchema.builder() | |
| 330 | + .name("create_time") | |
| 331 | + .dataType(DataType.Int64) | |
| 332 | + .description("创建时间戳") | |
| 333 | + .build(), | |
| 334 | + | |
| 335 | + // 7. 元数据字段(使用JSON类型存储额外数据) | |
| 336 | + CreateCollectionReq.FieldSchema.builder() | |
| 337 | + .name("metadata") | |
| 338 | + .dataType(DataType.JSON) | |
| 339 | + .description("额外元数据") | |
| 340 | + .build(), | |
| 341 | + CreateCollectionReq.FieldSchema.builder() | |
| 342 | + .name("sKey") | |
| 343 | + .dataType(DataType.VarChar) | |
| 344 | + .maxLength(100) | |
| 345 | + .description("存入的vector转换前数据") | |
| 346 | + .build() | |
| 347 | + ); | |
| 348 | + | |
| 349 | + // 创建集合schema | |
| 350 | + CreateCollectionReq.CollectionSchema schema = | |
| 351 | + CreateCollectionReq.CollectionSchema.builder() | |
| 352 | + .fieldSchemaList(fieldSchemas) | |
| 353 | + .enableDynamicField(true) | |
| 354 | + .build(); | |
| 355 | + | |
| 356 | + // 创建集合请求 | |
| 357 | + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder() | |
| 358 | + .collectionName(collectionName) | |
| 359 | + .collectionSchema(schema) | |
| 360 | + .consistencyLevel(ConsistencyLevel.BOUNDED) | |
| 361 | + .build(); | |
| 362 | + | |
| 363 | + // 执行创建集合 | |
| 364 | + milvusClient.createCollection(createCollectionReq); | |
| 365 | + | |
| 366 | + //创建索引 | |
| 367 | + createIndexesStepByStep(collectionName); | |
| 368 | + } | |
| 369 | + | |
| 370 | + /* | |
| 371 | + * 分步创建索引,便于监控每个索引的状态 | |
| 372 | + */ | |
| 373 | + private void createIndexesStepByStep(String collectionName) { | |
| 374 | + log.info("开始为集合创建索引: {}", collectionName); | |
| 375 | + createAllIndexes(collectionName); | |
| 376 | +// // 1. 创建向量索引 | |
| 377 | +// createVectorIndex(collectionName); | |
| 378 | +// | |
| 379 | +// // 2. 创建标量索引 | |
| 380 | +// createScalarIndexes(collectionName); | |
| 381 | + } | |
| 382 | + | |
| 383 | + /** | |
| 384 | + * 创建向量索引 | |
| 385 | + * IVF_FLAT 向量相似度搜索 常用的向量索引,平衡性能和召回率 | |
| 386 | + * STL_SORT 标量字段排序 适用于数字、时间等需要排序的字段 | |
| 387 | + * INVERTED 文本字段过滤 倒排索引,适用于文本字段的精确匹配 | |
| 388 | + * TRIE 字符串前缀匹配 适用于前缀查询 | |
| 389 | + * BITMAP 枚举值过滤 适用于低基数字段 | |
| 390 | + */ | |
| 391 | + private void createVectorIndex(String collectionName) { | |
| 392 | + log.info("创建向量索引: {}", collectionName); | |
| 393 | + | |
| 394 | + Map<String, Object> extraParams = new HashMap<>(); | |
| 395 | + extraParams.put("nlist", 128); | |
| 396 | + | |
| 397 | + IndexParam vectorIndex = IndexParam.builder() | |
| 398 | + .fieldName("vector") | |
| 399 | + .indexName("idx_vector") | |
| 400 | + .indexType(IndexParam.IndexType.IVF_FLAT) | |
| 401 | + .metricType(IndexParam.MetricType.IP) | |
| 402 | + .extraParams(extraParams) | |
| 403 | + .build(); | |
| 404 | + | |
| 405 | + CreateIndexReq createIndexReq = CreateIndexReq.builder() | |
| 406 | + .collectionName(collectionName) | |
| 407 | + .indexParams(Collections.singletonList(vectorIndex)) | |
| 408 | + .sync(true) | |
| 409 | + .timeout(60000L) | |
| 410 | + .build(); | |
| 411 | + | |
| 412 | + milvusClient.createIndex(createIndexReq); | |
| 413 | + log.info("向量索引创建完成"); | |
| 414 | + } | |
| 415 | + | |
| 416 | + /** | |
| 417 | + * 创建标量索引 | |
| 418 | + */ | |
| 419 | + private void createScalarIndexes(String collectionName) { | |
| 420 | + log.info("创建标量索引: {}", collectionName); | |
| 421 | + | |
| 422 | + // 为 create_time 字段创建索引 | |
| 423 | + IndexParam timeIndex = IndexParam.builder() | |
| 424 | + .fieldName("create_time") | |
| 425 | + .indexName("idx_create_time") | |
| 426 | + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引 | |
| 427 | + .build(); | |
| 428 | + | |
| 429 | + CreateIndexReq timeIndexReq = CreateIndexReq.builder() | |
| 430 | + .collectionName(collectionName) | |
| 431 | + .indexParams(Collections.singletonList(timeIndex)) | |
| 432 | + .sync(true) | |
| 433 | + .timeout(30000L) | |
| 434 | + .build(); | |
| 435 | + | |
| 436 | + milvusClient.createIndex(timeIndexReq); | |
| 437 | + log.info("create_time 索引创建完成"); | |
| 438 | + | |
| 439 | + // 为 question 字段创建倒排索引(支持文本过滤) | |
| 440 | + IndexParam questionIndex = IndexParam.builder() | |
| 441 | + .fieldName("sQuestion") | |
| 442 | + .indexName("idx_question") | |
| 443 | + .indexType(IndexParam.IndexType.TRIE) // 倒排索引 | |
| 444 | + .build(); | |
| 445 | + | |
| 446 | + CreateIndexReq questionIndexReq = CreateIndexReq.builder() | |
| 447 | + .collectionName(collectionName) | |
| 448 | + .indexParams(Collections.singletonList(questionIndex)) | |
| 449 | + .sync(true) | |
| 450 | + .timeout(30000L) | |
| 451 | + .build(); | |
| 452 | + | |
| 453 | + milvusClient.createIndex(questionIndexReq); | |
| 454 | + log.info("question 索引创建完成"); | |
| 455 | + | |
| 456 | + // 为 data_id 字段创建索引 | |
| 457 | + IndexParam idIndex = IndexParam.builder() | |
| 458 | + .fieldName("data_id") | |
| 459 | + .indexName("idx_data_id") | |
| 460 | + .indexType(IndexParam.IndexType.TRIE) | |
| 461 | + .build(); | |
| 462 | + | |
| 463 | + CreateIndexReq idIndexReq = CreateIndexReq.builder() | |
| 464 | + .collectionName(collectionName) | |
| 465 | + .indexParams(Collections.singletonList(idIndex)) | |
| 466 | + .sync(true) | |
| 467 | + .timeout(30000L) | |
| 468 | + .build(); | |
| 469 | + | |
| 470 | + milvusClient.createIndex(idIndexReq); | |
| 471 | + log.info("data_id 索引创建完成"); | |
| 472 | + } | |
| 473 | + | |
| 474 | + /** | |
| 475 | + * 重建索引(解决索引未就绪问题) | |
| 476 | + */ | |
| 477 | + public boolean rebuildIndex(String collectionName) { | |
| 478 | + log.info("========== 开始重建索引 =========="); | |
| 479 | + log.info("集合名称: {}", collectionName); | |
| 480 | + | |
| 481 | + try { | |
| 482 | + // 1. 先检查集合是否存在 | |
| 483 | + HasCollectionReq hasReq = HasCollectionReq.builder() | |
| 484 | + .collectionName(collectionName) | |
| 485 | + .build(); | |
| 486 | + | |
| 487 | + if (!milvusClient.hasCollection(hasReq)) { | |
| 488 | + log.error("集合不存在: {}", collectionName); | |
| 489 | + return false; | |
| 490 | + } | |
| 491 | + | |
| 492 | + // 2. 先释放集合(如果已加载) | |
| 493 | + try { | |
| 494 | + ReleaseCollectionReq releaseReq = ReleaseCollectionReq.builder() | |
| 495 | + .collectionName(collectionName) | |
| 496 | + .build(); | |
| 497 | + milvusClient.releaseCollection(releaseReq); | |
| 498 | + log.info("集合已释放: {}", collectionName); | |
| 499 | + Thread.sleep(2000); // 等待释放完成 | |
| 500 | + } catch (Exception e) { | |
| 501 | + log.warn("释放集合失败(可能未加载): {}", e.getMessage()); | |
| 502 | + } | |
| 503 | + | |
| 504 | + // 3. 删除原有索引 | |
| 505 | + try { | |
| 506 | + DropIndexReq dropIndexReq = DropIndexReq.builder() | |
| 507 | + .collectionName(collectionName) | |
| 508 | + .fieldName("vector") // 指定向量字段 | |
| 509 | + .build(); | |
| 510 | + | |
| 511 | + milvusClient.dropIndex(dropIndexReq); | |
| 512 | + log.info("原有索引已删除"); | |
| 513 | + Thread.sleep(2000); // 等待删除完成 | |
| 514 | + } catch (Exception e) { | |
| 515 | + log.warn("删除索引失败(可能不存在): {}", e.getMessage()); | |
| 516 | + } | |
| 517 | + | |
| 518 | + // 4. 创建新索引 | |
| 519 | + createVectorIndex(collectionName); | |
| 520 | + | |
| 521 | + // 5. 等待索引就绪 | |
| 522 | + boolean indexReady = waitForIndexReady(collectionName, "vector", 60); | |
| 523 | + if (!indexReady) { | |
| 524 | + log.error("索引未就绪"); | |
| 525 | + return false; | |
| 526 | + } | |
| 527 | + | |
| 528 | + // 6. 重新加载集合 | |
| 529 | + LoadCollectionReq loadReq = LoadCollectionReq.builder() | |
| 530 | + .collectionName(collectionName) | |
| 531 | + .build(); | |
| 532 | + | |
| 533 | + milvusClient.loadCollection(loadReq); | |
| 534 | + log.info("集合重新加载成功"); | |
| 535 | + | |
| 536 | + // 7. 验证加载状态 | |
| 537 | + boolean loaded = waitForLoad(collectionName, 30); | |
| 538 | + if (!loaded) { | |
| 539 | + log.error("集合加载失败"); | |
| 540 | + return false; | |
| 541 | + } | |
| 542 | + | |
| 543 | + log.info("✅ 索引重建完成,集合已就绪: {}", collectionName); | |
| 544 | + return true; | |
| 545 | + | |
| 546 | + } catch (Exception e) { | |
| 547 | + log.error("重建索引失败", e); | |
| 548 | + return false; | |
| 549 | + } | |
| 550 | + } | |
| 551 | + | |
| 552 | + /** | |
| 553 | + * 等待索引就绪 | |
| 554 | + */ | |
| 555 | + private boolean waitForIndexReady(String collectionName, String fieldName, int timeoutSeconds) { | |
| 556 | + log.info("等待索引就绪: {}.{},超时: {}秒", collectionName, fieldName, timeoutSeconds); | |
| 557 | + | |
| 558 | + for (int i = 0; i < timeoutSeconds; i++) { | |
| 559 | + try { | |
| 560 | + DescribeIndexReq describeIndexReq = DescribeIndexReq.builder() | |
| 561 | + .collectionName(collectionName) | |
| 562 | + .build(); | |
| 563 | + | |
| 564 | + DescribeIndexResp describeIndexResp = milvusClient.describeIndex(describeIndexReq); | |
| 565 | + | |
| 566 | + List<DescribeIndexResp.IndexDesc> indexDescs = describeIndexResp.getIndexDescriptions(); | |
| 567 | + | |
| 568 | + for (DescribeIndexResp.IndexDesc desc : indexDescs) { | |
| 569 | + if (fieldName.equals(desc.getFieldName())) { | |
| 570 | + IndexBuildState state = desc.getIndexState(); | |
| 571 | + | |
| 572 | + log.info("索引状态: {}, 进度: {}/{}", | |
| 573 | + state, desc.getIndexedRows(), desc.getTotalRows()); | |
| 574 | + | |
| 575 | + if (state == IndexBuildState.Finished) { | |
| 576 | + log.info("✅ 索引就绪"); | |
| 577 | + return true; | |
| 578 | + } else if (state == IndexBuildState.Failed) { | |
| 579 | + log.error("❌ 索引构建失败: {}", desc.getIndexFailedReason()); | |
| 580 | + return false; | |
| 581 | + } | |
| 582 | + break; | |
| 583 | + } | |
| 584 | + } | |
| 585 | + | |
| 586 | + Thread.sleep(1000); | |
| 587 | + | |
| 588 | + } catch (InterruptedException e) { | |
| 589 | + Thread.currentThread().interrupt(); | |
| 590 | + log.warn("等待被中断"); | |
| 591 | + return false; | |
| 592 | + } catch (Exception e) { | |
| 593 | + log.warn("检查索引状态失败: {}/{}", i + 1, timeoutSeconds); | |
| 594 | + } | |
| 595 | + } | |
| 596 | + | |
| 597 | + log.error("等待索引就绪超时"); | |
| 598 | + return false; | |
| 599 | + } | |
| 600 | + | |
| 601 | + /** | |
| 602 | + * 等待集合加载完成 | |
| 603 | + */ | |
| 604 | + private boolean waitForLoad(String collectionName, int timeoutSeconds) { | |
| 605 | + log.info("等待集合加载: {},超时: {}秒", collectionName, timeoutSeconds); | |
| 606 | + | |
| 607 | + for (int i = 0; i < timeoutSeconds; i++) { | |
| 608 | + try { | |
| 609 | + GetLoadStateReq loadStateReq = GetLoadStateReq.builder() | |
| 610 | + .collectionName(collectionName) | |
| 611 | + .build(); | |
| 612 | + | |
| 613 | + boolean isLoaded = milvusClient.getLoadState(loadStateReq); | |
| 614 | + | |
| 615 | + if (isLoaded) { | |
| 616 | + log.info("✅ 集合加载完成"); | |
| 617 | + return true; | |
| 618 | + } | |
| 619 | + | |
| 620 | + Thread.sleep(1000); | |
| 621 | + | |
| 622 | + } catch (InterruptedException e) { | |
| 623 | + Thread.currentThread().interrupt(); | |
| 624 | + return false; | |
| 625 | + } catch (Exception e) { | |
| 626 | + log.warn("检查加载状态失败: {}/{}", i + 1, timeoutSeconds); | |
| 627 | + } | |
| 628 | + } | |
| 629 | + | |
| 630 | + log.error("集合加载超时"); | |
| 631 | + return false; | |
| 632 | + } | |
| 633 | + | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * 批量创建所有索引(向量索引 + 多个标量索引) | |
| 637 | + */ | |
| 638 | + private void createAllIndexes(String collectionName) { | |
| 639 | + log.info("开始为集合批量创建索引: {}", collectionName); | |
| 640 | + | |
| 641 | + // 1. 准备所有索引参数 | |
| 642 | + List<IndexParam> allIndexParams = new ArrayList<>(); | |
| 643 | + | |
| 644 | + // 1.1 向量索引 | |
| 645 | + Map<String, Object> vectorExtraParams = new HashMap<>(); | |
| 646 | + vectorExtraParams.put("nlist", 256); // 聚类中心数:sqrt(384) * 13 ≈ 256 | |
| 647 | + vectorExtraParams.put("nprobe", 32); // 搜索时检查的聚类数 | |
| 648 | + | |
| 649 | + IndexParam vectorIndex = IndexParam.builder() | |
| 650 | + .fieldName("vector") | |
| 651 | + .indexName("idx_vector_rebuild") | |
| 652 | + .indexType(IndexParam.IndexType.IVF_FLAT) | |
| 653 | + .metricType(IndexParam.MetricType.IP) | |
| 654 | + .extraParams(vectorExtraParams) | |
| 655 | + .build(); | |
| 656 | + allIndexParams.add(vectorIndex); | |
| 657 | + | |
| 658 | + // 1.2 create_time 字段索引(用于时间范围查询) | |
| 659 | + IndexParam timeIndex = IndexParam.builder() | |
| 660 | + .fieldName("create_time") | |
| 661 | + .indexName("idx_create_time") | |
| 662 | + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引 | |
| 663 | + .build(); | |
| 664 | + allIndexParams.add(timeIndex); | |
| 665 | + | |
| 666 | + // 1.3 question 字段倒排索引(用于文本过滤) | |
| 667 | + IndexParam questionIndex = IndexParam.builder() | |
| 668 | + .fieldName("sQuestion") | |
| 669 | + .indexName("idx_question") | |
| 670 | + .indexType(IndexParam.IndexType.INVERTED) // 倒排索引 | |
| 671 | + .build(); | |
| 672 | + allIndexParams.add(questionIndex); | |
| 673 | + | |
| 674 | + // 1.4 data_id 字段索引(用于精确匹配) | |
| 675 | + IndexParam idIndex = IndexParam.builder() | |
| 676 | + .fieldName("data_id") | |
| 677 | + .indexName("idx_data_id") | |
| 678 | + .indexType(IndexParam.IndexType.INVERTED) | |
| 679 | + .build(); | |
| 680 | + allIndexParams.add(idIndex); | |
| 681 | + | |
| 682 | + IndexParam sKey = IndexParam.builder() | |
| 683 | + .fieldName("sKey") | |
| 684 | + .indexName("s_key") | |
| 685 | + .indexType(IndexParam.IndexType.INVERTED) | |
| 686 | + .build(); | |
| 687 | + allIndexParams.add(sKey); | |
| 688 | + | |
| 689 | + // 1.5 sql_content 字段索引(如果需要) | |
| 690 | +// IndexParam sqlIndex = IndexParam.builder() | |
| 691 | +// .fieldName("sql_content") | |
| 692 | +// .indexName("idx_sql_content") | |
| 693 | +// .indexType(IndexParam.IndexType.INVERTED) | |
| 694 | +// .build(); | |
| 695 | +// allIndexParams.add(sqlIndex); | |
| 696 | + | |
| 697 | + // 2. 批量创建索引 | |
| 698 | + try { | |
| 699 | + CreateIndexReq createIndexReq = CreateIndexReq.builder() | |
| 700 | + .collectionName(collectionName) | |
| 701 | + .indexParams(allIndexParams) // 一次性传入所有索引 | |
| 702 | + .sync(true) // 同步等待 | |
| 703 | + .timeout(120000L) // 总超时时间120秒 | |
| 704 | + .build(); | |
| 705 | + | |
| 706 | + milvusClient.createIndex(createIndexReq); | |
| 707 | + log.info("所有索引批量创建成功: {}", collectionName); | |
| 708 | + | |
| 709 | + } catch (Exception e) { | |
| 710 | + log.error("批量创建索引失败: {}", e.getMessage()); | |
| 711 | + createScalarIndexes(collectionName); | |
| 712 | + } | |
| 713 | + } | |
| 714 | + | |
| 715 | + | |
| 716 | +} | ... | ... |
src/main/java/com/xly/milvus/service/impl/EmbeddingServiceImpl.java
0 → 100644
| 1 | +package com.xly.milvus.service.impl; | |
| 2 | + | |
| 3 | +import com.xly.milvus.service.EmbeddingService; | |
| 4 | +import dev.langchain4j.data.embedding.Embedding; | |
| 5 | +import dev.langchain4j.data.segment.TextSegment; | |
| 6 | +import dev.langchain4j.model.embedding.EmbeddingModel; | |
| 7 | +import lombok.RequiredArgsConstructor; | |
| 8 | +import lombok.extern.slf4j.Slf4j; | |
| 9 | +import org.springframework.stereotype.Service; | |
| 10 | + | |
| 11 | +import java.util.ArrayList; | |
| 12 | +import java.util.Collections; | |
| 13 | +import java.util.List; | |
| 14 | +import java.util.stream.Collectors; | |
| 15 | + | |
| 16 | +@Slf4j | |
| 17 | +@Service("embeddingService") | |
| 18 | +@RequiredArgsConstructor | |
| 19 | +public class EmbeddingServiceImpl implements EmbeddingService { | |
| 20 | + | |
| 21 | + private final EmbeddingModel embeddingModel; | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 生成单个文本的向量 | |
| 25 | + */ | |
| 26 | + public List<Float> generateEmbedding(String text) { | |
| 27 | + if (text == null || text.trim().isEmpty()) { | |
| 28 | + log.warn("Input text is empty"); | |
| 29 | + return null; | |
| 30 | + } | |
| 31 | + | |
| 32 | + try { | |
| 33 | + // 0.35.0 API: embed 方法返回 Response<Embedding> | |
| 34 | + Embedding embedding = embeddingModel.embed(text).content(); | |
| 35 | + | |
| 36 | + // 将 float[] 转换为 List<Float> 供 Milvus 使用 | |
| 37 | + float[] vectorArray = embedding.vector(); | |
| 38 | + List<Float> vectorList = new ArrayList<>(vectorArray.length); | |
| 39 | + for (float v : vectorArray) { | |
| 40 | + vectorList.add(v); | |
| 41 | + } | |
| 42 | + return vectorList; | |
| 43 | + } catch (Exception e) { | |
| 44 | + log.error("Error generating embedding for text: {}", text, e); | |
| 45 | + return null; | |
| 46 | + } | |
| 47 | + } | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 批量生成向量(高效版) | |
| 51 | + * 利用 LangChain4j 内置的并行化能力,显著提升性能 | |
| 52 | + */ | |
| 53 | + public List<List<Float>> generateEmbeddings(List<String> texts) { | |
| 54 | + if (texts == null || texts.isEmpty()) { | |
| 55 | + return Collections.emptyList(); | |
| 56 | + } | |
| 57 | + | |
| 58 | + try { | |
| 59 | + // 将文本转换为 TextSegment 列表 | |
| 60 | + List<TextSegment> segments = texts.stream() | |
| 61 | + .map(TextSegment::from) | |
| 62 | + .collect(Collectors.toList()); | |
| 63 | + | |
| 64 | + // 批量嵌入,内部自动并行化 | |
| 65 | + List<Embedding> embeddings = embeddingModel.embedAll(segments).content(); | |
| 66 | + | |
| 67 | + // 转换格式 | |
| 68 | + return embeddings.stream() | |
| 69 | + .map(embedding -> { | |
| 70 | + float[] array = embedding.vector(); | |
| 71 | + List<Float> list = new ArrayList<>(array.length); | |
| 72 | + for (float v : array) { | |
| 73 | + list.add(v); | |
| 74 | + } | |
| 75 | + return list; | |
| 76 | + }) | |
| 77 | + .collect(Collectors.toList()); | |
| 78 | + | |
| 79 | + } catch (Exception e) { | |
| 80 | + log.error("Error generating embeddings in batch", e); | |
| 81 | + return Collections.emptyList(); | |
| 82 | + } | |
| 83 | + } | |
| 84 | +} | |
| 0 | 85 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/impl/MilvusServiceImpl.java
0 → 100644
| 1 | +package com.xly.milvus.service.impl; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.collection.ConcurrentHashSet; | |
| 5 | +import cn.hutool.core.date.DateUtil; | |
| 6 | +import cn.hutool.core.thread.ThreadUtil; | |
| 7 | +import cn.hutool.core.util.ObjectUtil; | |
| 8 | +import cn.hutool.core.util.StrUtil; | |
| 9 | +import com.google.gson.JsonArray; | |
| 10 | +import com.google.gson.JsonObject; | |
| 11 | +import com.xly.milvus.service.MilvusService; | |
| 12 | +import com.xly.milvus.service.VectorizationService; | |
| 13 | +import com.xly.milvus.util.MapToJsonConverter; | |
| 14 | +import com.xly.service.DynamicExeDbService; | |
| 15 | +import com.xly.tts.bean.TTSResponseDTO; | |
| 16 | +import io.milvus.response.SearchResultsWrapper; | |
| 17 | +import io.milvus.v2.client.MilvusClientV2; | |
| 18 | +import io.milvus.v2.common.ConsistencyLevel; | |
| 19 | +import io.milvus.v2.common.DataType; | |
| 20 | +import io.milvus.v2.common.IndexParam; | |
| 21 | +import io.milvus.v2.service.collection.request.*; | |
| 22 | +import io.milvus.v2.service.collection.response.DescribeCollectionResp; | |
| 23 | +import io.milvus.v2.service.vector.request.DeleteReq; | |
| 24 | +import io.milvus.v2.service.vector.request.InsertReq; | |
| 25 | +import io.milvus.v2.service.vector.request.SearchReq; | |
| 26 | +import io.milvus.v2.service.vector.request.data.FloatVec; | |
| 27 | +import io.milvus.v2.service.vector.response.InsertResp; | |
| 28 | +import io.milvus.v2.service.vector.response.QueryResp; | |
| 29 | +import io.milvus.v2.service.vector.response.SearchResp; | |
| 30 | +import lombok.RequiredArgsConstructor; | |
| 31 | +import lombok.extern.slf4j.Slf4j; | |
| 32 | +import org.springframework.beans.factory.annotation.Value; | |
| 33 | +import org.springframework.stereotype.Service; | |
| 34 | + | |
| 35 | +import java.math.BigDecimal; | |
| 36 | +import java.util.*; | |
| 37 | +import java.util.regex.Matcher; | |
| 38 | +import java.util.regex.Pattern; | |
| 39 | +import java.util.stream.Collectors; | |
| 40 | + | |
| 41 | +/** | |
| 42 | + * 向量化服务实现 - 使用LangChain4j的All-MiniLM-L6-v2模型 | |
| 43 | + */ | |
| 44 | +@Slf4j | |
| 45 | +@Service(value = "milvusService") | |
| 46 | +@RequiredArgsConstructor | |
| 47 | +public class MilvusServiceImpl implements MilvusService { | |
| 48 | + | |
| 49 | + private final MilvusClientV2 milvusClient; | |
| 50 | + private final VectorizationService vectorizationService; | |
| 51 | + private final DynamicExeDbService dynamicExeDbService; | |
| 52 | + | |
| 53 | + // 或者从配置文件读取 | |
| 54 | + @Value("${milvus.vector.dimension:384}") | |
| 55 | + private int VECTOR_DIM; | |
| 56 | + | |
| 57 | + // 缓存已经初始化过的 Milvus 集合(线程安全) | |
| 58 | + private final Set<String> loadedCollections = new ConcurrentHashSet<>(); | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + /*** | |
| 63 | + * @Author 钱豹 | |
| 64 | + * @Date 22:18 2026/3/24 | |
| 65 | + * @Param [reqMap] | |
| 66 | + * @return void | |
| 67 | + * @Description 初始化结构以及数据 | |
| 68 | + **/ | |
| 69 | + @Override | |
| 70 | + public TTSResponseDTO initDataToMilvus(Map<String, Object> reqMap) { | |
| 71 | + if(ObjectUtil.isEmpty(reqMap.get("sInputTabelName"))){ | |
| 72 | + return TTSResponseDTO.builder() | |
| 73 | + .code(-1) | |
| 74 | + .message("输入表名") | |
| 75 | + .build(); | |
| 76 | + } | |
| 77 | + if(ObjectUtil.isEmpty(reqMap.get("sVectorfiled"))){ | |
| 78 | + return TTSResponseDTO.builder() | |
| 79 | + .code(-1) | |
| 80 | + .message("向量库标量字段") | |
| 81 | + .build(); | |
| 82 | + } | |
| 83 | + if(ObjectUtil.isEmpty(reqMap.get("sVectorjson"))){ | |
| 84 | + return TTSResponseDTO.builder() | |
| 85 | + .code(-1) | |
| 86 | + .message("向量化内容JSON") | |
| 87 | + .build(); | |
| 88 | + } | |
| 89 | + | |
| 90 | + String sInputTabelName = reqMap.get("sInputTabelName").toString(); | |
| 91 | + String sVectorfiled = reqMap.get("sVectorfiled").toString(); | |
| 92 | + String sVectorjson = reqMap.get("sVectorjson").toString(); | |
| 93 | + | |
| 94 | + String tUpdateDate = DateUtil.now(); | |
| 95 | + String tUpdateDateUp = getUpdateDateUp(sInputTabelName); | |
| 96 | + //获取需要同步地数据 | |
| 97 | + List<Map<String,Object>> data = getAddData(sInputTabelName,tUpdateDate, tUpdateDateUp); | |
| 98 | + //创建集合 | |
| 99 | + createCollectionIfNotExists(sInputTabelName, sVectorfiled, sVectorjson,true); | |
| 100 | + if(ObjectUtil.isNotEmpty(data)){ | |
| 101 | + //插入数据 | |
| 102 | + long num= addDataToCollection(sInputTabelName, sVectorfiled, sVectorjson,data); | |
| 103 | + } | |
| 104 | + addAiMilvusVectorRecord(sInputTabelName,tUpdateDate, tUpdateDateUp); | |
| 105 | + return TTSResponseDTO.builder() | |
| 106 | + .code(200) | |
| 107 | + .message("success") | |
| 108 | + .build(); | |
| 109 | + } | |
| 110 | + | |
| 111 | + public String getUpdateDateUp(String sInputTabelName) { | |
| 112 | + Map<String,Object> serDataMap = new HashMap<>(); | |
| 113 | + String sSql ="SELECT DATE_FORMAT(tUpdateDate,'%Y-%m-%d %H:%i:%s') AS tUpdateDate FROM ai_milvus_vector_record WHERE sInputTabelName = #{sInputTabelName}"; | |
| 114 | + serDataMap.put("sInputTabelName",sInputTabelName); | |
| 115 | + List<Map<String,Object>> data = this.dynamicExeDbService.findSql(serDataMap,sSql); | |
| 116 | + if(ObjectUtil.isEmpty(data)){ | |
| 117 | + return "2000-03-24"; | |
| 118 | + } | |
| 119 | + return data.get(0).get("tUpdateDate").toString(); | |
| 120 | + } | |
| 121 | + | |
| 122 | + /*** | |
| 123 | + * @Author 钱豹 | |
| 124 | + * @Date 22:24 2026/3/24 | |
| 125 | + * @Param | |
| 126 | + * @return | |
| 127 | + * @Description 获取需要同步地数据 | |
| 128 | + **/ | |
| 129 | + public List<Map<String,Object>> getAddData(String sInputTabelName,String tUpdateDate,String tUpdateDateUp) { | |
| 130 | + //获取需要同步地数据 | |
| 131 | + Map<String,Object> serDataMap = new HashMap<>(); | |
| 132 | + serDataMap.put("tUpdateDate",tUpdateDate); | |
| 133 | + serDataMap.put("tUpdateDateUp",tUpdateDateUp); | |
| 134 | + String sSql = String.format("SELECT * FROM %s WHERE tUpdateDate >= #{tUpdateDateUp} AND tUpdateDate < #{tUpdateDate}",sInputTabelName); | |
| 135 | + return this.dynamicExeDbService.findSql(serDataMap,sSql); | |
| 136 | + } | |
| 137 | + | |
| 138 | + /*** | |
| 139 | + * @Author 钱豹 | |
| 140 | + * @Date 22:32 2026/3/24 | |
| 141 | + * @Param [sInputTabelName, tUpdateDate] | |
| 142 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 143 | + * @Description 获取更新地数据 | |
| 144 | + **/ | |
| 145 | + public void addAiMilvusVectorRecord(String sInputTabelName,String tUpdateDate,String tUpdateDateUp) { | |
| 146 | + //获取需要同步地数据 | |
| 147 | + delAiMilvusVectorRecord(sInputTabelName); | |
| 148 | + Map<String,Object> dMap = new HashMap<>(); | |
| 149 | + dMap.put("sInputTabelName",sInputTabelName); | |
| 150 | + dMap.put("tUpdateDate",tUpdateDate); | |
| 151 | + dMap.put("tUpdateDateUp",tUpdateDateUp); | |
| 152 | + String sSql = String.format("INSERT INTO ai_milvus_vector_record(sId,sInputTabelName,tUpdateDate,tUpdateDateUp)VALUES(newId(),#{sInputTabelName},#{tUpdateDate},#{tUpdateDateUp})"); | |
| 153 | + dynamicExeDbService.addSql(dMap,sSql); | |
| 154 | + } | |
| 155 | + | |
| 156 | + /*** | |
| 157 | + * @Author 钱豹 | |
| 158 | + * @Date 22:32 2026/3/24 | |
| 159 | + * @Param [sInputTabelName, tUpdateDate] | |
| 160 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 161 | + * @Description 获取更新地数据 | |
| 162 | + **/ | |
| 163 | + public void delAiMilvusVectorRecord(String sInputTabelName) { | |
| 164 | + //获取需要同步地数据 | |
| 165 | + Map<String,Object> dMap = new HashMap<>(); | |
| 166 | + dMap.put("sInputTabelName",sInputTabelName); | |
| 167 | + String sSql = "DELETE FROM ai_milvus_vector_record WHERE sInputTabelName = #{sInputTabelName}"; | |
| 168 | + dynamicExeDbService.delSql(dMap,sSql); | |
| 169 | + } | |
| 170 | + | |
| 171 | + | |
| 172 | + /*** | |
| 173 | + * @Author 钱豹 | |
| 174 | + * @Date 16:31 2026/3/24 | |
| 175 | + * @Param [collectionName] | |
| 176 | + * @return void | |
| 177 | + * @Description 创建集合 | |
| 178 | + **/ | |
| 179 | + @Override | |
| 180 | + public void createCollectionIfNotExists(String collectionName,String sVectorfiled,String sVectorjson,Boolean bRset) { | |
| 181 | + createCollection(collectionName,sVectorfiled,bRset); | |
| 182 | + log.info("集合 {} 创建成功", collectionName); | |
| 183 | + // 集合缓存:只加载一次 | |
| 184 | + if (!loadedCollections.contains(collectionName)) { | |
| 185 | + loadedCollections.add(collectionName); | |
| 186 | + } | |
| 187 | + | |
| 188 | + } | |
| 189 | + | |
| 190 | + /*** | |
| 191 | + * @Author 钱豹 | |
| 192 | + * @Date 20:59 2026/3/24 | |
| 193 | + * @Param [collectionName, sVectorfiled, sVectorjson, bRset] | |
| 194 | + * @return void | |
| 195 | + * @Description 新增数据集合 | |
| 196 | + **/ | |
| 197 | + @Override | |
| 198 | + public long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson,List<Map<String,Object>> data){ | |
| 199 | + | |
| 200 | + // 1. 参数校验(防止空参数导致崩溃) | |
| 201 | + if (ObjectUtil.isEmpty(collectionName) || CollUtil.isEmpty(data)) { | |
| 202 | + throw new IllegalArgumentException("参数异常:集合名/slaveId/数据不能为空"); | |
| 203 | + } | |
| 204 | + | |
| 205 | + // 1. 转换为Milvus格式 | |
| 206 | + List<JsonObject> rows = convertToMilvusRow(data, sVectorfiled, sVectorjson); | |
| 207 | + if (CollUtil.isEmpty(rows)) { | |
| 208 | + return 0l; // 无数据直接返回 | |
| 209 | + } | |
| 210 | + | |
| 211 | + // 3.先删除再插入 | |
| 212 | + // 1. 构建删除请求 | |
| 213 | + // 过滤条件:匹配唯一键 | |
| 214 | + // 核心:从 data 中提取所有 sSlaveId,批量删除 | |
| 215 | + List<String> slaveIdList = data.stream() | |
| 216 | + .map(map -> map.get("sSlaveId")) // 取每条数据的slaveId | |
| 217 | + .filter(Objects::nonNull) | |
| 218 | + .map(String::valueOf) | |
| 219 | + .distinct() // 去重 | |
| 220 | + .toList(); | |
| 221 | + | |
| 222 | + if (slaveIdList.isEmpty()) { | |
| 223 | + throw new RuntimeException("未获取到slaveId,无法删除旧数据"); | |
| 224 | + } | |
| 225 | + | |
| 226 | + // 拼接 Milvus 删除条件:sSlaveId in ['111','222','333'] | |
| 227 | + String filter = String.format("sSlaveId in [%s]", | |
| 228 | + slaveIdList.stream() | |
| 229 | + .map(id -> "'" + id + "'") | |
| 230 | + .collect(Collectors.joining(",")) | |
| 231 | + ); | |
| 232 | + | |
| 233 | + // 批量删除 | |
| 234 | + DeleteReq deleteReq = DeleteReq.builder() | |
| 235 | + .collectionName(collectionName) | |
| 236 | + .filter(filter) | |
| 237 | + .build(); | |
| 238 | + milvusClient.delete(deleteReq); | |
| 239 | + | |
| 240 | + // 短暂等待 Milvus 数据同步 | |
| 241 | + ThreadUtil.sleep(100); | |
| 242 | + | |
| 243 | + // 4. 插入到Milvus(批量) | |
| 244 | + InsertReq insertReq = InsertReq.builder() | |
| 245 | + .collectionName(collectionName) | |
| 246 | + .data(rows) | |
| 247 | + .build(); | |
| 248 | + InsertResp insertResp = milvusClient.insert(insertReq); | |
| 249 | + return insertResp.getInsertCnt(); | |
| 250 | + } | |
| 251 | + | |
| 252 | + /*** | |
| 253 | + * @Author 钱豹 | |
| 254 | + * @Date 13:29 2026/3/25 | |
| 255 | + * @Param [sVectorfiled] | |
| 256 | + * @return java.util.Map<java.lang.String,java.lang.Object> | |
| 257 | + * @Description 返回组装动态内容 | |
| 258 | + **/ | |
| 259 | + @Override | |
| 260 | + public Map<String,Object> getMilvusFiled(String sVectorfiled){ | |
| 261 | + String[] sVectorfiledArray = sVectorfiled.split(","); | |
| 262 | + List<String> sFileds = new ArrayList<>(); | |
| 263 | + List<String> sFiledDescriptions = new ArrayList<>(); | |
| 264 | + List<Map<String,String>> titleList = new LinkedList<>(); | |
| 265 | + for(String sVectorfiledOne : sVectorfiledArray){ | |
| 266 | + Map<String,String> title = new HashMap<>(); | |
| 267 | + | |
| 268 | + String[] sVectorfiledOneArray = sVectorfiledOne.split(":"); | |
| 269 | + String sDescriptions = sVectorfiledOneArray[0]; | |
| 270 | + String sName = sVectorfiledOneArray[1]; | |
| 271 | + sFileds.add(sName); | |
| 272 | + // 处理描述中可能包含的换行,保持缩进一致 | |
| 273 | +// String formattedDesc = sDescriptions.replace("\n", "\n "); | |
| 274 | +// sFiledDescriptions.add(String.format(" - %s: %s", sName, formattedDesc)); | |
| 275 | + String formattedDesc =String.format("%s: %s", sName, sDescriptions); | |
| 276 | + sFiledDescriptions.add(formattedDesc); | |
| 277 | + title.put("sName",sName); | |
| 278 | + title.put("sTitle",sDescriptions); | |
| 279 | + titleList.add(title); | |
| 280 | + } | |
| 281 | + Map<String,Object> rMap = new HashMap<>(); | |
| 282 | + rMap.put("sMilvusFiled", String.join(",", sFileds)); | |
| 283 | + rMap.put("sMilvusFiledDescription", String.join(",", sFiledDescriptions)); | |
| 284 | + rMap.put("sFileds", sFileds); | |
| 285 | + rMap.put("title", titleList); | |
| 286 | + return rMap; | |
| 287 | + } | |
| 288 | + | |
| 289 | + @Override | |
| 290 | + public List<Map<String, Object>> getDataToCollection(String collectionName, String milvusFilter,String searchText,Integer size,List<String> fields){ | |
| 291 | + log.info("开始相似度查询: collection={}, searchText={}", collectionName, searchText); | |
| 292 | + // 2. 设置范围搜索参数 | |
| 293 | + Map<String, Object> searchParams = new HashMap<>(); | |
| 294 | + searchParams.put("nprobe", 10); | |
| 295 | + // 对于 IP 度量,相似度范围在 [minScore, maxScore] | |
| 296 | + searchParams.put("radius", 0.9); // 最小相似度 | |
| 297 | + searchParams.put("range_filter", 1); // 最大相似度 | |
| 298 | + // 1. 确保集合已加载 | |
| 299 | +// ensureCollectionLoaded(collectionName); | |
| 300 | + // 1. 向量化搜索文本 | |
| 301 | + List<Float> vectorList = vectorizationService.textToVector(searchText); | |
| 302 | + if (vectorList == null || vectorList.isEmpty()) { | |
| 303 | + throw new RuntimeException("向量化失败"); | |
| 304 | + } | |
| 305 | + // 2. 转换为 float[] | |
| 306 | + float[] floatArray = new float[vectorList.size()]; | |
| 307 | + for (int i = 0; i < vectorList.size(); i++) { | |
| 308 | + floatArray[i] = vectorList.get(i); | |
| 309 | + } | |
| 310 | + | |
| 311 | + // 3. 创建 Milvus FloatVec 对象 | |
| 312 | + FloatVec floatVec = new FloatVec(floatArray); | |
| 313 | + log.info("查询向量库条件{}",milvusFilter); | |
| 314 | + milvusFilter = isValidMilvusFilter(milvusFilter)?milvusFilter : null; | |
| 315 | + log.info("实际查询向量库条件{}",milvusFilter); | |
| 316 | + // 4. 构建搜索请求 | |
| 317 | + SearchReq searchReq = SearchReq.builder() | |
| 318 | + .collectionName(collectionName) | |
| 319 | + .data(Collections.singletonList(floatVec)) | |
| 320 | + .annsField("vector") // 向量字段名 | |
| 321 | + .topK(size) // 返回最相似的10条 | |
| 322 | + .metricType(IndexParam.MetricType.IP) // 内积相似度 | |
| 323 | + .outputFields(fields) | |
| 324 | +// .searchParams(searchParams) | |
| 325 | + .filter(milvusFilter) | |
| 326 | + .build(); | |
| 327 | + // 5. 执行搜索 | |
| 328 | + SearchResp searchResp = milvusClient.search(searchReq); | |
| 329 | + | |
| 330 | + // 6. 处理结果 | |
| 331 | + return processMilvusResults(searchResp); | |
| 332 | + } | |
| 333 | + | |
| 334 | + | |
| 335 | + /** | |
| 336 | + * 判断 Milvus 过滤条件是否有效 | |
| 337 | + * @param milvusFilter 过滤条件字符串 | |
| 338 | + * @return true: 有效条件, false: 无效条件 | |
| 339 | + */ | |
| 340 | + public boolean isValidMilvusFilter(String milvusFilter) { | |
| 341 | + // 1. 空值判断 | |
| 342 | + if (milvusFilter == null || milvusFilter.trim().isEmpty()) { | |
| 343 | + return false; | |
| 344 | + } | |
| 345 | + | |
| 346 | + String filter = milvusFilter.trim(); | |
| 347 | + | |
| 348 | + // 2. 基本格式检查:不能是纯布尔值 | |
| 349 | + if ("true".equalsIgnoreCase(filter) || "false".equalsIgnoreCase(filter)) { | |
| 350 | + return false; | |
| 351 | + } | |
| 352 | + | |
| 353 | + // 3. 检查是否包含有效的操作符 | |
| 354 | + boolean hasValidOperator = filter.matches(".*[=!<>]=?.*") || filter.contains(" like "); | |
| 355 | + if (!hasValidOperator) { | |
| 356 | + return false; | |
| 357 | + } | |
| 358 | + | |
| 359 | + // 4. 检查字符串值是否使用单引号包裹 | |
| 360 | + // 匹配模式:字段名 操作符 '值' | |
| 361 | + Pattern pattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*\\s*(==|!=|>=|<=|>|<|like)\\s*('[^']*'|\\d+)"); | |
| 362 | + Matcher matcher = pattern.matcher(filter); | |
| 363 | + | |
| 364 | + // 5. 对于复合条件,递归检查 | |
| 365 | + if (filter.contains("&&") || filter.contains("||")) { | |
| 366 | + // 分割复合条件(简单处理,生产环境需要更完善的解析) | |
| 367 | + String[] conditions = filter.split("&&|\\|\\|"); | |
| 368 | + for (String condition : conditions) { | |
| 369 | + condition = condition.trim().replaceAll("^[()]+|[()]+$", ""); // 去除括号 | |
| 370 | + if (!isValidSimpleCondition(condition)) { | |
| 371 | + return false; | |
| 372 | + } | |
| 373 | + } | |
| 374 | + return true; | |
| 375 | + } | |
| 376 | + | |
| 377 | + // 6. 检查简单条件 | |
| 378 | + return isValidSimpleCondition(filter); | |
| 379 | + } | |
| 380 | + | |
| 381 | + /** | |
| 382 | + * 验证简单条件(不包含 && 和 ||) | |
| 383 | + */ | |
| 384 | + private boolean isValidSimpleCondition(String condition) { | |
| 385 | + if (condition == null || condition.trim().isEmpty()) { | |
| 386 | + return false; | |
| 387 | + } | |
| 388 | + condition = condition.trim(); | |
| 389 | + // 匹配简单条件的正则 | |
| 390 | + // 格式:字段名 操作符 值 | |
| 391 | + // 字段名:字母开头,包含字母数字下划线 | |
| 392 | + // 操作符:==, !=, >=, <=, >, <, like | |
| 393 | + // 值:单引号字符串 或 数字 | |
| 394 | + String regex = "^\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*" + // 字段名 | |
| 395 | + "(==|!=|>=|<=|>|<|like)\\s*" + // 操作符 | |
| 396 | + "('([^'\\\\]|\\\\.)*'|\\d+(\\.\\d+)?)\\s*$"; // 值 | |
| 397 | + | |
| 398 | + if (!condition.matches(regex)) { | |
| 399 | + return false; | |
| 400 | + } | |
| 401 | + // 额外检查:like 操作符的值必须包含 % | |
| 402 | + if (condition.contains(" like ")) { | |
| 403 | + String value = condition.split("like")[1].trim(); | |
| 404 | + if (!value.contains("%")) { | |
| 405 | + return false; // like 必须使用 % 通配符 | |
| 406 | + } | |
| 407 | + } | |
| 408 | + return true; | |
| 409 | + } | |
| 410 | + | |
| 411 | + | |
| 412 | + /** | |
| 413 | + * 处理 Milvus 查询结果(完整版) | |
| 414 | + */ | |
| 415 | + /** | |
| 416 | + * 处理 Milvus 查询结果(完整版) | |
| 417 | + */ | |
| 418 | + private List<Map<String, Object>> processMilvusResults(SearchResp response) { | |
| 419 | + List<Map<String, Object>> results = new ArrayList<>(); | |
| 420 | + if (response == null) { | |
| 421 | + log.warn("Milvus 响应为空"); | |
| 422 | + return results; | |
| 423 | + } | |
| 424 | + List<List<SearchResp.SearchResult>> searchResults = response.getSearchResults(); | |
| 425 | + if (searchResults == null || searchResults.isEmpty()) { | |
| 426 | + log.warn("Milvus 搜索结果为空"); | |
| 427 | + return results; | |
| 428 | + } | |
| 429 | + // 遍历每个查询的结果集(通常只有一个查询) | |
| 430 | + for (List<SearchResp.SearchResult> resultList : searchResults) { | |
| 431 | + // 遍历每个搜索结果 | |
| 432 | + for (SearchResp.SearchResult result : resultList) { | |
| 433 | + Map<String, Object> item = new HashMap<>(); | |
| 434 | + // 获取相似度分数 | |
| 435 | + Float score = result.getScore(); | |
| 436 | + if (score != null) { | |
| 437 | + item.put("score", score); | |
| 438 | + } | |
| 439 | + // 获取实体字段数据 | |
| 440 | + Map<String, Object> entity = result.getEntity(); | |
| 441 | + // 将所有字段添加到结果中 | |
| 442 | + item.putAll(entity); | |
| 443 | + results.add(item); | |
| 444 | + } | |
| 445 | + } | |
| 446 | + log.info("处理完成,共 {} 条搜索结果", results.size()); | |
| 447 | + return results; | |
| 448 | + } | |
| 449 | + /** | |
| 450 | + * 从实体对象构建Milvus插入数据 | |
| 451 | + */ | |
| 452 | + public List<JsonObject> convertToMilvusRow(List<Map<String,Object>> data, String sVectorfiled,String sVectorjson) { | |
| 453 | + List<JsonObject> rows = new ArrayList<>(); | |
| 454 | + if (CollUtil.isEmpty(data)) { | |
| 455 | + return rows; | |
| 456 | + } | |
| 457 | + // 批量遍历,逐个转换 | |
| 458 | + for (Map<String, Object> map : data) { | |
| 459 | + JsonObject jsonObject = convertToMilvusRowOne(map, sVectorfiled, sVectorjson); | |
| 460 | + rows.add(jsonObject); | |
| 461 | + } | |
| 462 | + return rows; | |
| 463 | + } | |
| 464 | + | |
| 465 | + /*** | |
| 466 | + * @Author 钱豹 | |
| 467 | + * @Date 21:31 2026/3/24 | |
| 468 | + * @Param [data, sVectorfiled, sVectorjson] | |
| 469 | + * @return com.google.gson.JsonObject | |
| 470 | + * @Description 单个转换 | |
| 471 | + **/ | |
| 472 | + public JsonObject convertToMilvusRowOne(Map<String,Object> data, String sVectorfiled,String sVectorjson) { | |
| 473 | + | |
| 474 | + // ====================== 修复 1:使用真实的向量化文本 ====================== | |
| 475 | + // 从 sVectorjson 或 data 中获取要向量化的字段值 | |
| 476 | + StringBuffer vectorText = new StringBuffer(); | |
| 477 | + getVectorText(data, vectorText, sVectorjson); | |
| 478 | + // 向量化 | |
| 479 | + List<Float> vector = vectorizationService.textToVector(vectorText.toString()); | |
| 480 | + if (vector == null || vector.isEmpty()) { | |
| 481 | + throw new RuntimeException("向量化失败,文本内容:" + vectorText); | |
| 482 | + } | |
| 483 | + JsonObject row = new JsonObject(); | |
| 484 | + // 添加向量 | |
| 485 | + JsonArray vectorArray = new JsonArray(); | |
| 486 | + vector.forEach(vectorArray::add); | |
| 487 | + row.add("vector", vectorArray); | |
| 488 | + | |
| 489 | + // ====================== 修复 2:sSlaveId 空值安全 ====================== | |
| 490 | + Object slaveIdObj = data.get("sSlaveId"); | |
| 491 | + if (slaveIdObj == null) { | |
| 492 | + throw new RuntimeException("数据中缺少 sSlaveId 字段,无法插入Milvus"); | |
| 493 | + } | |
| 494 | + row.addProperty("sSlaveId", slaveIdObj.toString()); | |
| 495 | + | |
| 496 | + // 创建时间 | |
| 497 | + row.addProperty("create_time", System.currentTimeMillis()); | |
| 498 | + | |
| 499 | + // 业务元数据 | |
| 500 | + JsonObject metadata = MapToJsonConverter.convert(data); | |
| 501 | + row.add("metadata", metadata); | |
| 502 | + | |
| 503 | + // 动态字段 | |
| 504 | + String[] sVectorfiledArray = sVectorfiled.split(","); | |
| 505 | + for (String vectorFieldOne : sVectorfiledArray) { | |
| 506 | + String[] fieldArr = vectorFieldOne.split(":"); | |
| 507 | + if (fieldArr.length < 2) { | |
| 508 | + continue; | |
| 509 | + } | |
| 510 | + String fieldName = fieldArr[1]; | |
| 511 | + Object value = ObjectUtil.isEmpty(data.get(fieldName)) ? getDefaultData(fieldName) : data.get(fieldName); | |
| 512 | + // 通用类型安全添加(你之前的优化方法) | |
| 513 | + addToJsonObject(row, fieldName, value); | |
| 514 | + } | |
| 515 | + | |
| 516 | + return row; | |
| 517 | + } | |
| 518 | + | |
| 519 | + private void getVectorText(Map<String,Object> data, StringBuffer vectorText,String sVectorjson){ | |
| 520 | + // 动态字段 | |
| 521 | + String[] sVectorjsonArray = sVectorjson.split(";"); | |
| 522 | + for (String sVectorjsonOne : sVectorjsonArray) { | |
| 523 | + String sText; | |
| 524 | + String[] fieldArr = sVectorjsonOne.split(":"); | |
| 525 | + if (fieldArr.length < 2) { | |
| 526 | + continue; | |
| 527 | + } | |
| 528 | + String fieldName = fieldArr[1]; | |
| 529 | + Object value = ObjectUtil.isEmpty(data.get(fieldName)) ? getDefaultData(fieldName) : data.get(fieldName); | |
| 530 | + if (ObjectUtil.isEmpty(value)) { | |
| 531 | + sText = StrUtil.EMPTY; | |
| 532 | + }else{ | |
| 533 | + sText = value.toString(); | |
| 534 | + } | |
| 535 | + vectorText.append(" ").append(fieldArr[0]).append(sText); | |
| 536 | + } | |
| 537 | + } | |
| 538 | + | |
| 539 | + | |
| 540 | + /*******************************************************内部方法********************************************************************************/ | |
| 541 | + | |
| 542 | + /** | |
| 543 | + * 安全将 Object 加入 Gson JsonObject,自动识别类型 | |
| 544 | + */ | |
| 545 | + private void addToJsonObject(JsonObject row, String fieldName, Object value) { | |
| 546 | + if (value == null) { | |
| 547 | + row.addProperty(fieldName, ""); | |
| 548 | + return; | |
| 549 | + } | |
| 550 | + // 基本类型直接添加 | |
| 551 | + if (value instanceof String) { | |
| 552 | + row.addProperty(fieldName, (String) value); | |
| 553 | + } else if (value instanceof Number) { | |
| 554 | + row.addProperty(fieldName, (Number) value); | |
| 555 | + } else if (value instanceof Boolean) { | |
| 556 | + row.addProperty(fieldName, (Boolean) value); | |
| 557 | + } | |
| 558 | + // List / 数组类型 | |
| 559 | + else if (value instanceof List<?>) { | |
| 560 | + JsonArray jsonArray = new JsonArray(); | |
| 561 | + for (Object item : (List<?>) value) { | |
| 562 | + addJsonElement(jsonArray, item); | |
| 563 | + } | |
| 564 | + row.add(fieldName, jsonArray); | |
| 565 | + } | |
| 566 | + // 其他对象转字符串 | |
| 567 | + else { | |
| 568 | + row.addProperty(fieldName, value.toString()); | |
| 569 | + } | |
| 570 | + } | |
| 571 | + /** | |
| 572 | + * 递归处理 List 元素(支持无限层嵌套 List) | |
| 573 | + */ | |
| 574 | + private void addJsonElement(JsonArray jsonArray, Object item) { | |
| 575 | + if (item == null) { | |
| 576 | + jsonArray.add((String) null); | |
| 577 | + return; | |
| 578 | + } | |
| 579 | + if (item instanceof String) { | |
| 580 | + jsonArray.add((String) item); | |
| 581 | + } else if (item instanceof Number) { | |
| 582 | + jsonArray.add((Number) item); | |
| 583 | + } else if (item instanceof Boolean) { | |
| 584 | + jsonArray.add((Boolean) item); | |
| 585 | + } else if (item instanceof List<?>) { | |
| 586 | + // 递归处理嵌套列表 | |
| 587 | + JsonArray nestedArray = new JsonArray(); | |
| 588 | + for (Object nestedItem : (List<?>) item) { | |
| 589 | + addJsonElement(nestedArray, nestedItem); | |
| 590 | + } | |
| 591 | + jsonArray.add(nestedArray); | |
| 592 | + } else { | |
| 593 | + jsonArray.add(item.toString()); | |
| 594 | + } | |
| 595 | + } | |
| 596 | + | |
| 597 | + /** | |
| 598 | + * 创建集合(定义字段结构) | |
| 599 | + */ | |
| 600 | + private void createCollection(String collectionName,String sVectorfiled,Boolean bRset) { | |
| 601 | + // 6. 重新加载集合 | |
| 602 | + LoadCollectionReq loadReq = LoadCollectionReq.builder() | |
| 603 | + .collectionName(collectionName) | |
| 604 | + .build(); | |
| 605 | + | |
| 606 | + //是否删除集合 重新创建 | |
| 607 | + if (bRset){ | |
| 608 | + // 1. 删除旧集合 | |
| 609 | + milvusClient.dropCollection(DropCollectionReq.builder() | |
| 610 | + .collectionName(collectionName) | |
| 611 | + .build()); | |
| 612 | + //删除对应的记录表 | |
| 613 | + delAiMilvusVectorRecord(collectionName); | |
| 614 | + } | |
| 615 | + // 检查集合是否存在 | |
| 616 | + HasCollectionReq hasCollectionReq = HasCollectionReq.builder() | |
| 617 | + .collectionName(collectionName) | |
| 618 | + .build(); | |
| 619 | + boolean exists = milvusClient.hasCollection(hasCollectionReq); | |
| 620 | + if (exists) { | |
| 621 | + return; | |
| 622 | + } | |
| 623 | + // 定义字段列表 | |
| 624 | + List<CreateCollectionReq.FieldSchema> fieldSchemas = new ArrayList<>(); | |
| 625 | + // 1. 准备所有索引参数 | |
| 626 | + List<IndexParam> allIndexParams = new ArrayList<>(); | |
| 627 | + //定义字段列表 | |
| 628 | + // 1. 主键字段 | |
| 629 | + fieldSchemas.add( CreateCollectionReq.FieldSchema.builder() | |
| 630 | + .name("id") | |
| 631 | + .dataType(DataType.Int64) | |
| 632 | + .isPrimaryKey(true) | |
| 633 | + .autoID(true) // 使用自动ID | |
| 634 | + .description("主键ID") | |
| 635 | + .build()); | |
| 636 | + // 3. 主键字段 | |
| 637 | + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder() | |
| 638 | + .name("sSlaveId") | |
| 639 | + .dataType(DataType.VarChar) | |
| 640 | +// .isPrimaryKey(true) //索引创建 | |
| 641 | + .maxLength(100) | |
| 642 | + .description("原始数据主键ID") | |
| 643 | + .build()); | |
| 644 | + // 2. 向量字段 | |
| 645 | + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder() | |
| 646 | + .name("vector") | |
| 647 | + .dataType(DataType.FloatVector) | |
| 648 | + .dimension(VECTOR_DIM) | |
| 649 | + .description("向量字段,用于相似性搜索") | |
| 650 | + .build()); | |
| 651 | + | |
| 652 | + // 4. 创建时间字段 | |
| 653 | + fieldSchemas.add( CreateCollectionReq.FieldSchema.builder() | |
| 654 | + .name("create_time") | |
| 655 | + .dataType(DataType.Int64) | |
| 656 | + .description("创建时间戳") | |
| 657 | + .build()); | |
| 658 | + // 5. 元数据字段(使用JSON类型存储额外数据) | |
| 659 | + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder() | |
| 660 | + .name("metadata") | |
| 661 | + .dataType(DataType.JSON) | |
| 662 | + .description("额外元数据") | |
| 663 | + .build()); | |
| 664 | + //动态字段创建 | |
| 665 | + String[] sVectorfiledArray = sVectorfiled.split(","); | |
| 666 | + for(String sVectorfiledOne : sVectorfiledArray){ | |
| 667 | + String[] sVectorfiledOneArray = sVectorfiledOne.split(":"); | |
| 668 | + String sName = sVectorfiledOneArray[1]; | |
| 669 | + String sDescription = sVectorfiledOneArray[0]; | |
| 670 | + DataType dataType = processField(sVectorfiledOneArray[1]); | |
| 671 | + fieldSchemas.add(CreateCollectionReq.FieldSchema.builder() | |
| 672 | + .name(sName) | |
| 673 | + .dataType(dataType) | |
| 674 | + .description(sDescription) | |
| 675 | + .isPrimaryKey(false) // 如果不是主键 | |
| 676 | + .isNullable(true) // 允许为空 | |
| 677 | +// .defaultValue("") // 如果有默认值 | |
| 678 | + // SQL可能较长 | |
| 679 | + .maxLength(1000) | |
| 680 | + .build()); | |
| 681 | + } | |
| 682 | + //创建索引 | |
| 683 | + createAllIndexes(sVectorfiled,allIndexParams); | |
| 684 | + | |
| 685 | + // 创建集合schema | |
| 686 | + CreateCollectionReq.CollectionSchema schema = | |
| 687 | + CreateCollectionReq.CollectionSchema.builder() | |
| 688 | + .fieldSchemaList(fieldSchemas) | |
| 689 | + .enableDynamicField(true) | |
| 690 | + .build(); | |
| 691 | + // 创建集合请求 | |
| 692 | + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder() | |
| 693 | + .collectionName(collectionName) | |
| 694 | + .collectionSchema(schema) | |
| 695 | + .indexParams(allIndexParams)//索引集合 | |
| 696 | + .consistencyLevel(ConsistencyLevel.BOUNDED) | |
| 697 | + .build(); | |
| 698 | + | |
| 699 | + // 执行创建集合 | |
| 700 | + milvusClient.createCollection(createCollectionReq); | |
| 701 | + milvusClient.loadCollection(loadReq); | |
| 702 | + log.info("集合重新加载成功"); | |
| 703 | + } | |
| 704 | + /** | |
| 705 | + * 批量创建所有索引(向量索引 + 多个标量索引) | |
| 706 | + */ | |
| 707 | + private void createAllIndexes(String sVectorfiled,List<IndexParam> allIndexParams) { | |
| 708 | + | |
| 709 | + // 1.1 向量索引 | |
| 710 | + Map<String, Object> vectorExtraParams = new HashMap<>(8); | |
| 711 | + vectorExtraParams.put("nlist", 256); // 聚类中心数:sqrt(384) * 13 ≈ 256 | |
| 712 | + vectorExtraParams.put("nprobe", 32); // 搜索时检查的聚类数 | |
| 713 | + | |
| 714 | + IndexParam vectorIndex = IndexParam.builder() | |
| 715 | + .fieldName("vector") | |
| 716 | + .indexName("idx_vector_rebuild") | |
| 717 | + .indexType(IndexParam.IndexType.IVF_FLAT) | |
| 718 | + .metricType(IndexParam.MetricType.IP) | |
| 719 | + .extraParams(vectorExtraParams) | |
| 720 | + .build(); | |
| 721 | + allIndexParams.add(vectorIndex); | |
| 722 | + | |
| 723 | + // 1.2 create_time 字段索引(用于时间范围查询) | |
| 724 | + IndexParam timeIndex = IndexParam.builder() | |
| 725 | + .fieldName("create_time") | |
| 726 | + .indexName("idx_create_time") | |
| 727 | + .indexType(IndexParam.IndexType.STL_SORT) // 排序索引 | |
| 728 | + .build(); | |
| 729 | + allIndexParams.add(timeIndex); | |
| 730 | + | |
| 731 | + // 1.4 data_id 字段索引(用于精确匹配) | |
| 732 | + IndexParam idIndex = IndexParam.builder() | |
| 733 | + .fieldName("sSlaveId") | |
| 734 | + .indexName("idx_data_id") | |
| 735 | + .indexType(IndexParam.IndexType.TRIE) | |
| 736 | + .build(); | |
| 737 | + allIndexParams.add(idIndex); | |
| 738 | + //动态字段创建 | |
| 739 | + String[] sVectorfiledArray = sVectorfiled.split(","); | |
| 740 | + for(String sVectorfiledOne : sVectorfiledArray){ | |
| 741 | + String[] sVectorfiledOneArray = sVectorfiledOne.split(":"); | |
| 742 | + String sName = sVectorfiledOneArray[1]; | |
| 743 | + IndexParam.IndexType indexType =indexField(sVectorfiledOneArray[1]); | |
| 744 | + allIndexParams.add(IndexParam.builder() | |
| 745 | + .fieldName(sName) | |
| 746 | + .indexName(sName) | |
| 747 | + .indexType(indexType) | |
| 748 | + .build()); | |
| 749 | + } | |
| 750 | + | |
| 751 | + } | |
| 752 | + | |
| 753 | + /*** | |
| 754 | + * @Author 钱豹 | |
| 755 | + * @Date 21:10 2026/3/24 | |
| 756 | + * @Param | |
| 757 | + * @return | |
| 758 | + * @Description //TODO | |
| 759 | + **/ | |
| 760 | + public Object getDefaultData(String sKey) { | |
| 761 | + if(sKey.startsWith("d") || sKey.startsWith("i")){ | |
| 762 | + return BigDecimal.ZERO; | |
| 763 | + }else if(sKey.startsWith("b")){ | |
| 764 | + return false; | |
| 765 | + }else{ | |
| 766 | + return StrUtil.EMPTY; | |
| 767 | + } | |
| 768 | + } | |
| 769 | + /*** | |
| 770 | + * @Author 钱豹 | |
| 771 | + * @Date 20:44 2026/3/24 | |
| 772 | + * @Param [sKey] | |
| 773 | + * @return io.milvus.v2.common.DataType | |
| 774 | + * @Description 字段类型 | |
| 775 | + **/ | |
| 776 | + public DataType processField(String sKey) { | |
| 777 | + if(sKey.startsWith("d")){ | |
| 778 | + return DataType.Double; | |
| 779 | + }else if(sKey.startsWith("i")){ | |
| 780 | + return DataType.Int64; | |
| 781 | + }else if(sKey.startsWith("b")){ | |
| 782 | + return DataType.Bool; | |
| 783 | + }else{ | |
| 784 | + return DataType.VarChar; | |
| 785 | + } | |
| 786 | + } | |
| 787 | + | |
| 788 | + /*** | |
| 789 | + * @Author 钱豹 | |
| 790 | + * @Date 20:44 2026/3/24 | |
| 791 | + * @Param [sKey] | |
| 792 | + * @return io.milvus.v2.common.DataType | |
| 793 | + * @Description 索引类型 | |
| 794 | + **/ | |
| 795 | + public IndexParam.IndexType indexField(String sKey) { | |
| 796 | + return IndexParam.IndexType.TRIE; | |
| 797 | + } | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | +} | |
| 0 | 804 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/service/impl/VectorizationServiceImpl.java
0 → 100644
| 1 | +package com.xly.milvus.service.impl; | |
| 2 | + | |
| 3 | +import com.xly.milvus.service.VectorizationService; | |
| 4 | +import dev.langchain4j.model.embedding.EmbeddingModel; | |
| 5 | +import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.stereotype.Service; | |
| 8 | + | |
| 9 | +import java.util.ArrayList; | |
| 10 | +import java.util.List; | |
| 11 | +import java.util.stream.Collectors; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * 向量化服务实现 - 使用LangChain4j的All-MiniLM-L6-v2模型 | |
| 15 | + */ | |
| 16 | +@Slf4j | |
| 17 | +@Service | |
| 18 | +public class VectorizationServiceImpl implements VectorizationService { | |
| 19 | + | |
| 20 | + private final EmbeddingModel embeddingModel; | |
| 21 | + | |
| 22 | + public VectorizationServiceImpl() { | |
| 23 | + // 初始化嵌入模型 | |
| 24 | + this.embeddingModel = new AllMiniLmL6V2EmbeddingModel(); | |
| 25 | + log.info("向量化模型初始化成功"); | |
| 26 | + } | |
| 27 | + | |
| 28 | + @Override | |
| 29 | + public List<Float> textToVector(String text) { | |
| 30 | + if (text == null || text.trim().isEmpty()) { | |
| 31 | + return new ArrayList<>(); | |
| 32 | + } | |
| 33 | + | |
| 34 | + try { | |
| 35 | + // 使用LangChain4j生成向量 | |
| 36 | + dev.langchain4j.data.embedding.Embedding embedding = embeddingModel.embed(text).content(); | |
| 37 | + float[] vectorArray = embedding.vector(); | |
| 38 | + | |
| 39 | + // 转换为List<Float> | |
| 40 | + List<Float> vector = new ArrayList<>(); | |
| 41 | + for (float v : vectorArray) { | |
| 42 | + vector.add(v); | |
| 43 | + } | |
| 44 | + | |
| 45 | + return vector; | |
| 46 | + } catch (Exception e) { | |
| 47 | + log.error("文本向量化失败: {}", e.getMessage(), e); | |
| 48 | + throw new RuntimeException("文本向量化失败", e); | |
| 49 | + } | |
| 50 | + } | |
| 51 | + | |
| 52 | + @Override | |
| 53 | + public List<List<Float>> batchTextToVector(List<String> texts) { | |
| 54 | + if (texts == null || texts.isEmpty()) { | |
| 55 | + return new ArrayList<>(); | |
| 56 | + } | |
| 57 | + | |
| 58 | + return texts.stream() | |
| 59 | + .map(this::textToVector) | |
| 60 | + .collect(Collectors.toList()); | |
| 61 | + } | |
| 62 | +} | |
| 0 | 63 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/test/MilvusCompleteTest.java
0 → 100644
| 1 | +package com.xly.milvus.test; | |
| 2 | + | |
| 3 | +import com.google.gson.JsonArray; | |
| 4 | +import com.google.gson.JsonObject; | |
| 5 | +import io.milvus.v2.client.ConnectConfig; | |
| 6 | +import io.milvus.v2.client.MilvusClientV2; | |
| 7 | +import io.milvus.v2.common.DataType; | |
| 8 | +import io.milvus.v2.common.IndexParam; | |
| 9 | +import io.milvus.v2.common.ConsistencyLevel; | |
| 10 | +import io.milvus.v2.service.collection.request.*; | |
| 11 | +import io.milvus.v2.service.collection.response.GetCollectionStatsResp; | |
| 12 | +import io.milvus.v2.service.index.request.CreateIndexReq; | |
| 13 | +import io.milvus.v2.service.vector.request.InsertReq; | |
| 14 | +import io.milvus.v2.service.vector.request.SearchReq; | |
| 15 | +import io.milvus.v2.service.vector.request.QueryReq; | |
| 16 | +import io.milvus.v2.service.vector.request.data.FloatVec; | |
| 17 | +import io.milvus.v2.service.vector.response.InsertResp; | |
| 18 | +import io.milvus.v2.service.vector.response.SearchResp; | |
| 19 | +import io.milvus.v2.service.vector.response.QueryResp; | |
| 20 | + | |
| 21 | +import java.util.*; | |
| 22 | +import java.util.concurrent.TimeUnit; | |
| 23 | + | |
| 24 | +/** | |
| 25 | + * Milvus Java SDK 2.6.15 完整测试代码 | |
| 26 | + * | |
| 27 | + * 功能: | |
| 28 | + * 1. 连接Milvus服务器 | |
| 29 | + * 2. 创建集合(包含多种字段类型) | |
| 30 | + * 3. 创建向量索引 | |
| 31 | + * 4. 插入测试数据 | |
| 32 | + * 5. 查询数据(标量过滤) | |
| 33 | + * 6. 向量相似性搜索 | |
| 34 | + * 7. 获取集合统计信息 | |
| 35 | + * 8. 清理资源 | |
| 36 | + * | |
| 37 | + * @author xly | |
| 38 | + * @version 1.0 | |
| 39 | + */ | |
| 40 | +public class MilvusCompleteTest { | |
| 41 | + | |
| 42 | + // ==================== 配置参数 ==================== | |
| 43 | + private static final String MILVUS_HOST = "121.43.128.225"; // 您的Milvus服务器IP | |
| 44 | + private static final int MILVUS_PORT = 19530; // Milvus服务端口 | |
| 45 | + private static final String COLLECTION_NAME = "complete_test"; // 集合名称 | |
| 46 | + private static final int VECTOR_DIM = 128; // 向量维度 | |
| 47 | + private static final int INSERT_COUNT = 15; // 插入数据条数 | |
| 48 | + | |
| 49 | + private MilvusClientV2 client; | |
| 50 | + | |
| 51 | + public static void main(String[] args) { | |
| 52 | + MilvusCompleteTest test = new MilvusCompleteTest(); | |
| 53 | + try { | |
| 54 | + // 执行测试流程 | |
| 55 | + test.run(); | |
| 56 | + } catch (Exception e) { | |
| 57 | + System.err.println("❌ 测试过程中发生错误: " + e.getMessage()); | |
| 58 | + e.printStackTrace(); | |
| 59 | + } | |
| 60 | + } | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 执行完整的测试流程 | |
| 64 | + */ | |
| 65 | + private void run() { | |
| 66 | + printHeader("Milvus Java SDK 2.6.15 完整测试"); | |
| 67 | + | |
| 68 | + // 1. 连接Milvus | |
| 69 | + connect(); | |
| 70 | + | |
| 71 | + try { | |
| 72 | + // 2. 清理旧集合 | |
| 73 | + cleanup(); | |
| 74 | + | |
| 75 | + // 3. 创建集合 | |
| 76 | + createCollection(); | |
| 77 | + | |
| 78 | + // 4. 创建索引 | |
| 79 | + createIndex(); | |
| 80 | + | |
| 81 | + // 5. 插入数据 | |
| 82 | + insertData(); | |
| 83 | + | |
| 84 | + // 6. 查询数据 | |
| 85 | + queryData(); | |
| 86 | + | |
| 87 | + // 7. 向量搜索 | |
| 88 | + searchData(); | |
| 89 | + | |
| 90 | + // 8. 获取集合统计 | |
| 91 | + getCollectionStats(); | |
| 92 | + | |
| 93 | + printSuccess("所有测试完成!"); | |
| 94 | + | |
| 95 | + } catch (Exception e) { | |
| 96 | + System.err.println("❌ 测试失败: " + e.getMessage()); | |
| 97 | + throw e; | |
| 98 | + } finally { | |
| 99 | + // 9. 清理资源 | |
| 100 | + releaseAndClose(); | |
| 101 | + } | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** | |
| 105 | + * 连接Milvus服务器 | |
| 106 | + */ | |
| 107 | + /** | |
| 108 | + * 连接Milvus服务器 | |
| 109 | + */ | |
| 110 | + private void connect() { | |
| 111 | + printSection("连接Milvus服务器"); | |
| 112 | + | |
| 113 | + try { | |
| 114 | + ConnectConfig connectConfig = ConnectConfig.builder() | |
| 115 | + .uri("http://" + MILVUS_HOST + ":" + MILVUS_PORT) | |
| 116 | + .connectTimeoutMs(TimeUnit.SECONDS.toMillis(30)) // 连接超时30秒 | |
| 117 | + .keepAliveTimeMs(TimeUnit.MINUTES.toMillis(5)) // 保持连接5分钟 | |
| 118 | + .keepAliveTimeoutMs(TimeUnit.SECONDS.toMillis(5)) // keep-alive超时5秒 | |
| 119 | + .keepAliveWithoutCalls(true) // 无调用时保持连接 | |
| 120 | + .rpcDeadlineMs(TimeUnit.SECONDS.toMillis(10)) // RPC超时10秒 | |
| 121 | + .enablePrecheck(true) // 启用预检查 | |
| 122 | + .build(); | |
| 123 | + | |
| 124 | + client = new MilvusClientV2(connectConfig); | |
| 125 | + | |
| 126 | + // 验证连接 | |
| 127 | + String serverVersion = client.getServerVersion(); | |
| 128 | + System.out.println("✅ 连接成功!"); | |
| 129 | + System.out.println(" - 服务器地址: " + MILVUS_HOST + ":" + MILVUS_PORT); | |
| 130 | + System.out.println(" - 服务器版本: " + serverVersion); | |
| 131 | + | |
| 132 | + } catch (Exception e) { | |
| 133 | + System.err.println("❌ 连接失败: " + e.getMessage()); | |
| 134 | + throw new RuntimeException("无法连接到Milvus服务器", e); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * 清理已存在的集合 | |
| 140 | + */ | |
| 141 | + private void cleanup() { | |
| 142 | + printSection("清理环境"); | |
| 143 | + | |
| 144 | + HasCollectionReq hasCollectionReq = HasCollectionReq.builder() | |
| 145 | + .collectionName(COLLECTION_NAME) | |
| 146 | + .build(); | |
| 147 | + | |
| 148 | + boolean exists = client.hasCollection(hasCollectionReq); | |
| 149 | + if (exists) { | |
| 150 | + DropCollectionReq dropCollectionReq = DropCollectionReq.builder() | |
| 151 | + .collectionName(COLLECTION_NAME) | |
| 152 | + .build(); | |
| 153 | + client.dropCollection(dropCollectionReq); | |
| 154 | + System.out.println("✅ 已删除旧集合: " + COLLECTION_NAME); | |
| 155 | + } else { | |
| 156 | + System.out.println("⏭️ 无需清理,集合不存在"); | |
| 157 | + } | |
| 158 | + } | |
| 159 | + | |
| 160 | + /** | |
| 161 | + * 创建集合 | |
| 162 | + */ | |
| 163 | + private void createCollection() { | |
| 164 | + printSection("创建集合"); | |
| 165 | + | |
| 166 | + // 定义字段列表 | |
| 167 | + List<CreateCollectionReq.FieldSchema> fieldSchemas = Arrays.asList( | |
| 168 | + // 1. 主键字段 | |
| 169 | + CreateCollectionReq.FieldSchema.builder() | |
| 170 | + .name("id") | |
| 171 | + .dataType(DataType.Int64) | |
| 172 | + .isPrimaryKey(true) | |
| 173 | + .autoID(false) | |
| 174 | + .description("主键ID") | |
| 175 | + .build(), | |
| 176 | + | |
| 177 | + // 2. 向量字段 | |
| 178 | + CreateCollectionReq.FieldSchema.builder() | |
| 179 | + .name("vector") | |
| 180 | + .dataType(DataType.FloatVector) | |
| 181 | + .dimension(VECTOR_DIM) | |
| 182 | + .description("向量字段,用于相似性搜索") | |
| 183 | + .build(), | |
| 184 | + | |
| 185 | + // 3. 标题字段 | |
| 186 | + CreateCollectionReq.FieldSchema.builder() | |
| 187 | + .name("title") | |
| 188 | + .dataType(DataType.VarChar) | |
| 189 | + .maxLength(200) | |
| 190 | + .description("标题") | |
| 191 | + .build(), | |
| 192 | + | |
| 193 | + // 4. 内容字段 | |
| 194 | + CreateCollectionReq.FieldSchema.builder() | |
| 195 | + .name("content") | |
| 196 | + .dataType(DataType.VarChar) | |
| 197 | + .maxLength(1000) | |
| 198 | + .description("详细内容") | |
| 199 | + .build(), | |
| 200 | + | |
| 201 | + // 5. 分类字段 | |
| 202 | + CreateCollectionReq.FieldSchema.builder() | |
| 203 | + .name("category") | |
| 204 | + .dataType(DataType.VarChar) | |
| 205 | + .maxLength(50) | |
| 206 | + .description("分类标签") | |
| 207 | + .build(), | |
| 208 | + | |
| 209 | + // 6. 得分字段 | |
| 210 | + CreateCollectionReq.FieldSchema.builder() | |
| 211 | + .name("score") | |
| 212 | + .dataType(DataType.Float) | |
| 213 | + .description("评分") | |
| 214 | + .build(), | |
| 215 | + | |
| 216 | + // 7. 时间戳字段 | |
| 217 | + CreateCollectionReq.FieldSchema.builder() | |
| 218 | + .name("create_time") | |
| 219 | + .dataType(DataType.Int64) | |
| 220 | + .description("创建时间戳") | |
| 221 | + .build(), | |
| 222 | + | |
| 223 | + // 8. 标签数组字段 | |
| 224 | + CreateCollectionReq.FieldSchema.builder() | |
| 225 | + .name("tags") | |
| 226 | + .dataType(DataType.Array) | |
| 227 | + .elementType(DataType.VarChar) | |
| 228 | + .maxCapacity(10) | |
| 229 | + .description("标签数组") | |
| 230 | + .build() | |
| 231 | + ); | |
| 232 | + | |
| 233 | + // 创建集合schema | |
| 234 | + CreateCollectionReq.CollectionSchema schema = | |
| 235 | + CreateCollectionReq.CollectionSchema.builder() | |
| 236 | + .fieldSchemaList(fieldSchemas) | |
| 237 | + .enableDynamicField(true) // 启用动态字段 | |
| 238 | + .build(); | |
| 239 | + | |
| 240 | + // 创建集合请求 | |
| 241 | + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder() | |
| 242 | + .collectionName(COLLECTION_NAME) | |
| 243 | + .collectionSchema(schema) | |
| 244 | + .consistencyLevel(ConsistencyLevel.BOUNDED) // 有界一致性 | |
| 245 | + .build(); | |
| 246 | + | |
| 247 | + // 执行创建 | |
| 248 | + client.createCollection(createCollectionReq); | |
| 249 | + System.out.println("✅ 集合创建成功: " + COLLECTION_NAME); | |
| 250 | + System.out.println(" - 向量维度: " + VECTOR_DIM); | |
| 251 | + System.out.println(" - 字段数量: " + fieldSchemas.size()); | |
| 252 | + } | |
| 253 | + | |
| 254 | + /** | |
| 255 | + * 创建索引 | |
| 256 | + */ | |
| 257 | + /** | |
| 258 | + * 创建索引 | |
| 259 | + */ | |
| 260 | + private void createIndex() { | |
| 261 | + printSection("创建索引"); | |
| 262 | + | |
| 263 | + // 创建索引参数 | |
| 264 | + Map<String, Object> extraParams = new HashMap<>(); | |
| 265 | + extraParams.put("nlist", 1024); | |
| 266 | + | |
| 267 | + IndexParam indexParam = IndexParam.builder() | |
| 268 | + .fieldName("vector") | |
| 269 | + .indexType(IndexParam.IndexType.IVF_FLAT) | |
| 270 | + .metricType(IndexParam.MetricType.L2) | |
| 271 | + .extraParams(extraParams) | |
| 272 | + .build(); | |
| 273 | + | |
| 274 | + // 创建索引请求 | |
| 275 | + CreateIndexReq createIndexReq = CreateIndexReq.builder() | |
| 276 | + .collectionName(COLLECTION_NAME) | |
| 277 | + .indexParams(Collections.singletonList(indexParam)) | |
| 278 | + .build(); | |
| 279 | + | |
| 280 | + // 执行创建 | |
| 281 | + client.createIndex(createIndexReq); | |
| 282 | + System.out.println("✅ 索引创建成功"); | |
| 283 | + System.out.println(" - 索引类型: IVF_FLAT"); | |
| 284 | + System.out.println(" - 度量方式: L2 (欧氏距离)"); | |
| 285 | + | |
| 286 | + // 修正:使用 LoadCollectionReq 加载集合到内存 | |
| 287 | + LoadCollectionReq loadCollectionReq = LoadCollectionReq.builder() | |
| 288 | + .collectionName(COLLECTION_NAME) | |
| 289 | + .build(); | |
| 290 | + client.loadCollection(loadCollectionReq); | |
| 291 | + System.out.println("✅ 集合已加载到内存"); | |
| 292 | + } | |
| 293 | + | |
| 294 | + /** | |
| 295 | + * 插入测试数据 | |
| 296 | + */ | |
| 297 | + private void insertData() { | |
| 298 | + printSection("插入测试数据"); | |
| 299 | + | |
| 300 | + List<JsonObject> rows = new ArrayList<>(); | |
| 301 | + Random random = new Random(42); // 固定种子,保证可重复性 | |
| 302 | + | |
| 303 | + // 测试数据 | |
| 304 | + String[] categories = {"科技", "体育", "娱乐", "教育", "财经"}; | |
| 305 | + String[] titles = { | |
| 306 | + "人工智能发展现状与未来趋势", "机器学习入门实战教程", | |
| 307 | + "深度学习在图像识别中的应用", "NBA季后赛精彩回顾", | |
| 308 | + "世界杯预选赛最新战况", "奥运会筹备工作进展", | |
| 309 | + "热门电影推荐排行榜", "音乐榜单TOP10", | |
| 310 | + "综艺节目收视率分析", "在线教育行业发展报告", | |
| 311 | + "编程语言流行趋势", "高效学习方法分享", | |
| 312 | + "股市投资策略分析", "区块链技术应用前景", | |
| 313 | + "数字货币市场动态" | |
| 314 | + }; | |
| 315 | + | |
| 316 | + long currentTime = System.currentTimeMillis() / 1000; | |
| 317 | + | |
| 318 | + // 生成测试数据 | |
| 319 | + for (int i = 1; i <= INSERT_COUNT; i++) { | |
| 320 | + JsonObject row = new JsonObject(); | |
| 321 | + | |
| 322 | + // ID | |
| 323 | + row.addProperty("id", (long) i); | |
| 324 | + | |
| 325 | + // 向量 | |
| 326 | + JsonArray vectorArray = new JsonArray(); | |
| 327 | + for (int j = 0; j < VECTOR_DIM; j++) { | |
| 328 | + vectorArray.add(random.nextFloat()); | |
| 329 | + } | |
| 330 | + row.add("vector", vectorArray); | |
| 331 | + | |
| 332 | + // 标题 | |
| 333 | + int index = (i - 1) % titles.length; | |
| 334 | + row.addProperty("title", titles[index]); | |
| 335 | + | |
| 336 | + // 内容 | |
| 337 | + row.addProperty("content", "这是第" + i + "条测试数据的详细内容。用于测试Milvus的插入和查询功能。"); | |
| 338 | + | |
| 339 | + // 分类 | |
| 340 | + row.addProperty("category", categories[random.nextInt(categories.length)]); | |
| 341 | + | |
| 342 | + // 得分 | |
| 343 | + row.addProperty("score", random.nextFloat() * 100); | |
| 344 | + | |
| 345 | + // 创建时间 | |
| 346 | + row.addProperty("create_time", currentTime - random.nextInt(86400) * i); | |
| 347 | + | |
| 348 | + // 标签数组 | |
| 349 | + JsonArray tagsArray = new JsonArray(); | |
| 350 | + tagsArray.add("tag_" + random.nextInt(5)); | |
| 351 | + tagsArray.add("tag_" + random.nextInt(5)); | |
| 352 | + row.add("tags", tagsArray); | |
| 353 | + | |
| 354 | + rows.add(row); | |
| 355 | + } | |
| 356 | + | |
| 357 | + // 批量插入 | |
| 358 | + InsertReq insertReq = InsertReq.builder() | |
| 359 | + .collectionName(COLLECTION_NAME) | |
| 360 | + .data(rows) | |
| 361 | + .build(); | |
| 362 | + | |
| 363 | + InsertResp insertResp = client.insert(insertReq); | |
| 364 | + System.out.println("✅ 成功插入 " + insertResp.getInsertCnt() + " 条数据"); | |
| 365 | + System.out.println(" - 数据预览:"); | |
| 366 | + | |
| 367 | + // 显示前3条数据的部分信息 | |
| 368 | + for (int i = 0; i < Math.min(3, rows.size()); i++) { | |
| 369 | + JsonObject row = rows.get(i); | |
| 370 | + System.out.printf(" [%d] ID: %d, 标题: %s, 分类: %s, 得分: %.2f%n", | |
| 371 | + i + 1, | |
| 372 | + row.get("id").getAsLong(), | |
| 373 | + row.get("title").getAsString(), | |
| 374 | + row.get("category").getAsString(), | |
| 375 | + row.get("score").getAsFloat()); | |
| 376 | + } | |
| 377 | + } | |
| 378 | + | |
| 379 | + /** | |
| 380 | + * 查询数据(标量过滤) | |
| 381 | + */ | |
| 382 | + private void queryData() { | |
| 383 | + printSection("标量查询"); | |
| 384 | + | |
| 385 | + // 查询得分大于50的数据 | |
| 386 | + QueryReq queryReq = QueryReq.builder() | |
| 387 | + .collectionName(COLLECTION_NAME) | |
| 388 | + .filter("score > 50") // 过滤条件 | |
| 389 | + .outputFields(Arrays.asList("id", "title", "category", "score")) | |
| 390 | + .limit(10) | |
| 391 | + .build(); | |
| 392 | + | |
| 393 | + QueryResp queryResp = client.query(queryReq); | |
| 394 | + | |
| 395 | + List<QueryResp.QueryResult> results = queryResp.getQueryResults(); | |
| 396 | + System.out.println("查询条件: score > 50"); | |
| 397 | + System.out.println("查询结果: " + results.size() + " 条数据"); | |
| 398 | + | |
| 399 | + if (!results.isEmpty()) { | |
| 400 | + System.out.println("结果列表:"); | |
| 401 | + for (QueryResp.QueryResult result : results) { | |
| 402 | + Map<String, Object> entity = result.getEntity(); | |
| 403 | + System.out.printf(" ID: %d | 标题: %s | 分类: %s | 得分: %.2f%n", | |
| 404 | + (Long) entity.get("id"), | |
| 405 | + entity.get("title"), | |
| 406 | + entity.get("category"), | |
| 407 | + (Float) entity.get("score")); | |
| 408 | + } | |
| 409 | + } | |
| 410 | + } | |
| 411 | + | |
| 412 | + /** | |
| 413 | + * 向量相似性搜索 | |
| 414 | + */ | |
| 415 | + private void searchData() { | |
| 416 | + printSection("向量相似性搜索"); | |
| 417 | + | |
| 418 | + // 创建查询向量 | |
| 419 | + List<Float> vectorList = new ArrayList<>(); | |
| 420 | + Random random = new Random(); | |
| 421 | + for (int j = 0; j < VECTOR_DIM; j++) { | |
| 422 | + vectorList.add(random.nextFloat()); | |
| 423 | + } | |
| 424 | + FloatVec queryVector = new FloatVec(vectorList); | |
| 425 | + | |
| 426 | + // 构建搜索参数 | |
| 427 | + Map<String, Object> searchParams = new HashMap<>(); | |
| 428 | + searchParams.put("nprobe", 10); | |
| 429 | + | |
| 430 | + // 构建搜索请求 | |
| 431 | + SearchReq searchReq = SearchReq.builder() | |
| 432 | + .collectionName(COLLECTION_NAME) | |
| 433 | + .data(Collections.singletonList(queryVector)) | |
| 434 | + .annsField("vector") | |
| 435 | + .limit(5) | |
| 436 | + .outputFields(Arrays.asList("title", "category", "score", "tags")) | |
| 437 | + .metricType(IndexParam.MetricType.L2) | |
| 438 | + .searchParams(searchParams) | |
| 439 | + .build(); | |
| 440 | + | |
| 441 | + // 执行搜索 | |
| 442 | + SearchResp searchResp = client.search(searchReq); | |
| 443 | + | |
| 444 | + // 打印结果 | |
| 445 | + System.out.println("最相似的5条结果:"); | |
| 446 | + List<List<SearchResp.SearchResult>> results = searchResp.getSearchResults(); | |
| 447 | + | |
| 448 | + if (!results.isEmpty() && !results.get(0).isEmpty()) { | |
| 449 | + for (int i = 0; i < results.get(0).size(); i++) { | |
| 450 | + SearchResp.SearchResult result = results.get(0).get(i); | |
| 451 | + Map<String, Object> entity = result.getEntity(); | |
| 452 | + | |
| 453 | + System.out.printf(" %d. ID: %d | 距离: %.4f%n", | |
| 454 | + i + 1, | |
| 455 | + (Long) result.getId(), | |
| 456 | + result.getScore()); | |
| 457 | + System.out.printf(" 标题: %s | 分类: %s | 得分: %.2f%n", | |
| 458 | + entity.get("title"), | |
| 459 | + entity.get("category"), | |
| 460 | + (Float) entity.get("score")); | |
| 461 | + System.out.printf(" 标签: %s%n", | |
| 462 | + entity.get("tags")); | |
| 463 | + } | |
| 464 | + } else { | |
| 465 | + System.out.println(" 没有找到结果"); | |
| 466 | + } | |
| 467 | + } | |
| 468 | + | |
| 469 | + /** | |
| 470 | + * 获取集合统计信息 | |
| 471 | + */ | |
| 472 | + private void getCollectionStats() { | |
| 473 | + printSection("集合统计信息"); | |
| 474 | + | |
| 475 | + GetCollectionStatsReq statsReq = GetCollectionStatsReq.builder() | |
| 476 | + .collectionName(COLLECTION_NAME) | |
| 477 | + .build(); | |
| 478 | + | |
| 479 | + GetCollectionStatsResp statsResp = client.getCollectionStats(statsReq); | |
| 480 | + | |
| 481 | + System.out.println("集合名称: " + COLLECTION_NAME); | |
| 482 | + System.out.println("实体数量: " + statsResp.getNumOfEntities()); | |
| 483 | + } | |
| 484 | + | |
| 485 | + /** | |
| 486 | + * 释放集合并关闭连接 | |
| 487 | + */ | |
| 488 | + private void releaseAndClose() { | |
| 489 | + printSection("清理资源"); | |
| 490 | + | |
| 491 | + if (client != null) { | |
| 492 | + try { | |
| 493 | + // 释放集合 | |
| 494 | + ReleaseCollectionReq releaseCollectionReq = ReleaseCollectionReq.builder() | |
| 495 | + .collectionName(COLLECTION_NAME) | |
| 496 | + .build(); | |
| 497 | + client.releaseCollection(releaseCollectionReq); | |
| 498 | + System.out.println("✅ 集合已释放"); | |
| 499 | + | |
| 500 | + // 关闭连接 | |
| 501 | + client.close(); | |
| 502 | + System.out.println("✅ 连接已关闭"); | |
| 503 | + | |
| 504 | + } catch (Exception e) { | |
| 505 | + System.err.println("⚠️ 清理资源时出错: " + e.getMessage()); | |
| 506 | + } | |
| 507 | + } | |
| 508 | + } | |
| 509 | + | |
| 510 | + /** | |
| 511 | + * 打印标题 | |
| 512 | + */ | |
| 513 | + private void printHeader(String title) { | |
| 514 | + System.out.println("\n" + "=".repeat(60)); | |
| 515 | + System.out.println(" " + title); | |
| 516 | + System.out.println("=".repeat(60)); | |
| 517 | + } | |
| 518 | + | |
| 519 | + /** | |
| 520 | + * 打印章节标题 | |
| 521 | + */ | |
| 522 | + private void printSection(String section) { | |
| 523 | + System.out.println("\n" + "-".repeat(40)); | |
| 524 | + System.out.println(" " + section); | |
| 525 | + System.out.println("-".repeat(40)); | |
| 526 | + } | |
| 527 | + | |
| 528 | + /** | |
| 529 | + * 打印成功信息 | |
| 530 | + */ | |
| 531 | + private void printSuccess(String message) { | |
| 532 | + System.out.println("\n" + "=".repeat(60)); | |
| 533 | + System.out.println(" ✅ " + message); | |
| 534 | + System.out.println("=".repeat(60)); | |
| 535 | + } | |
| 536 | +} | |
| 0 | 537 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/util/MapToJsonConverter.java
0 → 100644
| 1 | +package com.xly.milvus.util; | |
| 2 | + | |
| 3 | +import com.google.gson.JsonArray; | |
| 4 | +import com.google.gson.JsonObject; | |
| 5 | +import java.util.List; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * Map 转 JsonObject 工具类 | |
| 10 | + */ | |
| 11 | +public class MapToJsonConverter { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 将 Map<String, Object> 转换为 JsonObject | |
| 15 | + */ | |
| 16 | + public static JsonObject convert(Map<String, Object> map) { | |
| 17 | + JsonObject jsonObject = new JsonObject(); | |
| 18 | + | |
| 19 | + if (map == null || map.isEmpty()) { | |
| 20 | + return jsonObject; | |
| 21 | + } | |
| 22 | + | |
| 23 | + for (Map.Entry<String, Object> entry : map.entrySet()) { | |
| 24 | + String key = entry.getKey(); | |
| 25 | + Object value = entry.getValue(); | |
| 26 | + | |
| 27 | + addToJson(jsonObject, key, value); | |
| 28 | + } | |
| 29 | + | |
| 30 | + return jsonObject; | |
| 31 | + } | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 递归添加值到JsonObject | |
| 35 | + */ | |
| 36 | + @SuppressWarnings("unchecked") | |
| 37 | + private static void addToJson(JsonObject jsonObject, String key, Object value) { | |
| 38 | + if (value == null) { | |
| 39 | + jsonObject.add(key, null); | |
| 40 | + } else if (value instanceof String) { | |
| 41 | + jsonObject.addProperty(key, (String) value); | |
| 42 | + } else if (value instanceof Number) { | |
| 43 | + jsonObject.addProperty(key, (Number) value); | |
| 44 | + } else if (value instanceof Boolean) { | |
| 45 | + jsonObject.addProperty(key, (Boolean) value); | |
| 46 | + } else if (value instanceof Character) { | |
| 47 | + jsonObject.addProperty(key, (Character) value); | |
| 48 | + } else if (value instanceof Map) { | |
| 49 | + jsonObject.add(key, convert((Map<String, Object>) value)); | |
| 50 | + } else if (value instanceof List) { | |
| 51 | + jsonObject.add(key, convertList((List<?>) value)); | |
| 52 | + } else if (value instanceof Object[]) { | |
| 53 | + jsonObject.add(key, convertArray((Object[]) value)); | |
| 54 | + } else { | |
| 55 | + // 其他类型转换为字符串 | |
| 56 | + jsonObject.addProperty(key, value.toString()); | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * 转换List为JsonArray | |
| 62 | + */ | |
| 63 | + private static JsonArray convertList(List<?> list) { | |
| 64 | + JsonArray jsonArray = new JsonArray(); | |
| 65 | + | |
| 66 | + for (Object item : list) { | |
| 67 | + if (item == null) { | |
| 68 | + jsonArray.add((JsonObject) null); | |
| 69 | + } else if (item instanceof String) { | |
| 70 | + jsonArray.add((String) item); | |
| 71 | + } else if (item instanceof Number) { | |
| 72 | + jsonArray.add((Number) item); | |
| 73 | + } else if (item instanceof Boolean) { | |
| 74 | + jsonArray.add((Boolean) item); | |
| 75 | + } else if (item instanceof Character) { | |
| 76 | + jsonArray.add((Character) item); | |
| 77 | + } else if (item instanceof Map) { | |
| 78 | + jsonArray.add(convert((Map<String, Object>) item)); | |
| 79 | + } else if (item instanceof List) { | |
| 80 | + jsonArray.add(convertList((List<?>) item)); | |
| 81 | + } else { | |
| 82 | + jsonArray.add(item.toString()); | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + return jsonArray; | |
| 87 | + } | |
| 88 | + | |
| 89 | + /** | |
| 90 | + * 转换数组为JsonArray | |
| 91 | + */ | |
| 92 | + private static JsonArray convertArray(Object[] array) { | |
| 93 | + JsonArray jsonArray = new JsonArray(); | |
| 94 | + | |
| 95 | + for (Object item : array) { | |
| 96 | + if (item == null) { | |
| 97 | + jsonArray.add((JsonObject) null); | |
| 98 | + } else if (item instanceof String) { | |
| 99 | + jsonArray.add((String) item); | |
| 100 | + } else if (item instanceof Number) { | |
| 101 | + jsonArray.add((Number) item); | |
| 102 | + } else if (item instanceof Boolean) { | |
| 103 | + jsonArray.add((Boolean) item); | |
| 104 | + } else if (item instanceof Character) { | |
| 105 | + jsonArray.add((Character) item); | |
| 106 | + } else if (item instanceof Map) { | |
| 107 | + jsonArray.add(convert((Map<String, Object>) item)); | |
| 108 | + } else if (item instanceof List) { | |
| 109 | + jsonArray.add(convertList((List<?>) item)); | |
| 110 | + } else { | |
| 111 | + jsonArray.add(item.toString()); | |
| 112 | + } | |
| 113 | + } | |
| 114 | + | |
| 115 | + return jsonArray; | |
| 116 | + } | |
| 117 | +} | |
| 0 | 118 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/util/MilvusSchemaBuilder.java
0 → 100644
| 1 | +package com.xly.milvus.util; | |
| 2 | + | |
| 3 | +import io.milvus.v2.common.DataType; | |
| 4 | +import io.milvus.v2.service.collection.request.CreateCollectionReq; | |
| 5 | +import java.util.HashMap; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * Milvus字段Schema构建工具类 | |
| 10 | + * 基于源码的 FieldSchema.FieldSchemaBuilder | |
| 11 | + */ | |
| 12 | +public class MilvusSchemaBuilder { | |
| 13 | + | |
| 14 | + /** | |
| 15 | + * 创建主键ID字段(Int64类型,自增) | |
| 16 | + */ | |
| 17 | + public static CreateCollectionReq.FieldSchema createAutoIdField(String fieldName) { | |
| 18 | + return CreateCollectionReq.FieldSchema.builder() | |
| 19 | + .name(fieldName) | |
| 20 | + .dataType(DataType.Int64) | |
| 21 | + .isPrimaryKey(true) | |
| 22 | + .autoID(true) | |
| 23 | + .description("自增主键ID") | |
| 24 | + .build(); | |
| 25 | + } | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 创建主键ID字段(String类型,不自增) | |
| 29 | + */ | |
| 30 | + public static CreateCollectionReq.FieldSchema createStringIdField(String fieldName, int maxLength) { | |
| 31 | + return CreateCollectionReq.FieldSchema.builder() | |
| 32 | + .name(fieldName) | |
| 33 | + .dataType(DataType.VarChar) | |
| 34 | + .maxLength(maxLength) | |
| 35 | + .isPrimaryKey(true) | |
| 36 | + .autoID(false) | |
| 37 | + .description("业务主键ID") | |
| 38 | + .build(); | |
| 39 | + } | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 创建向量字段 | |
| 43 | + */ | |
| 44 | + public static CreateCollectionReq.FieldSchema createVectorField(String fieldName, int dimension) { | |
| 45 | + return CreateCollectionReq.FieldSchema.builder() | |
| 46 | + .name(fieldName) | |
| 47 | + .dataType(DataType.FloatVector) | |
| 48 | + .dimension(dimension) | |
| 49 | + .description("向量字段,维度: " + dimension) | |
| 50 | + .build(); | |
| 51 | + } | |
| 52 | + | |
| 53 | + /** | |
| 54 | + * 创建文本字段(VarChar类型) | |
| 55 | + */ | |
| 56 | + public static CreateCollectionReq.FieldSchema createTextField(String fieldName, int maxLength) { | |
| 57 | + return CreateCollectionReq.FieldSchema.builder() | |
| 58 | + .name(fieldName) | |
| 59 | + .dataType(DataType.VarChar) | |
| 60 | + .maxLength(maxLength) | |
| 61 | + .description("文本内容") | |
| 62 | + .build(); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * 创建JSON元数据字段 | |
| 67 | + */ | |
| 68 | + public static CreateCollectionReq.FieldSchema createJsonField(String fieldName) { | |
| 69 | + return CreateCollectionReq.FieldSchema.builder() | |
| 70 | + .name(fieldName) | |
| 71 | + .dataType(DataType.JSON) | |
| 72 | + .description("JSON元数据") | |
| 73 | + .build(); | |
| 74 | + } | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * 创建整型字段 | |
| 78 | + */ | |
| 79 | + public static CreateCollectionReq.FieldSchema createIntField(String fieldName, String description) { | |
| 80 | + return CreateCollectionReq.FieldSchema.builder() | |
| 81 | + .name(fieldName) | |
| 82 | + .dataType(DataType.Int64) | |
| 83 | + .description(description) | |
| 84 | + .build(); | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 创建整型字段(可空) | |
| 89 | + */ | |
| 90 | + public static CreateCollectionReq.FieldSchema createIntField(String fieldName, | |
| 91 | + String description, | |
| 92 | + boolean isNullable) { | |
| 93 | + return CreateCollectionReq.FieldSchema.builder() | |
| 94 | + .name(fieldName) | |
| 95 | + .dataType(DataType.Int64) | |
| 96 | + .description(description) | |
| 97 | + .isNullable(isNullable) | |
| 98 | + .build(); | |
| 99 | + } | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * 创建整型字段(带默认值) | |
| 103 | + */ | |
| 104 | + public static CreateCollectionReq.FieldSchema createIntField(String fieldName, | |
| 105 | + String description, | |
| 106 | + long defaultValue) { | |
| 107 | + return CreateCollectionReq.FieldSchema.builder() | |
| 108 | + .name(fieldName) | |
| 109 | + .dataType(DataType.Int64) | |
| 110 | + .description(description) | |
| 111 | + .defaultValue(defaultValue) | |
| 112 | + .build(); | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 创建浮点型字段 | |
| 117 | + */ | |
| 118 | + public static CreateCollectionReq.FieldSchema createFloatField(String fieldName, String description) { | |
| 119 | + return CreateCollectionReq.FieldSchema.builder() | |
| 120 | + .name(fieldName) | |
| 121 | + .dataType(DataType.Float) | |
| 122 | + .description(description) | |
| 123 | + .build(); | |
| 124 | + } | |
| 125 | + | |
| 126 | + /** | |
| 127 | + * 创建布尔型字段 | |
| 128 | + */ | |
| 129 | + public static CreateCollectionReq.FieldSchema createBoolField(String fieldName, String description) { | |
| 130 | + return CreateCollectionReq.FieldSchema.builder() | |
| 131 | + .name(fieldName) | |
| 132 | + .dataType(DataType.Bool) | |
| 133 | + .description(description) | |
| 134 | + .build(); | |
| 135 | + } | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 创建布尔型字段(带默认值) | |
| 139 | + */ | |
| 140 | + public static CreateCollectionReq.FieldSchema createBoolField(String fieldName, | |
| 141 | + String description, | |
| 142 | + boolean defaultValue) { | |
| 143 | + return CreateCollectionReq.FieldSchema.builder() | |
| 144 | + .name(fieldName) | |
| 145 | + .dataType(DataType.Bool) | |
| 146 | + .description(description) | |
| 147 | + .defaultValue(defaultValue) | |
| 148 | + .build(); | |
| 149 | + } | |
| 150 | + | |
| 151 | + /** | |
| 152 | + * 创建数组字段 | |
| 153 | + */ | |
| 154 | + public static CreateCollectionReq.FieldSchema createArrayField(String fieldName, | |
| 155 | + DataType elementType, | |
| 156 | + int maxCapacity, | |
| 157 | + String description) { | |
| 158 | + return CreateCollectionReq.FieldSchema.builder() | |
| 159 | + .name(fieldName) | |
| 160 | + .dataType(DataType.Array) | |
| 161 | + .elementType(elementType) | |
| 162 | + .maxCapacity(maxCapacity) | |
| 163 | + .description(description) | |
| 164 | + .build(); | |
| 165 | + } | |
| 166 | + | |
| 167 | + /** | |
| 168 | + * 创建分区键字段 | |
| 169 | + */ | |
| 170 | + public static CreateCollectionReq.FieldSchema createPartitionKeyField(String fieldName, | |
| 171 | + DataType dataType, | |
| 172 | + String description) { | |
| 173 | + return CreateCollectionReq.FieldSchema.builder() | |
| 174 | + .name(fieldName) | |
| 175 | + .dataType(dataType) | |
| 176 | + .isPartitionKey(true) | |
| 177 | + .description(description) | |
| 178 | + .build(); | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * 创建聚类键字段 | |
| 183 | + */ | |
| 184 | + public static CreateCollectionReq.FieldSchema createClusteringKeyField(String fieldName, | |
| 185 | + DataType dataType, | |
| 186 | + String description) { | |
| 187 | + return CreateCollectionReq.FieldSchema.builder() | |
| 188 | + .name(fieldName) | |
| 189 | + .dataType(dataType) | |
| 190 | + .isClusteringKey(true) | |
| 191 | + .description(description) | |
| 192 | + .build(); | |
| 193 | + } | |
| 194 | + | |
| 195 | + /** | |
| 196 | + * 创建带分析器的文本字段(用于全文搜索) | |
| 197 | + */ | |
| 198 | + public static CreateCollectionReq.FieldSchema createAnalyzedTextField(String fieldName, | |
| 199 | + int maxLength, | |
| 200 | + String analyzerType) { | |
| 201 | + Map<String, Object> analyzerParams = new HashMap<>(); | |
| 202 | + analyzerParams.put("type", analyzerType); // "english", "chinese", "standard" 等 | |
| 203 | + | |
| 204 | + return CreateCollectionReq.FieldSchema.builder() | |
| 205 | + .name(fieldName) | |
| 206 | + .dataType(DataType.VarChar) | |
| 207 | + .maxLength(maxLength) | |
| 208 | + .enableAnalyzer(true) | |
| 209 | + .analyzerParams(analyzerParams) | |
| 210 | + .enableMatch(true) | |
| 211 | + .description("支持全文搜索的文本字段") | |
| 212 | + .build(); | |
| 213 | + } | |
| 214 | +} | |
| 0 | 215 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/milvus/web/MilvusController.java
0 → 100644
| 1 | +package com.xly.milvus.web; | |
| 2 | + | |
| 3 | +import com.xly.milvus.service.MilvusService; | |
| 4 | +import com.xly.runner.AppStartupRunner; | |
| 5 | +import com.xly.service.DynamicExeDbService; | |
| 6 | +import com.xly.service.UserSceneSessionService; | |
| 7 | +import com.xly.tool.DynamicToolProvider; | |
| 8 | +import com.xly.tts.bean.*; | |
| 9 | +import com.xly.tts.service.LocalAudioCache; | |
| 10 | +import com.xly.tts.service.PythonTtsProxyService; | |
| 11 | +import jakarta.validation.Valid; | |
| 12 | +import lombok.RequiredArgsConstructor; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | +import org.springframework.core.io.InputStreamResource; | |
| 15 | +import org.springframework.http.MediaType; | |
| 16 | +import org.springframework.http.ResponseEntity; | |
| 17 | +import org.springframework.web.bind.annotation.*; | |
| 18 | +import reactor.core.publisher.Flux; | |
| 19 | +import reactor.core.publisher.Mono; | |
| 20 | + | |
| 21 | +import javax.annotation.PostConstruct; | |
| 22 | +import javax.annotation.PreDestroy; | |
| 23 | +import java.util.List; | |
| 24 | +import java.util.Map; | |
| 25 | +import java.util.concurrent.CompletableFuture; | |
| 26 | + | |
| 27 | +@Slf4j | |
| 28 | +@RestController | |
| 29 | +@RequestMapping("/api/milvus") | |
| 30 | +@RequiredArgsConstructor | |
| 31 | +public class MilvusController { | |
| 32 | + | |
| 33 | + private final MilvusService milvusService; | |
| 34 | + | |
| 35 | + /*** | |
| 36 | + * @Author 钱豹 | |
| 37 | + * @Date 14:32 2026/2/10 | |
| 38 | + * @Param [request] | |
| 39 | + * @return org.springframework.http.ResponseEntity<com.xly.tts.bean.TTSResponseDTO> | |
| 40 | + * @Description 初始化AI所有变量 热启动 | |
| 41 | + **/ | |
| 42 | + @PostMapping("/init") | |
| 43 | + public ResponseEntity<TTSResponseDTO> init(@RequestBody Map<String,Object> reqMap) { | |
| 44 | + TTSResponseDTO responseDTO = milvusService.initDataToMilvus(reqMap); | |
| 45 | + return ResponseEntity.ok(responseDTO); | |
| 46 | + } | |
| 47 | + | |
| 48 | + | |
| 49 | +} | |
| 0 | 50 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/XlyErpService.java
| ... | ... | @@ -7,6 +7,7 @@ import cn.hutool.core.util.IdUtil; |
| 7 | 7 | import cn.hutool.core.util.ObjectUtil; |
| 8 | 8 | import cn.hutool.core.util.StrUtil; |
| 9 | 9 | import com.alibaba.fastjson2.JSON; |
| 10 | +import com.alibaba.fastjson2.JSONObject; | |
| 10 | 11 | import com.xly.agent.ChatiAgent; |
| 11 | 12 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| 12 | 13 | import com.xly.agent.ErpAiAgent; |
| ... | ... | @@ -17,15 +18,14 @@ import com.xly.constant.ReturnTypeCode; |
| 17 | 18 | import com.xly.entity.*; |
| 18 | 19 | import com.xly.exception.sqlexception.SqlGenerateException; |
| 19 | 20 | import com.xly.exception.sqlexception.SqlValidateException; |
| 21 | +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService; | |
| 22 | +import com.xly.milvus.service.MilvusService; | |
| 20 | 23 | import com.xly.runner.AppStartupRunner; |
| 21 | 24 | import com.xly.thread.AiSqlErrorHistoryThread; |
| 22 | 25 | import com.xly.thread.AiUserAgentQuestionThread; |
| 23 | 26 | import com.xly.thread.MultiThreadPoolServer; |
| 24 | 27 | import com.xly.tool.DynamicToolProvider; |
| 25 | -import com.xly.util.EnhancedErrorGuidance; | |
| 26 | -import com.xly.util.InputPreprocessor; | |
| 27 | -import com.xly.util.SqlValidateUtil; | |
| 28 | -import com.xly.util.ValiDataUtil; | |
| 28 | +import com.xly.util.*; | |
| 29 | 29 | import dev.langchain4j.agent.tool.ToolExecutionRequest; |
| 30 | 30 | import dev.langchain4j.data.message.AiMessage; |
| 31 | 31 | import dev.langchain4j.data.message.ChatMessage; |
| ... | ... | @@ -33,15 +33,22 @@ import dev.langchain4j.data.message.ChatMessageType; |
| 33 | 33 | import dev.langchain4j.model.chat.ChatLanguageModel; |
| 34 | 34 | import dev.langchain4j.model.ollama.OllamaChatModel; |
| 35 | 35 | import dev.langchain4j.service.AiServices; |
| 36 | +import dev.langchain4j.service.MemoryId; | |
| 37 | +import dev.langchain4j.service.V; | |
| 38 | +import io.milvus.v2.common.DataType; | |
| 39 | +import io.milvus.v2.service.collection.request.CreateCollectionReq; | |
| 36 | 40 | import lombok.RequiredArgsConstructor; |
| 37 | 41 | import lombok.extern.slf4j.Slf4j; |
| 42 | +import org.apache.commons.lang3.time.DateFormatUtils; | |
| 38 | 43 | import org.springframework.beans.factory.annotation.Value; |
| 39 | 44 | import org.springframework.stereotype.Service; |
| 40 | 45 | import org.springframework.util.IdGenerator; |
| 46 | +import reactor.core.publisher.Flux; | |
| 41 | 47 | |
| 42 | 48 | import java.time.Duration; |
| 43 | 49 | import java.util.*; |
| 44 | 50 | import java.util.stream.Collectors; |
| 51 | +import java.util.stream.IntStream; | |
| 45 | 52 | |
| 46 | 53 | @Service |
| 47 | 54 | @RequiredArgsConstructor |
| ... | ... | @@ -56,6 +63,8 @@ public class XlyErpService { |
| 56 | 63 | private final OperableChatMemoryProvider operableChatMemoryProvider; |
| 57 | 64 | private final DynamicExeDbService dynamicExeDbService; |
| 58 | 65 | private final RedisService redisService; |
| 66 | + private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; | |
| 67 | + private final MilvusService milvusService; | |
| 59 | 68 | |
| 60 | 69 | //执行动态语句 执行异常的情况下 最多执行次数 |
| 61 | 70 | private final Integer maxRetries = 5; |
| ... | ... | @@ -67,6 +76,51 @@ public class XlyErpService { |
| 67 | 76 | |
| 68 | 77 | @Value("${langchain4j.ollama.sql-model-name}") |
| 69 | 78 | private String sqlModelName; |
| 79 | + | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 新的流式方法 - 返回 Flux<AiResponseDTO> | |
| 83 | + * 每个AiResponseDTO包含一个文本片段 | |
| 84 | + */ | |
| 85 | + /** | |
| 86 | + * 模拟的erpUserInputStream实现 | |
| 87 | + */ | |
| 88 | + public Flux<AiResponseDTO> erpUserInputStream(String userInput, String sUserId, | |
| 89 | + String sUserName, String sBrandsId, | |
| 90 | + String sSubsidiaryId, String sUserType, | |
| 91 | + String authorization) { | |
| 92 | + String requestId = UUID.randomUUID().toString(); | |
| 93 | + | |
| 94 | + // 按句子分割 | |
| 95 | + String[] sentences = userInput.split("(?<=[。!?.!?])"); | |
| 96 | + int totalChunks = sentences.length; | |
| 97 | + | |
| 98 | + return Flux.range(0, totalChunks) | |
| 99 | + .delayElements(Duration.ofMillis(200)) | |
| 100 | + .map(i -> { | |
| 101 | + String sentence = sentences[i].trim(); | |
| 102 | + if (sentence.isEmpty()) return null; | |
| 103 | + | |
| 104 | + return AiResponseDTO.builder() | |
| 105 | + .requestId(requestId) | |
| 106 | + .code(200) | |
| 107 | + .message("ERP_CHUNK") | |
| 108 | + .status("PROCESSING") | |
| 109 | + .textFragment(sentence) | |
| 110 | + .chunkIndex(i) | |
| 111 | + .totalChunks(totalChunks) | |
| 112 | + .isLastChunk(i == totalChunks - 1) | |
| 113 | + .progress((i + 1) * 100 / totalChunks) | |
| 114 | + .timestamp(System.currentTimeMillis()) | |
| 115 | + .sSceneName("客服咨询") | |
| 116 | + .sMethodName("chat") | |
| 117 | + .sReturnType("MARKDOWN") | |
| 118 | + .build(); | |
| 119 | + }) | |
| 120 | + .filter(Objects::nonNull); | |
| 121 | + } | |
| 122 | + | |
| 123 | + | |
| 70 | 124 | /*** |
| 71 | 125 | * @Author 钱豹 |
| 72 | 126 | * @Date 19:18 2026/1/27 |
| ... | ... | @@ -116,8 +170,15 @@ public class XlyErpService { |
| 116 | 170 | if (aiAgent == null){ |
| 117 | 171 | return getChatiAgent (input,session); |
| 118 | 172 | } |
| 119 | - //用户输入添加方法 | |
| 120 | - String sResponMessage = aiAgent.chat(userId, input); | |
| 173 | + String sResponMessage = StrUtil.EMPTY; | |
| 174 | + //用户输入添加方法(如果没有方法,动态SQL方法不需要) | |
| 175 | + if(!(ObjectUtil.isNotEmpty(session.getCurrentTool()) | |
| 176 | + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) | |
| 177 | + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) | |
| 178 | + ){ | |
| 179 | + sResponMessage = aiAgent.chat(userId, input); | |
| 180 | + } | |
| 181 | + | |
| 121 | 182 | if(ObjectUtil.isNotEmpty(session.getCurrentTool()) |
| 122 | 183 | && !ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) |
| 123 | 184 | ){ |
| ... | ... | @@ -132,7 +193,14 @@ public class XlyErpService { |
| 132 | 193 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) |
| 133 | 194 | && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) |
| 134 | 195 | ){ |
| 135 | - sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY, aiAgent); | |
| 196 | + //查询是否走向量库 还是数据库查询 | |
| 197 | + Boolean isAggregation = aiAgent.routeQuery(session.getUserId(), input); | |
| 198 | + if(!isAggregation){ | |
| 199 | + //获取常量库内容 | |
| 200 | + sResponMessage = getMilvus(session, input, aiAgent); | |
| 201 | + }else { | |
| 202 | + sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY, aiAgent); | |
| 203 | + } | |
| 136 | 204 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sResponMessage).sReturnType(ReturnTypeCode.HTML.getCode()).build(); |
| 137 | 205 | } else if (ObjectUtil.isNotEmpty(session.getCurrentTool())) { |
| 138 | 206 | //2.处理工具参数采集结束后业务逻辑处理 |
| ... | ... | @@ -198,6 +266,75 @@ public class XlyErpService { |
| 198 | 266 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); |
| 199 | 267 | } |
| 200 | 268 | |
| 269 | + /*** | |
| 270 | + * @Author 钱豹 | |
| 271 | + * @Date 10:16 2026/3/25 | |
| 272 | + * @Param [session, input, userId, userInput, attempt, errorSql, errorMessage, iErroCount, historySqlList, aiAgent] | |
| 273 | + * @return java.lang.String | |
| 274 | + * @Description 查询向量库 | |
| 275 | + **/ | |
| 276 | + private String getMilvus(UserSceneSession session,String userInput,ErpAiAgent aiAgent){ | |
| 277 | + String resultExplain = "信息模糊,请提供更具体的问题或指令"; | |
| 278 | + try{ | |
| 279 | + String sVectorfiled = session.getCurrentTool().getSVectorfiled(); | |
| 280 | + String sInputTabelName = session.getCurrentTool().getSInputTabelName(); | |
| 281 | + Map<String,Object> rMap = milvusService.getMilvusFiled(sVectorfiled); | |
| 282 | + String sMilvusFiled = rMap.get("sMilvusFiled").toString(); | |
| 283 | + String sMilvusFiledDescription = rMap.get("sMilvusFiledDescription").toString(); | |
| 284 | + List<String> fields = (List<String>) rMap.get("sFileds"); | |
| 285 | +// List<Map<String, String>> title = (List<Map<String, String>>) rMap.get("title"); | |
| 286 | + String milvusFilter = aiAgent.getMilvusFilter(session.getUserId(),userInput, sMilvusFiled, sMilvusFiledDescription); | |
| 287 | + List<Map<String,Object>> data = milvusService.getDataToCollection(sInputTabelName, milvusFilter,userInput,100,fields); | |
| 288 | + //采用表格形式显示 | |
| 289 | + resultExplain = aiAgent.explainMilvusResult(session.getUserId(),userInput,sMilvusFiledDescription,JSONObject.toJSONString(data)); | |
| 290 | + //buildMarkdownTableWithStream(data, title); | |
| 291 | + return resultExplain; | |
| 292 | + }catch (Exception e){ | |
| 293 | + e.printStackTrace(); | |
| 294 | + } | |
| 295 | + return resultExplain; | |
| 296 | + } | |
| 297 | + /*** | |
| 298 | + * @Author 钱豹 | |
| 299 | + * @Date 13:19 2026/3/25 | |
| 300 | + * @Param [data, title] | |
| 301 | + * @return java.lang.String | |
| 302 | + * @Description 数据转成MarkdownTable | |
| 303 | + **/ | |
| 304 | + public String buildMarkdownTableWithStream(List<Map<String, Object>> data, List<Map<String, String>> title) { | |
| 305 | + if (data == null || data.isEmpty()) { | |
| 306 | + return "暂无数据"; | |
| 307 | + } | |
| 308 | + | |
| 309 | + // 动态构建表头 | |
| 310 | + StringBuilder headerBuilder = new StringBuilder("|"); | |
| 311 | + StringBuilder separatorBuilder = new StringBuilder("|"); | |
| 312 | + | |
| 313 | + for (Map<String, String> column : title) { | |
| 314 | + String displayName = column.get("sTitle"); // 中文显示名称 | |
| 315 | + headerBuilder.append(" ").append(displayName).append(" |"); | |
| 316 | + separatorBuilder.append("------|"); | |
| 317 | + } | |
| 318 | + String header = headerBuilder.toString() + "\n" + separatorBuilder.toString() + "\n"; | |
| 319 | + // 构建数据行 | |
| 320 | + String rows = IntStream.range(0, data.size()) | |
| 321 | + .mapToObj(i -> { | |
| 322 | + Map<String, Object> item = data.get(i); | |
| 323 | + StringBuilder rowBuilder = new StringBuilder("|"); | |
| 324 | + | |
| 325 | + for (Map<String, String> column : title) { | |
| 326 | + String fieldName = column.get("sName"); | |
| 327 | + Object value = item.getOrDefault(fieldName, ""); | |
| 328 | + rowBuilder.append(" ").append(value).append(" |"); | |
| 329 | + } | |
| 330 | + | |
| 331 | + return rowBuilder.toString(); | |
| 332 | + }) | |
| 333 | + .collect(Collectors.joining("\n")); | |
| 334 | + | |
| 335 | + return header + rows; | |
| 336 | + } | |
| 337 | + | |
| 201 | 338 | |
| 202 | 339 | /*** |
| 203 | 340 | * @Author 钱豹 |
| ... | ... | @@ -283,48 +420,54 @@ public class XlyErpService { |
| 283 | 420 | **/ |
| 284 | 421 | private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage,String iErroCount,String historySqlList,ErpAiAgent aiAgent){ |
| 285 | 422 | // 1. 构建自然语言转SQLAgent, |
| 286 | - List<Map<String, Object>> sqlResult = new ArrayList<>(); | |
| 423 | + List<Map<String, Object>> sqlResult; | |
| 287 | 424 | String cleanSql = StrUtil.EMPTY; |
| 288 | 425 | String rawSql; |
| 289 | - String tableStruct; | |
| 290 | - String sError_mes = StrUtil.EMPTY; | |
| 291 | - Boolean doSqlErro = false; | |
| 292 | - List<ChatMessage> chatMessage = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId()); | |
| 426 | + String tableStruct = session.getCurrentTool().getSStructureMemo(); | |
| 427 | + String sError_mes; | |
| 428 | + Boolean doAddSql = false; | |
| 429 | + List<ChatMessage> chatMessage = new ArrayList<>(); | |
| 293 | 430 | try{ |
| 294 | - DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); | |
| 295 | - String tableNames = session.getCurrentTool().getSInputTabelName(); | |
| 296 | - // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart"; | |
| 297 | - tableStruct = session.getCurrentTool().getSStructureMemo(); | |
| 298 | - String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); | |
| 299 | - | |
| 300 | - if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ | |
| 301 | - rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); | |
| 302 | - }else{ | |
| 303 | - rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList); | |
| 304 | - } | |
| 305 | - log.info("rawSql:"+rawSql); | |
| 306 | - if (rawSql == null || rawSql.trim().isEmpty()) { | |
| 307 | - throw new SqlValidateException("SQL EMPTY"); | |
| 431 | + //获取缓存动态SQL | |
| 432 | +// cleanSql = getDynamicTableNl2Sql(session,input); | |
| 433 | + //如果之前已查询直接返回 | |
| 434 | + if(ObjectUtil.isEmpty(cleanSql)){ | |
| 435 | + DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); | |
| 436 | + chatMessage = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId()); | |
| 437 | + String tableNames = session.getCurrentTool().getSInputTabelName(); | |
| 438 | + // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart"; | |
| 439 | + String sDataNow = DateUtil.now(); | |
| 440 | + //DateFormatUtils.format(new Date(), "yyyy年MM月dd日HH时mm分ss秒"); | |
| 441 | +// String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); | |
| 442 | + | |
| 443 | + if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ | |
| 444 | + rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); | |
| 445 | + }else{ | |
| 446 | + rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList); | |
| 447 | + } | |
| 448 | + log.info("rawSql:"+rawSql); | |
| 449 | + if (rawSql == null || rawSql.trim().isEmpty()) { | |
| 450 | + throw new SqlValidateException("SQL EMPTY"); | |
| 451 | + } | |
| 452 | + // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) | |
| 453 | + cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); | |
| 454 | + SqlValidateUtil.validateMysqlSql(cleanSql); | |
| 455 | + doAddSql = true; | |
| 308 | 456 | } |
| 309 | - // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) | |
| 310 | - cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); | |
| 311 | - // String[] cleanSqlA = rawSql.split(";"); | |
| 312 | - // if(cleanSqlA.length>1){ | |
| 313 | - // cleanSql = cleanSqlA[cleanSqlA.length-1]; | |
| 314 | - // } | |
| 315 | - SqlValidateUtil.validateMysqlSql(cleanSql); | |
| 316 | - // 4. 执行SQL获取结构化结果 | |
| 317 | - // Map<String,Object> params = new HashMap<>(); | |
| 457 | +// List<ChatMessage> chatMessage2 = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId()); | |
| 318 | 458 | try{ |
| 319 | 459 | sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); |
| 320 | 460 | }catch (Exception e){ |
| 321 | 461 | throw new SqlGenerateException(e.getMessage()+" OLDSQL "+cleanSql); |
| 322 | 462 | } |
| 323 | 463 | }catch (SqlValidateException e){ |
| 464 | + //删除记录 | |
| 465 | +// operableChatMemoryProvider.deleteUserLasterMessageBySize(userId,3); | |
| 324 | 466 | sError_mes = e.getMessage(); |
| 325 | 467 | doAiSqlErrorHistoryThread(session, StrUtil.EMPTY, cleanSql, sError_mes,input); |
| 326 | 468 | throw e; |
| 327 | 469 | }catch (SqlGenerateException e){ |
| 470 | +// operableChatMemoryProvider.deleteUserLasterMessageBySize(userId,3); | |
| 328 | 471 | sError_mes = e.getMessage(); |
| 329 | 472 | doAiSqlErrorHistoryThread(session, StrUtil.EMPTY, cleanSql, sError_mes,input); |
| 330 | 473 | throw e; |
| ... | ... | @@ -337,9 +480,11 @@ public class XlyErpService { |
| 337 | 480 | if(Integer.valueOf(iErroCount)>0){ |
| 338 | 481 | doAiSqlErrorHistoryThread(session, cleanSql, StrUtil.EMPTY, StrUtil.EMPTY,input); |
| 339 | 482 | } |
| 340 | - | |
| 341 | - //执行操作记录表 | |
| 342 | - doAiUserAgentQuestion(session,input,cleanSql,chatMessage); | |
| 483 | + //插入常用操作 | |
| 484 | + if(doAddSql){ | |
| 485 | + //执行操作记录表 | |
| 486 | + doAiUserAgentQuestion(session,input,cleanSql,chatMessage); | |
| 487 | + } | |
| 343 | 488 | String sText = aiAgent.explainSqlResult( |
| 344 | 489 | userId, |
| 345 | 490 | userInput, |
| ... | ... | @@ -350,6 +495,28 @@ public class XlyErpService { |
| 350 | 495 | return sText; |
| 351 | 496 | } |
| 352 | 497 | |
| 498 | + /*** | |
| 499 | + * @Author 钱豹 | |
| 500 | + * @Date 17:04 2026/3/19 | |
| 501 | + * @Param [session] | |
| 502 | + * @return java.lang.String | |
| 503 | + * @Description 获取动态SQL(历史中查询) | |
| 504 | + **/ | |
| 505 | + private String getDynamicTableNl2Sql(UserSceneSession session,String input){ | |
| 506 | +// String sReidKey = SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), input); | |
| 507 | +// Object sSql = redisService.get(sReidKey); | |
| 508 | +// if(ObjectUtil.isNotEmpty(sSql)){ | |
| 509 | +// return sSql.toString(); | |
| 510 | +// } | |
| 511 | + String searchText = session.getCurrentScene().getSId()+"_"+session.getCurrentTool().getSId()+input; | |
| 512 | + //SqlValidateUtil.getsKey( session.getCurrentScene().getSId(), session.getCurrentTool().getSId(), SqlValidateUtil.getsQuestion(session.getSUserQuestionList())); | |
| 513 | + //根据问题查询向量库 | |
| 514 | + Map<String,Object> serMap = aiGlobalAgentQuestionSqlEmitterService.queryAiGlobalAgentQuestionSqlEmitter(searchText, "ai_global_agent_question_sql"); | |
| 515 | + if(ObjectUtil.isNotEmpty(serMap)){ | |
| 516 | + return serMap.get("sSqlContent").toString(); | |
| 517 | + } | |
| 518 | + return null; | |
| 519 | + } | |
| 353 | 520 | |
| 354 | 521 | /*** |
| 355 | 522 | * @Author 钱豹 |
| ... | ... | @@ -485,7 +652,11 @@ public class XlyErpService { |
| 485 | 652 | .build(); |
| 486 | 653 | UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); |
| 487 | 654 | // 初始化AiService 以防止热加载太慢 找不到相应的方法 |
| 488 | - aiAgent.chat(userId, "initAiService"); | |
| 655 | + try{ | |
| 656 | + aiAgent.chat(userId, "initAiService"); | |
| 657 | + }catch (Exception e){ | |
| 658 | + e.printStackTrace(); | |
| 659 | + } | |
| 489 | 660 | log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); |
| 490 | 661 | } |
| 491 | 662 | return aiAgent; | ... | ... |
src/main/java/com/xly/thread/AiUserAgentQuestionThread.java
| 1 | 1 | package com.xly.thread; |
| 2 | 2 | |
| 3 | 3 | |
| 4 | +import cn.hutool.core.lang.generator.UUIDGenerator; | |
| 4 | 5 | import cn.hutool.core.util.ObjectUtil; |
| 5 | 6 | import cn.hutool.core.util.StrUtil; |
| 6 | -import com.xly.config.OperableChatMemoryProvider; | |
| 7 | 7 | import com.xly.config.SpringContextHolder; |
| 8 | 8 | import com.xly.entity.UserSceneSession; |
| 9 | +import com.xly.milvus.service.AiGlobalAgentQuestionSqlEmitterService; | |
| 9 | 10 | import com.xly.service.DynamicExeDbService; |
| 11 | +import com.xly.service.RedisService; | |
| 12 | +import com.xly.util.MD5Util; | |
| 13 | +import com.xly.util.SqlValidateUtil; | |
| 10 | 14 | import dev.langchain4j.data.message.ChatMessage; |
| 11 | 15 | import dev.langchain4j.data.message.ChatMessageType; |
| 12 | -import jnr.ffi.annotations.In; | |
| 13 | - | |
| 14 | -import java.util.Arrays; | |
| 15 | 16 | import java.util.HashMap; |
| 16 | 17 | import java.util.List; |
| 17 | 18 | import java.util.Map; |
| ... | ... | @@ -36,17 +37,30 @@ public class AiUserAgentQuestionThread implements Runnable { |
| 36 | 37 | String sSceneId = session.getCurrentScene().getSId(); |
| 37 | 38 | String sMethodId = session.getCurrentTool().getSId(); |
| 38 | 39 | DynamicExeDbService dynamicExeDbService = SpringContextHolder.getBean(DynamicExeDbService.class); |
| 40 | + RedisService redisService = SpringContextHolder.getBean(RedisService.class); | |
| 41 | + AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService = SpringContextHolder.getBean(AiGlobalAgentQuestionSqlEmitterService.class); | |
| 39 | 42 | String sQuestionGroupNo = session.getSUserQuestionList().get(0); |
| 40 | 43 | Integer bRedis = (session.getSUserQuestionList().size()==1)?1:0; |
| 41 | 44 | Map<String, Object> data = getMap(sSceneId, sMethodId,bRedis,sQuestionGroupNo); |
| 42 | - data.put("sQuestion",getsQuestion(session.getSUserQuestionList())); | |
| 45 | + data.put("sQuestion",SqlValidateUtil.getsQuestion(session.getSUserQuestionList())); | |
| 46 | + data.put("sId",new UUIDGenerator().next()); | |
| 47 | + //插入Redis缓存 | |
| 48 | + if(bRedis==1 && ObjectUtil.isNotEmpty(sSqlContent)){ | |
| 49 | + String sReidKey = SqlValidateUtil.getsKey( sSceneId, sMethodId, sQuestionGroupNo); | |
| 50 | + redisService.set(sReidKey,sSqlContent); | |
| 51 | + } | |
| 52 | + String sKey = sSceneId+"_"+sMethodId +"_"+sQuestion; | |
| 53 | +// SqlValidateUtil.getsKey( sSceneId, sMethodId, SqlValidateUtil.getsQuestion(session.getSUserQuestionList())); | |
| 54 | + //存入向量库 | |
| 55 | + aiGlobalAgentQuestionSqlEmitterService.addAiGlobalAgentQuestionSqlEmitter(sKey,data,sQuestion,sSqlContent,"ai_global_agent_question_sql"); | |
| 56 | + //调用数据库插入数据库 | |
| 43 | 57 | Map<String, Object> searMap = dynamicExeDbService.getDoProMap(sProName, data); |
| 44 | 58 | dynamicExeDbService.getCallPro(searMap, sProName); |
| 45 | 59 | } |
| 46 | 60 | |
| 47 | - private String getsQuestion(List<String> sUserQuestionList){ | |
| 48 | - return String.join(",", sUserQuestionList); | |
| 49 | - } | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 50 | 64 | |
| 51 | 65 | //获取组ID |
| 52 | 66 | private String getQuestionGroupNo(){ | ... | ... |
src/main/java/com/xly/token/RedisTokenManager.java
src/main/java/com/xly/tts/bean/TTSResponseDTO.java
| ... | ... | @@ -6,6 +6,7 @@ import lombok.AllArgsConstructor; |
| 6 | 6 | import lombok.Builder; |
| 7 | 7 | import lombok.Data; |
| 8 | 8 | import lombok.NoArgsConstructor; |
| 9 | +import reactor.core.publisher.Flux; | |
| 9 | 10 | |
| 10 | 11 | import java.io.Serializable; |
| 11 | 12 | |
| ... | ... | @@ -70,6 +71,8 @@ public class TTSResponseDTO implements Serializable { |
| 70 | 71 | |
| 71 | 72 | private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode(); |
| 72 | 73 | |
| 74 | + private Boolean ErpComplete; | |
| 75 | + | |
| 73 | 76 | /** |
| 74 | 77 | * 创建失败响应 |
| 75 | 78 | */ | ... | ... |
src/main/java/com/xly/tts/service/PythonTtsProxyService.java
| ... | ... | @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil; |
| 4 | 4 | import cn.hutool.core.util.StrUtil; |
| 5 | 5 | import com.xly.constant.BusinessCode; |
| 6 | 6 | import com.xly.constant.ReturnTypeCode; |
| 7 | +import com.xly.entity.AiResponseAccumulator; | |
| 7 | 8 | import com.xly.entity.AiResponseDTO; |
| 8 | 9 | import com.xly.service.UserSceneSessionService; |
| 9 | 10 | import com.xly.service.XlyErpService; |
| ... | ... | @@ -16,15 +17,15 @@ import org.springframework.core.io.InputStreamResource; |
| 16 | 17 | import org.springframework.http.*; |
| 17 | 18 | import org.springframework.stereotype.Service; |
| 18 | 19 | import org.springframework.web.client.RestTemplate; |
| 20 | +import reactor.core.publisher.Flux; | |
| 19 | 21 | |
| 20 | 22 | import javax.annotation.PostConstruct; |
| 21 | 23 | import java.io.*; |
| 24 | +import java.time.Duration; | |
| 22 | 25 | import java.util.*; |
| 23 | 26 | import java.util.concurrent.CompletableFuture; |
| 24 | 27 | import java.util.concurrent.ExecutorService; |
| 25 | 28 | import java.util.concurrent.Executors; |
| 26 | -import java.net.URL; | |
| 27 | -import java.net.HttpURLConnection; | |
| 28 | 29 | import java.io.InputStream; |
| 29 | 30 | |
| 30 | 31 | @Slf4j |
| ... | ... | @@ -81,6 +82,111 @@ public class PythonTtsProxyService { |
| 81 | 82 | return synthesizeStreamAi(request, voiceText); |
| 82 | 83 | } |
| 83 | 84 | |
| 85 | + /** | |
| 86 | + * 流式ERP + 流式TTS合成 | |
| 87 | + * 先流式输出ERP文本,完成后自动开始TTS合成 | |
| 88 | + * 使用现有TTSResponseDTO字段 | |
| 89 | + */ | |
| 90 | + public Flux<TTSResponseDTO> synthesizeStreamAiStream(TTSRequestDTO request) { | |
| 91 | + String userInput = request.getText(); | |
| 92 | + String sUserId = request.getUserid(); | |
| 93 | + String sUserName = request.getUsername(); | |
| 94 | + String sBrandsId = request.getBrandsid(); | |
| 95 | + String sSubsidiaryId = request.getSubsidiaryid(); | |
| 96 | + String sUserType = request.getUsertype(); | |
| 97 | + String authorization = request.getAuthorization(); | |
| 98 | + | |
| 99 | + String requestId = UUID.randomUUID().toString(); | |
| 100 | + log.info("开始流式处理: requestId={}, userId={}", requestId, sUserId); | |
| 101 | + | |
| 102 | + // 创建累积器(用于累积完整的AiResponseDTO) | |
| 103 | + AiResponseAccumulator accumulator = new AiResponseAccumulator(requestId); | |
| 104 | + | |
| 105 | + // 1. 处理ERP流,将AiResponseDTO转换为TTSResponseDTO | |
| 106 | + Flux<TTSResponseDTO> erpStream = xlyErpService.erpUserInputStream( | |
| 107 | + userInput, sUserId, sUserName, sBrandsId, | |
| 108 | + sSubsidiaryId, sUserType, authorization | |
| 109 | + ) | |
| 110 | + .doOnNext(aiResponse -> { | |
| 111 | + // 设置请求ID | |
| 112 | + aiResponse.setRequestId(requestId); | |
| 113 | + // 后台累积完整文本(为后续TTS做准备) | |
| 114 | + accumulator.accumulate(aiResponse); | |
| 115 | + log.debug("收到ERP片段: requestId={}, chunk={}/{}", | |
| 116 | + requestId, | |
| 117 | + aiResponse.getChunkIndex(), | |
| 118 | + aiResponse.getTotalChunks()); | |
| 119 | + }) | |
| 120 | + .map(aiResponse -> { | |
| 121 | + // 将AiResponseDTO转换为TTSResponseDTO | |
| 122 | + // 使用processedText字段传递AI文本片段 | |
| 123 | + // 使用systemText字段传递系统文本片段 | |
| 124 | + return TTSResponseDTO.builder() | |
| 125 | + .code(200) | |
| 126 | + .message("ERP_CHUNK") // message字段标记为ERP文本块 | |
| 127 | + .requestId(requestId) | |
| 128 | + .processedText(aiResponse.getTextFragment()) // 用processedText传递AI文本片段 | |
| 129 | + .systemText(aiResponse.getSystemTextFragment()) // 用systemText传递系统文本片段 | |
| 130 | + .sSceneName(aiResponse.getSSceneName()) | |
| 131 | + .sMethodName(aiResponse.getSMethodName()) | |
| 132 | + .sReturnType(aiResponse.getSReturnType()) | |
| 133 | + .timestamp(System.currentTimeMillis()) | |
| 134 | + .build(); | |
| 135 | + }); | |
| 136 | + | |
| 137 | + // 2. ERP完成后,发送完成标记,然后开始TTS合成 | |
| 138 | + return erpStream | |
| 139 | + .concatWith(Flux.defer(() -> { | |
| 140 | + // 获取完整的累积结果 | |
| 141 | + AiResponseDTO completeResponse = accumulator.getCompleteResponse(); | |
| 142 | + | |
| 143 | + // 验证ERP结果 | |
| 144 | + if (StrUtil.isBlank(completeResponse.getAiText())) { | |
| 145 | + log.warn("ERP返回空文本: requestId={}", requestId); | |
| 146 | + return Flux.error(new RuntimeException("ERP返回空文本")); | |
| 147 | + } | |
| 148 | + | |
| 149 | + log.info("ERP流式处理完成,开始TTS合成: requestId={}, aiText长度={}", | |
| 150 | + requestId, completeResponse.getAiText().length()); | |
| 151 | + | |
| 152 | + // 3. 发送ERP完成消息(使用完整文本) | |
| 153 | + TTSResponseDTO erpCompleteDto = TTSResponseDTO.builder() | |
| 154 | + .code(200) | |
| 155 | + .message("ERP_COMPLETE") // message标记完成 | |
| 156 | + .requestId(requestId) | |
| 157 | + .processedText(completeResponse.getAiText()) // 完整AI文本 | |
| 158 | + .systemText(completeResponse.getSystemText()) // 完整系统文本 | |
| 159 | + .sSceneName(completeResponse.getSSceneName()) | |
| 160 | + .sMethodName(completeResponse.getSMethodName()) | |
| 161 | + .sReturnType(completeResponse.getSReturnType()) | |
| 162 | + .timestamp(System.currentTimeMillis()) | |
| 163 | + .build(); | |
| 164 | + | |
| 165 | + // 4. 调用TTS合成(返回TTSResponseDTO流) | |
| 166 | + Flux<TTSResponseDTO> ttsStream = synthesizeStreamAiNew(request, completeResponse) | |
| 167 | + .doOnNext(ttsResponse -> { | |
| 168 | + ttsResponse.setRequestId(requestId); | |
| 169 | + ttsResponse.setMessage("TTS_SEGMENT"); // message标记为TTS音频段 | |
| 170 | + }); | |
| 171 | + | |
| 172 | + // 先发送ERP完成消息,再发送TTS流 | |
| 173 | + return Flux.concat(Flux.just(erpCompleteDto), ttsStream); | |
| 174 | + })) | |
| 175 | + // 超时控制 | |
| 176 | + .timeout(Duration.ofSeconds(120)) | |
| 177 | + // 错误处理 | |
| 178 | + .onErrorResume(e -> { | |
| 179 | + log.error("流式处理失败: requestId={}, error={}", requestId, e.getMessage()); | |
| 180 | + return Flux.just(TTSResponseDTO.error(requestId, 500, e.getMessage())); | |
| 181 | + }) | |
| 182 | + // 日志记录 | |
| 183 | + .doOnComplete(() -> log.info("流式处理完成: requestId={}", requestId)) | |
| 184 | + .doOnCancel(() -> log.warn("流式处理取消: requestId={}", requestId)); | |
| 185 | + } | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 84 | 190 | public ResponseEntity<TTSResponseDTO> cleanMemory(TTSRequestDTO request) { |
| 85 | 191 | String sUserId = request.getUserid(); |
| 86 | 192 | String sUserName = request.getUsername(); |
| ... | ... | @@ -123,7 +229,7 @@ public class PythonTtsProxyService { |
| 123 | 229 | } |
| 124 | 230 | |
| 125 | 231 | /** |
| 126 | - * 【保持原有返回类型】不动!内部流式请求Python | |
| 232 | + * 内部流式请求Python | |
| 127 | 233 | */ |
| 128 | 234 | public ResponseEntity<TTSResponseDTO> synthesizeStreamAi(TTSRequestDTO request, AiResponseDTO aiResponseDTO) { |
| 129 | 235 | String aiText = aiResponseDTO.getAiText(); |
| ... | ... | @@ -132,12 +238,10 @@ public class PythonTtsProxyService { |
| 132 | 238 | systemText = StrUtil.EMPTY; |
| 133 | 239 | } |
| 134 | 240 | String voiceTextNew = AdvancedSymbolRemover.removePunctuationHtml(aiText); |
| 135 | - | |
| 136 | 241 | // ============================ |
| 137 | 242 | // 【绝对唯一】不会重复、不会覆盖 |
| 138 | 243 | // ============================ |
| 139 | 244 | String cacheKey = request.getUserid() + "_" + System.nanoTime(); |
| 140 | - | |
| 141 | 245 | TTSResponseDTO dto = TTSResponseDTO.builder() |
| 142 | 246 | .code(200) |
| 143 | 247 | .message("success") |
| ... | ... | @@ -159,7 +263,6 @@ public class PythonTtsProxyService { |
| 159 | 263 | if (!voiceless || ObjectUtil.isEmpty(voiceTextNew)) { |
| 160 | 264 | return ResponseEntity.ok(dto); |
| 161 | 265 | } |
| 162 | - | |
| 163 | 266 | // 平均分割文字 |
| 164 | 267 | List<String> textParts = splitTextSmart(voiceTextNew, 30); |
| 165 | 268 | dto.setAudioSize(textParts.size()); |
| ... | ... | @@ -199,14 +302,91 @@ public class PythonTtsProxyService { |
| 199 | 302 | } |
| 200 | 303 | } |
| 201 | 304 | }, executorService); |
| 202 | - | |
| 203 | 305 | return ResponseEntity.ok(dto); |
| 204 | 306 | } |
| 205 | 307 | |
| 308 | + | |
| 309 | + /** | |
| 310 | + * 内部流式请求Python | |
| 311 | + */ | |
| 312 | + public Flux<TTSResponseDTO> synthesizeStreamAiNew(TTSRequestDTO request, AiResponseDTO aiResponseDTO) { | |
| 313 | + String aiText = aiResponseDTO.getAiText(); | |
| 314 | + String systemText = aiResponseDTO.getSystemText(); | |
| 315 | + if (ObjectUtil.isEmpty(systemText)) { | |
| 316 | + systemText = StrUtil.EMPTY; | |
| 317 | + } | |
| 318 | + String voiceTextNew = AdvancedSymbolRemover.removePunctuationHtml(aiText); | |
| 319 | + // ============================ | |
| 320 | + // 【绝对唯一】不会重复、不会覆盖 | |
| 321 | + // ============================ | |
| 322 | + String cacheKey = request.getUserid() + "_" + System.nanoTime(); | |
| 323 | + TTSResponseDTO dto = TTSResponseDTO.builder() | |
| 324 | + .code(200) | |
| 325 | + .message("success") | |
| 326 | + .cacheKey(cacheKey) // 前端靠这个取自己的分段 | |
| 327 | + .originalText(request.getText()) | |
| 328 | + .processedText(aiText) | |
| 329 | + .audioText(voiceTextNew) | |
| 330 | + .systemText(systemText) | |
| 331 | + .voice(request.getVoice()) | |
| 332 | + .sSceneName(aiResponseDTO.getSSceneName()) | |
| 333 | + .sMethodName(aiResponseDTO.getSMethodName()) | |
| 334 | + .sReturnType(aiResponseDTO.getSReturnType()) | |
| 335 | + .sCommonts(BusinessCode.COMMONTS.getMessage()) | |
| 336 | + .timestamp(System.currentTimeMillis()) | |
| 337 | + .textLength((aiText + systemText).length()) | |
| 338 | + .build(); | |
| 339 | + | |
| 340 | + boolean voiceless = Boolean.TRUE.equals(request.getVoiceless()); | |
| 341 | + if (!voiceless || ObjectUtil.isEmpty(voiceTextNew)) { | |
| 342 | + return Flux.just(dto); | |
| 343 | + } | |
| 344 | + // 平均分割文字 | |
| 345 | + List<String> textParts = splitTextSmart(voiceTextNew, 30); | |
| 346 | + dto.setAudioSize(textParts.size()); | |
| 347 | + // 异步分段合成 | |
| 348 | + CompletableFuture.runAsync(() -> { | |
| 349 | + for (int i = 0; i < textParts.size(); i++) { | |
| 350 | + String part = textParts.get(i); | |
| 351 | + if (ObjectUtil.isEmpty(part)) continue; | |
| 352 | + | |
| 353 | + try { | |
| 354 | + Map<String, Object> params = new HashMap<>(); | |
| 355 | + params.put("text", part); | |
| 356 | + params.put("voice", request.getVoice()); | |
| 357 | + params.put("rate", request.getRate() != null ? request.getRate() : "+10%"); | |
| 358 | + params.put("volume", request.getVolume() != null ? request.getVolume() : "+0%"); | |
| 359 | + | |
| 360 | + HttpHeaders headers = new HttpHeaders(); | |
| 361 | + headers.setContentType(MediaType.APPLICATION_JSON); | |
| 362 | + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM)); | |
| 363 | + HttpEntity<Map<String, Object>> entity = new HttpEntity<>(params, headers); | |
| 364 | + | |
| 365 | + ResponseEntity<byte[]> response = restTemplate.exchange( | |
| 366 | + pythonServiceUrl + "/stream-synthesize", | |
| 367 | + HttpMethod.POST, entity, byte[].class | |
| 368 | + ); | |
| 369 | + | |
| 370 | + if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { | |
| 371 | + String base64 = Base64.getEncoder().encodeToString(response.getBody()); | |
| 372 | + | |
| 373 | + // ============================ | |
| 374 | + // 【关键】带序号存储!前端靠序号知道顺序! | |
| 375 | + // ============================ | |
| 376 | + LocalAudioCache.addPiece(cacheKey, i, part, base64); | |
| 377 | + } | |
| 378 | + } catch (Exception e) { | |
| 379 | + log.warn("分段合成失败: {}", e.getMessage()); | |
| 380 | + } | |
| 381 | + } | |
| 382 | + }, executorService); | |
| 383 | + return Flux.just(dto); | |
| 384 | + } | |
| 385 | + | |
| 386 | + // ============================================== | |
| 387 | + // 智能分段:优先按 。!?; , 空格 断开 | |
| 388 | + // 不会把一句话生硬切断,更自然 | |
| 206 | 389 | // ============================================== |
| 207 | -// 智能分段:优先按 。!?; , 空格 断开 | |
| 208 | -// 不会把一句话生硬切断,更自然 | |
| 209 | -// ============================================== | |
| 210 | 390 | private List<String> splitTextSmart(String text, int maxLength) { |
| 211 | 391 | List<String> parts = new ArrayList<>(); |
| 212 | 392 | if (text == null || text.isEmpty()) return parts; | ... | ... |
src/main/java/com/xly/util/MD5Util.java
0 → 100644
| 1 | +package com.xly.util; | |
| 2 | + | |
| 3 | +import java.security.MessageDigest; | |
| 4 | +import java.security.NoSuchAlgorithmException; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * MD5加密工具类 | |
| 8 | + */ | |
| 9 | +public class MD5Util { | |
| 10 | + | |
| 11 | + /** | |
| 12 | + * 将字符串进行MD5加密 | |
| 13 | + * @param input 输入字符串 | |
| 14 | + * @return MD5加密后的32位小写字符串 | |
| 15 | + */ | |
| 16 | + public static String encrypt(String input) { | |
| 17 | + if (input == null || input.isEmpty()) { | |
| 18 | + return null; | |
| 19 | + } | |
| 20 | + | |
| 21 | + try { | |
| 22 | + MessageDigest md = MessageDigest.getInstance("MD5"); | |
| 23 | + byte[] messageDigest = md.digest(input.getBytes()); | |
| 24 | + | |
| 25 | + // 将字节数组转换为十六进制字符串 | |
| 26 | + StringBuilder hexString = new StringBuilder(); | |
| 27 | + for (byte b : messageDigest) { | |
| 28 | + String hex = Integer.toHexString(0xff & b); | |
| 29 | + if (hex.length() == 1) { | |
| 30 | + hexString.append('0'); | |
| 31 | + } | |
| 32 | + hexString.append(hex); | |
| 33 | + } | |
| 34 | + return hexString.toString(); | |
| 35 | + | |
| 36 | + } catch (NoSuchAlgorithmException e) { | |
| 37 | + throw new RuntimeException("MD5加密失败", e); | |
| 38 | + } | |
| 39 | + } | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 将字符串进行MD5加密(大写) | |
| 43 | + * @param input 输入字符串 | |
| 44 | + * @return MD5加密后的32位大写字符串 | |
| 45 | + */ | |
| 46 | + public static String encryptToUpperCase(String input) { | |
| 47 | + String result = encrypt(input); | |
| 48 | + return result != null ? result.toUpperCase() : null; | |
| 49 | + } | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 验证字符串与MD5值是否匹配 | |
| 53 | + * @param input 输入字符串 | |
| 54 | + * @param md5 MD5值 | |
| 55 | + * @return 是否匹配 | |
| 56 | + */ | |
| 57 | + public static boolean verify(String input, String md5) { | |
| 58 | + String encrypted = encrypt(input); | |
| 59 | + return encrypted != null && encrypted.equalsIgnoreCase(md5); | |
| 60 | + } | |
| 61 | + | |
| 62 | + /** | |
| 63 | + * 获取文件的MD5值 | |
| 64 | + * @param bytes 文件字节数组 | |
| 65 | + * @return 文件MD5值 | |
| 66 | + */ | |
| 67 | + public static String getFileMD5(byte[] bytes) { | |
| 68 | + try { | |
| 69 | + MessageDigest md = MessageDigest.getInstance("MD5"); | |
| 70 | + byte[] messageDigest = md.digest(bytes); | |
| 71 | + | |
| 72 | + StringBuilder hexString = new StringBuilder(); | |
| 73 | + for (byte b : messageDigest) { | |
| 74 | + String hex = Integer.toHexString(0xff & b); | |
| 75 | + if (hex.length() == 1) { | |
| 76 | + hexString.append('0'); | |
| 77 | + } | |
| 78 | + hexString.append(hex); | |
| 79 | + } | |
| 80 | + return hexString.toString(); | |
| 81 | + | |
| 82 | + } catch (NoSuchAlgorithmException e) { | |
| 83 | + throw new RuntimeException("文件MD5计算失败", e); | |
| 84 | + } | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * MD5加盐加密 | |
| 89 | + * @param input 输入字符串 | |
| 90 | + * @param salt 盐值 | |
| 91 | + * @return 加盐后的MD5值 | |
| 92 | + */ | |
| 93 | + public static String encryptWithSalt(String input, String salt) { | |
| 94 | + return encrypt(input + salt); | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 双重MD5加密 | |
| 99 | + * @param input 输入字符串 | |
| 100 | + * @return 双重MD5加密结果 | |
| 101 | + */ | |
| 102 | + public static String doubleEncrypt(String input) { | |
| 103 | + String first = encrypt(input); | |
| 104 | + return encrypt(first); | |
| 105 | + } | |
| 106 | +} | |
| 0 | 107 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/util/SqlValidateUtil.java
| ... | ... | @@ -103,4 +103,13 @@ public class SqlValidateUtil { |
| 103 | 103 | .replaceAll("\\n|\\r", " ") |
| 104 | 104 | .trim(); |
| 105 | 105 | } |
| 106 | + | |
| 107 | + public static String getsKey(String sSceneId,String sMethodId,String sQuestion){ | |
| 108 | +// sSceneId+sMethodId+sQuestion | |
| 109 | + return sSceneId+"_"+sMethodId+"_"+ MD5Util.encrypt(sQuestion); | |
| 110 | + } | |
| 111 | + | |
| 112 | + public static String getsQuestion(List<String> sUserQuestionList){ | |
| 113 | + return String.join(",", sUserQuestionList); | |
| 114 | + } | |
| 106 | 115 | } | ... | ... |
src/main/java/com/xly/web/TTSStreamController.java
| ... | ... | @@ -7,6 +7,7 @@ import com.xly.tool.DynamicToolProvider; |
| 7 | 7 | import com.xly.tts.bean.*; |
| 8 | 8 | import com.xly.tts.service.LocalAudioCache; |
| 9 | 9 | import com.xly.tts.service.PythonTtsProxyService; |
| 10 | +import jakarta.validation.Valid; | |
| 10 | 11 | import lombok.RequiredArgsConstructor; |
| 11 | 12 | import lombok.extern.slf4j.Slf4j; |
| 12 | 13 | import org.springframework.core.io.InputStreamResource; |
| ... | ... | @@ -14,6 +15,8 @@ import org.springframework.http.MediaType; |
| 14 | 15 | import org.springframework.http.ResponseEntity; |
| 15 | 16 | import org.springframework.web.bind.annotation.*; |
| 16 | 17 | import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; |
| 18 | +import reactor.core.publisher.Flux; | |
| 19 | +import reactor.core.publisher.Mono; | |
| 17 | 20 | |
| 18 | 21 | import javax.annotation.PostConstruct; |
| 19 | 22 | import javax.annotation.PreDestroy; |
| ... | ... | @@ -82,6 +85,24 @@ public class TTSStreamController { |
| 82 | 85 | return pythonTtsProxyService.synthesizeStreamAi(request); |
| 83 | 86 | } |
| 84 | 87 | |
| 88 | + /** | |
| 89 | + * 流式合成语音(代理到Python服务) | |
| 90 | + */ | |
| 91 | + @PostMapping(value = "/stream/queryFlux", | |
| 92 | + consumes = {MediaType.APPLICATION_JSON_VALUE}, | |
| 93 | + produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE, | |
| 94 | + MediaType.APPLICATION_JSON_VALUE}) | |
| 95 | + public Flux<TTSResponseDTO> streamFlux(@Valid @RequestBody Mono<TTSRequestDTO> requestMono) { | |
| 96 | + return requestMono.flatMapMany(request -> { | |
| 97 | + log.info("处理请求: requestId={}, text长度={}", request.getUserid(), request.getText().length()); | |
| 98 | + return pythonTtsProxyService.synthesizeStreamAiStream(request); | |
| 99 | + }) | |
| 100 | + .doOnSubscribe(sub -> log.debug("流式订阅开始")) | |
| 101 | + .doOnCancel(() -> log.debug("流式请求被取消")) | |
| 102 | + .doOnComplete(() -> log.debug("流式响应完成")) | |
| 103 | + .doOnError(e -> log.error("流式处理错误", e)); | |
| 104 | + } | |
| 105 | + | |
| 85 | 106 | @GetMapping("/audio/piece") |
| 86 | 107 | public ResponseEntity<Map<String, String>> getPiece( |
| 87 | 108 | @RequestParam String cacheKey, |
| ... | ... | @@ -210,81 +231,6 @@ public class TTSStreamController { |
| 210 | 231 | } |
| 211 | 232 | |
| 212 | 233 | /** |
| 213 | - * SSE流式输出(Server-Sent Events) | |
| 214 | - */ | |
| 215 | - @GetMapping(value = "/sse-stream", produces = "text/event-stream") | |
| 216 | - public ResponseEntity<StreamingResponseBody> sseStream( | |
| 217 | - @RequestParam String text, | |
| 218 | - @RequestParam(defaultValue = "zh-CN-XiaoxiaoNeural") String voice) { | |
| 219 | - | |
| 220 | - log.info("收到SSE流式请求: voice={}", voice); | |
| 221 | - | |
| 222 | - TTSRequestDTO request = new TTSRequestDTO(); | |
| 223 | - request.setText(text); | |
| 224 | - request.setVoice(voice); | |
| 225 | - | |
| 226 | - StreamingResponseBody responseBody = outputStream -> { | |
| 227 | - try { | |
| 228 | - outputStream.write(("event: audio-start\ndata: \n\n").getBytes()); | |
| 229 | - outputStream.flush(); | |
| 230 | - | |
| 231 | - // 调用Python服务获取音频 | |
| 232 | - ResponseEntity<InputStreamResource> response = pythonTtsProxyService.synthesizeStream(request); | |
| 233 | - | |
| 234 | - if (response.getBody() != null) { | |
| 235 | - InputStream inputStream = response.getBody().getInputStream(); | |
| 236 | - byte[] buffer = new byte[1024]; | |
| 237 | - int bytesRead; | |
| 238 | - | |
| 239 | - int totalBytes = 0; | |
| 240 | - while ((bytesRead = inputStream.read(buffer)) != -1) { | |
| 241 | - totalBytes += bytesRead; | |
| 242 | - | |
| 243 | - // 发送进度事件 | |
| 244 | - String progressEvent = String.format( | |
| 245 | - "event: progress\ndata: {\"bytes\":%d}\n\n", totalBytes); | |
| 246 | - outputStream.write(progressEvent.getBytes()); | |
| 247 | - outputStream.flush(); | |
| 248 | - | |
| 249 | - // 发送音频数据(base64编码) | |
| 250 | - String base64Data = java.util.Base64.getEncoder().encodeToString( | |
| 251 | - java.util.Arrays.copyOfRange(buffer, 0, bytesRead)); | |
| 252 | - String audioEvent = String.format( | |
| 253 | - "event: audio-data\ndata: {\"chunk\":\"%s\"}\n\n", base64Data); | |
| 254 | - outputStream.write(audioEvent.getBytes()); | |
| 255 | - outputStream.flush(); | |
| 256 | - } | |
| 257 | - | |
| 258 | - // 发送完成事件 | |
| 259 | - String completeEvent = String.format( | |
| 260 | - "event: audio-complete\ndata: {\"total_bytes\":%d}\n\n", totalBytes); | |
| 261 | - outputStream.write(completeEvent.getBytes()); | |
| 262 | - outputStream.flush(); | |
| 263 | - } else { | |
| 264 | - outputStream.write(("event: error\ndata: {\"message\":\"合成失败\"}\n\n").getBytes()); | |
| 265 | - outputStream.flush(); | |
| 266 | - } | |
| 267 | - | |
| 268 | - } catch (Exception e) { | |
| 269 | -// log.error("SSE流式输出异常: {}", e.getMessage(), e); | |
| 270 | - try { | |
| 271 | - outputStream.write(("event: error\ndata: {\"message\":\"" + | |
| 272 | - e.getMessage().replace("\"", "\\\"") + "\"}\n\n").getBytes()); | |
| 273 | - outputStream.flush(); | |
| 274 | - } catch (Exception ex) { | |
| 275 | - // 忽略关闭错误 | |
| 276 | - } | |
| 277 | - } | |
| 278 | - }; | |
| 279 | - | |
| 280 | - return ResponseEntity.ok() | |
| 281 | - .header("Content-Type", "text/event-stream") | |
| 282 | - .header("Cache-Control", "no-cache") | |
| 283 | - .header("X-Accel-Buffering", "no") // 禁用Nginx缓冲 | |
| 284 | - .body(responseBody); | |
| 285 | - } | |
| 286 | - | |
| 287 | - /** | |
| 288 | 234 | * 测试接口 |
| 289 | 235 | */ |
| 290 | 236 | @GetMapping("/test") | ... | ... |
src/main/resources/application.yml
| ... | ... | @@ -7,6 +7,8 @@ logging: |
| 7 | 7 | com.xly: debug |
| 8 | 8 | com.xlyflow: debug |
| 9 | 9 | org.springframework: warn |
| 10 | + ai.djl: DEBUG | |
| 11 | + dev.langchain4j: DEBUG | |
| 10 | 12 | |
| 11 | 13 | server: |
| 12 | 14 | port: 8099 |
| ... | ... | @@ -52,35 +54,99 @@ spring: |
| 52 | 54 | # REDIS (RedisProperties) |
| 53 | 55 | data: |
| 54 | 56 | redis: |
| 55 | - host: localhost | |
| 56 | - port: 6379 # Redis 端口 | |
| 57 | - database: 0 # 使用的数据库索引(默认为0) | |
| 58 | - password: # 密码(如果没有设置则为空) | |
| 59 | - timeout: 3000ms # 连接超时时间 | |
| 57 | + host: 127.0.0.1 | |
| 58 | + password: xlyXLY2015 | |
| 59 | + port: 16379 | |
| 60 | + database: 0 # index | |
| 61 | + timeout: 30000ms # 连接超时时长(毫秒) | |
| 62 | + block-when-exhausted: true #redis配置结束 | |
| 60 | 63 | lettuce: |
| 61 | 64 | pool: |
| 62 | 65 | max-active: 8 # 连接池最大连接数 |
| 63 | 66 | max-idle: 8 # 连接池最大空闲连接 |
| 64 | - min-idle: 0 # 连接池最小空闲连接 | |
| 67 | + min-idle: 0 # 连接池最小空闲连接' | |
| 68 | + | |
| 69 | +milvus: | |
| 70 | + host: 112.82.245.194 | |
| 71 | + port: 19530 | |
| 72 | + database: xlymilvus | |
| 73 | + username: | |
| 74 | + password: | |
| 75 | + collection: | |
| 76 | + question-sql: question_sql_vectors | |
| 77 | + sync: | |
| 78 | + batch-size: 100 | |
| 79 | + vector: | |
| 80 | + dimension: 384 # All-MiniLM-L6-v2 模型的维度 | |
| 81 | + text: | |
| 82 | + max-length: 65535 | |
| 83 | + schema: | |
| 84 | + type: base # base, detailed, partitioned, searchable | |
| 85 | + # 重试配置 | |
| 86 | + retry: | |
| 87 | + enabled: true # 是否启用重试 | |
| 88 | + max-retry-times: 75 # 最大重试次数(SDK默认值) | |
| 89 | + initial-backoff-ms: 10 # 初始退避时间(ms) | |
| 90 | + max-backoff-ms: 3000 # 最大退避时间(ms) | |
| 91 | + backoff-multiplier: 3 # 退避乘数 | |
| 92 | + retry-on-rate-limit: true # 遇到限流是否重试 | |
| 93 | + max-retry-timeout-ms: 0 # 最大重试超时时间(0表示不限制) | |
| 94 | + | |
| 95 | + # 操作级别的重试配置(可选,覆盖默认值) | |
| 96 | + operations: | |
| 97 | + query: | |
| 98 | + max-retry-times: 3 | |
| 99 | + initial-backoff-ms: 100 | |
| 100 | + max-backoff-ms: 2000 | |
| 101 | + insert: | |
| 102 | + max-retry-times: 2 | |
| 103 | + initial-backoff-ms: 50 | |
| 104 | + max-backoff-ms: 1000 | |
| 105 | + search: | |
| 106 | + max-retry-times: 3 | |
| 107 | + initial-backoff-ms: 200 | |
| 108 | + max-backoff-ms: 3000 | |
| 109 | + max-retry-timeout-ms: 10000 | |
| 110 | + | |
| 111 | +# Actuator配置 | |
| 112 | +management: | |
| 113 | + endpoints: | |
| 114 | + web: | |
| 115 | + exposure: | |
| 116 | + include: health,info,metrics | |
| 117 | + endpoint: | |
| 118 | + health: | |
| 119 | + show-details: always | |
| 120 | + health: | |
| 121 | + milvus: | |
| 122 | + enabled: true | |
| 65 | 123 | |
| 66 | 124 | # application.yml 或 application.properties |
| 67 | 125 | langchain4j: |
| 68 | 126 | ollama: |
| 69 | 127 | # 聊天模型配置(用于一般对话) |
| 70 | - base-url: http://121.43.128.225:11434 | |
| 128 | + base-url: http://112.82.245.194:11434 | |
| 71 | 129 | chat-model-name: qwen2.5:7b-instruct |
| 130 | +# chat-model-name: qwen3.5:9b | |
| 72 | 131 | # SQL/代码模型配置(专门用于代码和SQL生成) |
| 73 | - sql-model-name: qwen2.5-coder:32b | |
| 132 | + sql-model-name: qwen2.5-coder:7b | |
| 133 | +# sql-model-name: qwen2.5-coder:32b | |
| 134 | +# sql-model-name: mdq100/qwen3.5-coder:35b | |
| 74 | 135 | # 或者如果两个模型在同一服务器,可以使用同一个URL |
| 75 | 136 | |
| 137 | +# ollama: | |
| 138 | +# # 聊天模型配置(用于一般对话) | |
| 139 | +# base-url: http://112.82.245.194:11434 | |
| 140 | +# chat-model-name: qwen3.5:9b | |
| 141 | +# # SQL/代码模型配置(专门用于代码和SQL生成) | |
| 142 | +# sql-model-name: mdq100/qwen3.5-coder:35b | |
| 143 | + | |
| 76 | 144 | mybatis: |
| 77 | 145 | mapper-locations: classpath:mapper/*.xml |
| 78 | 146 | type-aliases-package: com.xly.entity |
| 79 | 147 | configuration: |
| 80 | 148 | map-underscore-to-camel-case: true |
| 81 | 149 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl |
| 82 | - | |
| 83 | - | |
| 84 | 150 | # 情感预设缓存 |
| 85 | 151 | cache: |
| 86 | 152 | enabled: true | ... | ... |
src/main/resources/logback-spring.xml
| ... | ... | @@ -118,7 +118,8 @@ |
| 118 | 118 | <logger name="java.sql.Connection" level="DEBUG"/> |
| 119 | 119 | <logger name="java.sql.Statement" level="DEBUG"/> |
| 120 | 120 | <logger name="java.sql.PreparedStatement" level="DEBUG"/> |
| 121 | - | |
| 121 | + <logger name="ai.djl" level="DEBUG"/> | |
| 122 | + <logger name="dev.langchain4j" level="DEBUG"/> | |
| 122 | 123 | <logger name="com.xly" additivity="false"> |
| 123 | 124 | <appender-ref ref="CONSOLE"/> |
| 124 | 125 | <appender-ref ref="DEBUG" /> | ... | ... |
src/main/resources/mapper/DynamicExeDbMapper.xml
src/main/resources/templates/chat.html
| ... | ... | @@ -466,7 +466,7 @@ |
| 466 | 466 | let brandsid= "1111111111"; |
| 467 | 467 | let subsidiaryid= "1111111111"; |
| 468 | 468 | let usertype= "sysadmin"; |
| 469 | - let authorization="CE444885A9BCFDDE1FD793F8A0931301E9D7DE6CEDD9DE4B83ECE2219C7829A8F3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D4CAE6F9AC893752209A98011A981375391D4466816B7D3D1AF306E28B989121C538155B7ADAEE71E899235DC1122F426"; | |
| 469 | + let authorization="1EDB99C9BF070115F7A57AC43D8CB09F0B8C49F979DAB63A2AEA84B372B2B42BF3419238942A93E9AD666629E18D159AF7FE144A6407DE745BA0AEC8B235FC1D35611629BD9166D2BBFC3B7AF31FDF60A31A297DF9BF51740C90173D4CC922B3538155B7ADAEE71E899235DC1122F426"; | |
| 470 | 470 | let hrefLock = window.location.origin+"/xlyAi"; |
| 471 | 471 | |
| 472 | 472 | const CONFIG = { | ... | ... |