当前位置: 首页 > news >正文

SpringBoot整合Milvus向量数据库

SpringBoot 整合 Milvus 向量数据库实战

从环境搭建到代码落地,一文搞懂如何在 SpringBoot 项目中集成 Milvus 向量数据库,构建自己的 RAG(检索增强生成)知识库系统。


一、前言

在 AI 大模型时代,向量数据库 已成为构建智能应用的核心基础设施。无论是知识库问答(RAG)、语义搜索、图片检索还是推荐系统,都离不开向量数据库的身影。

本文将基于一个真实的 SpringBoot 项目,手把手带你完成 Milvus 向量数据库 的集成,涵盖:

  • Milvus 服务端安装与配置
  • BGE-M3 本地 Embedding 模型部署(CPU/GPU/在线三种方案)
  • SpringBoot 中 Milvus 客户端的配置与 CRUD 操作
  • 一个完整的 RAG 知识库查询流程

二、技术栈概览

组件 版本/选型 说明
SpringBoot 2.0.0.RELEASE 项目基础框架
JDK 1.8 Java 运行环境
Milvus 2.6.1 向量数据库(服务端)
milvus-sdk-java 2.6.1 Java 客户端 SDK
BGE-M3 ONNX 格式 本地 Embedding 模型
ONNX Runtime 1.23.2 模型推理引擎
LangChain4j 0.31.0 文档分割工具
Python 3.10.10 用于 pymilvus 调试管理

三、环境准备

3.1 Milvus 服务端安装(Linux)

Milvus 支持 Docker 部署和 RPM 包安装两种方式。生产环境推荐使用 RPM 包方式,更稳定且便于管理。

下载 RPM 包

从 Milvus GitHub Releases 下载对应版本的 RPM 包:

# 以 milvus 2.6.9 为例(推荐使用与 SDK 匹配的 2.6.x 版本)
wget https://github.com/milvus-io/milvus/releases/download/v2.6.1/milvus_2.6.9-1_amd64.rpm

安装与启动

# 安装 RPM 包
yum install -y ./milvus_2.6.9-1_amd64.rpm# 验证安装
rpm -qa | grep milvus# 启动 Milvus 服务
systemctl start milvus# 查看服务状态
systemctl status milvus# 设置开机自启
systemctl enable milvus

默认启动后,Milvus 监听 19530 端口,支持 gRPC 和 HTTP 两种协议。

3.2 管理工具 Attu

Attu 是 Zilliz 官方出品的 Milvus 可视化管理工具,强烈推荐安装:

  • 支持 Collection 的创建、查看、删除
  • 可视化数据浏览与查询
  • 索引管理

直接下载对应平台的客户端即可使用,连接时填写 Milvus 服务地址和端口(默认 host:19530)。

3.3 Python 客户端(可选)

用于日常调试和数据管理:

# Python 版本要求 3.10.10
pip install pymilvus==2.4.6
pip install milvus

3.4 本地 Embedding 模型 BGE-M3

向量化是向量数据库的核心前置步骤。本项目使用 BGE-M3 模型,支持本地 CPU 推理和 GPU 推理两种模式。

模型下载

从 bge-m3-onnx 下载 ONNX 格式的模型文件,包含两个核心文件:

  • bge_m3_tokenizer.onnx — 分词器模型
  • bge_m3_model.onnx — 主模型(输出 1024 维向量)

将模型文件放置到本地目录(如 I:/bgem3/onnx),后续 SpringBoot 会通过 ONNX Runtime 加载。

3.5 Ollama 部署(备选方案)

如果不想在 Java 端加载 ONNX 模型,也可以通过 Ollama 提供 embedding 服务:

安装 Ollama

从 cnb.cool/hex/ollama 下载 Windows 客户端,或使用 Docker 部署。

配置镜像加速

编辑 %USERPROFILE%\.ollama\config.json

{"registry": {"mirrors": {"registry.ollama.ai": "https://registry.ollama.ai"}}
}

拉取 BGE-M3 模型

ollama pull bge-m3
ollama list

启动后 Ollama 会在本地 11434 端口提供 API 服务,可作为在线 embedding 的备选方案。


四、项目配置

4.1 Maven 依赖

pom.xml 中引入核心依赖:

<!-- Milvus Java SDK -->
<dependency><groupId>io.milvus</groupId><artifactId>milvus-sdk-java</artifactId><version>2.6.1</version>
</dependency><!-- ONNX Runtime(本地 CPU 推理) -->
<dependency><groupId>com.microsoft.onnxruntime</groupId><artifactId>onnxruntime_gpu</artifactId><version>1.23.2</version>
</dependency><!-- ONNX Runtime 扩展(分词器) -->
<dependency><groupId>com.microsoft.onnxruntime</groupId><artifactId>onnxruntime-extensions</artifactId><version>0.13.0</version>
</dependency><!-- LangChain4j(文档分割) -->
<dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j</artifactId><version>0.31.0</version>
</dependency><!-- FastJSON(JSON 处理) -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version>
</dependency>

版本说明:Milvus SDK 版本 2.6.1 与服务端 2.6.x 保持一致,使用 V2 版 API(MilvusClientV2),代码风格更简洁,推荐使用。

4.2 配置文件

application.yml 中 Milvus 相关配置:

milvus:# 连接协议:grpc 或 httpproxy: grpc# Milvus 服务 IPhost: 192.168.1.250# Milvus 服务端口port: 19530# 数据库名(默认 default)database: default# Embedding 硬件模式:cpu / gpu / onlinehardware: cpu# ONNX 模型本地路径onnxpath: I:/bgem3/onnx# 在线 Embedding 模型名modelname: bge-m3-yidong

五、核心代码实现

5.1 配置类 — MilvusConfig

application.yml 中的 Milvus 配置映射为 Java Bean:

package com.haopan.ai.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "milvus")
public class MilvusConfig {// 连接协议:grpc / httpprivate String proxy;// Milvus 服务 IPprivate String host;// Milvus 端口private Integer port;// 数据库名private String database;// 硬件模式:cpu / gpu / onlineprivate String hardware;// ONNX 模型路径private String onnxpath;// 在线 embedding 模型名称private String modelname;public MilvusConfig() {hardware = "cpu";   // 默认 CPU 模式}// getter / setter 略...
}

5.2 MilvusClient Bean 注册

Application.java 启动类中创建 MilvusClientV2 Bean:

@Bean(destroyMethod = "close")
@Lazy
public MilvusClientV2 milvusClient(MilvusConfig milvusConfig) {ConnectConfig connectConfig = ConnectConfig.builder().uri(milvusConfig.getProxy() + "://" + milvusConfig.getHost() + ":" + milvusConfig.getPort()).dbName(milvusConfig.getDatabase()).build();return new MilvusClientV2(connectConfig);
}

关键点说明:

  • @Lazy 延迟初始化,避免启动时连接失败导致项目无法启动
  • destroyMethod = "close" 确保应用关闭时释放连接
  • URI 格式:grpc://192.168.1.250:19530

5.3 Embedding 服务(多模式设计)

项目设计了 策略模式 解耦 Embedding 的实现,通过 milvus.hardware 配置自动切换:

// 接口定义
public interface EmbeddingService {float[] embed(String text);
}

模式一:CPU 本地推理(默认)

@Service
@ConditionalOnProperty(name = "milvus.hardware", havingValue = "cpu", matchIfMissing = true)
public class CPUEmbeddingService implements EmbeddingService {@Autowiredprivate MilvusConfig milvusConfig;@Overridepublic float[] embed(String text) {try {M3Embedder embedder = M3Embedder.getInstance(milvusConfig.getOnnxpath());M3EmbeddingOutput result = embedder.generateEmbeddings(text);return result.getDenseEmbedding();   // 返回 1024 维向量} catch (Exception e) {throw new RuntimeException("Local embedding failed", e);}}
}

模式二:GPU 本地推理

@Service
@ConditionalOnProperty(name = "milvus.hardware", havingValue = "gpu")
public class GPUEmbeddingService implements EmbeddingService {// 与 CPU 模式相同,底层 ONNX Runtime 自动使用 CUDA 加速// 需配置 CUDA 环境变量和 onnxruntime_gpu 依赖
}

模式三:在线 API 调用

@Service
@ConditionalOnProperty(name = "milvus.hardware", havingValue = "online")
public class OnlineEmbeddingService implements EmbeddingService {@Autowiredprivate AiProperties aiProperties;@Autowiredprivate MilvusConfig milvusConfig;@Overridepublic float[] embed(String text) {// 从配置中获取模型信息(支持 OpenAI 兼容 API)ModelConfig config = aiProperties.getConfig(milvusConfig.getModelname());String requestUrl = config.getUrl() + "/embeddings";// 构建请求体Map<String, Object> requestBody = new HashMap<>();requestBody.put("model", config.getModel());requestBody.put("input", text);// 通过 WebClient 调用 /embeddings 接口String result = webClient.post().uri(requestUrl).headers(httpHeaders -> httpHeaders.addAll(headers)).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromObject(requestBody)).retrieve().bodyToMono(String.class).block();// 解析返回的向量JSONObject obj = JSONObject.parseObject(result);JSONArray embeddingArray = obj.getJSONArray("data").getJSONObject(0).getJSONArray("embedding");float[] queryVector = new float[embeddingArray.size()];for (int i = 0; i < embeddingArray.size(); i++) {queryVector[i] = embeddingArray.getFloat(i);}return queryVector;}
}

三种模式对比:

模式 优点 缺点 适用场景
CPU 本地 无网络依赖,数据安全 推理速度较慢 小数据量、内网环境
GPU 本地 推理速度极快 需 GPU 硬件、CUDA 环境 大批量数据处理
在线 API 免部署模型,跨平台 依赖网络,有费用 快速验证、云端部署

5.4 核心服务 — VectorService

VectorService 封装了 Milvus 的全部 CRUD 操作,是整个系统的核心。

5.4.1 向量查询(语义搜索)

public List<String> query(String queryText, List<String> collectNameList, int limit) {List<String> queryResult = new ArrayList<>();// 1. 将查询文本转为向量float[] queryVector = getEmbedding(queryText);FloatVec floatVector = new FloatVec(queryVector);// 2. 设置搜索参数(内积相似度)JSONObject searchParams = new JSONObject();searchParams.put("metric_type", "IP");  // Inner Product// 3. 遍历多个 Collection 进行搜索for (String collectionName : collectNameList) {SearchReq searchReq = SearchReq.builder().collectionName(collectionName).data(Collections.singletonList(floatVector)).annsField("embeddings")           // 向量字段.outputFields(Collections.singletonList("text"))  // 返回字段.searchParams(searchParams).topK(limit)                        // 返回 Top-K.consistencyLevel(ConsistencyLevel.STRONG).build();SearchResp searchResp = milvusClient.search(searchReq);// 4. 提取搜索结果if (searchResp.getSearchResults() != null && !searchResp.getSearchResults().isEmpty()) {List<SearchResp.SearchResult> searchResultList = searchResp.getSearchResults().get(0);for (SearchResp.SearchResult searchItem : searchResultList) {String text = String.valueOf(searchItem.getEntity().get("text"));queryResult.add(text);}}}return queryResult;
}

搜索流程:

用户查询文本  →  Embedding 向量化  →  Milvus 相似度搜索  →  返回 Top-K 文本

5.4.2 文档分块

使用 LangChain4j 的 DocumentByCharacterSplitter 进行智能分块:

public List<String> splitText(String content) {// 每块 800 字符,块间重叠 20 字符(保留上下文连贯性)DocumentByCharacterSplitter charSplitter = new DocumentByCharacterSplitter(800, 20);Document document = Document.from(content);List<TextSegment> segments = charSplitter.split(document);return segments.stream().map(TextSegment::text).collect(Collectors.toList());
}

5.4.3 数据插入

public void insert(List<String> contentList, String source, String user_id, String collection_name) {List<JsonObject> dataList = new ArrayList<>();for (int i = 0; i < contentList.size(); i++) {String content = contentList.get(i);float[] insertVector = getEmbedding(content);JsonObject dataItem = new JsonObject();dataItem.addProperty("text", content);dataItem.addProperty("source", source);dataItem.addProperty("uid", user_id);dataItem.addProperty("page", i);// 将 float[] 转为 JSON 数组Gson gson = new Gson();JsonArray jsonArray = gson.toJsonTree(insertVector, new TypeToken<float[]>(){}.getType()).getAsJsonArray();dataItem.add("embeddings", jsonArray);dataList.add(dataItem);}// 自动创建 Collection(如果不存在)if (!checkCollectionExist(collection_name)) {addCollection(collection_name);}// 执行插入InsertReq insertReq = InsertReq.builder().data(dataList).collectionName(collection_name).build();milvusClient.insert(insertReq);
}

5.4.4 Collection 创建

public void addCollection(String collection_name) {if (!checkCollectionExist(collection_name)) {// 定义字段结构List<CreateCollectionReq.FieldSchema> fieldSchemaList = new ArrayList<>();// 主键(自增)fieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("id").dataType(DataType.Int64).isPrimaryKey(true).autoID(true).build());// 用户 IDfieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("uid").dataType(DataType.VarChar).maxLength(500).build());// 文本内容fieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("text").dataType(DataType.VarChar).maxLength(50000).build());// 页码fieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("page").dataType(DataType.Int16).build());// 向量字段(1024 维)fieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("embeddings").dataType(DataType.FloatVector).dimension(1024)     // BGE-M3 输出 1024 维.build());// 文档来源fieldSchemaList.add(CreateCollectionReq.FieldSchema.builder().name("source").dataType(DataType.VarChar).maxLength(5000).build());// 构建 SchemaCreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema.builder().fieldSchemaList(fieldSchemaList).build();// 索引配置(IVF_FLAT + 内积相似度)HashMap indexExtraParam = new HashMap();indexExtraParam.put("nlist", 16384);IndexParam indexParam = IndexParam.builder().fieldName("embeddings").metricType(IndexParam.MetricType.IP).indexType(IndexParam.IndexType.IVF_FLAT).extraParams(indexExtraParam).build();// 创建 CollectionCreateCollectionReq createCollectionReq = CreateCollectionReq.builder().collectionName(collection_name).description("database").autoID(true).consistencyLevel(ConsistencyLevel.BOUNDED).collectionSchema(collectionSchema).primaryFieldName("id").vectorFieldName("embeddings").indexParam(indexParam).build();milvusClient.createCollection(createCollectionReq);}
}

Collection 字段设计:

字段名 类型 说明
id Int64 主键,自增
uid VarChar(500) 用户标识
text VarChar(50000) 文档分块后的文本
page Int16 分块页码
embeddings FloatVector(1024) BGE-M3 向量
source VarChar(5000) 文档来源标识

5.4.5 数据删除

// 按 source 删除指定 Collection 中的数据
public void delete(String source, String user_id, String collection_name) {if (checkCollectionExist(collection_name)) {DeleteReq deleteReq = DeleteReq.builder().collectionName(collection_name).filter("source=='" + source + "'").build();milvusClient.delete(deleteReq);}
}// 删除整个 Collection
public void deleteCollection(String collection_name) {if (checkCollectionExist(collection_name)) {DropCollectionReq deleteReq = DropCollectionReq.builder().collectionName(collection_name).build();milvusClient.dropCollection(deleteReq);}
}

5.5 REST API 层 — VectorController

对外暴露 RESTful 接口:

@RestController
public class VectorController {@Autowiredprivate VectorService vectorService;// 文本向量化@PostMapping("/embedding/")public Object embedding(@RequestBody Map<String, Object> params) {String input = ConvertOp.convert2String(params.get("input"));return vectorService.getEmbedding(input);}// 向量相似度搜索@PostMapping("/milvus/query/")public Object query(@RequestBody Map<String, Object> params) {String query = ConvertOp.convert2String(params.get("query"));String collection_name = ConvertOp.convert2String(params.get("collection_name"));int limit = StringUtils.isEmpty(params.get("limit")) ? 5 : ConvertOp.convert2Int(params.get("limit"));List<String> collectionNameList = Arrays.asList(collection_name.split("\\;"));return vectorService.query(query, collectionNameList, limit);}// 文本分块 + 插入@PostMapping("/milvus/split_insert/")public Object split_insert(@RequestBody Map<String, Object> params) {String content = ConvertOp.convert2String(params.get("content"));String source = ConvertOp.convert2String(params.get("source"));String user_id = ConvertOp.convert2String(params.get("user_id"));String collection_name = ConvertOp.convert2String(params.get("collection_name"));// 1. 分块List<String> splitList = vectorService.splitText(content);// 2. 插入vectorService.insert(splitList, source, user_id, collection_name);return success();}// 直接插入(已分块数据)@PostMapping("/milvus/insert/")public Object insert(@RequestBody DocumentModel documentModel) {vectorService.insert(documentModel.getContent().stream().map(DocumentPageModel::getPage_content).collect(Collectors.toList()),documentModel.getSource(),documentModel.getUser_id(),documentModel.getCollection_name());return success();}// 删除数据@PostMapping("/milvus/delete/")public Object delete(@RequestBody Map<String, Object> params) {vectorService.delete(ConvertOp.convert2String(params.get("source")),ConvertOp.convert2String(params.get("user_id")),ConvertOp.convert2String(params.get("collection_name")));return success();}// 删除 Collection@PostMapping("/milvus/delete_collection/")public Object delete_collection(@RequestBody Map<String, Object> params) {vectorService.deleteCollection(ConvertOp.convert2String(params.get("collection_name")));return success();}
}

5.6 RAG 应用集成

项目将 Milvus 向量搜索深度集成到了大模型对话中(OpenAiService),实现完整的 RAG 流程:

// 对话时自动进行向量搜索增强
if (aiProperties.isQueryvector()) {if (!StringUtils.isEmpty(collection_name) && !collection_name.equals("common")) {// 1. 向量搜索List<String> collectionNameList = Arrays.asList(collection_name.split("\\;"));List<String> promptList = vectorService.query(query, collectionNameList, 5);// 2. 组装 Prompt 模板String promptTemplate = "你是一位文献专家,请结合上下文和检索出的内容回答问题:\n" +"### 上下文\n" + context + "\n" +"### 检索内容\n" + String.join(",", promptList) + "\n" +"### 问题\n" + query + "\n" +"## 回答必须实事求是。\n" +"## 若上下文与问题无关请忽略。只根据检索内容回答。\n" +"## 若检索内容与问题无关请忽略,自己回答。";// 3. 发送给大模型// ...}
}

六、完整调用流程

6.1 知识入库流程

┌──────────┐    ┌──────────────┐    ┌───────────┐    ┌──────────┐
│ 原始文档  │ →  │ 智能分块(800) │ →  │ 向量化生成  │ →  │ Milvus   │
│ (PDF等)  │    │ 重叠(20)     │    │ BGE-M3    │    │ Insert   │
└──────────┘    └──────────────┘    └───────────┘    └──────────┘

API 调用示例:

curl -X POST http://localhost:9050/milvus/split_insert/ \-H "Content-Type: application/json" \-d '{"content": "Milvus 是由 Zilliz 开发的开源向量数据库...","source": "doc_001","user_id": "user_123","collection_name": "my_knowledge"}'

6.2 语义检索流程

┌──────────┐    ┌──────────────┐    ┌───────────┐    ┌──────────┐
│ 用户提问  │ →  │ 向量化        │ →  │ Milvus    │ →  │ 检索结果  │
│ "什么是.."│    │ BGE-M3      │    │ Search    │    │ Top-K    │
└──────────┘    └──────────────┘    └───────────┘    └──────────┘↓
┌──────────┐    ┌──────────────┐    ┌───────────┐    ┌──────────┐
│ 最终回答  │ ←  │ LLM 生成      │ ←  │ Prompt    │ ←  │ 拼接上下文│
└──────────┘    └──────────────┘    └───────────┘    └──────────┘

API 调用示例:

curl -X POST http://localhost:9050/milvus/query/ \-H "Content-Type: application/json" \-d '{"query": "什么是向量数据库?","collection_name": "my_knowledge","limit": 5}'

七、总结

本文完整呈现了 SpringBoot 整合 Milvus 向量数据库的全过程,核心要点回顾:

模块 关键技术 要点
服务端 Milvus 2.6.1 RPM 部署 默认 gRPC 19530 端口
客户端 milvus-sdk-java 2.6.1 使用 MilvusClientV2 API
Embedding BGE-M3 + ONNX Runtime 支持 CPU/GPU/在线三种模式
文档处理 LangChain4j 800 字符分块 + 20 字符重叠
索引策略 IVF_FLAT + 内积相似度 nlist=16384
一致性 BOUNDED 兼顾性能与一致性

项目亮点:

  1. 多模式 Embedding:通过 @ConditionalOnProperty 实现 CPU/GPU/在线三种方案的自动切换,满足不同部署场景
  2. 策略模式解耦EmbeddingService 接口 + 多种实现,清晰分离向量化逻辑与业务代码
  3. 完整的RAG链路:从文档入库 → 分块 → 向量化 → 相似搜索 → LLM 增强回答,端到端闭环
  4. 生产级设计:Lazy 连接、destroyMethod 资源释放、自动建 Collection、异常处理

希望本文能帮助你在自己的项目中快速集成 Milvus 向量数据库,构建出强大的智能应用!


参考资料

  • Milvus 官方文档
  • Milvus Java SDK
  • BGE-M3 ONNX 模型
  • Attu 管理工具
  • LangChain4j
http://www.gsyq.cn/news/1435185.html

相关文章:

  • 从平面点云到清晰轮廓:结合RANSAC与AC方法,搞定复杂场景下的轮廓提取
  • d2dx终极指南:让暗黑破坏神2在现代PC上完美运行的完整解决方案
  • Android逆向分析的终极利器:Androguard完全指南
  • 揭秘Open Claw:从概念到工业应用的开放式夹持技术全解析
  • 2026年7月长春黄金回收市场实测:金价高位下的变现选择 - 黄金回收
  • 2026 济南名表回收专业测评,添价收综合表现稳居优选 - 薛定谔的梨花猫
  • 3个步骤让Windows系统飞起来:AtlasOS性能优化完全指南
  • Android crash、anr
  • DamaiHelper:三分钟掌握高效抢票的完整解决方案
  • 3分钟重塑Windows 11任务栏:从实用工具到个性化桌面艺术
  • 2026 重庆奢侈品回收综合测评,添价收品类齐全实力雄厚 - 薛定谔的梨花猫
  • 构建跨平台直播聚合系统的Dart架构设计与实现
  • 2026年海南注册公司代办测评前五名|亲测真实推荐|主流财税公司全解析 - GrowthUME
  • 2026西安黄金回收去哪里最安全?7家正规门店口碑实测唐王珠宝(无折旧无隐形扣费) - 西安闲转记
  • 智慧职教刷课脚本:5分钟实现全平台自动学习,轻松解放学习时间
  • tchMaterial-parser:智慧教育平台电子课本下载的完整解决方案
  • 2026 重庆翡翠回收出手指南:添价收简化流程便捷变现 - 薛定谔的梨花猫
  • 如何彻底解决PCL2启动器整合包Mod注入失败的终极指南
  • 实测30+门店!2026大理婚纱照前十名靠谱推荐,这一家闭眼入 - charlieruizvin
  • 如何用现代Web技术实现GitHub下载加速:Fast-GitHub的技术实现解析
  • OBS Advanced Timer:6种计时模式打造专业直播体验的终极指南
  • 在 Simulink 中推挽式(Push-Pull)DC-DC 变换器,并搭建一套完整的磁芯饱和抑制仿真模型
  • 2026河源黄金奢侈品回收机构排名出炉!闲置变现避坑首选这几家 - 小仙贝贝
  • 破解酱料代加工同质化痛点:R-P-S全链路定制方法论如何赋能品牌增长? - 资讯纵览
  • 3大黑科技揭秘:如何用TripoSR实现0.5秒单图像3D重建
  • Keepalived总结
  • OBS StreamFX完整指南:免费插件打造电影级直播画面
  • AT_abc453_f 解题报告
  • 5分钟掌握ChanlunX缠论插件:告别手工画图,实现自动化技术分析
  • 基于Arduino的35mm幻灯片自动化数字化系统:从改造投影仪到批量处理