Skip to content

第十三阶段:Qdrant 向量数据库 ​

一、为什么换 Qdrant ​

上一阶段的接口从 kb_storage 读取索引。kb_storage 只是本地文件夹里的文件,只能被一个进程加载使用。要是想启动多个 API 服务一起分担请求,每个服务都得自己存一份、自己加载一份,数据也很难保持一致。

Qdrant 是一个独立运行的向量数据库服务。数据集中存放在它那里,多个服务可以同时通过网络连接它。把"存向量"和"回答问题"拆成两个服务,是走向分布式的第一步。

二、用 Docker 启动 Qdrant ​

bash
docker run -d --name qdrant -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant
参数含义
docker run用一个镜像创建并启动容器
-d后台运行,不占用当前终端
--name qdrant给容器起名 qdrant,以后可以用 docker stop qdrant / docker start qdrant 停止或启动
-p 6333:6333端口映射:本机 6333 端口转发到容器内的 6333 端口,代码才能访问
-v qdrant_data:/qdrant/storage把数据存进名为 qdrant_data 的数据卷,删掉容器数据也还在
qdrant/qdrant镜像名,第一次运行会自动下载

启动后,在浏览器打开控制台 http://localhost:6333/dashboard,可以看到里面有哪些数据。

三、安装依赖 ​

bash
pip install llama-index-vector-stores-qdrant qdrant-client

四、先建索引,再读取 ​

分成两个脚本:

  1. 13_build_qdrant.py:只运行一次,把文档切块、生成向量,写入 Qdrant;
  2. 13_api.py:接口启动时用 VectorStoreIndex.from_vector_store 直接连接 Qdrant 里已经存好的数据,不再读本地文件。

collection 相当于数据库里的一张表,这里起名为 kb。

先整理 data 文件夹

建索引会读取 data 里所有的 PDF。请先把扫描版 PDF(移动热修复.pdf)移出 data 文件夹。它读不出文字,只会产生无用的数据。

1. 建索引 13_build_qdrant.py ​

提示

model_name 里的"这里填你的 Embedding 模型名"是占位文字。LM Studio 只加载了一个 Embedding 模型时,照这样写也能运行;想写规范一点,可以换成 LM Studio 里显示的模型名。

python
import qdrant_client
from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, StorageContext, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

Settings.embed_model = OpenAILikeEmbedding(
    model_name="这里填你的 Embedding 模型名",
    api_base="http://localhost:1234/v1", api_key="<redacted>")

client = qdrant_client.QdrantClient(url="http://localhost:6333")

# collection 相当于数据库里的一张表
vector_store = QdrantVectorStore(client=client, collection_name="kb")

docs = SimpleDirectoryReader("data", required_exts=[".pdf"]).load_data()

index = VectorStoreIndex.from_documents(
    docs,
    storage_context=StorageContext.from_defaults(vector_store=vector_store),
    transformations=[SentenceSplitter(chunk_size=512, chunk_overlap=50)],
    show_progress=True)

print("已写入 Qdrant,向量数:", client.count("kb").count)

运行 python 13_build_qdrant.py,最后会打印写入的向量数。

2. 接口 13_api.py ​

提示

model_name 里的"这里填你的 Embedding 模型名"是占位文字。LM Studio 只加载了一个 Embedding 模型时,照这样写也能运行;想写规范一点,可以换成 LM Studio 里显示的模型名。

说明

这份代码是从 12_api.py 改过来的。开头的注释"复用项目 1 的 kb_storage"是旧的,现在索引其实来自 Qdrant;load_index_from_storage 这些导入也已经用不到了,删掉也不影响运行。

python
import os
import qdrant_client
from llama_index.core import VectorStoreIndex
from llama_index.vector_stores.qdrant import QdrantVectorStore
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from openai import OpenAI
from llama_index.core import StorageContext, load_index_from_storage, Settings
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

load_dotenv()

# 启动时只加载一次索引(复用项目 1 的 kb_storage),不要每次请求都加载
Settings.embed_model = OpenAILikeEmbedding(
    model_name="这里填你的 Embedding 模型名",
    api_base="http://localhost:1234/v1", api_key="<redacted>")

client = qdrant_client.QdrantClient(url="http://localhost:6333")

index = VectorStoreIndex.from_vector_store(QdrantVectorStore(client=client, collection_name="kb"))
retriever = index.as_retriever(similarity_top_k=3)

llm = OpenAI(api_key=<redacted>("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"))

app = FastAPI(title="知识库问答服务")

# 请求体格式:{"question": "..."},FastAPI 会自动校验
class AskRequest(BaseModel):
    question: str

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/ask")
def ask(req: AskRequest):
    nodes = retriever.retrieve(req.question)
    context = "\n\n".join(n.get_content() for n in nodes)
    resp = llm.chat.completions.create(
        model=os.getenv("LLM_MODEL"),
        messages=[
            {"role": "system", "content": "根据资料回答,资料里没有就说不知道。\n资料:\n" + context},
            {"role": "user", "content": req.question},
        ])
    return {"answer": resp.choices[0].message.content,
            "sources": [round(n.score, 3) for n in nodes]}

启动方式和上一阶段一样:

bash
uvicorn 13_api:app --port 8000

五、实测 ​

打开 http://localhost:8000/docs,用 Try it out 提问"什么时候可以保修":

  • 返回 200;
  • 回答:签收 7 天内可退换,8~30 天可换货或维修,12 个月内可维修;
  • sources 是 [0.618, 0.492, 0.386]。

接口和第十二阶段一模一样,只是数据换成了从 Qdrant 读取,调用方完全感觉不到变化。

下一阶段预告 ​

第十四阶段:用 Dockerfile 和 docker compose 把 API 服务和 Qdrant 打包在一起,一条命令同时启动。