AI BI Helper 开发实录 01:文本向量与 Milvus 向量数据库集成

AI BI Helper 开发实录 01:文本向量与 Milvus 向量数据库集成

跟着课程《Java转AI高薪领域必备------从0到1打通生产级AI Agent开发》做的实战记录。技术栈:Spring AI Alibaba 1.1.2.0 + Milvus 3.0 + Redis(备选)+ MySQL 8.x。项目代号 ai-bi-helper,这是第 01 期,聚焦文本向量技术,打通文档上传、自定义分割、向量存储全流程。

AI BI Helper 功能分为两大部分:一是文本向量 ,用于文档语义检索;二是Graph 工作流编排,用于实现智能数据分析和决策。本期先搞定第一部分------文本向量,把文档上传、自定义标题分割、向量存储(Redis → Milvus)这套流程跑通。

一. 基础环境搭建

1.1 新建项目 ivy-service-ai-bi-helper-bootstrap

在 ivy-services 中新增 ivy-service-ai-bi-helper-bootstrap 项目。这次项目比较简单,就不再新建 models 模块了,代码都放在一起,结构更紧凑。

1.2 添加 pom 依赖

核心依赖包括:数据库支持、Graph 工作流、阿里百炼大模型。

xml 复制代码
<dependencies>
    <dependency>
        <groupId>vip.wayhua.ivy.ai</groupId>
        <artifactId>ivy-starter-core-db</artifactId>
        <version>1.0.1</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-graph-core</artifactId>
    </dependency>
    <!--        <dependency>-->
    <!--            <groupId>org.springframework.ai</groupId>-->
    <!--            <artifactId>spring-ai-starter-model-ollama</artifactId>-->
    <!--        </dependency>-->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <!-- 阿里百炼大模型服务平台 -->
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>
   
</dependencies>

补充说明:Ivy 项目已经封装了很多常用 starter,比如 ivy-starter-core-db 包含了 MySQL 驱动、tk-mybatis、Druid 连接池等,开箱即用。这也是我一直强调的------一定要把常用的东西封装成 starter,避免重复造轮子

1.3 新建 Spring Boot 启动类

java 复制代码
package vip.wayhua.ivy.ai.bi;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import tk.mybatis.spring.annotation.MapperScan;
import vip.wayhua.ivy.ai.core.utils.StringUtils;

@MapperScan("vip.wayhua.ivy.ai.*.dao")
@SpringBootApplication(scanBasePackages = "vip.wayhua.ivy")
public class BIHelperApplication {
    private static final Logger log= LoggerFactory.getLogger(BIHelperApplication.class);
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(BIHelperApplication.class, args);
        ConfigurableEnvironment env = run.getEnvironment();
        String appName = "AI BI Helper后台接口";
        String port = env.getProperty("server.port");

        String slog = StringUtils.banner(appName, port, true);
        log.error(slog);
    }
}

1.4 application.yml 配置

yaml 复制代码
server:
  port: 8910

logging: # SpringBoot日志配置
  level:
    # ChatClient 监控日志的等级
    org.springframework.ai.chat.client.advisor: debug
ivy:
  mysql:
    ip: 192.168.55.140
    port: 3306
    driverClassName: com.mysql.cj.jdbc.Driver
    database: ai-agent
    username: root
    password: Ivy@2024


spring:
  ai:
    dashscope:
      # 阿里百炼大模型服务平台 API-Key申请文档
      # https://help.aliyun.com/zh/model-studio/get-api-key
      api-key: sk-b956cd85b6cd****
      chat:
        options:
          model: qwen3.7-max


#    ollama:
#        base-url: http://192.168.55.130:11434
#        chat:
##          model: deepseek-r1:7b
#          model: qwen3.5:2b

  datasource:
    url: jdbc:mysql://${ivy.mysql.ip}:${ivy.mysql.port}/${ivy.mysql.database}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&nullCatalogMeansCurrent=true
    username: ${ivy.mysql.username}
    password: ${ivy.mysql.password}
    db-type: mysql
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: ${ivy.mysql.driverClassName}
    druid:
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: true
      test-on-return: false
      db-type: mysql



 
  mail:
    host: smtp.126.com
    port: 465
    username: wayhua@126.com
    password: CDnDp94-***
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true

          socketFactory:
            port: 465
            class: javax.net.ssl.SSLSocketFactory
            fallback: false


mybatis:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapperLocations: classpath*:mapper/*Mapper.xml

# 开启接口文档
nextdoc4j:
  enabled: true

1.5 测试 Controller

java 复制代码
package vip.wayhua.ivy.ai.bi.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vip.wayhua.ivy.ai.core.dto.R;


@Tag(name = "测试Controller")
@RestController
@RequestMapping("/test")
public class TestController {

    @Operation(summary = "测试", description = "测试")
    @GetMapping("/test")
    public R test() {
        return R.success();
    }

}

1.6 运行测试

启动服务后访问接口文档地址:

http://127.0.0.1:8910/doc.html#/document/all/%E6%B5%8B%E8%AF%95Controller/test

1.7 小结

基础环境搭建比较顺利,能复用的都封装过了,直接引入即可。这也是我一直强调的:一定要将常用的东西进行封装,形成自己的 starter 体系,这样新项目才能快速上手

二. 文本向量 - Redis 版本

2.1 什么是文本向量

先搞清楚核心概念,引用豆包的解释:

复制代码
文本向量:把自然语言(句子、段落、词语)转换成固定长度的浮点数数组,让计算机能理解文字语义。
文字本身是字符串,机器无法直接计算相似度;转为向量后,就可以用数学方法衡量语义关系。

简单说就是:把文字变成数字数组,方便计算机做语义匹配和相似度计算

2.2 向量模型选择

本来想使用硅基流动的向量服务(申请了代金券),但一直无法正常使用,最终选择了阿里百炼的文本向量模型:

模型名称:qwen3.7-text-embedding

2.3 配置 Embedding

application.yml 中添加向量模型配置:

yaml 复制代码
spring:
  ai: 
    embedding:
      api-key:  sk-b956cd85b6**
      options:
        model: qwen3.7-text-embedding
        dimensions: 1024

补充说明:dimensions: 1024 指定向量维度为 1024,这是该模型的默认维度。维度越高,语义表达越精细,但存储和计算成本也越高。

2.4 添加 Redis 向量存储依赖

xml 复制代码
 
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>  
</dependency>

2.5 配置 Redis 向量存储

yaml 复制代码
spring:
  ai:
    vectorstore:
      redis:
        index-name: bi-helper
        prefix: bi-helper-prefix
        initialize-schema: true

2.6 增加文档上传接口

java 复制代码
package vip.wayhua.ivy.ai.bi.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import vip.wayhua.ivy.ai.core.dto.R;

@Tag(name = "文档上传Controller")
@RestController
@RequestMapping("/document")
public class DocumentController {
    @Autowired
    DocumentService documentService;

    @PostMapping("/upload")
    public R upload(@RequestParam("file") MultipartFile file) {
        documentService.saveInVector(file);
        return R.success();
    }
}

2.7 DocumentServiceImpl 实现

2.7.1 添加 Tika 文档解析依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

补充说明:Tika 是 Apache 的文档解析工具,支持 PDF、Word、Excel 等多种格式,Spring AI 对其做了封装,使用非常方便。

2.7.2 代码实现(基础版本)
java 复制代码
package vip.wayhua.ivy.ai.bi.service.impl;

import jakarta.annotation.Resource;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import vip.wayhua.ivy.ai.bi.service.DocumentService;

import java.util.List;

@Service
public class DocumentServiceImpl implements DocumentService {
    @Resource
    VectorStore vectorStore;

    /****
     * 业务逻辑
     * 1.读取文档
     * 2.进行分档的拆分,拆成不同的块
     * 3.借助向量大模型,将文本转成向量
     * 4.入库
     * @param file
     */
    @Override
    public void saveInVector(MultipartFile file) {
        // 1.读取文档,2.进行分档的拆分,拆成不同的块
        TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
        List<Document> documents = documentReader.get();

        //4.入库
        vectorStore.add(documents);
    }
}
2.7.3 测试

查看 Redis 中的向量数据:

踩坑记录:使用 Redis Plus 查看向量数据会报错,建议使用 Another Redis Desktop Manager

2.7.4 问题发现

默认的文本拆分策略是按固定字符数拆分,导致所有文档内容混在一起,语义不连贯。比如一个标题下的内容被拆到了两个块里,后续检索时上下文就不完整了。

2.8 自定义标题分隔器 HeadingBasedSplitter

针对上述问题,自定义一个基于标题的文本分割器,按文档中的数字标题(如 "1."、"1.1"、"2.3.1")进行拆分,确保每个块都是一个完整的章节。

java 复制代码
package vip.wayhua.ivy.ai.bi.splitter;

import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 基于标题的文本分割器
 * <p>
 * 继承 Spring AI 的 {@link org.springframework.ai.transformer.splitter.TextSplitter},通过重写 {@link #splitText(String)} 方法,
 * 识别文本中形如 "1."、"1.1"、"2.3.1" 的数字标题行,并按照最低层级标题将文档切分为独立的块。
 * <p>
 * 主要特性:
 * <ul>
 *     <li>自动识别多级数字标题(例如:1.、1.1、2.1.3)。</li>
 *     <li>通过选项 {@code prependHeadingPath} 可将当前标题的完整层级路径(如 "1. 概述 > 1.1 背景")
 *         前置到每个块的内容开头,帮助后续检索理解上下文。</li>
 *     <li>可配置最小块大小,过滤掉内容过短的空章节。</li>
 * </ul>
 * <p>
 * 注意:此拆分器仅处理数字编号的标题,不支持中文数字(如"一、")或其他自定义格式,
 * 如有需要可自行扩展匹配模式。
 **/


public class HeadingBasedSplitter extends TextSplitter {

    /**
     * 用于匹配标题行的正则表达式。
     * <ul>
     *     <li>{@code ^}                            ------ 行首</li>
     *     <li>{@code (\d+(?:\.\d+)*)}              ------ 数字编号,如 "1"、"1.1"、"2.3.1"</li>
     *     <li>{@code \s+}                          ------ 至少一个空白字符(分隔编号与标题文字)</li>
     *     <li>{@code (.*)$}                        ------ 标题文字,直到行尾</li>
     * </ul>
     * 使用 {@link Pattern#MULTILINE} 模式,使 ^ 和 $ 匹配每一行的开头和结尾。
     */
    private static final Pattern HEADING_PATTERN =
            Pattern.compile("^(\\d+(?:\\.\\d+)*)\\s+(.*)$", Pattern.MULTILINE);

    /**
     * 是否将标题路径前置到每个块内容的最前面
     */
    private final boolean prependHeadingPath;

    /**
     * 允许的最小块字符数,小于此值的章节将被忽略
     */
    private final int minChunkSize;

    /**
     * 构造一个标题拆分器。
     *
     * @param prependHeadingPath 是否在块内容中前置完整的标题路径(例如 "1. 概述 > 1.2 背景")
     * @param minChunkSize       最小块大小,低于此大小的标题小节将被丢弃
     */
    public HeadingBasedSplitter(boolean prependHeadingPath, int minChunkSize) {
        this.prependHeadingPath = prependHeadingPath;
        this.minChunkSize = minChunkSize;
    }

    /**
     * 对单段文本进行基于标题的拆分。
     * <p>
     * 实现逻辑:
     * <ol>
     *     <li>扫描全文,找出所有匹配标题模式的位置。</li>
     *     <li>若无任何标题,且整个文本长度不少于最小块大小,则原样作为一个块返回。</li>
     *     <li>若存在标题,则遍历每个标题:
     *         <ul>
     *             <li>维护一个路径栈(双端队列),根据当前标题的层级深度调整栈中内容。</li>
     *             <li>提取从前一个标题结束到下一个标题开始之间的正文部分。</li>
     *             <li>根据配置决定是否将标题路径前置到正文前。</li>
     *             <li>过滤掉正文长度小于最小块大小的章节。</li>
     *         </ul>
     *     </li>
     * </ol>
     *
     * @param text 待切分的原始文本
     * @return 切分后的字符串列表,每个元素对应一个标题下的内容块
     */
    @Override
    protected List<String> splitText(String text) {
        if (!StringUtils.hasText(text)) {
            return List.of();
        }

        // 收集所有标题匹配的位置信息
        Matcher matcher = HEADING_PATTERN.matcher(text);
        List<HeadingMatch> matches = new ArrayList<>();
        while (matcher.find()) {
            String numberStr = matcher.group(1);              // 如 "1.1"
            String titleText = matcher.group(2) != null ? matcher.group(2).trim() : "";
            matches.add(new HeadingMatch(matcher.start(), matcher.end(), numberStr, titleText));
        }

        // 没有标题:整个文本视为一个块(需满足最小长度)
        if (matches.isEmpty()) {
            return text.trim().length() >= minChunkSize ? List.of(text.trim()) : List.of();
        }

        List<String> chunks = new ArrayList<>();
        // 使用双端队列维护当前标题的完整路径
        Deque<String> pathStack = new LinkedList<>();

        for (int i = 0; i < matches.size(); i++) {
            HeadingMatch match = matches.get(i);
            int currentDepth = match.numberStr.split("\\.").length; // 层级深度 = 数字编号中小数点数量 + 1
            String headingFull = match.numberStr + " " + match.titleText;

            // 1. 更新路径栈:弹出层级大于等于当前深度的旧标题,保证栈顶为直接上级
            while (pathStack.size() >= currentDepth) {
                pathStack.pollLast();
            }
            pathStack.offerLast(headingFull);

            // 2. 确定正文范围:当前标题结束位置 到 下一个标题开始位置(或文档结尾)
            int bodyStart = match.endIndex;
            int bodyEnd = (i + 1 < matches.size()) ? matches.get(i + 1).startIndex : text.length();
            String body = text.substring(bodyStart, bodyEnd).trim();

            // 3. 过滤掉内容过短的章节(如纯标题段)
            if (body.length() < minChunkSize) {
                // 但仍需保持路径栈已更新(因为它可能作为后续更深层标题的上级)------ 此处通过 continue 可以保留栈状态
                continue;
            }

            // 4. 组装最终块内容
            String headingPath = String.join(" > ", pathStack); // 如 "1. 概述 > 1.1 背景"
            String chunkContent = prependHeadingPath ? headingPath + "\n" + body : body;

            chunks.add(chunkContent);
        }

        return chunks;
    }

    /**
     * 内部类,记录单个标题匹配的关键信息。
     */
    private static class HeadingMatch {
        /**
         * 标题在原文中的起始索引
         */
        final int startIndex;
        /**
         * 标题结束索引(即正文开始位置)
         */
        final int endIndex;
        /**
         * 纯数字编号,例如 "1.2"
         */
        final String numberStr;
        /**
         * 标题文字部分,不包含编号
         */
        final String titleText;

        HeadingMatch(int start, int end, String numberStr, String title) {
            this.startIndex = start;
            this.endIndex = end;
            this.numberStr = numberStr;
            this.titleText = title;
        }
    }
}

2.9 集成自定义分隔器

修改 DocumentServiceImpl,引入自定义分割器:

java 复制代码
    public void saveInVector(MultipartFile file) {
        // 1.读取文档,2.进行分档的拆分,拆成不同的块
        TikaDocumentReader documentReader = new TikaDocumentReader(file.getResource());
        List<Document> documents = documentReader.get();

        //3.
        HeadingBasedSplitter splitter = new HeadingBasedSplitter(true,50);

        List<Document> split = splitter.split(documents);
        //4.入库
        vectorStore.add(split);
    }

注意事项:查看源码发现标题格式要求是 数字 + 空格 + 标题名称(如 "1 概述"、"1.1 背景"),这样才能被正则匹配到。

2.10 测试带分隔的效果

三. 文本向量 - Milvus 数据库版本

Redis 作为向量数据库适合小规模场景,生产环境推荐使用专业的向量数据库。这里选择 Milvus 3.0,它是一款开源的云原生向量数据库,性能优异,社区活跃。

3.1 移除 Redis 向量存储依赖

将 Redis 向量存储的引用注释掉(application.yml 中的配置可以保留,不会产生影响):

xml 复制代码
<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-starter-vector-store-redis</artifactId>-->

<!--        </dependency>-->

3.2 安装 Milvus 3.0

参考官方文档:milvus.io/docs/zh/qui...

3.2.1 下载 docker-compose.yml
bash 复制代码
mkdir milvus
cd milvus/
yum install wget -y

wget https://github.com/milvus-io/milvus/releases/download/v3.0-beta/milvus-standalone-docker-compose.yml -O docker-compose.yml

踩坑记录:GitHub 有时无法访问,如果下载失败,可直接使用下面的配置内容。

3.2.2 docker-compose.yml 配置
yaml 复制代码
version: '3.5'

services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.25
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
    command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: ["CMD", "etcdctl", "endpoint", "health"]
      interval: 30s
      timeout: 20s
      retries: 3

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2024-12-18T13-15-44Z
    environment:
      MINIO_ACCESS_KEY: minioadmin
      MINIO_SECRET_KEY: minioadmin
    ports:
      - "9001:9001"
      - "9000:9000"
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3

  standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v3.0-beta
    command: ["milvus", "run", "standalone"]
    security_opt:
    - seccomp:unconfined
    environment:
      ETCD_ENDPOINTS: etcd:2379
      MINIO_ADDRESS: minio:9000
      MQ_TYPE: woodpecker
    volumes:
      - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - "19530:19530"
      - "9091:9091"
    depends_on:
      - "etcd"
      - "minio"

networks:
  default:
    name: milvus
3.2.3 启动 Milvus
bash 复制代码
docker-compose up -d
3.2.4 权限问题处理

启动后遇到权限错误:

错误信息:

ruby 复制代码
["failed to mkdir"] [localStoragePath=/var/lib/milvus/data/] [error="mkdir /var/lib/milvus/data/: permission denied"] 

解决方案:Milvus 默认以 UID 1000 运行,需要给数据目录设置正确的权限:

bash 复制代码
# milvus 默认运行uid:1000
mkdir -p /data/milvus

chmod -R 777 /export/server/milvus/volumes/milvus/
# 或者指定uid
chown -R 1000:1000 /export/server/milvus/volumes/milvus/

最好是在当前目录执行:

bash 复制代码
chmod -R 777 ./volumes/milvus/
# 或者指定uid
chown -R 1000:1000 ./volumes/milvus/
3.2.5 重启 Milvus
bash 复制代码
docker-compose stop
./autorun.sh

访问 Milvus Web UI:http://192.168.55.130:9091/webui/

3.3 添加 Milvus 向量存储依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>

3.4 配置 Milvus

3.4.1 初始配置
yaml 复制代码
Spring:
   ai:
     vectorstore:
        milvus:
          client:
          host: 192.168.55.130
          port: 19530
        collection-name: bi-helper
        database-name: default
3.4.2 踩坑记录:Collection 名称格式错误

启动时报错:

sql 复制代码
Fail to describe collection 'bi-helper' in database 'default' failed, error code: 1100, reason: Invalid collection name: bi-helper. collection name can only contain numbers, letters and underscores: invalid parameter
[18:22:37.955] ERROR i.m.c.AbstractMilvusGrpcClient - InsertRequest collectionName:bi-helper failed! Exception:{}
io.milvus.exception.ServerException: Invalid collection name: bi-helper. collection name can only contain numbers, letters and underscores: invalid parameter

问题分析 :Milvus 的 collection 名称只能包含数字、字母和下划线,不能包含连字符 -

解决方案 :将 bi-helper 改为 biHelper,并添加自动建表配置:

yaml 复制代码
spring:
  ai:
    vectorstore: 
      milvus:
        client:
          host: 192.168.55.130
          port: 19530
        collection-name: biHelper
        database-name: default
        id-field-name: id
        auto-id: false
        embedding-dimension: 1024
        initialize-schema: true

补充说明:

  • embedding-dimension: 1024:与 Embedding 模型的维度保持一致
  • initialize-schema: true:自动创建 collection schema,无需手动建表

3.5 测试 Milvus 向量存储

已成功插入 8 条向量数据!

3.6 安装 Milvus 可视化工具 Attu

Milvus 3.0 默认没有自带图形化管理界面,需要单独安装 Attu:

重要提示:尽可能使用正式版本(如 2.6.5),beta 版本可能会有访问问题。正式版本可以正常查询和管理数据。

四. 数据库准备

为后续的 BI 分析和 Graph 工作流准备业务数据。

4.1 创建数据库

创建数据库 bi-helper

4.2 创建数据库表

sql 复制代码
-- ========================================
-- 库存/商品/仓库/销售/调拨管理数据库脚本
-- ========================================

-- 1. 商品表:记录商品的基本信息
DROP TABLE IF EXISTS bb_product;
CREATE TABLE bb_product
(
    id           BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    product_code VARCHAR(50)  NOT NULL UNIQUE COMMENT '商品编码',
    product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
    spec         VARCHAR(100) DEFAULT NULL COMMENT '规格型号',
    unit         VARCHAR(20)  DEFAULT '件' COMMENT '计量单位',
    category     VARCHAR(50)  DEFAULT NULL COMMENT '分类',
    status       TINYINT      DEFAULT 1 COMMENT '状态(1:启用,0:停用)',
    created_time DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表';


-- 2. 仓库表:存放仓库基本信息
DROP TABLE IF EXISTS bb_warehouse;
CREATE TABLE bb_warehouse
(
    id             BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    warehouse_code VARCHAR(50)  NOT NULL UNIQUE COMMENT '仓库编码',
    warehouse_name VARCHAR(100) NOT NULL COMMENT '仓库名称',
    location       VARCHAR(255) DEFAULT NULL COMMENT '仓库位置描述',
    manager        VARCHAR(50)  DEFAULT NULL COMMENT '负责人',
    status         TINYINT      DEFAULT 1 COMMENT '状态(1:启用,0:停用)',
    created_time   DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time   DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='仓库信息表';


-- 3. 库存表:记录每个仓库中每种商品的库存量
DROP TABLE IF EXISTS bb_inventory;
CREATE TABLE bb_inventory
(
    id              BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    warehouse_id    BIGINT NOT NULL COMMENT '仓库ID',
    product_id      BIGINT NOT NULL COMMENT '商品ID',
    quantity        DECIMAL(18, 2) DEFAULT 0 COMMENT '当前库存数量',
    locked_quantity DECIMAL(18, 2) DEFAULT 0 COMMENT '锁定库存数量(例如调拨中未完成的部分)',
    updated_time    DATETIME       DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE KEY uk_inventory_wh_prod (warehouse_id, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';


-- 4. 销售表
DROP TABLE IF EXISTS bb_sales_record;
CREATE TABLE bb_sales_record
(
    id           BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    warehouse_id BIGINT         NOT NULL COMMENT '仓库ID',
    product_id   BIGINT         NOT NULL COMMENT '商品ID',
    sale_date    DATE           NOT NULL COMMENT '销售日期',
    quantity     DECIMAL(18, 2) NOT NULL COMMENT '销售数量',
    revenue      DECIMAL(18, 2) DEFAULT 0 COMMENT '销售金额',
    created_time DATETIME       DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品销售记录表';


-- 5. 调拨单主表
DROP TABLE IF EXISTS bb_transfer_order;
CREATE TABLE bb_transfer_order
(
    id                  BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    order_no            VARCHAR(50) NOT NULL UNIQUE COMMENT '调拨单号',
    source_warehouse_id BIGINT      NOT NULL COMMENT '调出仓库ID',
    target_warehouse_id BIGINT      NOT NULL COMMENT '调入仓库ID',
    status              TINYINT     DEFAULT 0 COMMENT '状态(0:待审核,1:已审核,2:调拨中,3:已完成,4:已驳回)',
    transfer_type       INT         DEFAULT 0 COMMENT '调拨类型:1智能,0人工',
    transfer_date       DATE        DEFAULT NULL COMMENT '调拨日期',
    comment             TEXT COMMENT '说明',
    created_by          VARCHAR(50) DEFAULT NULL COMMENT '创建人',
    approved_by         VARCHAR(50) DEFAULT NULL COMMENT '审核人',
    created_time        DATETIME    DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time        DATETIME    DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调拨单主表';


-- 6. 调拨单明细表
DROP TABLE IF EXISTS bb_transfer_order_item;
CREATE TABLE bb_transfer_order_item
(
    id                BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    transfer_order_id BIGINT         NOT NULL COMMENT '调拨单ID',
    product_id        BIGINT         NOT NULL COMMENT '商品ID',
    transfer_quantity DECIMAL(18, 2) NOT NULL COMMENT '调拨数量',
    actual_quantity   DECIMAL(18, 2) DEFAULT 0 COMMENT '实际到货数量',
    remark            VARCHAR(255)   DEFAULT NULL COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='调拨单明细表';


-- 7. 库存操作日志表(用于追踪库存变动)
DROP TABLE IF EXISTS bb_inventory_log;
CREATE TABLE bb_inventory_log
(
    id              BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    warehouse_id    BIGINT         NOT NULL COMMENT '仓库ID',
    product_id      BIGINT         NOT NULL COMMENT '商品ID',
    change_type     VARCHAR(50)    NOT NULL COMMENT '变动类型(IN:入库,OUT:出库,TRANSFER_OUT:调出,TRANSFER_IN:调入)',
    quantity_change DECIMAL(18, 2) NOT NULL COMMENT '变动数量(正数增加,负数减少)',
    reference_no    VARCHAR(50)  DEFAULT NULL COMMENT '关联单据号(如调拨单号)',
    created_time    DATETIME     DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
    remark          VARCHAR(255) DEFAULT NULL COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作日志表';

4.3 创建数据

sql 复制代码
-- ========================================
-- 1. 创建仓库样例数据
-- ========================================
INSERT INTO bb_warehouse (warehouse_code, warehouse_name, location, manager)
VALUES
    ('WH001', '华北仓', '北京', '张三'),
    ('WH002', '华东仓', '上海', '李四'),
    ('WH003', '华南仓', '广州', '王五');


-- ========================================
-- 2. 创建商品样例数据
-- ========================================
INSERT INTO bb_product (product_code, product_name, spec, unit, category)
VALUES
    ('P001', '智能手机', '6.5英寸 128G', '台', '电子产品'),
    ('P002', '蓝牙耳机', '入耳式', '副', '电子产品'),
    ('P003', '办公椅', '可调节靠背', '张', '家具'),
    ('P004', '显示器', '27寸 IPS', '台', '电子产品'),
    ('P005', '机械键盘', '104键 RGB', '个', '外设');


-- ========================================
-- 3. 初始化库存(每仓库每商品)
-- ========================================
INSERT INTO bb_inventory (warehouse_id, product_id, quantity, locked_quantity)
SELECT
    w.id,
    p.id,
    FLOOR(RAND() * 200 + 100),
    0
FROM bb_warehouse w, bb_product p;


-- ========================================
-- 4. 创建过去三年的季度销售数据(2023-2025)
-- 每季度每仓库每商品一条
-- ========================================
SET @start_date = '2023-01-01';
SET @end_date   = '2025-12-31';

INSERT INTO bb_sales_record (warehouse_id, product_id, sale_date, quantity, revenue)
SELECT *
FROM (
         SELECT
             w.id AS warehouse_id,
             p.id AS product_id,
             DATE_ADD('2023-01-01', INTERVAL (FLOOR(RAND() * 12 * 3)) MONTH) AS sale_date,
             ROUND(RAND() * 500 + 50, 2) AS quantity,
             ROUND(RAND() * 50000 + 5000, 2) AS revenue
         FROM bb_warehouse w,
              bb_product p,
              (
                  SELECT 1 FROM dual
                  UNION SELECT 2
                  UNION SELECT 3
                  UNION SELECT 4
              ) q
     ) t
WHERE t.sale_date BETWEEN @start_date AND @end_date;


-- ========================================
-- 5. 创建调拨单主表(模拟每季度一次调拨)
-- 插入随机调拨单数据(排除相同仓库)
-- ========================================
INSERT INTO bb_transfer_order (
    order_no,
    source_warehouse_id,
    target_warehouse_id,
    status,
    created_by,
    transfer_type,
    transfer_date
)
SELECT
    t.order_no,
    t.source_warehouse_id,
    t.target_warehouse_id,
    t.status,
    t.created_by,
    t.transfer_type,
    t.transfer_date
FROM (
         SELECT
             CONCAT('TO', LPAD(FLOOR(RAND() * 99999), 5, '0')) AS order_no,
             FLOOR(1 + RAND() * 3) AS source_warehouse_id,
             FLOOR(1 + RAND() * 3) AS target_warehouse_id,
             3 AS status,                  -- 已完成
             '系统自动' AS created_by,
             1 AS transfer_type,           -- 假设 1=系统自动
             DATE_ADD('2023-01-01', INTERVAL FLOOR(RAND() * 36) MONTH) AS transfer_date
         FROM (
                  SELECT 1 FROM dual
                  UNION SELECT 2
                  UNION SELECT 3
                  UNION SELECT 4
                  UNION SELECT 5
              ) tmp
     ) t
WHERE t.source_warehouse_id <> t.target_warehouse_id;


-- ========================================
-- 6. 创建调拨明细数据
-- ========================================
INSERT INTO bb_transfer_order_item (
    transfer_order_id,
    product_id,
    transfer_quantity,
    actual_quantity,
    remark
)
SELECT
    o.id,
    p.id,
    ROUND(RAND() * 50 + 10, 2),
    ROUND(RAND() * 50 + 10, 2),
    '季度调拨'
FROM bb_transfer_order o, bb_product p
WHERE o.status = 3
ORDER BY o.id
    LIMIT 50;


-- ========================================
-- 7. 为库存操作日志生成随机调拨记录 - 调出
-- ========================================
INSERT INTO bb_inventory_log (
    warehouse_id,
    product_id,
    change_type,
    quantity_change,
    reference_no,
    remark
)
SELECT
    t.source_warehouse_id,
    item.product_id,
    'TRANSFER_OUT',
    -item.transfer_quantity,
    o.order_no,
    '系统模拟调拨出库'
FROM bb_transfer_order o
         JOIN bb_transfer_order_item item ON o.id = item.transfer_order_id
         JOIN bb_transfer_order t ON o.id = t.id;


-- ========================================
-- 8. 为库存操作日志生成随机调拨记录 - 调入
-- ========================================
INSERT INTO bb_inventory_log (
    warehouse_id,
    product_id,
    change_type,
    quantity_change,
    reference_no,
    remark
)
SELECT
    t.target_warehouse_id,
    item.product_id,
    'TRANSFER_IN',
    item.transfer_quantity,
    o.order_no,
    '系统模拟调拨入库'
FROM bb_transfer_order o
         JOIN bb_transfer_order_item item ON o.id = item.transfer_order_id
         JOIN bb_transfer_order t ON o.id = t.id;

五. 小结

本期内容主要聚焦于文本向量技术Milvus 向量数据库集成,完成了以下工作:

已完成内容

  1. 基础环境搭建 :新建 ivy-service-ai-bi-helper-bootstrap 项目,配置数据库和大模型
  2. 文本向量核心功能
    • 集成阿里百炼 qwen3.7-text-embedding 向量模型
    • 实现文档上传接口,支持 PDF、Word 等多种格式
    • 自定义 HeadingBasedSplitter 标题分割器,按数字标题拆分文档
    • 支持 Redis 和 Milvus 两种向量存储方案
  3. Milvus 3.0 部署
    • Docker 部署 Milvus standalone 版本
    • 解决权限问题(UID 1000)
    • 安装 Attu 可视化工具
  4. 数据库准备:创建商品、仓库、库存、销售、调拨等业务表,并插入测试数据

踩坑记录

问题 解决方案
Redis Plus 无法查看向量数据 使用 Another Redis Desktop Manager
Milvus 启动报权限错误 chown -R 1000:1000 ./volumes/milvus/
Milvus Collection 名称包含 - 改为 biHelper,只能包含数字、字母、下划线
Attu beta 版本无法正常查询 使用正式版本 2.6.5

下期预告

AI BI Helper 的第二部分------Graph 工作流编排,将实现基于 AI 的智能数据分析和调拨建议生成。


本文是 AI BI Helper 开发实录的第一期,主要完成了文本向量和 Milvus 数据库的集成。感谢阅读!

相关推荐
MacroZheng1 小时前
同事:“Claude Code都能自动写代码了,还要什么Spec Coding?” 我反问:“屎山代码你来维护?”
java·人工智能·后端
孙高飞1 小时前
从 0 开始学习 AI 测试 - SKILL的编写实战
人工智能
鸢尾掠地平1 小时前
RAG与上下文工程的讲解
人工智能
leeyi1 小时前
Document 组件源码:Loader / Transformer / Parser 为什么分成三个接口(第67篇-E53)
aigc·agent·ai编程
星辰员1 小时前
会追问才算面试官:云面 YunMian AI 模拟面试官的具身交互智能工程实录
人工智能
qq_454245031 小时前
Cline智能体系统提示词
人工智能·prompt
平行宇宙喜欢吃橙1 小时前
Agent Harness 实战指南:构建生产级 AI Agent 的“马具“框架
网络·人工智能
武子康1 小时前
从随机动作块到真实闭环:Diffusion 与 Flow 策略的执行账本
人工智能·stable diffusion·agent
唐老板1 小时前
Prompt 已死,Loop 永生
ai编程