Commit 7fbca0d72a278e9acefa525cccb2e01a7de2a8c7
1 parent
02c40701
启动脚本优化
Showing
21 changed files
with
135 additions
and
430 deletions
Show diff stats
.env.example
CLIP_SERVICE_README.md
| 1 | -## 基于 `clip-server` 的向量服务(平级替代 `embeddings`) | |
| 1 | +## CLIP 服务说明(已收敛到 CN-CLIP) | |
| 2 | 2 | |
| 3 | -本模块说明如何在 **独立环境** 中部署基于 `jina-ai/clip-as-service` 仓库的向量服务(实际安装包为 `clip-server` / `clip-client`),用于替代当前仓库里的本地 `embeddings` 服务(`embeddings/server.py`)。 | |
| 3 | +原有 `start_clip_service.sh / stop_clip_service.sh` 已移除,避免与 `cnclip` 重名和端口状态混淆。 | |
| 4 | 4 | |
| 5 | -> 设计目标: | |
| 6 | -> - 与项目主环境(`searchengine` conda env)**完全隔离** | |
| 7 | -> - 使用官方开源项目 [`jina-ai/clip-as-service`](https://github.com/jina-ai/clip-as-service)(对应 PyPI 包:`clip-server` / `clip-client`) | |
| 8 | -> - 提供简单的 **安装 / 启动 / 停止脚本** | |
| 9 | - | |
| 10 | ---- | |
| 11 | - | |
| 12 | -## 1. 环境准备(独立环境) | |
| 13 | - | |
| 14 | -推荐使用 Conda 新建一个专用环境(与本项目的 `searchengine` 环境隔离): | |
| 15 | - | |
| 16 | -```bash | |
| 17 | -# 1)加载 conda(你的 conda 是 ~/anaconda3/bin/conda → CONDA_ROOT=~/anaconda3) | |
| 18 | -export CONDA_ROOT=${CONDA_ROOT:-$HOME/anaconda3} # 或你的 Conda 安装路径 | |
| 19 | -source "$CONDA_ROOT/etc/profile.d/conda.sh" | |
| 20 | - | |
| 21 | -# 2)创建 clip 向量服务专用环境 | |
| 22 | -conda create -n clip_service python=3.9 -y | |
| 23 | - | |
| 24 | -# 3)激活环境 | |
| 25 | -conda activate clip_service | |
| 26 | - | |
| 27 | -# 4)安装 clip-server / clip-client(其内部依赖 jina) | |
| 28 | -# 如需绕过镜像问题,可显式使用官方 PyPI 源: | |
| 29 | -# pip install -i https://pypi.org/simple "clip-server" "clip-client" | |
| 30 | -pip install "clip-server" "clip-client" | |
| 31 | -``` | |
| 32 | - | |
| 33 | -> 如果你不使用 Conda,也可以改用 `python -m venv` 创建虚拟环境, | |
| 34 | -> 但务必保证 **不要与主项目共用同一个 Python 环境**。 | |
| 35 | - | |
| 36 | ---- | |
| 37 | - | |
| 38 | -## 2. 启动 / 停止脚本 | |
| 39 | - | |
| 40 | -本仓库在 `scripts/` 目录下提供了两个脚本(需要手动赋权一次): | |
| 41 | - | |
| 42 | -```bash | |
| 43 | -chmod +x scripts/start_clip_service.sh | |
| 44 | -chmod +x scripts/stop_clip_service.sh | |
| 45 | -``` | |
| 46 | - | |
| 47 | -### 2.1 启动服务 | |
| 48 | - | |
| 49 | -```bash | |
| 50 | -cd /data/saas-search | |
| 51 | -./scripts/start_clip_service.sh | |
| 52 | -``` | |
| 53 | - | |
| 54 | -脚本行为: | |
| 55 | - | |
| 56 | -- 自动 `cd` 到仓库根目录 | |
| 57 | -- 尝试加载 `$CONDA_ROOT/etc/profile.d/conda.sh` 并激活 `clip_service` 环境(可通过 `export CONDA_ROOT=...` 适配新机器) | |
| 58 | -- 使用 `nohup python -m clip_server` 启动服务到后台 | |
| 59 | -- 将日志写入 `logs/clip_service.log` | |
| 60 | -- 将进程号写入 `logs/clip_service.pid` | |
| 61 | - | |
| 62 | -默认情况下,`clip-server` 会监听在 **`grpc://0.0.0.0:51000`**(gRPC 协议,端口 51000)。 | |
| 63 | - | |
| 64 | -> ⚠️ **重要**:客户端连接时请使用端口 **51000**,不是 23456 或其他端口。 | |
| 65 | - | |
| 66 | -### 2.2 停止服务 | |
| 67 | - | |
| 68 | -```bash | |
| 69 | -cd /data/saas-search | |
| 70 | -./scripts/stop_clip_service.sh | |
| 71 | -``` | |
| 72 | - | |
| 73 | -脚本行为: | |
| 74 | - | |
| 75 | -- 读取 `logs/clip_service.pid` 中的 PID | |
| 76 | -- 如果进程存在则发送 `kill` 终止 | |
| 77 | -- 清理 `logs/clip_service.pid` | |
| 78 | - | |
| 79 | ---- | |
| 80 | - | |
| 81 | -## 3. 与现有 `embeddings` 服务的关系 | |
| 82 | - | |
| 83 | -- 现有本地向量服务: | |
| 84 | - - 启动脚本:`./scripts/start_embedding_service.sh` | |
| 85 | - - 实现:`embeddings/server.py`(FastAPI + 本地模型 `qwen3_model.py` / `clip_model.py`) | |
| 86 | -- 新增的 `clip-server`: | |
| 87 | - - 使用官方实现,单独进程、单独环境 | |
| 88 | - - 面向图像 / 文本的 CLIP 向量化服务 | |
| 89 | - | |
| 90 | -### 使用建议 | |
| 91 | - | |
| 92 | -- 如果你想继续使用本仓库自带的本地模型服务,保持原有脚本不变即可: | |
| 93 | - - `./scripts/start_embedding_service.sh` | |
| 94 | -- 如果你想用 `clip-as-service` 替代原来的本地服务,可以: | |
| 95 | - - 在上游调用代码中,将向量请求切换到 `clip-as-service` 对应的端口 / 接口 | |
| 96 | - - 或者增加一个适配层,将 `clip-as-service` 封装成与 `POST /embed/text` / `POST /embed/image` 相同的接口(视具体场景而定) | |
| 97 | - | |
| 98 | ---- | |
| 99 | - | |
| 100 | -## 4. 基本验证 | |
| 101 | - | |
| 102 | -1. 确认 `clip_service` 环境创建并安装成功: | |
| 103 | - | |
| 104 | - ```bash | |
| 105 | - export CONDA_ROOT=${CONDA_ROOT:-$HOME/anaconda3} | |
| 106 | - source "$CONDA_ROOT/etc/profile.d/conda.sh" | |
| 107 | - conda activate clip_service | |
| 108 | - python -c "import jina; print('jina version:', jina.__version__)" | |
| 109 | - ``` | |
| 110 | - | |
| 111 | -2. 启动服务并查看日志: | |
| 112 | - | |
| 113 | - ```bash | |
| 114 | - cd /data/saas-search | |
| 115 | - ./scripts/start_clip_service.sh | |
| 116 | - tail -f logs/clip_service.log | |
| 117 | - ``` | |
| 118 | - | |
| 119 | - 服务启动后,默认监听在 **`grpc://0.0.0.0:51000`**(gRPC 协议,端口 51000)。 | |
| 120 | - | |
| 121 | -3. 测试客户端连接(在 `clip_service` 环境中): | |
| 122 | - | |
| 123 | - ```python | |
| 124 | - from clip_client import Client | |
| 125 | - | |
| 126 | - # 注意:默认端口是 51000,不是 23456 | |
| 127 | - c = Client('grpc://0.0.0.0:51000') | |
| 128 | - | |
| 129 | - # 测试连接 | |
| 130 | - c.profile() | |
| 131 | - | |
| 132 | - # 测试文本向量化 | |
| 133 | - r = c.encode(['First do it', 'then do it right', 'then do it better']) | |
| 134 | - print(r.shape) # 应该输出 [3, 512] 或类似形状 | |
| 135 | - | |
| 136 | - # 测试图像向量化 | |
| 137 | - r = c.encode(['https://picsum.photos/200']) | |
| 138 | - print(r.shape) # 应该输出 [1, 512] 或类似形状 | |
| 139 | - ``` | |
| 140 | - | |
| 141 | -4. 如果不再需要服务,执行: | |
| 142 | - | |
| 143 | - ```bash | |
| 144 | - ./scripts/stop_clip_service.sh | |
| 145 | - ``` | |
| 146 | - | |
| 147 | -### 常见问题 | |
| 148 | - | |
| 149 | -**Q: 连接被拒绝(Connection refused)?** | |
| 150 | -A: 请确认: | |
| 151 | -- 服务已启动(检查 `logs/clip_service.log` 和进程) | |
| 152 | -- 客户端使用的端口是 **51000**(不是 23456) | |
| 153 | -- 客户端地址格式正确:`grpc://0.0.0.0:51000` 或 `grpc://localhost:51000` | |
| 154 | - | |
| 155 | -**Q: Gateway 启动了但 worker 连接失败?** | |
| 156 | -A: 可能原因: | |
| 157 | -- Worker 进程(clip_t)还在启动中,模型加载需要时间(首次启动可能需要下载模型) | |
| 158 | -- 检查日志中是否有模型下载或加载错误: | |
| 159 | - ```bash | |
| 160 | - tail -f logs/clip_service.log | grep -E "(ERROR|WARNING|model|download)" | |
| 161 | - ``` | |
| 162 | -- 如果持续失败,尝试重启服务: | |
| 163 | - ```bash | |
| 164 | - ./scripts/stop_clip_service.sh | |
| 165 | - ./scripts/start_clip_service.sh | |
| 166 | - ``` | |
| 167 | - | |
| 168 | -**Q: 如何查看服务实际监听的端口?** | |
| 169 | -A: 查看启动日志: | |
| 170 | -```bash | |
| 171 | -tail -f logs/clip_service.log | grep "bound to" | |
| 172 | -``` | |
| 173 | -或检查进程监听的端口: | |
| 174 | -```bash | |
| 175 | -lsof -i :51000 | |
| 176 | -# 或 | |
| 177 | -netstat -tlnp | grep 51000 | |
| 178 | -``` | |
| 179 | - | |
| 180 | -**Q: 如何确认服务完全就绪?** | |
| 181 | -A: 查看日志,确认看到类似输出: | |
| 182 | -``` | |
| 183 | -INFO gateway/rep-0@XXXXX start server bound to 0.0.0.0:51000 | |
| 184 | -``` | |
| 185 | -然后等待几秒让 worker 进程启动,再测试客户端连接。 | |
| 186 | - | |
| 187 | ---- | |
| 188 | - | |
| 189 | -## 5. 参考 | |
| 190 | - | |
| 191 | -- 项目地址:`https://github.com/jina-ai/clip-as-service` | |
| 192 | -- 本项目向量模块文档:`embeddings/README.md` | |
| 5 | +当前统一入口: | |
| 193 | 6 | |
| 7 | +- 环境:`./scripts/setup_cnclip_venv.sh` | |
| 8 | +- 启动:`./scripts/start_cnclip_service.sh` | |
| 9 | +- 停止:`./scripts/stop_cnclip_service.sh` | |
| 10 | +- 编排:`./scripts/service_ctl.sh start cnclip` | |
| 194 | 11 | |
| 12 | +请以 `docs/CNCLIP_SERVICE说明文档.md` 为唯一文档入口。 | ... | ... |
README.md
| ... | ... | @@ -28,8 +28,8 @@ source activate.sh |
| 28 | 28 | # 启动核心服务(backend/indexer/frontend) |
| 29 | 29 | ./run.sh |
| 30 | 30 | |
| 31 | -# 可选:附加能力服务 | |
| 32 | -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 ./run.sh | |
| 31 | +# 可选:附加能力服务(按需开启) | |
| 32 | +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh | |
| 33 | 33 | |
| 34 | 34 | # 查看状态 |
| 35 | 35 | ./scripts/service_ctl.sh status | ... | ... |
docs/CNCLIP_SERVICE说明文档.md
| ... | ... | @@ -70,11 +70,9 @@ cd /data/saas-search |
| 70 | 70 | ### 6.1 通过统一编排启动 |
| 71 | 71 | |
| 72 | 72 | ```bash |
| 73 | -START_EMBEDDING=1 START_TEI=1 ./scripts/service_ctl.sh start | |
| 73 | +START_EMBEDDING=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start | |
| 74 | 74 | ``` |
| 75 | 75 | |
| 76 | -当 `USE_CLIP_AS_SERVICE=true` 且 `START_EMBEDDING=1` 时,`service_ctl` 会自动拉起 `cnclip`。 | |
| 77 | - | |
| 78 | 76 | ### 6.2 设备选择优先级 |
| 79 | 77 | |
| 80 | 78 | - 显式传入 `CNCLIP_DEVICE` 时,以该值为准: |
| ... | ... | @@ -153,4 +151,3 @@ curl -sS -X POST "http://127.0.0.1:6005/embed/image" \ |
| 153 | 151 | - TEI 专项:`docs/TEI_SERVICE说明文档.md` |
| 154 | 152 | - 体系规范:`docs/DEVELOPER_GUIDE.md` |
| 155 | 153 | - embedding 模块:`embeddings/README.md` |
| 156 | - | ... | ... |
docs/QUICKSTART.md
| ... | ... | @@ -66,10 +66,9 @@ source activate.sh |
| 66 | 66 | ```bash |
| 67 | 67 | ./run.sh |
| 68 | 68 | # 启动全部能力 |
| 69 | -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh | |
| 69 | +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh | |
| 70 | 70 | # 等价方式(直接使用服务控制器) |
| 71 | -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./scripts/service_ctl.sh start | |
| 72 | -# 说明:当 USE_CLIP_AS_SERVICE=true(默认)且 START_EMBEDDING=1 时,会自动启动 cnclip(51000) | |
| 71 | +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start | |
| 73 | 72 | # 说明: |
| 74 | 73 | # - reranker 为 GPU 强制模式(资源不足会直接启动失败) |
| 75 | 74 | # - TEI 默认使用 GPU;当 TEI_USE_GPU=1 且 GPU 不可用时会直接失败(不会自动降级到 CPU) | ... | ... |
docs/TEI_SERVICE说明文档.md
| ... | ... | @@ -152,7 +152,7 @@ curl -sS -X POST "http://127.0.0.1:6005/embed/text" \ |
| 152 | 152 | 启动全套(含 TEI): |
| 153 | 153 | |
| 154 | 154 | ```bash |
| 155 | -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 TEI_USE_GPU=1 ./scripts/service_ctl.sh start | |
| 155 | +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 TEI_USE_GPU=1 ./scripts/service_ctl.sh start | |
| 156 | 156 | ``` |
| 157 | 157 | |
| 158 | 158 | 仅启动 TEI: | ... | ... |
docs/Usage-Guide.md
| ... | ... | @@ -135,10 +135,10 @@ cd /data/saas-search |
| 135 | 135 | - **API文档**: http://localhost:6002/docs |
| 136 | 136 | - **索引API**: http://localhost:6004/docs |
| 137 | 137 | |
| 138 | -可选:全功能模式(同时启动 embedding/translator/reranker/tei): | |
| 138 | +可选:全功能模式(同时启动 embedding/translator/reranker/tei/cnclip): | |
| 139 | 139 | |
| 140 | 140 | ```bash |
| 141 | -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh | |
| 141 | +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh | |
| 142 | 142 | ``` |
| 143 | 143 | |
| 144 | 144 | ### 方式2: 统一控制脚本(推荐) |
| ... | ... | @@ -151,7 +151,7 @@ START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh |
| 151 | 151 | ./scripts/service_ctl.sh start |
| 152 | 152 | |
| 153 | 153 | # 启动指定服务 |
| 154 | -./scripts/service_ctl.sh start backend indexer frontend translator reranker tei | |
| 154 | +./scripts/service_ctl.sh start backend indexer frontend translator reranker tei cnclip | |
| 155 | 155 | |
| 156 | 156 | # 停止全部服务(含可选服务) |
| 157 | 157 | ./scripts/service_ctl.sh stop |
| ... | ... | @@ -311,6 +311,8 @@ RERANKER_PORT=6007 |
| 311 | 311 | START_EMBEDDING=0 |
| 312 | 312 | START_TRANSLATOR=0 |
| 313 | 313 | START_RERANKER=0 |
| 314 | +START_TEI=0 | |
| 315 | +START_CNCLIP=0 | |
| 314 | 316 | ``` |
| 315 | 317 | |
| 316 | 318 | ### 修改配置 | ... | ... |
docs/搜索API对接指南.md
| ... | ... | @@ -1726,7 +1726,7 @@ curl "http://localhost:6005/health" |
| 1726 | 1726 | |
| 1727 | 1727 | ```bash |
| 1728 | 1728 | ./scripts/start_tei_service.sh |
| 1729 | -START_TEI=0 ./scripts/service_ctl.sh restart embedding | |
| 1729 | +./scripts/service_ctl.sh restart embedding | |
| 1730 | 1730 | ``` |
| 1731 | 1731 | |
| 1732 | 1732 | 默认端口: | ... | ... |
restart.sh
scripts/frontend_server.py
| ... | ... | @@ -27,7 +27,7 @@ frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend') |
| 27 | 27 | os.chdir(frontend_dir) |
| 28 | 28 | |
| 29 | 29 | # Get port from environment variable or default |
| 30 | -PORT = int(os.getenv('PORT', 6003)) | |
| 30 | +PORT = int(os.getenv('FRONTEND_PORT', 6003)) | |
| 31 | 31 | |
| 32 | 32 | # Configure logging to suppress scanner noise |
| 33 | 33 | logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') | ... | ... |
scripts/service_ctl.sh
| ... | ... | @@ -15,11 +15,10 @@ mkdir -p "${LOG_DIR}" |
| 15 | 15 | source "${PROJECT_ROOT}/scripts/lib/load_env.sh" |
| 16 | 16 | |
| 17 | 17 | CORE_SERVICES=("backend" "indexer" "frontend") |
| 18 | -OPTIONAL_SERVICES=("embedding" "translator" "reranker" "tei") | |
| 19 | -LEGACY_SERVICES=("clip" "cnclip") | |
| 18 | +OPTIONAL_SERVICES=("embedding" "translator" "reranker" "tei" "cnclip") | |
| 20 | 19 | |
| 21 | 20 | all_services() { |
| 22 | - echo "${CORE_SERVICES[@]} ${OPTIONAL_SERVICES[@]} ${LEGACY_SERVICES[@]}" | |
| 21 | + echo "${CORE_SERVICES[@]} ${OPTIONAL_SERVICES[@]}" | |
| 23 | 22 | } |
| 24 | 23 | |
| 25 | 24 | get_port() { |
| ... | ... | @@ -32,7 +31,6 @@ get_port() { |
| 32 | 31 | translator) echo "${TRANSLATION_PORT:-${TRANSLATOR_PORT:-6006}}" ;; |
| 33 | 32 | reranker) echo "${RERANKER_PORT:-6007}" ;; |
| 34 | 33 | tei) echo "${TEI_PORT:-8080}" ;; |
| 35 | - clip) echo "${CLIP_PORT:-51000}" ;; | |
| 36 | 34 | cnclip) echo "${CNCLIP_PORT:-51000}" ;; |
| 37 | 35 | *) echo "" ;; |
| 38 | 36 | esac |
| ... | ... | @@ -41,7 +39,6 @@ get_port() { |
| 41 | 39 | pid_file() { |
| 42 | 40 | local service="$1" |
| 43 | 41 | case "${service}" in |
| 44 | - clip) echo "${LOG_DIR}/clip_service.pid" ;; | |
| 45 | 42 | cnclip) echo "${LOG_DIR}/cnclip_service.pid" ;; |
| 46 | 43 | *) echo "${LOG_DIR}/${service}.pid" ;; |
| 47 | 44 | esac |
| ... | ... | @@ -62,7 +59,6 @@ service_start_cmd() { |
| 62 | 59 | translator) echo "./scripts/start_translator.sh" ;; |
| 63 | 60 | reranker) echo "./scripts/start_reranker.sh" ;; |
| 64 | 61 | tei) echo "./scripts/start_tei_service.sh" ;; |
| 65 | - clip) echo "./scripts/start_clip_service.sh" ;; | |
| 66 | 62 | cnclip) echo "./scripts/start_cnclip_service.sh" ;; |
| 67 | 63 | *) return 1 ;; |
| 68 | 64 | esac |
| ... | ... | @@ -158,7 +154,10 @@ start_one() { |
| 158 | 154 | local service="$1" |
| 159 | 155 | cd "${PROJECT_ROOT}" |
| 160 | 156 | local cmd |
| 161 | - cmd="$(service_start_cmd "${service}")" | |
| 157 | + if ! cmd="$(service_start_cmd "${service}")"; then | |
| 158 | + echo "[error] unknown service: ${service}" >&2 | |
| 159 | + return 1 | |
| 160 | + fi | |
| 162 | 161 | local pf lf |
| 163 | 162 | pf="$(pid_file "${service}")" |
| 164 | 163 | lf="$(log_file "${service}")" |
| ... | ... | @@ -186,7 +185,7 @@ start_one() { |
| 186 | 185 | fi |
| 187 | 186 | |
| 188 | 187 | case "${service}" in |
| 189 | - clip|cnclip|tei) | |
| 188 | + cnclip|tei) | |
| 190 | 189 | echo "[start] ${service} (managed by native script)" |
| 191 | 190 | if [ "${service}" = "cnclip" ]; then |
| 192 | 191 | CNCLIP_DEVICE="${CNCLIP_DEVICE:-cuda}" bash -lc "${cmd}" >> "${lf}" 2>&1 |
| ... | ... | @@ -248,11 +247,6 @@ start_one() { |
| 248 | 247 | stop_one() { |
| 249 | 248 | local service="$1" |
| 250 | 249 | cd "${PROJECT_ROOT}" |
| 251 | - if [ "${service}" = "clip" ]; then | |
| 252 | - echo "[stop] clip (managed by native script)" | |
| 253 | - bash -lc "./scripts/stop_clip_service.sh" || true | |
| 254 | - return 0 | |
| 255 | - fi | |
| 256 | 250 | if [ "${service}" = "cnclip" ]; then |
| 257 | 251 | echo "[stop] cnclip (managed by native script)" |
| 258 | 252 | bash -lc "./scripts/stop_cnclip_service.sh" || true |
| ... | ... | @@ -347,17 +341,9 @@ resolve_targets() { |
| 347 | 341 | case "${scope}" in |
| 348 | 342 | start) |
| 349 | 343 | local targets=("${CORE_SERVICES[@]}") |
| 350 | - # Start TEI before embedding when both are enabled, because embedding | |
| 351 | - # tei backend performs strict startup health checks against TEI. | |
| 352 | 344 | if [ "${START_TEI:-0}" = "1" ]; then targets+=("tei"); fi |
| 353 | - if [ "${START_EMBEDDING:-0}" = "1" ]; then | |
| 354 | - local use_clip="${USE_CLIP_AS_SERVICE:-true}" | |
| 355 | - use_clip="$(echo "${use_clip}" | tr '[:upper:]' '[:lower:]')" | |
| 356 | - if [[ "${use_clip}" == "1" || "${use_clip}" == "true" || "${use_clip}" == "yes" ]]; then | |
| 357 | - targets+=("cnclip") | |
| 358 | - fi | |
| 359 | - targets+=("embedding") | |
| 360 | - fi | |
| 345 | + if [ "${START_CNCLIP:-0}" = "1" ]; then targets+=("cnclip"); fi | |
| 346 | + if [ "${START_EMBEDDING:-0}" = "1" ]; then targets+=("embedding"); fi | |
| 361 | 347 | if [ "${START_TRANSLATOR:-0}" = "1" ]; then targets+=("translator"); fi |
| 362 | 348 | if [ "${START_RERANKER:-0}" = "1" ]; then targets+=("reranker"); fi |
| 363 | 349 | echo "${targets[@]}" |
| ... | ... | @@ -366,8 +352,7 @@ resolve_targets() { |
| 366 | 352 | echo "$(all_services)" |
| 367 | 353 | ;; |
| 368 | 354 | restart) |
| 369 | - # Restart with no explicit services should preserve start-order dependency | |
| 370 | - # behavior (e.g. tei/cnclip before embedding). | |
| 355 | + # Restart with no explicit services uses the same explicit start target order. | |
| 371 | 356 | echo "$(resolve_targets start)" |
| 372 | 357 | ;; |
| 373 | 358 | *) |
| ... | ... | @@ -391,9 +376,8 @@ Default target set (when no service provided): |
| 391 | 376 | status -> all known services |
| 392 | 377 | |
| 393 | 378 | Optional startup flags: |
| 394 | - START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh | |
| 395 | - START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./scripts/service_ctl.sh start | |
| 396 | - # when USE_CLIP_AS_SERVICE=true (default), START_EMBEDDING=1 will auto-start cnclip | |
| 379 | + START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh | |
| 380 | + START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start | |
| 397 | 381 | CNCLIP_DEVICE=cuda|cpu ./scripts/service_ctl.sh start cnclip |
| 398 | 382 | EOF |
| 399 | 383 | } |
| ... | ... | @@ -411,7 +395,7 @@ main() { |
| 411 | 395 | local stop_targets="" |
| 412 | 396 | local targets |
| 413 | 397 | # For restart without explicit services, stop everything first, then start |
| 414 | - # with dependency-aware start targets. | |
| 398 | + # with the default start target order. | |
| 415 | 399 | if [ "${action}" = "restart" ] && [ "$#" -eq 0 ]; then |
| 416 | 400 | stop_targets="$(resolve_targets stop)" |
| 417 | 401 | fi | ... | ... |
scripts/start.sh
| ... | ... | @@ -12,7 +12,7 @@ echo "saas-search 服务启动" |
| 12 | 12 | echo "========================================" |
| 13 | 13 | echo "默认启动核心服务: backend/indexer/frontend" |
| 14 | 14 | echo "可选服务通过环境变量开启:" |
| 15 | -echo " START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh" | |
| 15 | +echo " START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh" | |
| 16 | 16 | echo |
| 17 | 17 | |
| 18 | 18 | ./scripts/service_ctl.sh start | ... | ... |
scripts/start_clip_service.sh deleted
| ... | ... | @@ -1,57 +0,0 @@ |
| 1 | -#!/bin/bash | |
| 2 | -# | |
| 3 | -# Start CLIP vector service (clip-server) in an independent environment. | |
| 4 | -# | |
| 5 | -# This service is designed to be a drop-in alternative to the local | |
| 6 | -# `embeddings` service, but runs in its own Python environment and depends | |
| 7 | -# on `jina` via `clip-server`. | |
| 8 | -# | |
| 9 | -set -e | |
| 10 | - | |
| 11 | -cd "$(dirname "$0")/.." | |
| 12 | - | |
| 13 | -LOG_DIR="$(pwd)/logs" | |
| 14 | -mkdir -p "${LOG_DIR}" | |
| 15 | -PID_FILE="${LOG_DIR}/clip_service.pid" | |
| 16 | -LOG_FILE="${LOG_DIR}/clip_service.log" | |
| 17 | - | |
| 18 | -echo "========================================" | |
| 19 | -echo "Starting CLIP vector service (clip-server)" | |
| 20 | -echo "========================================" | |
| 21 | - | |
| 22 | -# Force isolated CN-CLIP runtime env | |
| 23 | -CLIP_VENV="$(pwd)/.venv-cnclip" | |
| 24 | -if [ ! -x "${CLIP_VENV}/bin/python" ]; then | |
| 25 | - echo "Error: isolated clip runtime not found: ${CLIP_VENV}" >&2 | |
| 26 | - echo "Please run: ./scripts/setup_cnclip_venv.sh" >&2 | |
| 27 | - exit 1 | |
| 28 | -fi | |
| 29 | -PYTHON_BIN="${CLIP_VENV}/bin/python" | |
| 30 | - | |
| 31 | -if [ -f "${PID_FILE}" ]; then | |
| 32 | - EXISTING_PID="$(cat "${PID_FILE}")" | |
| 33 | - if ps -p "${EXISTING_PID}" > /dev/null 2>&1; then | |
| 34 | - echo "clip-server already appears to be running with PID ${EXISTING_PID}." | |
| 35 | - echo "If this is incorrect, remove ${PID_FILE} and try again." | |
| 36 | - exit 0 | |
| 37 | - else | |
| 38 | - echo "Stale PID file found at ${PID_FILE}, removing..." | |
| 39 | - rm -f "${PID_FILE}" | |
| 40 | - fi | |
| 41 | -fi | |
| 42 | - | |
| 43 | -echo "Log file: ${LOG_FILE}" | |
| 44 | -echo "PID file: ${PID_FILE}" | |
| 45 | -echo | |
| 46 | -echo "Starting clip-server in background..." | |
| 47 | - | |
| 48 | -nohup "${PYTHON_BIN}" -m clip_server > "${LOG_FILE}" 2>&1 & | |
| 49 | -SERVICE_PID=$! | |
| 50 | -echo "${SERVICE_PID}" > "${PID_FILE}" | |
| 51 | - | |
| 52 | -echo "clip-server started with PID ${SERVICE_PID}." | |
| 53 | -echo "You can check logs with:" | |
| 54 | -echo " tail -f ${LOG_FILE}" | |
| 55 | - | |
| 56 | - | |
| 57 | - |
scripts/start_embedding_service.sh
| ... | ... | @@ -21,32 +21,10 @@ if [[ ! -x "${PYTHON_BIN}" ]]; then |
| 21 | 21 | exit 1 |
| 22 | 22 | fi |
| 23 | 23 | |
| 24 | -# Load .env if present (same behavior as activate.sh, without activating main venv) | |
| 25 | -ENV_FILE="${PROJECT_ROOT}/.env" | |
| 26 | -if [ -f "${ENV_FILE}" ]; then | |
| 27 | - while IFS= read -r line || [ -n "${line}" ]; do | |
| 28 | - line="${line%$'\r'}" | |
| 29 | - [[ -z "${line//[[:space:]]/}" ]] && continue | |
| 30 | - [[ "${line}" =~ ^[[:space:]]*# ]] && continue | |
| 31 | - [[ "${line}" != *=* ]] && continue | |
| 32 | - | |
| 33 | - key="${line%%=*}" | |
| 34 | - value="${line#*=}" | |
| 35 | - key="${key#"${key%%[![:space:]]*}"}" | |
| 36 | - key="${key%"${key##*[![:space:]]}"}" | |
| 37 | - value="${value#"${value%%[![:space:]]*}"}" | |
| 38 | - | |
| 39 | - if [[ ${#value} -ge 2 ]]; then | |
| 40 | - first="${value:0:1}" | |
| 41 | - last="${value: -1}" | |
| 42 | - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then | |
| 43 | - value="${value:1:${#value}-2}" | |
| 44 | - fi | |
| 45 | - fi | |
| 46 | - | |
| 47 | - export "${key}=${value}" | |
| 48 | - done < "${ENV_FILE}" | |
| 49 | -fi | |
| 24 | +# Load .env without activating main venv. | |
| 25 | +# shellcheck source=scripts/lib/load_env.sh | |
| 26 | +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" | |
| 27 | +load_env_file "${PROJECT_ROOT}/.env" | |
| 50 | 28 | |
| 51 | 29 | DEFAULT_EMBEDDING_SERVICE_HOST=$("${PYTHON_BIN}" -c "from embeddings.config import CONFIG; print(CONFIG.HOST)") |
| 52 | 30 | DEFAULT_EMBEDDING_SERVICE_PORT=$("${PYTHON_BIN}" -c "from embeddings.config import CONFIG; print(CONFIG.PORT)") | ... | ... |
scripts/start_frontend.sh
| ... | ... | @@ -15,13 +15,14 @@ echo -e "${GREEN}========================================${NC}" |
| 15 | 15 | echo -e "${GREEN}Starting Frontend Server${NC}" |
| 16 | 16 | echo -e "${GREEN}========================================${NC}" |
| 17 | 17 | |
| 18 | -PORT=6003 | |
| 18 | +FRONTEND_PORT="${FRONTEND_PORT:-6003}" | |
| 19 | +API_PORT="${API_PORT:-6002}" | |
| 19 | 20 | |
| 20 | 21 | echo -e "\n${YELLOW}Frontend will be available at:${NC}" |
| 21 | -echo -e " ${GREEN}http://localhost:$PORT${NC}" | |
| 22 | +echo -e " ${GREEN}http://localhost:${FRONTEND_PORT}${NC}" | |
| 22 | 23 | echo "" |
| 23 | 24 | echo -e "${YELLOW}Make sure the backend API is running at:${NC}" |
| 24 | -echo -e " ${GREEN}http://localhost:6002${NC}" | |
| 25 | +echo -e " ${GREEN}http://localhost:${API_PORT}${NC}" | |
| 25 | 26 | echo "" |
| 26 | 27 | |
| 27 | 28 | python scripts/frontend_server.py | ... | ... |
scripts/start_reranker.sh
| ... | ... | @@ -16,32 +16,10 @@ if [[ ! -x "${PYTHON_BIN}" ]]; then |
| 16 | 16 | exit 1 |
| 17 | 17 | fi |
| 18 | 18 | |
| 19 | -# Load .env if present (without activating main venv) | |
| 20 | -ENV_FILE="${PROJECT_ROOT}/.env" | |
| 21 | -if [ -f "${ENV_FILE}" ]; then | |
| 22 | - while IFS= read -r line || [ -n "${line}" ]; do | |
| 23 | - line="${line%$'\r'}" | |
| 24 | - [[ -z "${line//[[:space:]]/}" ]] && continue | |
| 25 | - [[ "${line}" =~ ^[[:space:]]*# ]] && continue | |
| 26 | - [[ "${line}" != *=* ]] && continue | |
| 27 | - | |
| 28 | - key="${line%%=*}" | |
| 29 | - value="${line#*=}" | |
| 30 | - key="${key#"${key%%[![:space:]]*}"}" | |
| 31 | - key="${key%"${key##*[![:space:]]}"}" | |
| 32 | - value="${value#"${value%%[![:space:]]*}"}" | |
| 33 | - | |
| 34 | - if [[ ${#value} -ge 2 ]]; then | |
| 35 | - first="${value:0:1}" | |
| 36 | - last="${value: -1}" | |
| 37 | - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then | |
| 38 | - value="${value:1:${#value}-2}" | |
| 39 | - fi | |
| 40 | - fi | |
| 41 | - | |
| 42 | - export "${key}=${value}" | |
| 43 | - done < "${ENV_FILE}" | |
| 44 | -fi | |
| 19 | +# Load .env without activating main venv. | |
| 20 | +# shellcheck source=scripts/lib/load_env.sh | |
| 21 | +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" | |
| 22 | +load_env_file "${PROJECT_ROOT}/.env" | |
| 45 | 23 | |
| 46 | 24 | RERANKER_HOST="${RERANKER_HOST:-0.0.0.0}" |
| 47 | 25 | RERANKER_PORT="${RERANKER_PORT:-6007}" | ... | ... |
scripts/start_tei_service.sh
| ... | ... | @@ -7,32 +7,10 @@ set -euo pipefail |
| 7 | 7 | PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 8 | 8 | cd "${PROJECT_ROOT}" |
| 9 | 9 | |
| 10 | -# Load .env if present | |
| 11 | -ENV_FILE="${PROJECT_ROOT}/.env" | |
| 12 | -if [ -f "${ENV_FILE}" ]; then | |
| 13 | - while IFS= read -r line || [ -n "${line}" ]; do | |
| 14 | - line="${line%$'\r'}" | |
| 15 | - [[ -z "${line//[[:space:]]/}" ]] && continue | |
| 16 | - [[ "${line}" =~ ^[[:space:]]*# ]] && continue | |
| 17 | - [[ "${line}" != *=* ]] && continue | |
| 18 | - | |
| 19 | - key="${line%%=*}" | |
| 20 | - value="${line#*=}" | |
| 21 | - key="${key#"${key%%[![:space:]]*}"}" | |
| 22 | - key="${key%"${key##*[![:space:]]}"}" | |
| 23 | - value="${value#"${value%%[![:space:]]*}"}" | |
| 24 | - | |
| 25 | - if [[ ${#value} -ge 2 ]]; then | |
| 26 | - first="${value:0:1}" | |
| 27 | - last="${value: -1}" | |
| 28 | - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then | |
| 29 | - value="${value:1:${#value}-2}" | |
| 30 | - fi | |
| 31 | - fi | |
| 32 | - | |
| 33 | - export "${key}=${value}" | |
| 34 | - done < "${ENV_FILE}" | |
| 35 | -fi | |
| 10 | +# Load .env. | |
| 11 | +# shellcheck source=scripts/lib/load_env.sh | |
| 12 | +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" | |
| 13 | +load_env_file "${PROJECT_ROOT}/.env" | |
| 36 | 14 | |
| 37 | 15 | if ! command -v docker >/dev/null 2>&1; then |
| 38 | 16 | echo "ERROR: docker is required to run TEI service." >&2 | ... | ... |
scripts/stop_clip_service.sh deleted
| ... | ... | @@ -1,49 +0,0 @@ |
| 1 | -#!/bin/bash | |
| 2 | -# | |
| 3 | -# Stop CLIP vector service (clip-as-service) started by start_clip_service.sh | |
| 4 | -# | |
| 5 | -set -e | |
| 6 | - | |
| 7 | -PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" | |
| 8 | -LOG_DIR="${PROJECT_ROOT}/logs" | |
| 9 | -PID_FILE="${LOG_DIR}/clip_service.pid" | |
| 10 | - | |
| 11 | -echo "========================================" | |
| 12 | -echo "Stopping CLIP vector service (clip-as-service)" | |
| 13 | -echo "========================================" | |
| 14 | - | |
| 15 | -if [ ! -f "${PID_FILE}" ]; then | |
| 16 | - echo "No PID file found at ${PID_FILE}." | |
| 17 | - echo "clip-as-service may not be running (or was not started via start_clip_service.sh)." | |
| 18 | - exit 0 | |
| 19 | -fi | |
| 20 | - | |
| 21 | -PID="$(cat "${PID_FILE}")" | |
| 22 | - | |
| 23 | -if [ -z "${PID}" ]; then | |
| 24 | - echo "PID file exists but is empty. Removing it." | |
| 25 | - rm -f "${PID_FILE}" | |
| 26 | - exit 0 | |
| 27 | -fi | |
| 28 | - | |
| 29 | -if ps -p "${PID}" > /dev/null 2>&1; then | |
| 30 | - echo "Sending SIGTERM to clip-as-service (PID ${PID})..." | |
| 31 | - kill "${PID}" || true | |
| 32 | - sleep 1 | |
| 33 | - | |
| 34 | - if ps -p "${PID}" > /dev/null 2>&1; then | |
| 35 | - echo "Process still alive, sending SIGKILL..." | |
| 36 | - kill -9 "${PID}" || true | |
| 37 | - fi | |
| 38 | - | |
| 39 | - echo "clip-as-service (PID ${PID}) has been stopped." | |
| 40 | -else | |
| 41 | - echo "No process with PID ${PID} found. Assuming it's already stopped." | |
| 42 | -fi | |
| 43 | - | |
| 44 | -rm -f "${PID_FILE}" | |
| 45 | -echo "PID file removed: ${PID_FILE}" | |
| 46 | - | |
| 47 | - | |
| 48 | - | |
| 49 | - |
search/es_query_builder.py
| ... | ... | @@ -277,6 +277,17 @@ class ESQueryBuilder: |
| 277 | 277 | "num_candidates": knn_num_candidates, |
| 278 | 278 | "boost": knn_boost |
| 279 | 279 | } |
| 280 | + # Top-level knn does not inherit query.bool.filter automatically. | |
| 281 | + # Apply conjunctive + range filters here so vector recall respects hard filters. | |
| 282 | + if filter_clauses: | |
| 283 | + if len(filter_clauses) == 1: | |
| 284 | + knn_clause["filter"] = filter_clauses[0] | |
| 285 | + else: | |
| 286 | + knn_clause["filter"] = { | |
| 287 | + "bool": { | |
| 288 | + "filter": filter_clauses | |
| 289 | + } | |
| 290 | + } | |
| 280 | 291 | es_query["knn"] = knn_clause |
| 281 | 292 | |
| 282 | 293 | # 5. Add post_filter for disjunctive (multi-select) filters | ... | ... |
search/searcher.py
| ... | ... | @@ -593,11 +593,14 @@ class Searcher: |
| 593 | 593 | if filters or range_filters: |
| 594 | 594 | filter_clauses = self.query_builder._build_filters(filters, range_filters) |
| 595 | 595 | if filter_clauses: |
| 596 | - es_query["query"] = { | |
| 597 | - "bool": { | |
| 598 | - "filter": filter_clauses | |
| 596 | + if len(filter_clauses) == 1: | |
| 597 | + es_query["knn"]["filter"] = filter_clauses[0] | |
| 598 | + else: | |
| 599 | + es_query["knn"]["filter"] = { | |
| 600 | + "bool": { | |
| 601 | + "filter": filter_clauses | |
| 602 | + } | |
| 599 | 603 | } |
| 600 | - } | |
| 601 | 604 | |
| 602 | 605 | # Execute search |
| 603 | 606 | es_response = self.es_client.search( | ... | ... |
| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | +from types import SimpleNamespace | |
| 2 | + | |
| 3 | +import numpy as np | |
| 4 | + | |
| 5 | +from search.es_query_builder import ESQueryBuilder | |
| 6 | + | |
| 7 | + | |
| 8 | +def _builder() -> ESQueryBuilder: | |
| 9 | + return ESQueryBuilder( | |
| 10 | + match_fields=["title.en^3.0", "brief.en^1.0"], | |
| 11 | + text_embedding_field="title_embedding", | |
| 12 | + default_language="en", | |
| 13 | + ) | |
| 14 | + | |
| 15 | + | |
| 16 | +def test_knn_prefilter_includes_range_filters(): | |
| 17 | + qb = _builder() | |
| 18 | + q = qb.build_query( | |
| 19 | + query_text="bags", | |
| 20 | + query_vector=np.array([0.1, 0.2, 0.3]), | |
| 21 | + range_filters={"min_price": {"gte": 50, "lt": 100}}, | |
| 22 | + enable_knn=True, | |
| 23 | + ) | |
| 24 | + | |
| 25 | + assert "knn" in q | |
| 26 | + assert q["knn"]["filter"] == {"range": {"min_price": {"gte": 50, "lt": 100}}} | |
| 27 | + | |
| 28 | + | |
| 29 | +def test_knn_prefilter_uses_only_conjunctive_filters_when_disjunctive_present(): | |
| 30 | + qb = _builder() | |
| 31 | + facets = [SimpleNamespace(field="category_name", disjunctive=True)] | |
| 32 | + q = qb.build_query( | |
| 33 | + query_text="bags", | |
| 34 | + query_vector=np.array([0.1, 0.2, 0.3]), | |
| 35 | + filters={"category_name": ["A", "B"], "vendor": "Nike"}, | |
| 36 | + range_filters={"min_price": {"gte": 50, "lt": 100}}, | |
| 37 | + facet_configs=facets, | |
| 38 | + enable_knn=True, | |
| 39 | + ) | |
| 40 | + | |
| 41 | + assert "knn" in q | |
| 42 | + assert "filter" in q["knn"] | |
| 43 | + knn_filter = q["knn"]["filter"] | |
| 44 | + assert knn_filter == { | |
| 45 | + "bool": { | |
| 46 | + "filter": [ | |
| 47 | + {"term": {"vendor": "Nike"}}, | |
| 48 | + {"range": {"min_price": {"gte": 50, "lt": 100}}}, | |
| 49 | + ] | |
| 50 | + } | |
| 51 | + } | |
| 52 | + assert q["post_filter"] == {"terms": {"category_name": ["A", "B"]}} | |
| 53 | + | |
| 54 | + | |
| 55 | +def test_knn_prefilter_not_added_without_filters(): | |
| 56 | + qb = _builder() | |
| 57 | + q = qb.build_query( | |
| 58 | + query_text="bags", | |
| 59 | + query_vector=np.array([0.1, 0.2, 0.3]), | |
| 60 | + enable_knn=True, | |
| 61 | + ) | |
| 62 | + | |
| 63 | + assert "knn" in q | |
| 64 | + assert "filter" not in q["knn"] | ... | ... |