第一次使用 Milvus:给我的 AI 知识库装上"记忆"
系列:《从 0 打造我的本地 AI 知识库:Obsidian + Ollama + Milvus + RAG + MCP + Agent》
第 04 篇
上一篇:《我终于搞懂 Embedding:为什么一句话可以变成 1024 个数字?》
本文关键词:
MilvusMilvus LiteVector Database向量数据库CollectionCosine SimilarityPython

上一篇,我们已经解决了一个非常关键的问题:
text
文字
↓
Embedding
↓
1024维向量
例如:
text
MySQL MVCC 是多版本并发控制机制
经过 Embedding 模型以后,会变成一串数字:
text
[-0.0655, -0.0861, -0.0121, ...]
但是新的问题来了。
这些向量应该放在哪里?
如果只有一条数据,我们完全可以:
python
vectors = []
然后把向量放进去。
但如果我的 Obsidian 以后有:
text
100篇
1000篇
10000篇
甚至每篇文章又被切成几十个 Chunk。
很快就会变成:
text
几十万个向量
这时候,简单的 Python List 显然不够用了。
于是,我需要一个专门负责:
保存向量 + 搜索相似向量
的数据库。
这就是:
Vector Database【向量数据库】
而我这次选择的是:
Milvus
一、Milvus 到底是什么?
第一次看到 Milvus 的时候,我其实把它理解成:
"是不是一个专门存 Embedding 的数据库?"
这个理解虽然不算错,但还不够准确。
更准确地说:
Milvus 是一个专门用于向量数据存储和相似度检索的向量数据库。
普通数据库擅长:
text
id = 1001
name = Tom
age = 20
然后查询:
sql
SELECT *
FROM user
WHERE age = 20;
而向量数据库主要解决的是:
text
给我一个向量
↓
帮我找到数据库中
与它最相似的向量。
例如:
text
用户问题
↓
Embedding
↓
[0.12, -0.32, 0.45, ...]
↓
Milvus
↓
找最相似的向量
这正好就是我的 RAG 所需要的能力。
二、为什么不用 MySQL?
这是一个很好的问题。
我本身就是 Java 后端开发,第一反应其实是:
"我已经会 MySQL 了,为什么不直接用 MySQL?"
当然,现在很多数据库已经具备向量搜索能力。
但是在这个项目里,我希望真正理解:
向量数据库到底是干什么的。
所以第一版我选择专门的向量数据库 Milvus。
这样可以把概念拆得非常清楚:
text
MySQL
↓
传统结构化数据
Milvus
↓
向量数据
当然,实际生产系统里到底选 MySQL、PostgreSQL + pgvector、Milvus、Elasticsearch 等,要根据规模和场景决定。
但对于我这个学习型项目:
Milvus 非常适合拿来理解 Vector Search。
三、我为什么选择 Milvus Lite?
如果直接部署完整的 Milvus 集群,事情一下子就复杂起来了。
可能涉及:
text
Docker
Kubernetes
Etcd
对象存储
消息系统
Milvus Server
但我现在只是:
text
Mac M1
16GB
个人项目
完全没必要一开始就搞这么复杂。
所以我选择:
Milvus Lite
它可以直接嵌入到 Python 项目里。
我的目标非常简单:
text
Mac
│
├── Ollama
│
├── Python
│
└── Milvus Lite
甚至不需要先部署一个庞大的数据库集群。
这对于学习和小规模个人知识库来说非常方便。
四、先准备 Python 环境
我的项目目录:
text
local-ai-kb/
├── .venv/
├── data/
├── obsidian/
└── ...
进入项目:
bash
cd ~/local-ai-kb
启动虚拟环境:
bash
source .venv/bin/activate
然后安装 Milvus Lite:
bash
pip install -U "pymilvus[milvus-lite]"
安装完成以后,就可以开始写第一段代码。
五、创建我的第一个 Milvus Collection
我先创建:
text
milvus_test.py
代码:
python
from pymilvus import MilvusClient
DB_PATH = "./data/milvus.db"
COLLECTION_NAME = "knowledge_base"
VECTOR_DIMENSION = 1024
client = MilvusClient(DB_PATH)
if client.has_collection(collection_name=COLLECTION_NAME):
client.drop_collection(collection_name=COLLECTION_NAME)
client.create_collection(
collection_name=COLLECTION_NAME,
dimension=VECTOR_DIMENSION,
metric_type="COSINE"
)
print("================================")
print("Milvus 初始化成功")
print("================================")
print("数据库:", DB_PATH)
print("Collection:", COLLECTION_NAME)
print("向量维度:", VECTOR_DIMENSION)
print("距离算法:", "COSINE")
运行:
bash
python milvus_test.py
最终看到:
text
================================
Milvus 初始化成功
================================
数据库: ./data/milvus.db
Collection: knowledge_base
向量维度: 1024
距离算法: COSINE
到这里,我的第一个向量数据库已经创建出来了。
六、这里出现了一个新概念:Collection
第一次接触 Milvus 时,我看到:
python
COLLECTION_NAME = "knowledge_base"
一开始不知道:
Collection 是什么?
可以先简单理解成:
Collection ≈ 一组相关向量数据的集合。
如果类比传统数据库:
text
MySQL
↓
Database
↓
Table
Milvus:
text
Milvus
↓
Collection
↓
Vector Data
比如以后我可以设计:
text
knowledge_base
interview_questions
blog_knowledge
novel_knowledge
第一阶段,我只需要:
text
knowledge_base
就够了。
七、为什么必须指定 1024?
这里和上一篇的 Embedding 就连接起来了。
上一篇我们已经知道:
text
Qwen3-Embedding 0.6B
↓
1024维
那么 Milvus 就必须知道:
我要保存的向量到底是多少维?
所以:
python
VECTOR_DIMENSION = 1024
如果 Embedding 模型输出:
text
1024个数字
那么 Milvus Collection 就应该配置:
text
dimension = 1024
它们必须匹配。
也就是:
text
Embedding 模型
│
│ 1024维
▼
Milvus
│
│ 1024维
▼
保存向量
如果以后换成其他 Embedding 模型,而它输出的是:
text
768维
那么对应的 Collection 设计也需要匹配。
这也是为什么:
Embedding 模型一旦确定,向量维度就是整个向量数据库设计中的重要参数。
八、Cosine 又出现了
上一篇讲 Embedding 的时候,我们已经认识了:
text
Cosine Similarity
【余弦相似度】
现在它又出现了:
python
metric_type="COSINE"
这是什么意思?
就是告诉 Milvus:
以后进行向量搜索时,我希望使用 Cosine 这一类相似度/距离度量。
于是:
text
用户问题
↓
Embedding
↓
问题向量
↓
Milvus
↓
Cosine
↓
找到相似向量
整个链路就连接起来了。
九、Milvus 现在还没有"记忆"
这里我要特别强调一下。
运行完:
bash
python milvus_test.py
并不代表知识库已经有知识了。
现在只是:
text
Milvus
↓
创建了一个空的 Collection
就像:
text
你创建了一个数据库
但里面:
text
没有数据
所以现在的状态是:
text
knowledge_base
空
↓
[ ]
下一步,我们才真正给它放入第一条知识。
十、把我的第一篇 Markdown 变成向量
我准备了一篇测试笔记:
text
obsidian/test.md
内容:
markdown
# MySQL MVCC
MVCC 是 Multi-Version Concurrency Control,
即多版本并发控制。
InnoDB 通过 Undo Log 和 Read View 实现 MVCC。
MVCC 可以在保证事务隔离性的同时,
减少读操作和写操作之间的锁竞争。
现在要做的事情就是:
text
test.md
↓
读取 Markdown
↓
Embedding
↓
1024维向量
↓
Milvus
十一、第一次向 Milvus 插入数据
我创建:
text
insert_test.py
代码:
python
import requests
from pymilvus import MilvusClient
OLLAMA_URL = "http://localhost:11434/api/embed"
EMBEDDING_MODEL = "qwen3-embedding:0.6b"
DB_PATH = "./data/milvus.db"
COLLECTION_NAME = "knowledge_base"
FILE_PATH = "./obsidian/test.md"
# 1. 读取 Markdown
with open(FILE_PATH, "r", encoding="utf-8") as f:
text = f.read()
print("读取文件:")
print(FILE_PATH)
print("文本长度:", len(text))
# 2. 调用 Embedding 模型
response = requests.post(
OLLAMA_URL,
json={
"model": EMBEDDING_MODEL,
"input": text
}
)
response.raise_for_status()
data = response.json()
embedding = data["embeddings"][0]
print("Embedding 维度:", len(embedding))
# 3. 连接 Milvus
client = MilvusClient(DB_PATH)
# 4. 准备数据
data = [{
"id": 1,
"vector": embedding,
"text": text,
"source": FILE_PATH
}]
# 5. 插入
result = client.insert(
collection_name=COLLECTION_NAME,
data=data
)
print("================================")
print("数据插入 Milvus 成功")
print("================================")
print("插入结果:", result)
运行:
bash
python insert_test.py
得到:
text
读取文件:
./obsidian/test.md
文本长度:147
Embedding 维度:1024
================================
数据插入 Milvus 成功
================================
插入结果:
{'insert_count': 1, 'ids': [1]}
这一刻,我的 Milvus 终于不再是空的。
十二、Milvus 里面现在到底有什么?
现在可以把数据库想象成:
text
knowledge_base
│
└── id = 1
│
├── vector
│ └── [1024个数字]
│
├── text
│ └── MySQL MVCC......
│
└── source
└── ./obsidian/test.md
这里我故意保存了三个东西:
text
vector
text
source
为什么?
因为:
vector
用于:
text
相似度搜索
text
用于:
text
找到以后,把原始知识取出来
source
用于:
text
告诉 AI:
这段知识来自哪个文件?
所以一条知识实际上变成了:
text
┌─────────────────────────┐
│ ID │
├─────────────────────────┤
│ Vector │
│ [1024个数字] │
├─────────────────────────┤
│ Text │
│ MySQL MVCC...... │
├─────────────────────────┤
│ Source │
│ obsidian/test.md │
└─────────────────────────┘
这就开始有点像真正的知识库了。
十三、第一次查询,我遇到了一个坑
接下来我当然想:
"那我能不能搜索一下?"
于是我第一次查询的时候遇到了:
text
Collection 'knowledge_base'
is in state 'released';
call load() before search/get/query
刚看到这个错误,我还有点懵。
是不是数据库坏了?
是不是 Milvus 没启动?
都不是。
十四、Collection 的 Load 是什么?
Milvus 为了进行搜索,需要把 Collection 加载到可用于搜索的状态。
所以在搜索之前,需要:
python
client.load_collection(
collection_name=COLLECTION_NAME
)
于是:
text
创建 Collection
↓
插入数据
↓
Collection
↓
Load
↓
Search
这个错误反而让我真正理解了一件事情:
Milvus 的"数据存在"和"当前可搜索状态"不是完全一回事。
十五、第一次真正的向量搜索
现在准备一个问题:
text
MySQL 的 MVCC 是什么?
首先:
text
问题
↓
Embedding
↓
1024维向量
然后:
text
问题向量
↓
Milvus
↓
Search
代码:
python
import requests
from pymilvus import MilvusClient
OLLAMA_EMBED_URL = "http://localhost:11434/api/embed"
EMBEDDING_MODEL = "qwen3-embedding:0.6b"
DB_PATH = "./data/milvus.db"
COLLECTION_NAME = "knowledge_base"
question = "MySQL 的 MVCC 是什么?"
# 1. 问题 Embedding
response = requests.post(
OLLAMA_EMBED_URL,
json={
"model": EMBEDDING_MODEL,
"input": question
}
)
response.raise_for_status()
query_vector = response.json()["embeddings"][0]
# 2. 连接 Milvus
client = MilvusClient(DB_PATH)
# 3. 加载 Collection
client.load_collection(
collection_name=COLLECTION_NAME
)
# 4. 搜索
results = client.search(
collection_name=COLLECTION_NAME,
data=[query_vector],
limit=3,
output_fields=[
"text",
"source"
]
)
# 5. 打印结果
for result in results[0]:
print("相似度:", result["distance"])
print("来源:", result["entity"]["source"])
print("内容:")
print(result["entity"]["text"])
运行以后,我第一次真正看到了:
text
================================
用户问题
================================
MySQL 的 MVCC 是什么?
问题向量维度:1024
Collection 加载成功
================================
向量搜索结果
================================
相似度:
0.8407926559448242
来源:
./obsidian/test.md
内容:
# MySQL MVCC
MVCC 是 Multi-Version Concurrency Control,
即多版本并发控制。
InnoDB 通过 Undo Log 和 Read View 实现 MVCC。
MVCC 可以在保证事务隔离性的同时,
减少读操作和写操作之间的锁竞争。
十六、这一刻,RAG 的前半部分终于跑通了
回头看整个过程:
text
test.md
│
▼
Qwen3-Embedding
0.6B
│
▼
1024维向量
│
▼
Milvus
│
│
用户问题 ──→ Embedding ─┘
│
▼
相似度搜索
│
▼
test.md
这其实已经是:
Retrieval【检索】
了。
还没有生成最终答案。
但我们已经完成了 RAG 中非常关键的一半:
text
RAG
│
├── Retrieval ← 现在已经跑通
│
└── Generation ← 下一步
十七、Milvus 在整个系统中的位置
现在整个架构可以画成:
text
Obsidian
│
▼
Markdown
│
▼
Qwen3-Embedding 0.6B
│
▼
1024维 Vector
│
▼
┌───────────┐
│ Milvus │
│ Lite │
└─────┬─────┘
│
Vector Search
│
▼
相关知识片段
│
▼
Qwen3.5 9B
│
▼
AI回答
注意:
Milvus 本身不会回答问题。
它只负责:
帮我找到相关知识。
真正负责回答的是:
text
Qwen3.5 9B
十八、到这里,我终于理解了"向量数据库"
以前我对向量数据库的理解非常模糊。
现在可以用一句非常简单的话总结:
向量数据库就是帮我们保存向量,并快速找到与目标向量相似的数据。
在我的项目里面:
text
Embedding
负责:
"把文字变成向量"
Milvus
负责:
"保存向量 + 找相似向量"
Qwen3.5
负责:
"根据找到的知识生成答案"
三者职责完全不同。
十九、但现在这个知识库还有一个巨大问题
目前我的数据库只有:
text
test.md
一篇文章。
这当然没有任何实际意义。
真正的 Obsidian 可能是:
text
Obsidian
│
├── Java
│ ├── HashMap.md
│ ├── ConcurrentHashMap.md
│ └── ThreadPool.md
│
├── JVM
│ ├── G1.md
│ ├── GC.md
│ └── JVM内存.md
│
├── MySQL
│ ├── MVCC.md
│ ├── 索引.md
│ ├── 事务.md
│ └── 锁.md
│
├── Redis
│
├── Spring
│
└── AI
如果还是:
python
FILE_PATH = "./obsidian/test.md"
一个一个手动处理:
text
test1.md
test2.md
test3.md
test4.md
...
那我迟早会疯掉。
所以接下来真正的问题变成:
如何自动处理整个 Obsidian?
二十、而且还有另一个问题:一篇文章不能永远是一个向量
假设以后有一篇:
text
JVM 垃圾回收机制.md
有:
text
10000 字
我把整篇文章直接做一次 Embedding:
text
10000字
↓
一个向量
虽然能工作,但效果未必好。
因为用户问:
G1 的 Remembered Set 是干什么的?
而整个 10000 字文章里面可能包含:
text
G1
CMS
Parallel GC
Young GC
Old GC
Remembered Set
Write Barrier
Region
一个向量把这么多知识全部混在了一起。
这就会影响检索的精准度。
所以真正的 RAG 还需要一个非常重要的步骤:
Chunk【文本分块】
也就是:
text
一篇长文章
↓
切成多个知识片段
↓
Chunk 1
Chunk 2
Chunk 3
Chunk 4
↓
分别 Embedding
↓
分别保存到 Milvus
这样:
text
"G1 的 Remembered Set 是什么?"
就更容易精准找到:
text
G1 / Remembered Set
相关的那个 Chunk。
二十一、这一篇我真正学到了什么?
如果把今天的内容压缩成一张图:
text
Markdown
│
▼
Embedding
│
▼
1024维 Vector
│
▼
Milvus
│
┌────────┴────────┐
│ │
保存向量 相似度搜索
│
▼
相关知识
其中:
| 组件 | 作用 |
|---|---|
| Markdown | 保存我的原始知识 |
| Embedding | 把文字转换成向量 |
| Vector | 数字化的语义表示 |
| Milvus | 保存和搜索向量 |
| Cosine | 衡量向量相似程度 |
到这里:
RAG 的 Retrieval 部分,我终于亲手跑通了。
下一篇
下一篇进入整个项目真正的关键:
《第一次完整 RAG:让本地大模型真正"读懂"我的知识库》
我们终于可以把:
text
用户问题
↓
Embedding
↓
Milvus
↓
找到相关知识
↓
Qwen3.5 9B
↓
最终答案
全部连接起来。
也就是说,下一篇之后,我就不再只是拥有:
一个会搜索向量的数据库。
而是真正拥有:
一个能够根据我的 Obsidian 知识回答问题的本地 AI。
这也是整个系列从"AI 基础组件实验"正式进入:
RAG 实战
的转折点。