diff --git a/backend/main.py b/backend/main.py index b00520b..ada7b84 100644 --- a/backend/main.py +++ b/backend/main.py @@ -81,7 +81,7 @@ async def search_v1(req: SearchRequest): # ========================================== router_v2 = APIRouter() -@router_v2.post("/auto/map") +@router_v2.post("/crawler/map") async def auto_map(req: AutoMapRequest): """ [同步] 输入首页 URL,自动调用 Firecrawl Map 并入库 @@ -93,7 +93,7 @@ async def auto_map(req: AutoMapRequest): except Exception as e: return make_response(0, str(e)) -@router_v2.post("/auto/process") +@router_v2.post("/crawler/process") async def auto_process(req: AutoProcessRequest, background_tasks: BackgroundTasks): """ [异步] 触发后台任务:消费队列 -> 抓取 -> Embedding -> 入库 diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..5eae05c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,140 @@ + +# Wiki Crawler Backend 部署操作手册 + +## 核心配置信息 (每次只需修改这里) + + **在执行命令前,请先确定本次发布的** **版本号**。 + +| **字段** | **当前值 (示例)** | **说明** | **每次要改吗?** | +| -------------------- | ---------------------------------- | ------------------------------ | ---------------------- | +| **Version** | **v1.0.3** | **镜像的版本标签 (Tag)** | **是 (必须改)** | +| **Image Name** | **wiki-crawl-backend** | **镜像/容器的名字** | **否 (固定)** | +| **Namespace** | **qg-demo** | **阿里云命名空间** | **否 (固定)** | +| **Registry** | **crpi-1rwd6fvain6t49g2...** | **阿里云仓库地址** | **否 (固定)** | + +--- + +## 第一阶段:本地电脑 (Windows) - 打包与上传 + +**打开 PowerShell 或 CMD,进入项目根目录。** + +### 1. 构建镜像 (Build) + +**修改命令最后的版本号** **v1.0.3** + +```powershell +docker build -t crpi-1rwd6fvain6t49g2.cn-hangzhou.personal.cr.aliyuncs.com/qg-demo/wiki-crawl-backend:v1.0.3 . +``` + +### 2. 推送镜像 (Push) + +**修改命令最后的版本号** **v1.0.3** + +```powershell +docker push crpi-1rwd6fvain6t49g2.cn-hangzhou.personal.cr.aliyuncs.com/qg-demo/wiki-crawl-backend:v1.0.3 +``` + +> **成功标准:** **看到进度条走完,且最后显示** **Pushed**。 + +--- + +## 第二阶段:云服务器 (Linux) - 部署更新 + +**使用 SSH 登录阿里云服务器,按顺序执行。** + +### 1. 拉取新镜像 (Pull) + +**修改命令最后的版本号** **v1.0.3** + +```bash +docker pull crpi-1rwd6fvain6t49g2.cn-hangzhou.personal.cr.aliyuncs.com/qg-demo/wiki-crawl-backend:v1.0.3 +``` + +### 2. 停止并删除旧容器 + +**这一步是为了释放端口,不会删除镜像文件** + +```bash +docker stop wiki-crawl-backend +docker rm wiki-crawl-backend +``` + +### 3. 启动新容器 (Run) - 关键步骤 + +**修改命令最后的版本号** **v1.0.3** + +**code**Bash + +```bash +docker run -d --name wiki-crawl-backend \ + -e PYTHONUNBUFFERED=1 \ + -p 80:8000 \ + crpi-1rwd6fvain6t49g2.cn-hangzhou.personal.cr.aliyuncs.com/qg-demo/wiki-crawl-backend:v1.0.3 +``` + +### 4. 验证与日志查看 + +**code**Bash + +``` +# 查看容器状态 (STATUS 应该是 Up) +docker ps + +# 查看实时日志 (按 Ctrl+C 退出) +docker logs -f wiki-crawl-backend +``` + +--- + +## 第三阶段:清理工作 (可选) + +**为了防止服务器硬盘被旧版本的镜像塞满,建议定期执行清理。** + +**code**Bash + +``` +# 删除所有“未被使用”的旧镜像 +docker image prune -a -f +``` + +--- + +## 附录:命令参数详解 (小白必读) + +**在** **docker run** **命令中,各个参数的含义如下:** + +### 1. **-d** **(Detached)** + +* **含义:** **后台运行。** +* **作用:** **容器启动后会默默在后台跑,不会占用你的黑窗口。如果不加这个,你一关 SSH 窗口,服务就停了。** + +### 2. **--name wiki-crawl-backend** + +* **含义:** **给容器起个名字。** +* **作用:** **有了名字,以后你要停止它、重启它、看日志,直接叫它的名字就行(比如** **docker stop wiki-crawl-backend**),不用去查那串随机的容器 ID。 + +### 3. **-e PYTHONUNBUFFERED=1** + +* **含义:** **设置环境变量 (Environment Variable)。** +* **作用:** **这是一个 Python 专用的设置。意思是**“不要缓存输出”**。** + + * **如果不加:Python 会把日志憋在肚子里,攒够了一坨才吐出来,导致你用** **docker logs** **只能看到几分钟前的日志。** + * **加了:日志实时打印,报错立刻能看到。** + +### 4. **-p 80:8000** **(Port Mapping)** + +* **含义:** **端口映射。格式是** **宿主机端口:容器内部端口**。 +* **解析:** + + * **80 (左边)**:这是阿里云服务器对外的门牌号。用户访问 **http://1.2.3.4** **时,默认就是找 80 端口。** + * **8000 (右边)**:这是你 Python 代码 (FastAPI/Uvicorn) 实际监听的端口。 + * **作用:** **把服务器大门 (80) 接到的客人,领到 Python 小房间 (8000) 去。** + +### 5. 那个超长的 URL + +**crpi-1rwd6fvain6t49g2.cn-hangzhou.personal.cr.aliyuncs.com/qg-demo/wiki-crawl-backend:v1.0.3** + +* **Registry (仓库地址)**: **crpi-1rwd...aliyuncs.com** **-> 你的专属阿里云仓库服务器。** +* **Namespace (命名空间)**: **qg-demo** **-> 你在仓库里划出的个人地盘。** +* **Image Name (镜像名)**: **wiki-crawl-backend** **-> 这个项目的名字。** +* **Tag (标签)**: **v1.0.3** **-> 相当于软件的版本号。如果不写 Tag,默认就是** **latest**。**生产环境强烈建议写明确的版本号**,方便回滚(比如 1.0.3 挂了,你可以立马用 1.0.2 启动)。 diff --git a/docs/t.md b/docs/t.md deleted file mode 100644 index fe583f3..0000000 --- a/docs/t.md +++ /dev/null @@ -1,149 +0,0 @@ -针对你希望实现“通用、支持全库或特定任务搜索”的需求,我重新设计并实现了搜索 API。 - -这一版代码继续采用 **SQLAlchemy Core API**,实现了逻辑上的“存取分离”:Dify 只管发向量,后端决定怎么搜。 - -### 1. 修改 `schemas.py` - -增加搜索请求模型,将 `task_id` 设为可选(`Optional`),从而支持全局搜索。 - -```python -from pydantic import BaseModel -from typing import List, Optional - -class SearchRequest(BaseModel): - # 如果不传 task_id,则进行全库搜索 - task_id: Optional[int] = None - query_embedding: List[float] - limit: Optional[int] = 5 -``` - -### 2. 重新实现 `service.py` 中的搜索方法 - -利用 `pgvector` 的 `cosine_distance` 算子。注意这里使用了动态构建查询的技巧。 - -```python -from sqlalchemy import select, and_ -from .database import db_instance - -class CrawlerService: - def __init__(self): - self.db = db_instance - - def search_knowledge(self, query_embedding: list, task_id: int = None, limit: int = 5): - """ - 高性能向量搜索方法 - :param query_embedding: 问题的向量 - :param task_id: 可选的任务ID,不传则搜全表 - :param limit: 返回结果数量 - """ - with self.db.engine.connect() as conn: - # 1. 选择需要的字段 - # 我们同时返回 task_id,方便在全库搜索时知道来源哪个任务 - stmt = select( - self.db.chunks.c.task_id, - self.db.chunks.c.source_url, - self.db.chunks.c.title, - self.db.chunks.c.content, - self.db.chunks.c.chunk_index - ) - - # 2. 动态添加过滤条件 - if task_id is not None: - stmt = stmt.where(self.db.chunks.c.task_id == task_id) - - # 3. 按余弦距离排序(1 - 余弦相似度) - # 距离越小,相似度越高 - stmt = stmt.order_by( - self.db.chunks.c.embedding.cosine_distance(query_embedding) - ).limit(limit) - - # 4. 执行并解析结果 - rows = conn.execute(stmt).fetchall() - - results = [] - for r in rows: - results.append({ - "task_id": r[0], - "source_url": r[1], - "title": r[2], - "content": r[3], - "chunk_index": r[4] - }) - - return results -``` - -### 3. 重新实现 `main.py` 中的接口 - -增加了对向量维度的基础校验,确保系统健壮性。 - -```python -@app.post("/search") -async def search(req: SearchRequest): - """ - 通用搜索接口: - 支持基于 task_id 的局部搜索,也支持不传 task_id 的全库搜索。 - """ - try: - # 1. 基础校验:确保向量不为空且维度正确(阿里 v4 模型通常为 1536) - if not req.query_embedding or len(req.query_embedding) != 1536: - return make_response( - code=2, - msg=f"向量维度错误。期望 1536, 实际收到 {len(req.query_embedding) if req.query_embedding else 0}", - data=None - ) - - # 2. 调用业务类执行搜索 - data = crawler_service.search_knowledge( - query_embedding=req.query_embedding, - task_id=req.task_id, - limit=req.limit - ) - - # 3. 统一返回 - return make_response( - code=1, - msg="搜索完成", - data=data - ) - - except Exception as e: - # 记录日志并返回失败信息 - print(f"搜索接口异常: {str(e)}") - return make_response(code=0, msg=f"搜索失败: {str(e)}") -``` - -### 4. 配合性能优化的 SQL 建议 - -如果你在 Dify 中运行一段时间后发现搜索变慢(数据达到万级或十万级),请在数据库执行以下语句开启 **HNSW 索引**。这是目前向量搜索性能最高、内存占用最平衡的方案。 - -```sql --- 针对余弦距离优化搜索速度 --- m: 列表最大连接数,ef_construction: 动态候选列表大小 --- 这两个参数决定了索引的精度和构建速度 -CREATE INDEX idx_chunks_embedding_hnsw -ON knowledge_chunks -USING hnsw (embedding vector_cosine_ops) -WITH (m = 16, ef_construction = 64); -``` - -### 5. 在 Dify 中这个节点的输出怎么用? - -由于 `/search` 返回的是一个列表,在 Dify 中接一个 **“代码节点”** 处理成文本最方便: - -```python -def main(search_data: list) -> dict: - # search_data 是从 HTTP 节点获得的 data 数组 - context_list = [] - for i, item in enumerate(search_data): - # 格式化每条资料,包含来源和内容 - block = f"【资料{i+1}】来源: {item['source_url']}\n内容: {item['content']}" - context_list.append(block) - - # 用换行符连接所有资料 - return { - "final_context": "\n\n".join(context_list) - } -``` - -最后把这个 `final_context` 塞进 LLM 节点的 Prompt 即可。这样的设计确保了你的 Dify 流程非常干净:**输入 -> 转向量 -> 搜后端 -> 出答案**。