Milvus是一种专门用来存储和搜索大量向量数据的数据库。
Milvus的优势
- 1、速度超快:在几毫秒内可以从数十亿个向量数据中找到最相似的向量
- 2、容易使用:即便不是专业程序员也能很快学会并使用
- 3、开源免费:所有人都可以使用,无需付费
Milvus能做什么?
- 1、图片搜索:上传一张图片,它能找到相似的图片
- 2、语音识别:说一句话,它能理解话语的意思
- 3、推荐系统:根据喜好,推荐可能喜欢的东西
- 4、智能问答:描述一个问题,找到最相关的答案
Milvus的工作流程
- 1、搜集数据:将文字、图片、语音等转换为向量(数字列表)
- 2、存储向量:将这些向量存储到数据库中
- 3、快速搜索:找相似内容时,快速从中找到最相似的向量
Milvus的安装
这里不详细展开说明,参考官网:milvus.io/docs/zh/ins…
Milvus的图形世界
Attu 是专为 Milvus 向量数据库打造的开源可视化管理工具,能通过图形界面大幅简化 Milvus 的运维与操作,覆盖从集群监控到数据操作的全流程,你可以把它想象成DBeaver和Navicat。
Attu能做什么?
- 1、查看数据:看到存储在
Milvus中所有集合(这里可以理解为表格)和向量数据 - 2、创建集合:建立新的数据存储空间
- 3、导入数据:把数据添加到
Milvus中 - 4、搜索相似内容:找到某个向量最相似的其它向量
- 5、管理数据库:监控数据库的运行状态和性能
Attu链接Milvus
下面这条命令用于启动 Attu,它是 Milvus 向量数据库的图形用户界面工具
docker run -p 8000:3000 -e MILVUS_URL=[你的IP]:19530 zilliz/attu:v2.5 http://[你的IP]:8000/#/
Attu使用方式
- 1、确保
Milvus在运行中 - 2、打开网页浏览器,访问Attu的地址。通常是 http://localhost:8000
- 3、连接到
Milvus服务器 - 4、开始浏览和管理向量数据
创建数据库
图一(创建数据库)
上图所示:
- 名称:创建的集合(
Collection)的名字,可以想象成数据库中的表名,这里叫user表示存储用户相关的数据 - 描述:对集合(
Collection)的描述
ID、向量字段(2)
- 主键字段:每条数据的唯一标识,相当于
身份证号 - 类型:64位证书,适合做主键
- 描述:对主键字段的描述
- 自动ID:系统会自动为每条数据分配唯一ID,无需手动填写
- 向量字段:存储
名称的向量表示 - 类型:浮点型向量
- 维度:每个向量有128个数字(特征维度)
- 描述:对向量字段的描述
标量字段(1)
- 字段:存储用户的名字
- 类型:可变长度字符串,适合存储文本
- Max Length:字段的最大长度
- 默认值:字段的默认值
- 描述:对字段的描述
可选配置
分词器:分词器(Tokenizer)是用来把一段文本拆分成词语或关键词的工具。它是文本检索(如BM25)中非常重要的环节。Standard(标准分词器):适合英文、数字、标点分隔的文本。Whitespace(空格分词):只按空格切分,适合英文、代码等。Chinese(中文分词):适合中文句子,能智能识别词语边界。Simple(简单分词):按字母、数字等简单规则切分。
为要分词?
- 让系统能理解和检索文本中的
关键词 - 支持
BM25等算法的高效匹配 - 提升搜索、推荐、问答、等场景的准确性
合理的选择分词器,是提升Milvus文本检索效果的关键一步。
启用匹配:让集合支持这些条件的匹配与相似度检索,例如输入电脑,系统会在已经分词的词条里找到计算机、笔记本等相关词。类似于在搜索引擎中输入关键字,即便不是完全一样的词也可以搜索到相关的内容。分区键:用来将大量数据分散存储在不同的服务器上(如代码示例一所示)。Nullable:表示这个字段可以为空。类似填写表格时,有些项目是必填(不可为空),有些项目是选填(可以为空)。勾选了Nullable就意味着这个字段可以不填写任何值。
"""将公司同事按照姓氏进行分组(姓氏为分区键)"""
# 导入默认字典,用于分区聚合
from collections import defaultdict
# 导入结巴分词工具
import jieba
"""
定义一个字典,用于存储姓氏和对应同事爱好的列表
"""
dict = [
{
"id": 1,
"partition_key": "周",
"content": "周昊喜欢计算机科学,在他的笔记本电脑上研究人工智能和机器学习算法",
},
{
"id": 2,
"partition_key": "吴",
"content": "吴宇热衷于网络安全技术,使用专业电脑进行漏洞分析和系统防护测试",
},
{
"id": 3,
"partition_key": "郑",
"content": "郑伟对云计算和分布式系统有深入研究,经常搭建服务器计算机集群",
},
{
"id": 4,
"partition_key": "王",
"content": "王涛精通前端开发,在台式电脑上学习最新的Web框架技术",
},
{
"id": 5,
"partition_key": "周",
"content": "周敏精通多种编程语言,在笔记本电脑上编写高效的算法和数据结构",
},
{
"id": 6,
"partition_key": "钱",
"content": "钱峰热衷于系统编程,使用电脑开发底层程序和操作系统组件",
},
{
"id": 7,
"partition_key": "孙",
"content": "孙悦专注于软件架构设计,在计算机上构建大型分布式应用程序",
},
{
"id": 8,
"partition_key": "王",
"content": "李王娜擅长面向对象编程,使用笔记本设计可扩展的软件系统和模块",
},
]
# 定义同义词映射,“键”为代表词,“值”为同义词集合(用于扩展匹配)
synonyms_map = {
"电脑": ["计算机", "笔记本"],
"编程": ["程序设计"],
"人工智能": ["机器学习"],
"网络安全": ["系统防护"],
"前端开发": ["Web"],
"系统编程": ["底层程序"],
"软件架构": ["应用程序"],
"面向对象": ["编程"],
"数据结构": ["算法"],
"操作系统": ["系统组件"],
"分布式": ["云计算", "应用程序"],
"软件系统": ["模块"],
}
# 按“分区键”对字典进行聚合,返回分区字典
def build_partitions(dict):
partitioned_dict = defaultdict(list)
for item in dict:
partitioned_dict[item["partition_key"]].append(item)
return partitioned_dict
# 在所有分区中查找包含指定关键词(或同义词)的字典项
def match_query(partitioned_dict, keyWord: str, use_synonyms=True):
# 构建本次要匹配的词集合,默认为仅包含keyWord
match_words = {keyWord}
# 如果启用同义词且keyword在同义词表中,则扩展为所有同义词
if use_synonyms and keyWord in synonyms_map:
match_words.update(synonyms_map[keyWord])
# 结果列表,用于收集匹配字典
results = []
# 遍历每个分区和分区下的字典项
for partition_key, items in partitioned_dict.items():
for item in items:
# 将字典项的content字段进行分词
words = simple_seg(item["content"])
# 判断有无交集(有则说明匹配)
if match_words.intersection(words):
results.append((partition_key, item["id"], item["content"]))
# 返回所有匹配结果
return results
# 对输入文本进行分词(搜索模型),返回词元列表
def simple_seg(text: str):
return [word for word in jieba.lcut_for_search(text) if word.strip()]
# 程序主入口
if __name__ == "__main__":
# 按分区键聚合字典
partitioned_dict = build_partitions(dict)
# 示例1:只匹配“电脑”本词
hits = match_query(partitioned_dict, keyWord="电脑", use_synonyms=False)
print("仅匹配“电脑”的结果:")
for row in hits:
print(row)
# 示例2:匹配“电脑”及其同义词
hits_with_synonym = match_query(partitioned_dict, keyWord="电脑", use_synonyms=True)
print("\n启用同义词匹配后的结果:")
for row in hits_with_synonym:
print(row)
代码示例一(分区键)
集合属性设置
- 一致性模式
Bounded(有界一致性):可以理解为公司给每个人发了一封邮件,并要求在一定时间内员工都会把邮件读取完,比如在一小时后内,但不要求立即。需要在一定时间内保证一致性时选择。Session(会话一致性):可以理解为公司高层和你的直属领导私聊,然后直属领导下的每一个员工都能按顺序收到消息,其他同事可能暂时收不到消息或顺序不同。需要保证某个“会话拥有者”能立即看到自己提交的内容,其他人稍后同步时选择。Strong(强一致性):可以理解为你的直属领导发布一条通知后,所有组内同事必须立即收到并且确认,直属领导才会继续下一步,这种方式最靠谱,但速度较慢。需要每次都等待所有人确认时选择。Eventually(最终一致性):可以理解为口口相传的小道消息,最终会传到每个人那里,但可能需要较长的时间,并且不保证具体何时。需要在不在意收到消息的顺序和时间时选择。
代码演示
import random
import time
from typing import List, Dict
TEAM_MEMBERS = ["小周", "小吴", "小郑", "小王"]
def bounded_consistency(message: str, bound_seconds: int = 5) -> None:
"""说明:有界一致性,在指定时间内同步即可,不需要立刻"""
print(f"[Bounded] {message},请 {bound_seconds} 秒内同步")
sync_message: Dict[str, float] = {}
for member in TEAM_MEMBERS:
delay = random.uniform(0, bound_seconds)
time.sleep(delay / 10) # 缩短真实等待,方便演示
sync_message[member] = delay
print(f"[Bounded] {member} 在 {delay:.2f} 秒后同步了消息")
print(f"[Bounded] 所有人在 {bound_seconds} 秒内都同步了消息")
def strong_consistency(message: str) -> None:
"""说明:强一致性,必须等所有人收到通知后,才算成功"""
print(f"[Strong] {message},请所有人同步")
for member in TEAM_MEMBERS:
print(f"[Strong] {member} 同步了消息 {message}")
time.sleep(0.1) # 模拟等待同步
print(f"[Strong] 所有人都同步了消息")
def session_consistency(message: str, session_owner: str):
"""说明:会话一致性,保证当前会话看到消息,其他同事稍后再同步"""
print(f"[Session] {session_owner} 发布了消息:{message}")
print(f" -> {session_owner} 立即看到自己提交的内容")
print(f" -> 其他同事稍后同步,但不保证时间顺序")
def eventual_consistency(message: str) -> None:
"""说明:最终一致性,通知顺序和到达时间都不确定,但保证最终所有人都能看到消息"""
print(f"[Eventual] 消息口口相传 {message}")
remaining = TEAM_MEMBERS.copy()
random.shuffle(remaining) # 打乱顺序
total_delay = 0.0
while remaining:
member = remaining.pop()
delay = random.uniform(0.5, 3.0)
total_delay += delay
time.sleep(delay / 10) # 缩短真实等待,方便演示
print(f" → {member} 在约 {total_delay:.1f} 秒后收到到消息")
print("[Eventually] 虽然有延迟,但最终每个人都收到了消息\n")
if __name__ == "__main__":
# bounded_consistency("10 分钟内打包上线", bound_seconds=10)
# strong_consistency("今晚 22 点项目上线")
# session_consistency("我提交了一次代码", session_owner="小王")
eventual_consistency("下周一开早会,注意关注群通知")
代码示例二(一致性模式)
schema
图二(schema)
界面
-
集合基本信息
- 名称:集合名称
- 描述:对集的描述
- 创建时间:创建集合的时间
- 状态:是否加载,在Milvus中,必须先加载集合到内存,才能创建和使用索引
- 为什么要先加载到集合?
- Milvus采用了内存计算的架构。数据存储在磁盘上,但搜索、索引等操作需要在内存中运行,加载操作就是将集合数据从磁盘读入内存
- 为什么要先加载到集合?
- 副本:当前集合的副本数量,就像是重要文件的复印件
提高可靠性:如果一个数据副本损坏或丢失,系统可以使用其他副本继续工作提高查询性能:多个副本可以同时处理不同的查询请求负载均衡:查询请求可以分散到不同的副本上,避免单个服务器压力过大
- Entity数量:向量数据库中的“数据实体
-
集合特性
- 自动ID:启用自动生成ID
- 一致性:Bounded(有界一致性,这是数据一致性的一种模式)
- mmap设置:向量数据库的存储优化配置
-
字段信息
- id
- 类型:Int64(64位整数)
- 不可为空
- 是主键
- 可以创建标量索引
- name_vector
- 类型:FloatVector(128)(128维浮点向量)
- 不可为空
- 可以创建向量索引
- 用户向量名
- name
- 类型:VarChar(32)(最多32个字符的可变长度字符串)
- 不可为空
- 使用分词器
- 可以创建标量索引
- id
Milvus集合生命周期管理
在Milvus中,集合有几种状态
-
已创建但未加载
- 集合存在磁盘上
- 不能进行搜索或索引操作
- 占用最少资源
-
已加载
- 集合数据在内存中
- 可以进行所有操作(搜索、索引等)
- 占用内存资源
-
已释放
- 集合数据从内存中释放
- 回到“未加载”状态
- 释放内存资源
加载前需保证所有向量都有索引
创建索引
什么是索引?
简单的说,索引就像是一本书的目录,有了目录就可以快速找到你想要的内容在哪一页。在数据库中,索引也是这样工作的,它帮助计算机快速找到你想要的数据。
图三(创建索引)
索引的创建界面
图三显示正则为"name_vector"字段(也就是用户名的向量表示)创建索引:
-
索引类型:Milvus会根据数据特点自动选择最合适的索引结构
-
索引名称:这里可以输入索引的名字
-
度量类型:这决定改了如何计算两个向量之间的“距离”或相似度
L2(欧几里得距离): 测量两点之间的直线距离,类似尺子测量两点间的距离一样。距离越小,表示越相似。公式如图四所示。
- 适用场景:
- 图像检索
- 人脸识别
- 当向量的大小(模长)有意义时
图四 欧几里得距离(L2)
import math
from typing import List, Sequence
# 计算两个向量的欧几里得距离
def l2_distance(vec_a: Sequence[float], vec_b: Sequence[float]) -> float:
# 判断两个向量的长度是否相等
if len(vec_a) != len(vec_b):
raise ValueError("向量维度必须一致")
# 计算每个对应元素之差的平方和,再开平方根
return math.sqrt(sum((a - b) ** 2 for a, b in zip(vec_a, vec_b)))
# 程序主入口
if __name__ == "__main__":
# 定义两个向量
vec_a: List[float] = [1, 2, 3]
vec_b: List[float] = [4, 5, 6]
print(format(l2_distance(vec_a, vec_b), ".2f"))
代码示例三(欧几里得距离 L2)
IP(内积): 测量两个向量的方向相似度和大小的乘积。值越大,表示越相似。公式如图五所示
- 适用场景:
- 推荐系统
- 当向量已经归一化时
- 当需要考虑向量大小和方向时
图五 内积(IP)
from typing import List, Sequence
# 计算两个向量的内积
def inner_product(vec_a: Sequence[float], vec_b: Sequence[float]) -> float:
# 判断两个向量的长度是否相等
if len(vec_a) != len(vec_b):
raise ValueError("向量维度必须一致")
# 计算每个对应元素之差的平方和,再开平方根
return sum(a * b for a, b in zip(vec_a, vec_b))
# 程序主入口
if __name__ == "__main__":
# 定义两个向量
vec_a: List[float] = [1, 2, 3]
vec_b: List[float] = [4, 5, 6]
print(format(inner_product(vec_a, vec_b), ".2f"))
代码示例4(内积 IP)
COSINE(余玄相似度): 测量两个向量之间的夹角,忽略向量的大小,只关注方向。值越接近1,表示越相似。公式如图六所示。
- 适用场景:
-
文本相似度比较
-
当只关心方向而不关心大小时
-
自然语言处理
-
图六 余玄相似度(Cosine)
import math
from typing import List, Sequence
# 计算两个向量的余玄相似度
def cosine_similarity(vec_a: Sequence[float], vec_b: Sequence[float]) -> float:
# 判断两个向量的长度是否相等
if len(vec_a) != len(vec_b):
raise ValueError("向量维度必须一致")
# 计算内积
dot = inner_product(vec_a, vec_b)
# 计算第一个向量的模长
norm_a = math.sqrt(sum(a * a for a in vec_a))
# 计算第二个向量的模长
norm_b = math.sqrt(sum(b * b for b in vec_b))
# 判断模长是否为0,避免除以0
if norm_a == 0 or norm_b == 0:
# 如果模长为0,抛出异常
raise ValueError("向量模长不能为 0")
# 返回内积除以模长乘积的结果
return dot / (norm_a * norm_b)
# 程序主入口
if __name__ == "__main__":
# 定义两个向量
vec_a: List[float] = [1, 2, 3]
vec_b: List[float] = [4, 5, 6]
print(format(cosine_similarity(vec_a, vec_b), ".2f"))
代码示例5 余玄相似度(Cosine)
归一化
什么是向量归一化?
向量归一化就是将一个向量的长度变为1,同时保持它的方向不变。例如:不管一只箭有多长,它只想的方向是一样的。归一化就是把所有的箭都调整成相同的长度(1),但仍然指向原来的方向。
为什么需要归一化?
统一尺度:让不同长度的向量可以公平比较只关注方向:有时我们只关心方向,不关心大小避免计算问题:防止数值计算中的溢出问题
如何计算归一化向量?
假设向量 ,归一化步骤:
- 计算向量长度:
- 每个分量除以长度:
计算归一化向量示例
有一个向量
- 计算长度:
- 归一化:
验证 ✔
举例:站在原点,用手指向某个方向,不管手臂伸的多长,指的方向都是一样的。归一化就是把手臂调整到刚好1米的长度,方向保持不变,只是长度变成了标准长度。
在实际中的应用
游戏开发:控制角色移动方向,无论玩家按键力度如何计算机图形学:计算光照效果,确定表面法线方向机器学习:特征归一化,提高算法性能
import math
from typing import List, Sequence
# 定义归一化函数,将向量的长度缩放为1,方向保持不变
def normalize(vector: Sequence[float]) -> List[float]:
# 计算向量模长
norm = math.sqrt(sum(vec * vec for vec in vector))
# 如果模长为0,则无法归一化零向量
if norm == 0:
raise ValueError("零向量无法归一化,因为长度为0!")
# 返回归一化后的向量
return [vec / norm for vec in vector]
# 主程序入口
if __name__ == "__main__":
vec = [3.0, 4.0]
normalized_vec = normalize(vec)
print(f"原始向量: {vec}")
print(f"归一化向量: {normalized_vec}")
# 验证归一化后向量的长度是否为1
length = math.sqrt(sum(vec * vec for vec in normalized_vec))
print(f"归一化后向量的长度: {length:.2f}")
代码示例6 归一化
参考链接
- cloud.tencent.com/developer/a… 《云原生向量数据库Milvus知识大全,看完这篇就够了[基本概念、系统架构、主要组件、应用场景]》