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

15 阅读16分钟

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 工作流、阿里百炼大模型。

<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 启动类

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 配置

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

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

QQ_1785219795791.png

1.7 小结

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

二. 文本向量 - Redis 版本

2.1 什么是文本向量

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

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

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

2.2 向量模型选择

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

QQ_1785220438148.png

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

2.3 配置 Embedding

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

spring:
  ai: 
    embedding:
      api-key:  sk-b956cd85b6**
      options:
        model: qwen3.7-text-embedding
        dimensions: 1024

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

2.4 添加 Redis 向量存储依赖

 
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>  
</dependency>

2.5 配置 Redis 向量存储

spring:
  ai:
    vectorstore:
      redis:
        index-name: bi-helper
        prefix: bi-helper-prefix
        initialize-schema: true

2.6 增加文档上传接口

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 文档解析依赖
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>

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

2.7.2 代码实现(基础版本)
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 测试

QQ_1785225328480.png 查看 Redis 中的向量数据:

QQ_1785225355796.png

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

QQ_1785225580609.png

2.7.4 问题发现

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

2.8 自定义标题分隔器 HeadingBasedSplitter

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

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,引入自定义分割器:

    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 测试带分隔的效果

QQ_1785226545167.png

QQ_1785228506263.png

QQ_1785228568274.png

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

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

3.1 移除 Redis 向量存储依赖

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

<!--        <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…

QQ_1785228776066.png

3.2.1 下载 docker-compose.yml
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 配置
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
docker-compose up -d

QQ_1785229474612.png

QQ_1785229690086.png

3.2.4 权限问题处理

启动后遇到权限错误:

QQ_1785229851833.png

错误信息:

["failed to mkdir"] [localStoragePath=/var/lib/milvus/data/] [error="mkdir /var/lib/milvus/data/: permission denied"] 

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

# 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/

最好是在当前目录执行:

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

QQ_1785230823701.png

3.2.5 重启 Milvus
docker-compose stop
./autorun.sh

QQ_1785230886942.png 访问 Milvus Web UI:http://192.168.55.130:9091/webui/

QQ_1785231085728.png

3.3 添加 Milvus 向量存储依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-milvus</artifactId>
</dependency>

3.4 配置 Milvus

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

启动时报错:

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,并添加自动建表配置:

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 向量存储

QQ_1785234965103.png

QQ_1785234984694.png

QQ_1785235013843.png

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

3.6 安装 Milvus 可视化工具 Attu

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

QQ_1785238324154.png

QQ_1785238341721.png

QQ_1785238429681.png

QQ_1785238477298.png

QQ_1785238502131.png

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

QQ_1785240664405.png

四. 数据库准备

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

4.1 创建数据库

创建数据库 bi-helper

QQ_1785241145210.png

4.2 创建数据库表

 -- ============================================
-- 维度表:商品维度 (dim_product)
-- ============================================
DROP TABLE IF EXISTS dim_product;
CREATE TABLE dim_product
(
    product_id    BIGINT PRIMARY KEY COMMENT '商品唯一ID',
    product_name  VARCHAR(255) NOT NULL COMMENT '商品名称',
    category_id   BIGINT NULL COMMENT '商品分类ID',
    category_name VARCHAR(255) NULL COMMENT '商品分类名称',
    brand         VARCHAR(255) NULL COMMENT '品牌',
    cost_price    DECIMAL(10, 2) NULL COMMENT '成本价',
    retail_price  DECIMAL(10, 2) NULL COMMENT '建议零售价'
) COMMENT='商品维度表';


-- ============================================
-- 维度表:门店维度 (dim_store)
-- ============================================
DROP TABLE IF EXISTS dim_store;
CREATE TABLE dim_store
(
    store_id   BIGINT PRIMARY KEY COMMENT '门店唯一ID',
    store_name VARCHAR(255) NOT NULL COMMENT '门店名称',
    province   VARCHAR(100) NULL COMMENT '所在省份',
    city       VARCHAR(100) NULL COMMENT '所在城市',
    address    VARCHAR(255) NULL COMMENT '门店详细地址',
    open_date  DATE NULL COMMENT '开业日期'
) COMMENT='门店维度表';


-- ============================================
-- 维度表:时间维度 (dim_date)
-- ============================================
DROP TABLE IF EXISTS dim_date;
CREATE TABLE dim_date
(
    date_id DATE PRIMARY KEY COMMENT '日期ID',
    year    INT NOT NULL,
    quarter INT NOT NULL,
    month   INT NOT NULL,
    day     INT NOT NULL,
    weekday INT NOT NULL
) COMMENT='时间维度表';


-- ============================================
-- 维度表:客户维度 (dim_customer)
-- ============================================
DROP TABLE IF EXISTS dim_customer;
CREATE TABLE dim_customer
(
    customer_id   BIGINT PRIMARY KEY COMMENT '客户ID',
    customer_name VARCHAR(255) NOT NULL,
    gender        VARCHAR(10) NULL,
    age           INT NULL,
    city          VARCHAR(100) NULL,
    province      VARCHAR(100) NULL
) COMMENT='客户维度表';


-- ============================================
-- 事实表:销售事实表 (fact_sales)
-- ============================================
DROP TABLE IF EXISTS fact_sales;
CREATE TABLE fact_sales
(
    sales_id     BIGINT PRIMARY KEY COMMENT '销售记录ID',
    product_id   BIGINT         NOT NULL COMMENT '商品ID',
    store_id     BIGINT         NOT NULL COMMENT '门店ID',
    customer_id  BIGINT NULL COMMENT '客户ID',
    date_id      DATE           NOT NULL COMMENT '销售日期',
    quantity     INT            NOT NULL COMMENT '销售数量',
    sales_amount DECIMAL(10, 2) NOT NULL COMMENT '销售金额',
    discount     DECIMAL(10, 2) NULL COMMENT '折扣金额',
    FOREIGN KEY (product_id) REFERENCES dim_product (product_id),
    FOREIGN KEY (store_id) REFERENCES dim_store (store_id),
    FOREIGN KEY (customer_id) REFERENCES dim_customer (customer_id),
    FOREIGN KEY (date_id) REFERENCES dim_date (date_id)
) COMMENT='销售事实表';


-- ============================================
-- 事实表:库存事实表 (fact_inventory)
-- ============================================
DROP TABLE IF EXISTS fact_inventory;
CREATE TABLE fact_inventory
(
    inventory_id BIGINT PRIMARY KEY COMMENT '库存记录ID',
    product_id   BIGINT NOT NULL COMMENT '商品ID',
    store_id     BIGINT NOT NULL COMMENT '门店ID',
    date_id      DATE   NOT NULL COMMENT '库存日期',
    quantity     INT    NOT NULL COMMENT '库存数量',
    FOREIGN KEY (product_id) REFERENCES dim_product (product_id),
    FOREIGN KEY (store_id) REFERENCES dim_store (store_id),
    FOREIGN KEY (date_id) REFERENCES dim_date (date_id)
) COMMENT='库存事实表';

4.3 创建数据

 -- 扩展商品维度数据
INSERT INTO dim_product (product_id, product_name, category_id, category_name, brand, cost_price, retail_price)
VALUES (6, 'iPad Air', 102, '平板电脑', 'Apple', 3500.00, 4599.00),
       (7, '小米平板6', 102, '平板电脑', 'Xiaomi', 1800.00, 2499.00),
       (8, '索尼电视 75寸', 103, '电视', 'Sony', 5200.00, 6999.00),
       (9, '海尔冰箱 450L', 104, '家电', 'Haier', 2600.00, 3299.00),
       (10, '美的洗衣机 10kg', 104, '家电', 'Midea', 2200.00, 2899.00),
       (11, '荣耀 Magic6', 101, '手机', 'Honor', 3800.00, 4999.00),
       (12, 'OPPO Find X7', 101, '手机', 'OPPO', 3600.00, 4799.00),
       (13, 'vivo X200', 101, '手机', 'vivo', 3400.00, 4599.00),
       (14, 'MacBook Air M3', 102, '笔记本', 'Apple', 7800.00, 9999.00),
       (15, '惠普战X', 102, '笔记本', 'HP', 5200.00, 6999.00),
       (16, '雷蛇游戏本 16', 102, '笔记本', 'Razer', 9000.00, 12999.00),
       (17, '戴尔 XPS 13', 102, '笔记本', 'Dell', 6800.00, 8999.00),
       (18, '海信电视 55寸', 103, '电视', 'Hisense', 2100.00, 2799.00),
       (19, 'TCL 50寸电视', 103, '电视', 'TCL', 1800.00, 2499.00),
       (20, '科沃斯扫地机器人 T10', 104, '家电', 'Ecovacs', 2400.00, 3299.00);

-- 扩展门店维度
INSERT INTO dim_store (store_id, store_name, province, city, address, open_date)
VALUES (1006, '南京新街口店', '江苏', '南京市', '新街口中央路18号', DATE ('2021-02-12')),
       (1007, '成都春熙路店', '四川', '成都市', '锦江区春熙路66号', DATE ('2020-08-07')),
       (1008, '武汉光谷店', '湖北', '武汉市', '东湖高新区光谷大道88号', DATE ('2021-10-03')),
       (1009, '西安小寨店', '陕西', '西安市', '雁塔区小寨路88号', DATE ('2019-12-30')),
       (1010, '重庆解放碑店', '重庆', '重庆市', '渝中区解放碑CBD', DATE ('2020-03-11')),
       (1011, '苏州园区店', '江苏', '苏州市', '工业园区金鸡湖大道88号', DATE ('2022-01-15')),
       (1012, '长沙五一广场店', '湖南', '长沙市', '芙蓉区五一大道66号', DATE ('2022-05-10')),
       (1013, '天津滨江道店', '天津', '天津市', '和平区滨江道99号', DATE ('2020-06-18')),
       (1014, '青岛万象城店', '山东', '青岛市', '市南区香港中路8号', DATE ('2021-09-28')),
       (1015, '沈阳太原街店', '辽宁', '沈阳市', '和平区太原街66号', DATE ('2019-04-03'));


-- 扩展日期维度(2025-01-04 ~ 2025-01-31)
INSERT INTO dim_date (date_id, year, quarter, month, day, weekday)
VALUES ('2025-01-04', 2025, 1, 1, 4, 6),
       ('2025-01-05', 2025, 1, 1, 5, 7),
       ('2025-01-06', 2025, 1, 1, 6, 1),
       ('2025-01-07', 2025, 1, 1, 7, 2),
       ('2025-01-08', 2025, 1, 1, 8, 3),
       ('2025-01-09', 2025, 1, 1, 9, 4),
       ('2025-01-10', 2025, 1, 1, 10, 5),
       ('2025-01-11', 2025, 1, 1, 11, 6),
       ('2025-01-12', 2025, 1, 1, 12, 7),
       ('2025-01-13', 2025, 1, 1, 13, 1),
       ('2025-01-14', 2025, 1, 1, 14, 2),
       ('2025-01-15', 2025, 1, 1, 15, 3),
       ('2025-01-16', 2025, 1, 1, 16, 4),
       ('2025-01-17', 2025, 1, 1, 17, 5),
       ('2025-01-18', 2025, 1, 1, 18, 6),
       ('2025-01-19', 2025, 1, 1, 19, 7),
       ('2025-01-20', 2025, 1, 1, 20, 1),
       ('2025-01-21', 2025, 1, 1, 21, 2),
       ('2025-01-22', 2025, 1, 1, 22, 3),
       ('2025-01-23', 2025, 1, 1, 23, 4),
       ('2025-01-24', 2025, 1, 1, 24, 5),
       ('2025-01-25', 2025, 1, 1, 25, 6),
       ('2025-01-26', 2025, 1, 1, 26, 7),
       ('2025-01-27', 2025, 1, 1, 27, 1),
       ('2025-01-28', 2025, 1, 1, 28, 2),
       ('2025-01-29', 2025, 1, 1, 29, 3),
       ('2025-01-30', 2025, 1, 1, 30, 4),
       ('2025-01-31', 2025, 1, 1, 31, 5);

-- 扩展客户维度
INSERT INTO dim_customer (customer_id, customer_name, gender, age, city, province)
VALUES (4, '赵六', '男', 29, '深圳', '广东'),
       (5, '孙七', '女', 24, '南京', '江苏'),
       (6, '钱八', '男', 31, '成都', '四川'),
       (7, '周九', '女', 27, '杭州', '浙江'),
       (8, '吴十', '男', 35, '武汉', '湖北'),
       (9, '郑一', '女', 22, '天津', '天津'),
       (10, '王二', '男', 33, '重庆', '重庆'),
       (11, '李三', '男', 30, '西安', '陕西'),
       (12, '张四', '女', 28, '长沙', '湖南'),
       (13, '陈五', '男', 40, '青岛', '山东'),
       (14, '王小明', '男', 20, '广州', '广东'),
       (15, '李丽', '女', 23, '北京', '北京'),
       (16, '赵雪', '女', 26, '上海', '上海'),
       (17, '周浩', '男', 34, '成都', '四川'),
       (18, '钱娜', '女', 25, '深圳', '广东'),
       (19, '吴峰', '男', 36, '南京', '江苏'),
       (20, '孙美', '女', 29, '杭州', '浙江'),
       (21, '丁强', '男', 31, '武汉', '湖北'),
       (22, '贾玲', '女', 38, '天津', '天津'),
       (23, '刘伟', '男', 41, '重庆', '重庆'),
       (24, '王冬', '男', 27, '西安', '陕西'),
       (25, '李雪', '女', 33, '长沙', '湖南'),
       (26, '张亮', '男', 39, '青岛', '山东'),
       (27, '赵婷', '女', 22, '广州', '广东'),
       (28, '孙诚', '男', 24, '上海', '上海'),
       (29, '周影', '女', 32, '北京', '北京'),
       (30, '钱军', '男', 37, '深圳', '广东'),
       (31, '吴倩', '女', 30, '成都', '四川'),
       (32, '郑凯', '男', 35, '南京', '江苏');

![QQ_1785309582481.png](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/949051d19a5243c09ce2460e6051c16a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgOWnnvJbnqIs=:q75.awebp?rk3s=f64ab15b&x-expires=1785914646&x-signature=o6CIVnz7mtRJPqeeEaAjxHmZgLg%3D)

QQ_1785309582481.png QQ_1785241277134.png

五. 小结

本期内容主要聚焦于文本向量技术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 数据库的集成。感谢阅读!