Milvus 向量数据库 Python 实战教程

本教程基于项目实际代码(pymilvus + MilvusClient),面向有 Python 基础的开发者,从概念到实战快速上手。


目录

  1. [Milvus 是什么](#Milvus 是什么)
  2. 核心概念
  3. 环境准备
  4. [连接 Milvus](#连接 Milvus)
  5. 创建集合(Collection)
  6. 插入数据
  7. 向量搜索
  8. [混合搜索(稠密 + 稀疏向量)](#混合搜索(稠密 + 稀疏向量))
  9. 标量过滤查询
  10. 删除与清理
  11. 工程实践:客户端管理
  12. 常见问题

1. Milvus 是什么

Milvus 是一款开源的向量数据库,专为 AI 应用设计。它解决了传统数据库无法高效处理"语义相似度搜索"的问题。

典型应用场景:

场景 说明
RAG(检索增强生成) 将文档切片 → 向量化 → 存入 Milvus → 用户提问时检索最相关的片段 → 交给 LLM 生成回答
以图搜图 将图片特征向量存入 Milvus,用查询图片的向量检索相似图片
推荐系统 将用户/商品向量化,检索最相似的用户或商品

为什么不用传统数据库? 传统数据库用精确匹配(=LIKE),而向量搜索是找"最相似的"------就像用"意思相近"来搜索,而不是"字面匹配"。


2. 核心概念

复制代码
┌─────────────────────────────────────────────────┐
│                  Collection(集合)               │
│  ┌─────────────────────────────────────────────┐ │
│  │              Schema(模式/表结构)             │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│  │  │ chunk_id │ │  dense   │ │   content    │ │ │
│  │  │ (主键)   │ │ _vector  │ │  (VARCHAR)   │ │ │
│  │  │ INT64    │ │FLOAT_VEC │ │              │ │ │
│  │  └──────────┘ └──────────┘ └──────────────┘ │ │
│  └─────────────────────────────────────────────┘ │
│                                                   │
│  ┌─────────────────────────────────────────────┐ │
│  │              Index(索引)                    │ │
│  │   为向量字段建立索引,加速相似度搜索            │ │
│  │   类型:AUTOINDEX / HNSW / IVF_FLAT 等       │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
概念 类比 说明
Collection 数据库中的"表" 存储数据的基本单位
Schema 表的"列定义" 定义每个字段的名称、类型、约束
Field 表中的"列" 分为标量字段(文本、数字)和向量字段
Index 数据库的"索引" 加速向量搜索,必须在搜索前创建
Partition 表的"分区" 可选,用于数据隔离和加速查询
Entity 表中的"行" 一条完整的数据记录

3. 环境准备

3.1 安装 Milvus 服务端

方式一:Docker 部署(推荐开发用)

bash 复制代码
# 下载并启动 Milvus Standalone
wget https://github.com/milvus-io/milvus/releases/download/v2.5.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
docker compose up -d

方式二:Milvus Lite(零部署,适合学习)

bash 复制代码
pip install -U pymilvus

Milvus Lite 是 pymilvus 内置的轻量版,数据保存在本地文件,无需启动服务端:

python 复制代码
from pymilvus import MilvusClient

# 数据保存在本地文件中
client = MilvusClient("my_milvus.db")

3.2 安装 Python SDK

bash 复制代码
pip install -U pymilvus

3.3 环境变量配置

.env 文件中配置连接信息:

env 复制代码
# Milvus 连接地址
MILVUS_URL=http://192.168.6.160:19530
# 知识库集合名
CHUNKS_COLLECTION=kb_chunks_v1

4. 连接 Milvus

4.1 基础连接

python 复制代码
from pymilvus import MilvusClient

# 连接到远程 Milvus 服务
client = MilvusClient(uri="http://192.168.6.160:19530")

# 连接 Milvus Lite(本地文件)
client = MilvusClient("my_data.db")

# 连接带认证的 Milvus
client = MilvusClient(
    uri="http://localhost:19530",
    token="root:Milvus"
)

4.2 工程化:线程安全的客户端管理

在生产环境中,推荐使用双重检查锁 + 延迟初始化模式管理客户端,避免重复创建和并发问题:

python 复制代码
import os
import threading
from typing import Optional
from pymilvus import MilvusClient


class StorageClients:
    """存储客户端管理器"""

    _milvus_client: Optional[MilvusClient] = None
    _milvus_lock = threading.Lock()

    @classmethod
    def get_milvus_client(cls) -> MilvusClient:
        """获取 Milvus 客户端(线程安全单例)"""
        # 第一次检查(无锁,快速返回)
        if cls._milvus_client is not None:
            return cls._milvus_client

        with cls._milvus_lock:
            # 第二次检查(有锁,防止并发重复创建)
            if cls._milvus_client is not None:
                return cls._milvus_client

            milvus_uri = os.getenv("MILVUS_URL")
            if not milvus_uri:
                raise EnvironmentError("缺少环境变量 MILVUS_URL")

            cls._milvus_client = MilvusClient(milvus_uri)
            return cls._milvus_client

为什么要延迟初始化? 把"创建"动作延迟到真正需要的那一刻,避免程序启动时因 Milvus 未就绪而报错。


5. 创建集合(Collection)

创建集合需要三步:定义 Schema → 创建索引 → 创建集合

5.1 快速创建(简单场景)

MilvusClient 提供了一行代码创建集合的快捷方式:

python 复制代码
client.create_collection(
    collection_name="my_collection",
    dimension=1024,           # 向量维度,必须与 Embedding 模型一致
    metric_type="COSINE",     # 余弦相似度
)

5.2 自定义 Schema(生产推荐)

当需要多个标量字段或混合向量时,需要手动构建 Schema:

python 复制代码
from pymilvus import DataType, MilvusClient

client = MilvusClient(uri="http://192.168.6.160:19530")

# ── 第 1 步:构建 Schema ──
schema = client.create_schema(enable_dynamic_field=True)

# 主键字段(自增 ID)
schema.add_field(
    field_name="chunk_id",
    datatype=DataType.INT64,
    is_primary=True,
    auto_id=True
)

# 稠密向量字段
schema.add_field(
    field_name="dense_vector",
    datatype=DataType.FLOAT_VECTOR,
    dim=1024                # 维度必须与 Embedding 模型输出一致
)

# 稀疏向量字段(用于混合搜索)
schema.add_field(
    field_name="sparse_vector",
    datatype=DataType.SPARSE_FLOAT_VECTOR
)

# 标量字段
schema.add_field(field_name="content", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="title", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="item_name", datatype=DataType.VARCHAR, max_length=65535)


# ── 第 2 步:创建索引 ──
index_params = client.prepare_index_params()

# 稠密向量索引
index_params.add_index(
    field_name="dense_vector",
    index_name="dense_vector_index",
    index_type="AUTOINDEX",     # 自动选择最优索引类型
    metric_type="COSINE"        # 余弦相似度
)

# 稀疏向量索引
index_params.add_index(
    field_name="sparse_vector",
    index_name="sparse_vector_index",
    index_type="SPARSE_INVERTED_INDEX",  # 稀疏向量专用索引
    metric_type="IP"                     # 内积
)


# ── 第 3 步:创建集合 ──
client.create_collection(
    collection_name="kb_chunks_v1",
    schema=schema,
    index_params=index_params
)

5.3 常用向量索引类型

索引类型 适用场景 说明
AUTOINDEX 通用场景 Milvus 自动选择最优索引,推荐新手使用
HNSW 高召回、高 QPS 基于图的索引,搜索速度快,内存占用较高
IVF_FLAT 大规模数据 倒排索引 + 精确距离计算,平衡精度和速度
SPARSE_INVERTED_INDEX 稀疏向量 稀疏向量专用索引

5.4 常用相似度度量

度量方式 说明 适用场景
COSINE 余弦相似度 文本语义搜索(最常用)
L2 欧氏距离 图像特征匹配
IP 内积 稀疏向量、归一化后的向量

5.5 检查集合是否存在

python 复制代码
if client.has_collection("kb_chunks_v1"):
    print("集合已存在")
else:
    print("集合不存在")

6. 插入数据

6.1 插入单条/多条数据

python 复制代码
data = [
    {
        "dense_vector": [0.1] * 1024,
        "sparse_vector": {1: 0.5, 100: 0.3, 500: 0.8},
        "content": "Milvus 是一款高性能向量数据库",
        "title": "Milvus 简介",
        "item_name": "Milvus"
    },
    {
        "dense_vector": [0.2] * 1024,
        "sparse_vector": {2: 0.4, 200: 0.6},
        "content": "RAG 技术通过外挂知识库解决大模型幻觉问题",
        "title": "RAG 技术",
        "item_name": "RAG"
    }
]

result = client.insert(
    collection_name="kb_chunks_v1",
    data=data
)

print(f"插入数量: {result['insert_count']}")
print(f"自增 ID: {result['ids']}")

6.2 批量插入(大数据量)

python 复制代码
# 分批插入,每批 100 条
batch_size = 100
for i in range(0, len(all_data), batch_size):
    batch = all_data[i : i + batch_size]
    client.insert(collection_name="kb_chunks_v1", data=batch)

注意sparse_vector 字段的值是一个字典 {token_id: weight},例如 {1: 0.5, 100: 0.3},表示第 1 维权重 0.5、第 100 维权重 0.3。


7. 向量搜索

7.1 基础向量搜索

python 复制代码
# 查询向量(通常由 Embedding 模型生成)
query_vector = [0.12] * 1024

results = client.search(
    collection_name="kb_chunks_v1",
    data=[query_vector],
    limit=5,                          # 返回最相似的 5 条
    output_fields=["content", "title"],  # 需要返回的标量字段
    search_params={"metric_type": "COSINE"}
)

# 结果是一个列表,每个元素对应一个查询向量的结果
for hit in results[0]:
    print(f"ID: {hit['id']}, 距离: {hit['distance']}, 内容: {hit['entity']['content']}")

7.2 带过滤条件的搜索

python 复制代码
results = client.search(
    collection_name="kb_chunks_v1",
    data=[query_vector],
    limit=5,
    filter='item_name == "Milvus"',   # 标量过滤
    output_fields=["content", "title", "item_name"],
    search_params={"metric_type": "COSINE"}
)

8. 混合搜索(稠密 + 稀疏向量)

混合搜索同时使用稠密向量和稀疏向量,通过重排序器融合两路结果,兼顾语义理解和关键词精确匹配,效果优于单一向量搜索。

8.1 流程概览

复制代码
用户查询
  │
  ├──→ BGE-M3 编码 ──→ 稠密向量 ──→ AnnSearchRequest ──┐
  │                                                      ├──→ WeightedRanker ──→ 最终结果
  └──→ BGE-M3 编码 ──→ 稀疏向量 ──→ AnnSearchRequest ──┘

8.2 创建混合搜索请求

python 复制代码
from pymilvus import AnnSearchRequest, WeightedRanker


def create_hybrid_search_requests(
    dense_vector, sparse_vector,
    expr=None, expr_params=None, limit=5
):
    """创建稠密 + 稀疏两路搜索请求"""

    # 稠密向量搜索请求
    dense_req = AnnSearchRequest(
        data=[dense_vector],
        anns_field="dense_vector",
        param={"metric_type": "COSINE"},
        expr=expr,
        expr_params=expr_params,
        limit=limit
    )

    # 稀疏向量搜索请求
    sparse_req = AnnSearchRequest(
        data=[sparse_vector],
        anns_field="sparse_vector",
        param={"metric_type": "IP"},
        expr=expr,
        expr_params=expr_params,
        limit=limit
    )

    return [dense_req, sparse_req]

8.3 执行混合搜索

python 复制代码
def execute_hybrid_search(
    client, collection_name, search_requests,
    ranker_weights=(0.5, 0.5), limit=5, output_fields=None
):
    """执行混合搜索并融合结果"""

    # 创建权重融合排序器
    reranker = WeightedRanker(
        ranker_weights[0],   # 稠密向量权重
        ranker_weights[1],   # 稀疏向量权重
        norm_score=True      # 归一化分数
    )

    if output_fields is None:
        output_fields = ["content", "item_name"]

    results = client.hybrid_search(
        collection_name=collection_name,
        reqs=search_requests,
        ranker=reranker,
        limit=limit,
        output_fields=output_fields
    )

    return results

8.4 完整调用示例

单个向量查询

python 复制代码
from pymilvus.model.hybrid import BGEM3EmbeddingFunction
from pymilvus import MilvusClient, AnnSearchRequest, WeightedRanker

# 1. 加载 BGE-M3 模型
model = BGEM3EmbeddingFunction("BAAI/bge-m3", device="cpu", use_fp16=False)

# 2. 编码查询文本
query_result = model.encode_queries(["什么是向量数据库?"])
dense_vec = query_result["dense"][0].tolist()
csr_array = query_result['sparse']

# 处理稀疏向量
sparse_vec = []
for i in range(csr_array.shape[0]):
    start = csr_array.indptr[i]
    end = csr_array.indptr[i + 1]
    token_ids = csr_array.indices[start:end].tolist()
    weights = csr_array.data[start:end].tolist()
    sparse_vec.append(dict(zip(token_ids, weights)))
sparse_vec = sparse_vec[0]  # 取第一个查询的稀疏向量

# 3. 创建搜索请求(修正为正确的 API)
dense_search_req = AnnSearchRequest(
    data=[dense_vec],
    anns_field="dense_vector",  # 替换为实际的稠密向量字段名
    param={"metric_type": "COSINE", "params": {"nprobe": 10}},
    limit=5
)

sparse_search_req = AnnSearchRequest(
    data=[sparse_vec],
    anns_field="sparse_vector",  # 替换为实际的稀疏向量字段名
    param={"metric_type": "IP"},  # 稀疏向量通常使用 IP
    limit=5
)

# 4. 执行混合搜索
client = MilvusClient(uri="http://192.168.6.160:19530")
results = client.hybrid_search(
    collection_name="kb_chunks_v1",
    reqs=[dense_search_req, sparse_search_req],
    ranker=WeightedRanker(0.7, 0.3),  # 稠密权重 0.7,稀疏权重 0.3
    limit=5,
    output_fields=["content", "title"]
)

# 5. 输出结果
for hit in results[0]:
    print(f"距离: {hit['distance']:.4f} | {hit['entity']}")

多个向量查询

python 复制代码
from pymilvus.model.hybrid import BGEM3EmbeddingFunction
from pymilvus import MilvusClient, AnnSearchRequest, WeightedRanker

# 1. 加载 BGE-M3 模型
model = BGEM3EmbeddingFunction("BAAI/bge-m3", device="cpu", use_fp16=False)

# 2. 编码多个查询文本
query_texts = ["什么是向量数据库?", "如何实现混合搜索?", "BGE-M3 模型有什么特点?"]
query_result = model.encode_queries(query_texts)

# 提取所有查询的稠密向量
dense_vectors = [vec.tolist() for vec in query_result["dense"]]

# 提取所有查询的稀疏向量
csr_array = query_result['sparse']
sparse_vectors = []
for i in range(csr_array.shape[0]):  # 遍历所有查询
    start = csr_array.indptr[i]
    end = csr_array.indptr[i + 1]
    token_ids = csr_array.indices[start:end].tolist()
    weights = csr_array.data[start:end].tolist()
    sparse_vectors.append(dict(zip(token_ids, weights)))

# 3. 为每个查询创建独立的搜索请求
dense_search_reqs = []
sparse_search_reqs = []

for i in range(len(query_texts)):
    # 稠密搜索请求
    dense_req = AnnSearchRequest(
        data=[dense_vectors[i]],
        anns_field="dense_vector",
        param={"metric_type": "COSINE", "params": {"nprobe": 10}},
        limit=5
    )
    dense_search_reqs.append(dense_req)

    # 稀疏搜索请求
    sparse_req = AnnSearchRequest(
        data=[sparse_vectors[i]],
        anns_field="sparse_vector",
        param={"metric_type": "IP"},
        limit=5
    )
    sparse_search_reqs.append(sparse_req)

# 4. 执行混合搜索(对每个查询分别搜索)
client = MilvusClient(uri="http://192.168.6.160:19530")
all_results = []

for i in range(len(query_texts)):
    print(f"\n查询 {i+1}: {query_texts[i]}")
    print("-" * 50)

    # 对当前查询执行混合搜索
    results = client.hybrid_search(
        collection_name="kb_chunks_v1",
        reqs=[dense_search_reqs[i], sparse_search_reqs[i]],
        ranker=WeightedRanker(0.7, 0.3),
        limit=5,
        output_fields=["content", "title"]
    )
    all_results.append(results)

# 5. 输出所有查询的结果
for query_idx, results in enumerate(all_results):
    print(f"\n{'='*60}")
    print(f"查询 {query_idx + 1}: {query_texts[query_idx]}")
    print('='*60)

    # 每个查询返回的结果
    for hit_idx, hit in enumerate(results[0]):
        print(f"排名 {hit_idx + 1}: 距离={hit['distance']:.4f}")
        print(f"  标题: {hit['entity'].get('title', 'N/A')}")
        print(f"  内容: {hit['entity'].get('content', 'N/A')[:100]}...")
        print()
python 复制代码
# 4. 执行混合搜索(一次性处理所有查询)
client = MilvusClient(uri="http://192.168.6.160:19530")

# 注意:AnnSearchRequest 的 data 参数可以传入多个向量
dense_search_req = AnnSearchRequest(
    data=dense_vectors,  # 传入所有稠密向量
    anns_field="dense_vector",
    param={"metric_type": "IP", "params": {"nprobe": 10}},
    limit=5
)

sparse_search_req = AnnSearchRequest(
    data=sparse_vectors,  # 传入所有稀疏向量
    anns_field="sparse_vector",
    param={"metric_type": "IP"},
    limit=5
)

results = client.hybrid_search(
    collection_name="kb_chunks_v1",
    reqs=[dense_search_req, sparse_search_req],
    ranker=WeightedRanker(0.7, 0.3),
    limit=5,
    output_fields=["content", "title"]
)

# 结果按查询分组
for query_idx, query_results in enumerate(results):
    print(f"\n查询 {query_idx + 1}: {query_texts[query_idx]}")
    for hit in query_results:
        print(f"  距离: {hit['distance']:.4f} | {hit['entity']}")

encode_queries vs encode_documents :BGE-M3 采用非对称检索策略。查询文本用 encode_queries,入库文档用 encode_documents,两者会在文本前添加不同的指令前缀,让模型生成更适合匹配的向量。


9. 标量过滤查询

除了向量搜索,Milvus 也支持纯标量字段的过滤查询(类似 SQL 的 WHERE):

python 复制代码
# 精确匹配
results = client.query(
    collection_name="kb_chunks_v1",
    filter='item_name == "Milvus"',
    output_fields=["content", "title"]
)

# IN 查询
results = client.query(
    collection_name="kb_chunks_v1",
    filter='item_name in ["Milvus", "RAG", "LLM"]',
    output_fields=["content", "title"]
)

# 带变量的表达式(更安全,避免注入)
expr = "item_name in {item_names}"
expr_params = {"item_names": ["Milvus", "RAG"]}

results = client.query(
    collection_name="kb_chunks_v1",
    filter=expr,
    filter_params=expr_params,
    output_fields=["content", "title"]
)

在搜索中使用过滤

过滤条件也可以用在向量搜索中,缩小搜索范围:

python 复制代码
results = client.search(
    collection_name="kb_chunks_v1",
    data=[query_vector],
    limit=5,
    filter='item_name in {item_names}',
    filter_params={"item_names": ["Milvus", "RAG"]},
    output_fields=["content", "title"]
)

10. 删除与清理

python 复制代码
# 按条件删除
client.delete(
    collection_name="kb_chunks_v1",
    filter='item_name == "Milvus"'
)

# 按 ID 删除
client.delete(
    collection_name="kb_chunks_v1",
    ids=[1, 2, 3]
)

# 删除整个集合
client.drop_collection("kb_chunks_v1")

11. 工程实践:客户端管理

11.1 完整的客户端管理器

项目中使用了基类 + 双重检查锁的模式,统一管理 Milvus、MinIO、MongoDB 等多种客户端:

python 复制代码
import os
import threading
from typing import Optional
from pymilvus import MilvusClient


class BaseClientManager:
    """客户端管理器基类"""

    @staticmethod
    def _require_env(key: str) -> str:
        """读取环境变量,缺失则抛异常"""
        value = os.getenv(key)
        if not value:
            raise EnvironmentError(f"缺少环境变量: {key}")
        return value

    @classmethod
    def _get_or_create(cls, attr_name, lock, factory):
        """双重检查锁模板方法"""
        instance = getattr(cls, attr_name, None)
        if instance is not None:
            return instance

        with lock:
            instance = getattr(cls, attr_name, None)
            if instance is not None:
                return instance
            instance = factory()
            setattr(cls, attr_name, instance)
            return instance


class StorageClients(BaseClientManager):
    _milvus_client: Optional[MilvusClient] = None
    _milvus_lock = threading.Lock()

    @classmethod
    def get_milvus_client(cls) -> MilvusClient:
        return cls._get_or_create(
            "_milvus_client", cls._milvus_lock, cls._create_milvus_client
        )

    @classmethod
    def _create_milvus_client(cls) -> MilvusClient:
        milvus_uri = cls._require_env("MILVUS_URL")
        return MilvusClient(milvus_uri)

11.2 使用方式

python 复制代码
# 全局获取客户端(线程安全,自动复用)
client = StorageClients.get_milvus_client()

# 后续调用直接返回已有实例,不会重复创建
client2 = StorageClients.get_milvus_client()
assert client is client2  # True

12. 常见问题

Q1: dimension 不匹配会怎样?

创建集合时 dim 必须与 Embedding 模型的输出维度严格一致 。例如 BGE-M3 的稠密向量维度是 1024,则 dim=1024。不匹配会报错。

Q2: 搜索前需要 load() 吗?

使用 MilvusClient(新版 API)时,创建集合后会自动加载到内存,无需手动调用 load() 。如果使用旧版 Collection API,则需要手动加载。

Q3: AUTOINDEX 够用吗?

对于中小规模数据(百万级以下),AUTOINDEX 完全够用,Milvus 会自动选择最优索引。只有在对性能有极致要求时,才需要手动选择 HNSWIVF_FLAT 等索引类型并调参。

Q4: 混合搜索的权重怎么调?

WeightedRanker(0.7, 0.3) 表示稠密向量权重 0.7、稀疏向量权重 0.3。一般建议:

  • 语义理解为主:稠密权重高(0.7~0.8)
  • 关键词匹配为主:稀疏权重高(0.6~0.7)
  • 根据实际效果微调

Q5: 如何查看集合的统计信息?

python 复制代码
# 查看集合中的实体数量
stats = client.get_collection_stats("kb_chunks_v1")
print(stats)

# 列出所有集合
collections = client.list_collections()
print(collections)

附录:完整流程速查

python 复制代码
from pymilvus import MilvusClient, DataType, AnnSearchRequest, WeightedRanker

# 1. 连接
client = MilvusClient(uri="http://localhost:19530")

# 2. 创建集合(快捷方式)
client.create_collection(
    collection_name="demo",
    dimension=1024,
    metric_type="COSINE"
)

# 3. 插入数据
client.insert("demo", data=[
    {"id": 1, "vector": [0.1] * 1024, "text": "Hello Milvus"},
    {"id": 2, "vector": [0.2] * 1024, "text": "向量数据库教程"},
])

# 4. 搜索
results = client.search(
    "demo",
    data=[[0.12] * 1024],
    limit=2,
    output_fields=["text"]
)
for hit in results[0]:
    print(f"{hit['distance']:.4f} | {hit['entity']['text']}")

# 5. 清理
client.drop_collection("demo")
相关推荐
这个人懒得名字都没写18 分钟前
OpenCV 官方教程中的测试图片Can’t find required data file
python·opencv
智驭未来掌门人23 分钟前
Python HTTP CRUD 完全指南:从入门到实战
python
烂蜻蜓33 分钟前
Flask入门教程(二十四):请求对象API——获取客户端数据的完整指南
后端·python·flask
专注API从业者36 分钟前
Open‑Claw 实战|无需逆向,快速搭建电商商品监控与数据分析系统
开发语言·数据结构·数据库·数据分析·php
2301_8000742138 分钟前
MySQL部分语句
数据库·mysql
开开心心就好41 分钟前
手机悬屏翻译工具外语游戏漫画APP全覆盖
android·前端·javascript·python·游戏·pdf·html
梦在远山后42 分钟前
Python 中两种 Queue 的区别
python·langchain
CodeStats1 小时前
MySQL与Oracle数据类型底层设计原理对比(下)
数据库·mysql·oracle
狮子雨恋1 小时前
anaconda python环境和QT python环境不一致如何解决
开发语言·python·qt