milvus实战-基于Ollama+bge-large-zh搭建嵌入模型,fastAPI提供http服务将PDF文件写入milvus向量库

0 环境准备

  • ollama已部署嵌入模型quentinz/bge-large-zh-v1.5:latest
  • 已安装miniconda环境
  • 具备科学上网条件(docker安装milvus时需要)

1 milvus安装

1.1 启动Docker Desktop

windows环境下,docker拉取镜像需要先启动Docker  Desktop。否则拉取镜像时会报错,无法拉取镜像。

1.2 下载milvus的docker-compose.yml

在powershell输入以下命令

Invoke-WebRequest https://github.com/milvus-io/milvus/releases/download/v2.4.15/milvus-standalone-docker-compose.yml -OutFile docker-compose.yml

1.3 启动milvus

docker compose up -d

2 开发环境准备

2.1 创建python环境

        通过conda命令创建python环境,保持买个项目的python环境独立,防止项目之间包冲突,方便管理项目依赖。

conda create -n LangchainDemo python=3.10

2.2 pycharm创建项目

  1. 解释器类型:选择自定义环境
  2. 环境:选择现有
  3. 类型:选择conda
  4. 环境:选择上一步创建的环境

2.3 激活python环境

 conda activate LangchainDemo

2.4 安装项目依赖包

        安装项目必要的依赖,包含fastapi、milvus、pdfplumber、ollama等。pdfpy解析可能存在乱码,选用pdfplumber效果更佳。

pip install fastapi uvicorn pymilvus python-multipart pdfplumber ollama

3 程序实现

3.1 导入依赖包

import os
import uuid
import asyncio
import pdfplumber
from fastapi import FastAPI, UploadFile, File
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
from pymilvus.orm import utility
from tenacity import retry, stop_after_attempt
import ollama

3.2 定义FastAPI

app = FastAPI()

3.3 定义文本切割逻辑

        使用pdfplumber打开pdf文件,按自然段落分割文本,设置默认500字符一个分块,且有100个字符重叠。


def extract_text_with_pdfnumber(pdf_path):
    """使用pdfplumber提取PDF文本(保留段落结构)[2]()[5]()"""
    with pdfplumber.open(pdf_path) as pdf:
        text = []
        for page in pdf.pages:
            # 按自然段落分割文本
            paragraphs = page.extract_text()
            text.append(paragraphs.replace('\n', ''))
        return '\n\n'.join(text)

def chunk_with_overlap(text, chunk_size=500, overlap=100):
    """带重叠的分块策略[1]()"""
    chunks = []
    words = text.split()
    start_idx = 0
    while start_idx < len(words):
        end_idx = start_idx + chunk_size
        chunk = ' '.join(words[start_idx:end_idx])
        chunks.append(chunk)
        start_idx = end_idx - overlap  #  设置重叠部分

        # 处理末尾不足的情况
        if end_idx < len(words):
            break

    return chunks

3.4 构建嵌入模型

        连接ollama部署的嵌入模型,bge-large-zh对中文字符处理较好。设置调用嵌入模型失败时可重试3次。


@retry(stop=stop_after_attempt(3))
async def generate_embeddings(text):
    """使用Ollama生成文本嵌入"""
    loop = asyncio.get_event_loop()

    return await loop.run_in_executor(None,
                                      lambda: ollama.Client(host='http://localhost:11434').embeddings(
                                          model="quentinz/bge-large-zh-v1.5:latest", prompt=text)['embedding']
           

3.5 连接milvus

connections.connect("default", host="localhost", port="19530")
collection_name = "pdf_documents"

3.6 构建milvus collection

        定义pdf文本存储的collection的schema,对应数据库的表和字段。

if not utility.has_collection(collection_name):
    # 创建集合
    fields = [
        FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=64),
        FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=20000),
        FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
        FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=256)
    ]
    schema = CollectionSchema(fields=fields, description="pdf_documents")
    collection = Collection(name=collection_name, schema=schema)

    # 创建索引
    index_params = {
        "index_type": "IVF_FLAT",
        "metric_type": "L2",
        "params": {"nlist": 128}
    }
    collection.create_index("vector", index_params)
else:
    collection = Collection(collection_name)

注意:bge-large-zh只能支持到1024维度,500字符对应content的大于需要20000的长度。如果前面修改了嵌入模型或者分块大小,此处也需要调整。

3.7 定义http上传pdf文件处理流程

@app.post("/upload_pdf")
async def upload_pdf(file: UploadFile = File(...),
                     chunk_size=500,
                     overlap=100):
    print(f"开始上传文件《{file.filename}》")
    """上传PDF文件"""
    try:
        #  临时保存文件
        temp_path = f"temp_{uuid.uuid4()}.pdf"
        with open(temp_path, "wb") as f:
            # 流式写入文件
            while chunk := await file.read(1024):
                f.write(chunk)

        # 解析PDF
        text = extract_text_with_pdfnumber(temp_path)
        os.remove(temp_path)

        # 分块处理
        chunks = chunk_with_overlap(text, chunk_size, overlap)

        # 批量生成嵌入
        embeddings = []
        for chunk in chunks:
            embeddings.append(await generate_embeddings(chunk))

        # 构建插入数据
        entities = [
            {
                "id": str(uuid.uuid4()),
                "content": chunk,
                "vector": emb,
                "source": file.filename
            } for chunk, emb in zip(chunks, embeddings)
        ]

        batch_size = 100
        for i in range(0, len(entities), batch_size):
            insert_result = collection.insert(entities[i:i+batch_size])

        collection.flush()
        return {"status": "success", "chunks_processed": len(chunks)}

    except Exception as e:
        return {"error": str(e)}, 500

3.8 实现查询milvus逻辑

@app.get("/search")
async def semantic_search(query: str, top_k=5):
    query_embedding = await generate_embeddings(query)
    search_params = {"metric_type": "L2", "params": {"nprobe": 10}}

    # 加载集合到内存中
    collection.load()

    results = collection.search(
        data=[query_embedding],
        anns_field="vector",
        param=search_params,
        limit=top_k,
        output_fields=["content", "source"]
    )
    return [{"score": hit.score, "metadata": hit.entity.to_dict()} for hit in results[0]]

3.9 启动http服务

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8321)

4 测试

        建议使用apifox请求http接口。

4.1 测试上传PDF文件解析入库

        用post请求,在body标签页选择form-data填写file参数,参数类型选择file,然后上传文件。上传成功后返回success。

4.2 查询milvus测试

        用get请求,在param中填写query字段,并填写需要查询的内容,如下图:

附录一:完整代码示例

import os
import uuid
import asyncio
import pdfplumber
from fastapi import FastAPI, UploadFile, File
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
from pymilvus.orm import utility
from tenacity import retry, stop_after_attempt
import ollama

app = FastAPI()

def extract_text_with_pdfnumber(pdf_path):
    """使用pdfplumber提取PDF文本(保留段落结构)[2]()[5]()"""
    with pdfplumber.open(pdf_path) as pdf:
        text = []
        for page in pdf.pages:
            # 按自然段落分割文本
            paragraphs = page.extract_text()
            text.append(paragraphs.replace('\n', ''))
        return '\n\n'.join(text)

def chunk_with_overlap(text, chunk_size=500, overlap=100):
    """带重叠的分块策略[1]()"""
    chunks = []
    words = text.split()
    start_idx = 0
    while start_idx < len(words):
        end_idx = start_idx + chunk_size
        chunk = ' '.join(words[start_idx:end_idx])
        chunks.append(chunk)
        start_idx = end_idx - overlap  #  设置重叠部分

        # 处理末尾不足的情况
        if end_idx < len(words):
            break

    return chunks

@retry(stop=stop_after_attempt(3))
async def generate_embeddings(text):
    """使用Ollama生成文本嵌入"""
    loop = asyncio.get_event_loop()

    return await loop.run_in_executor(None,
                                      lambda: ollama.Client(host='http://localhost:11434').embeddings(
                                          model="quentinz/bge-large-zh-v1.5:latest", prompt=text)['embedding']
                                      )

connections.connect("default", host="localhost", port="19530")
collection_name = "pdf_documents"

# 检查集合是否存在,如果存在则删除
# if utility.has_collection(collection_name):
#     collection = Collection(collection_name)
#     collection.drop()

if not utility.has_collection(collection_name):
    # 创建集合
    fields = [
        FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=64),
        FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=20000),
        FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=1024),
        FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=256)
    ]
    schema = CollectionSchema(fields=fields, description="pdf_documents")
    collection = Collection(name=collection_name, schema=schema)

    # 创建索引
    index_params = {
        "index_type": "IVF_FLAT",
        "metric_type": "L2",
        "params": {"nlist": 128}
    }
    collection.create_index("vector", index_params)
else:
    collection = Collection(collection_name)

@app.post("/upload_pdf")
async def upload_pdf(file: UploadFile = File(...),
                     chunk_size=500,
                     overlap=100):
    print(f"开始上传文件《{file.filename}》")
    """上传PDF文件"""
    try:
        #  临时保存文件
        temp_path = f"temp_{uuid.uuid4()}.pdf"
        with open(temp_path, "wb") as f:
            # 流式写入文件
            while chunk := await file.read(1024):
                f.write(chunk)

        # 解析PDF
        text = extract_text_with_pdfnumber(temp_path)
        os.remove(temp_path)

        # 分块处理
        chunks = chunk_with_overlap(text, chunk_size, overlap)

        # 批量生成嵌入
        embeddings = []
        for chunk in chunks:
            embeddings.append(await generate_embeddings(chunk))

        # 构建插入数据
        entities = [
            {
                "id": str(uuid.uuid4()),
                "content": chunk,
                "vector": emb,
                "source": file.filename
            } for chunk, emb in zip(chunks, embeddings)
        ]

        batch_size = 100
        for i in range(0, len(entities), batch_size):
            insert_result = collection.insert(entities[i:i+batch_size])

        collection.flush()
        return {"status": "success", "chunks_processed": len(chunks)}

    except Exception as e:
        return {"error": str(e)}, 500


@app.get("/search")
async def semantic_search(query: str, top_k=5):
    query_embedding = await generate_embeddings(query)
    search_params = {"metric_type": "L2", "params": {"nprobe": 10}}

    # 加载集合到内存中
    collection.load()

    results = collection.search(
        data=[query_embedding],
        anns_field="vector",
        param=search_params,
        limit=top_k,
        output_fields=["content", "source"]
    )
    return [{"score": hit.score, "metadata": hit.entity.to_dict()} for hit in results[0]]


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8321)

附录二:错误处理

        apifox请求http接口填写接口地址时,如果代码路径search或者upload_pdf后面有"/"斜杠则在apifox请求时也要加"/"斜杠,否则会报错307 Temporary Redirect,Expected boundary character 45, got 8 at index 2,如下:

此时错误的请求,url最后没有斜杠"/"

此时正确的请求url应该最后有斜杠"/",如下:

<think>好的,用户想了解如何搭建DeepSeek加上BGE-M3,以及选择什么样的向量数据库。首先,我需要确认这两个组件的具体所指。DeepSeek可能是指深度求索公司开发的大模型,比如DeepSeek-R1或者他们的其他模型。而BGE-M3是北京智源研究院发布的BAAI General Embedding-M3,这是一个支持多语言、多模态、多粒度的文本嵌入模型,适合作为检索模型嵌入层。 用户的需求可能是构建一个结合了大模型和高效检索的RAG(检索增强生成)系统。需要先明确步骤:首先加载和微调BGE-M3模型,将其作为文本编码器生成嵌入向量;然后选择合适的向量数据库存储和检索这些向量;接着将检索结果输入到DeepSeek大模型中进行生成;最后整合整个流程并优化性能。 在选择向量数据库时,用户可能需要考虑几个因素:数据规模、查询性能、扩展性、社区支持和易用性。常见的选项包括FAISS、Milvus、Pinecone、Chroma等。需要根据具体需求分析每个数据库的优缺点,比如FAISS适合中小规模数据,而Milvus适合分布式和大规模场景。用户可能需要处理高并发的查询,或者需要低延迟,这些都会影响选择。 接下来,用户可能对如何具体部署和集成这些组件有疑问。例如,如何将BGE-M3生成的向量存入数据库,如何设计检索逻辑,以及如何将结果传递给DeepSeek模型。可能需要代码示例或者步骤说明,比如使用Hugging Face Transformers加载模型,生成嵌入,然后用向量数据库的SDK进行存储和查询。 另外,用户可能关心是否需要微调BGE-M3以适应特定领域的数据,或者如何优化检索的准确性和速度。比如,调整嵌入模型的参数,或者对向量数据库进行索引配置,比如使用HNSW算法。 最后,需要考虑整个系统的架构设计,比如是否需要分布式部署,如何处理高可用性,以及监控和维护方面的问题。可能需要建议一些最佳实践,比如定期更新嵌入模型、监控数据库性能、进行负载测试等。 总结来说,用户需要一个从模型准备、数据库选择到系统搭建的完整指南,可能需要分步骤详细说明,并针对不同场景推荐合适的工具和配置。同时,要注意可能遇到的挑战,如模型兼容性、数据库性能调优、系统延迟等,并提供解决方案或参考资源。</think>搭建 **DeepSeek + BGE-M3** 的组合主要用于构建高效的检索增强生成(RAG)系统,结合大语言模型(DeepSeek)的生成能力和嵌入模型BGE-M3)的语义检索能力。以下是关键步骤和向量数据库选择建议: --- ### **一、搭建流程概述** #### 1. **模型准备** - **BGE-M3**(Embedding 模型) 作用:将文本转换为高维向量,支持多语言、多模态、多粒度检索。 使用方式: - 通过 Hugging Face 直接加载模型: ```python from transformers import AutoModel, AutoTokenizer model = AutoModel.from_pretrained("BAAI/bge-m3") tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-m3") ``` - 输入文本生成向量: ```python inputs = tokenizer(texts, padding=True, return_tensors="pt") embeddings = model(**inputs).last_hidden_state.mean(dim=1) ``` - **DeepSeek**(生成模型) 作用:基于检索结果生成高质量回答。 使用方式: - 通过 API 调用(如 DeepSeek-API)或本地部署模型: ```python from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("deepseek-ai/deepseek-llm-7b") tokenizer = AutoTokenizer.from_pretrained("deepseek-ai/deepseek-llm-7b") ``` #### 2. **向量数据库选择** 根据场景需求选择数据库: | 数据库 | 特点 | 适用场景 | |--------------|----------------------------------------------------------------------|-----------------------------| | **FAISS** | 轻量级、内存计算,适合中小规模数据;无需服务化部署。 | 本地快速验证、小规模应用 | | **Milvus** | 分布式、高扩展性,支持海量数据;提供 RESTful API 和 SDK。 | 企业级生产环境、高并发场景 | | **Pinecone** | 全托管云服务,自动管理索引和扩展;低运维成本。 | 云原生应用、快速搭建 | | **Chroma** | 轻量级、易集成,适合简单应用;支持本地和内存模式。 | 原型开发、小规模实验 | **推荐组合**: - **快速验证**:BGE-M3 + FAISS/Chroma - **生产环境**:BGE-M3 + Milvus/Pinecone #### 3. **系统集成流程** 1. **数据预处理**:将文档切分(chunking),使用 BGE-M3 生成向量。 2. **向量入库**:将向量和元数据存入数据库(如 Milvus)。 3. **检索逻辑**:用户提问时,用 BGE-M3 编码问题,检索 Top-K 相关文档。 4. **生成回答**:将检索结果作为上下文,输入 DeepSeek 生成最终答案。 ```python # 示例代码框架 query = "用户问题" query_embedding = bge_m3.encode(query) # 生成查询向量 results = vector_db.search(query_embedding, top_k=5) # 检索 context = "\n".join(results) answer = deepseek.generate(f"基于以下上下文回答:{context}\n问题:{query}") ``` --- ### **二、关键优化点** 1. **Embedding 模型调优**: -BGE-M3 进行领域适配微调(如使用 LoRA)。 - 调整文本分块策略(chunk size 和 overlap)。 2. **检索增强策略**: - 混合检索:结合语义向量 + 关键词(BM25)提升召回率。 - 重排序(Rerank):对检索结果二次排序(如使用 BGE-M3 的 colbert 重排)。 3. **向量数据库配置**: - 索引类型:HNSW(高精度)或 IVF(快速检索)。 - 分片与副本:分布式部署提高吞吐量(Milvus 支持)。 --- ### **三、部署建议** - **轻量级方案**:Docker 部署 Milvus + FastAPI 封装服务- **云原生方案**:AWS/GCP 上使用 Pinecone + 异步调用 DeepSeek API。 - **性能监控**:跟踪检索延迟(P99)、召回率(Recall@K)和生成质量(ROUGE)。 --- ### **四、常见问题** - **延迟高**:优化索引参数(如 HNSW 的 `ef_search`)、使用 GPU 加速 Embedding。 - **数据更新**:支持增量更新索引(Milvus 支持动态数据插入)。 - **多模态支持**:BGE-M3 可扩展为图像/文本混合检索(需适配多模态编码)。 通过合理选择工具链和优化流程,可以构建高性能的 DeepSeek + BGE-M3 RAG 系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值