Commit 7fbca0d72a278e9acefa525cccb2e01a7de2a8c7

Authored by tangwang
1 parent 02c40701

启动脚本优化

.env.example
... ... @@ -48,6 +48,7 @@ START_EMBEDDING=0
48 48 START_TRANSLATOR=0
49 49 START_RERANKER=0
50 50 START_TEI=0
  51 +START_CNCLIP=0
51 52  
52 53 # Cache Directory
53 54 CACHE_DIR=.cache
... ...
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
... ... @@ -4,7 +4,4 @@
4 4  
5 5 cd "$(dirname "$0")"
6 6  
7   -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 CNCLIP_DEVICE=cuda TEI_USE_GPU=1 ./scripts/service_ctl.sh restart
8   -
9   -# ./scripts/service_ctl.sh restart
10   -
  7 +./scripts/service_ctl.sh restart
... ...
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 &quot;${GREEN}========================================${NC}&quot;
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 &quot;${PYTHON_BIN}&quot; ]]; 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(
... ...
tests/test_es_query_builder.py 0 → 100644
... ... @@ -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"]
... ...