From 7fbca0d72a278e9acefa525cccb2e01a7de2a8c7 Mon Sep 17 00:00:00 2001 From: tangwang Date: Wed, 11 Mar 2026 19:23:57 +0800 Subject: [PATCH] 启动脚本优化 --- .env.example | 1 + CLIP_SERVICE_README.md | 198 ++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- README.md | 4 ++-- docs/CNCLIP_SERVICE说明文档.md | 5 +---- docs/QUICKSTART.md | 5 ++--- docs/TEI_SERVICE说明文档.md | 2 +- docs/Usage-Guide.md | 8 +++++--- docs/搜索API对接指南.md | 2 +- restart.sh | 5 +---- scripts/frontend_server.py | 2 +- scripts/service_ctl.sh | 42 +++++++++++++----------------------------- scripts/start.sh | 2 +- scripts/start_clip_service.sh | 57 --------------------------------------------------------- scripts/start_embedding_service.sh | 30 ++++-------------------------- scripts/start_frontend.sh | 7 ++++--- scripts/start_reranker.sh | 30 ++++-------------------------- scripts/start_tei_service.sh | 30 ++++-------------------------- scripts/stop_clip_service.sh | 49 ------------------------------------------------- search/es_query_builder.py | 11 +++++++++++ search/searcher.py | 11 +++++++---- tests/test_es_query_builder.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 21 files changed, 135 insertions(+), 430 deletions(-) delete mode 100755 scripts/start_clip_service.sh delete mode 100755 scripts/stop_clip_service.sh create mode 100644 tests/test_es_query_builder.py diff --git a/.env.example b/.env.example index 3a50c45..b0a3bfb 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,7 @@ START_EMBEDDING=0 START_TRANSLATOR=0 START_RERANKER=0 START_TEI=0 +START_CNCLIP=0 # Cache Directory CACHE_DIR=.cache diff --git a/CLIP_SERVICE_README.md b/CLIP_SERVICE_README.md index 0a1f1fa..ba2ca16 100644 --- a/CLIP_SERVICE_README.md +++ b/CLIP_SERVICE_README.md @@ -1,194 +1,12 @@ -## 基于 `clip-server` 的向量服务(平级替代 `embeddings`) +## CLIP 服务说明(已收敛到 CN-CLIP) -本模块说明如何在 **独立环境** 中部署基于 `jina-ai/clip-as-service` 仓库的向量服务(实际安装包为 `clip-server` / `clip-client`),用于替代当前仓库里的本地 `embeddings` 服务(`embeddings/server.py`)。 +原有 `start_clip_service.sh / stop_clip_service.sh` 已移除,避免与 `cnclip` 重名和端口状态混淆。 -> 设计目标: -> - 与项目主环境(`searchengine` conda env)**完全隔离** -> - 使用官方开源项目 [`jina-ai/clip-as-service`](https://github.com/jina-ai/clip-as-service)(对应 PyPI 包:`clip-server` / `clip-client`) -> - 提供简单的 **安装 / 启动 / 停止脚本** - ---- - -## 1. 环境准备(独立环境) - -推荐使用 Conda 新建一个专用环境(与本项目的 `searchengine` 环境隔离): - -```bash -# 1)加载 conda(你的 conda 是 ~/anaconda3/bin/conda → CONDA_ROOT=~/anaconda3) -export CONDA_ROOT=${CONDA_ROOT:-$HOME/anaconda3} # 或你的 Conda 安装路径 -source "$CONDA_ROOT/etc/profile.d/conda.sh" - -# 2)创建 clip 向量服务专用环境 -conda create -n clip_service python=3.9 -y - -# 3)激活环境 -conda activate clip_service - -# 4)安装 clip-server / clip-client(其内部依赖 jina) -# 如需绕过镜像问题,可显式使用官方 PyPI 源: -# pip install -i https://pypi.org/simple "clip-server" "clip-client" -pip install "clip-server" "clip-client" -``` - -> 如果你不使用 Conda,也可以改用 `python -m venv` 创建虚拟环境, -> 但务必保证 **不要与主项目共用同一个 Python 环境**。 - ---- - -## 2. 启动 / 停止脚本 - -本仓库在 `scripts/` 目录下提供了两个脚本(需要手动赋权一次): - -```bash -chmod +x scripts/start_clip_service.sh -chmod +x scripts/stop_clip_service.sh -``` - -### 2.1 启动服务 - -```bash -cd /data/saas-search -./scripts/start_clip_service.sh -``` - -脚本行为: - -- 自动 `cd` 到仓库根目录 -- 尝试加载 `$CONDA_ROOT/etc/profile.d/conda.sh` 并激活 `clip_service` 环境(可通过 `export CONDA_ROOT=...` 适配新机器) -- 使用 `nohup python -m clip_server` 启动服务到后台 -- 将日志写入 `logs/clip_service.log` -- 将进程号写入 `logs/clip_service.pid` - -默认情况下,`clip-server` 会监听在 **`grpc://0.0.0.0:51000`**(gRPC 协议,端口 51000)。 - -> ⚠️ **重要**:客户端连接时请使用端口 **51000**,不是 23456 或其他端口。 - -### 2.2 停止服务 - -```bash -cd /data/saas-search -./scripts/stop_clip_service.sh -``` - -脚本行为: - -- 读取 `logs/clip_service.pid` 中的 PID -- 如果进程存在则发送 `kill` 终止 -- 清理 `logs/clip_service.pid` - ---- - -## 3. 与现有 `embeddings` 服务的关系 - -- 现有本地向量服务: - - 启动脚本:`./scripts/start_embedding_service.sh` - - 实现:`embeddings/server.py`(FastAPI + 本地模型 `qwen3_model.py` / `clip_model.py`) -- 新增的 `clip-server`: - - 使用官方实现,单独进程、单独环境 - - 面向图像 / 文本的 CLIP 向量化服务 - -### 使用建议 - -- 如果你想继续使用本仓库自带的本地模型服务,保持原有脚本不变即可: - - `./scripts/start_embedding_service.sh` -- 如果你想用 `clip-as-service` 替代原来的本地服务,可以: - - 在上游调用代码中,将向量请求切换到 `clip-as-service` 对应的端口 / 接口 - - 或者增加一个适配层,将 `clip-as-service` 封装成与 `POST /embed/text` / `POST /embed/image` 相同的接口(视具体场景而定) - ---- - -## 4. 基本验证 - -1. 确认 `clip_service` 环境创建并安装成功: - - ```bash - export CONDA_ROOT=${CONDA_ROOT:-$HOME/anaconda3} - source "$CONDA_ROOT/etc/profile.d/conda.sh" - conda activate clip_service - python -c "import jina; print('jina version:', jina.__version__)" - ``` - -2. 启动服务并查看日志: - - ```bash - cd /data/saas-search - ./scripts/start_clip_service.sh - tail -f logs/clip_service.log - ``` - - 服务启动后,默认监听在 **`grpc://0.0.0.0:51000`**(gRPC 协议,端口 51000)。 - -3. 测试客户端连接(在 `clip_service` 环境中): - - ```python - from clip_client import Client - - # 注意:默认端口是 51000,不是 23456 - c = Client('grpc://0.0.0.0:51000') - - # 测试连接 - c.profile() - - # 测试文本向量化 - r = c.encode(['First do it', 'then do it right', 'then do it better']) - print(r.shape) # 应该输出 [3, 512] 或类似形状 - - # 测试图像向量化 - r = c.encode(['https://picsum.photos/200']) - print(r.shape) # 应该输出 [1, 512] 或类似形状 - ``` - -4. 如果不再需要服务,执行: - - ```bash - ./scripts/stop_clip_service.sh - ``` - -### 常见问题 - -**Q: 连接被拒绝(Connection refused)?** -A: 请确认: -- 服务已启动(检查 `logs/clip_service.log` 和进程) -- 客户端使用的端口是 **51000**(不是 23456) -- 客户端地址格式正确:`grpc://0.0.0.0:51000` 或 `grpc://localhost:51000` - -**Q: Gateway 启动了但 worker 连接失败?** -A: 可能原因: -- Worker 进程(clip_t)还在启动中,模型加载需要时间(首次启动可能需要下载模型) -- 检查日志中是否有模型下载或加载错误: - ```bash - tail -f logs/clip_service.log | grep -E "(ERROR|WARNING|model|download)" - ``` -- 如果持续失败,尝试重启服务: - ```bash - ./scripts/stop_clip_service.sh - ./scripts/start_clip_service.sh - ``` - -**Q: 如何查看服务实际监听的端口?** -A: 查看启动日志: -```bash -tail -f logs/clip_service.log | grep "bound to" -``` -或检查进程监听的端口: -```bash -lsof -i :51000 -# 或 -netstat -tlnp | grep 51000 -``` - -**Q: 如何确认服务完全就绪?** -A: 查看日志,确认看到类似输出: -``` -INFO gateway/rep-0@XXXXX start server bound to 0.0.0.0:51000 -``` -然后等待几秒让 worker 进程启动,再测试客户端连接。 - ---- - -## 5. 参考 - -- 项目地址:`https://github.com/jina-ai/clip-as-service` -- 本项目向量模块文档:`embeddings/README.md` +当前统一入口: +- 环境:`./scripts/setup_cnclip_venv.sh` +- 启动:`./scripts/start_cnclip_service.sh` +- 停止:`./scripts/stop_cnclip_service.sh` +- 编排:`./scripts/service_ctl.sh start cnclip` +请以 `docs/CNCLIP_SERVICE说明文档.md` 为唯一文档入口。 diff --git a/README.md b/README.md index 8c4ecc7..e6a7641 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ source activate.sh # 启动核心服务(backend/indexer/frontend) ./run.sh -# 可选:附加能力服务 -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 ./run.sh +# 可选:附加能力服务(按需开启) +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh # 查看状态 ./scripts/service_ctl.sh status diff --git a/docs/CNCLIP_SERVICE说明文档.md b/docs/CNCLIP_SERVICE说明文档.md index 8f9c3ab..36c74c1 100644 --- a/docs/CNCLIP_SERVICE说明文档.md +++ b/docs/CNCLIP_SERVICE说明文档.md @@ -70,11 +70,9 @@ cd /data/saas-search ### 6.1 通过统一编排启动 ```bash -START_EMBEDDING=1 START_TEI=1 ./scripts/service_ctl.sh start +START_EMBEDDING=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start ``` -当 `USE_CLIP_AS_SERVICE=true` 且 `START_EMBEDDING=1` 时,`service_ctl` 会自动拉起 `cnclip`。 - ### 6.2 设备选择优先级 - 显式传入 `CNCLIP_DEVICE` 时,以该值为准: @@ -153,4 +151,3 @@ curl -sS -X POST "http://127.0.0.1:6005/embed/image" \ - TEI 专项:`docs/TEI_SERVICE说明文档.md` - 体系规范:`docs/DEVELOPER_GUIDE.md` - embedding 模块:`embeddings/README.md` - diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index fc36e61..d921996 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -66,10 +66,9 @@ source activate.sh ```bash ./run.sh # 启动全部能力 -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh # 等价方式(直接使用服务控制器) -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./scripts/service_ctl.sh start -# 说明:当 USE_CLIP_AS_SERVICE=true(默认)且 START_EMBEDDING=1 时,会自动启动 cnclip(51000) +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start # 说明: # - reranker 为 GPU 强制模式(资源不足会直接启动失败) # - TEI 默认使用 GPU;当 TEI_USE_GPU=1 且 GPU 不可用时会直接失败(不会自动降级到 CPU) diff --git a/docs/TEI_SERVICE说明文档.md b/docs/TEI_SERVICE说明文档.md index dda7dea..103a9fa 100644 --- a/docs/TEI_SERVICE说明文档.md +++ b/docs/TEI_SERVICE说明文档.md @@ -152,7 +152,7 @@ curl -sS -X POST "http://127.0.0.1:6005/embed/text" \ 启动全套(含 TEI): ```bash -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 TEI_USE_GPU=1 ./scripts/service_ctl.sh start +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 TEI_USE_GPU=1 ./scripts/service_ctl.sh start ``` 仅启动 TEI: diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 65ca0de..e4c53d8 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -135,10 +135,10 @@ cd /data/saas-search - **API文档**: http://localhost:6002/docs - **索引API**: http://localhost:6004/docs -可选:全功能模式(同时启动 embedding/translator/reranker/tei): +可选:全功能模式(同时启动 embedding/translator/reranker/tei/cnclip): ```bash -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh +START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh ``` ### 方式2: 统一控制脚本(推荐) @@ -151,7 +151,7 @@ START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh ./scripts/service_ctl.sh start # 启动指定服务 -./scripts/service_ctl.sh start backend indexer frontend translator reranker tei +./scripts/service_ctl.sh start backend indexer frontend translator reranker tei cnclip # 停止全部服务(含可选服务) ./scripts/service_ctl.sh stop @@ -311,6 +311,8 @@ RERANKER_PORT=6007 START_EMBEDDING=0 START_TRANSLATOR=0 START_RERANKER=0 +START_TEI=0 +START_CNCLIP=0 ``` ### 修改配置 diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md index 477d734..d98d811 100644 --- a/docs/搜索API对接指南.md +++ b/docs/搜索API对接指南.md @@ -1726,7 +1726,7 @@ curl "http://localhost:6005/health" ```bash ./scripts/start_tei_service.sh -START_TEI=0 ./scripts/service_ctl.sh restart embedding +./scripts/service_ctl.sh restart embedding ``` 默认端口: diff --git a/restart.sh b/restart.sh index d513584..cf2d62a 100755 --- a/restart.sh +++ b/restart.sh @@ -4,7 +4,4 @@ cd "$(dirname "$0")" -START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 CNCLIP_DEVICE=cuda TEI_USE_GPU=1 ./scripts/service_ctl.sh restart - -# ./scripts/service_ctl.sh restart - +./scripts/service_ctl.sh restart diff --git a/scripts/frontend_server.py b/scripts/frontend_server.py index b2e8008..1c44e8b 100755 --- a/scripts/frontend_server.py +++ b/scripts/frontend_server.py @@ -27,7 +27,7 @@ frontend_dir = os.path.join(os.path.dirname(__file__), '../frontend') os.chdir(frontend_dir) # Get port from environment variable or default -PORT = int(os.getenv('PORT', 6003)) +PORT = int(os.getenv('FRONTEND_PORT', 6003)) # Configure logging to suppress scanner noise logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') diff --git a/scripts/service_ctl.sh b/scripts/service_ctl.sh index 6593349..8d99aae 100755 --- a/scripts/service_ctl.sh +++ b/scripts/service_ctl.sh @@ -15,11 +15,10 @@ mkdir -p "${LOG_DIR}" source "${PROJECT_ROOT}/scripts/lib/load_env.sh" CORE_SERVICES=("backend" "indexer" "frontend") -OPTIONAL_SERVICES=("embedding" "translator" "reranker" "tei") -LEGACY_SERVICES=("clip" "cnclip") +OPTIONAL_SERVICES=("embedding" "translator" "reranker" "tei" "cnclip") all_services() { - echo "${CORE_SERVICES[@]} ${OPTIONAL_SERVICES[@]} ${LEGACY_SERVICES[@]}" + echo "${CORE_SERVICES[@]} ${OPTIONAL_SERVICES[@]}" } get_port() { @@ -32,7 +31,6 @@ get_port() { translator) echo "${TRANSLATION_PORT:-${TRANSLATOR_PORT:-6006}}" ;; reranker) echo "${RERANKER_PORT:-6007}" ;; tei) echo "${TEI_PORT:-8080}" ;; - clip) echo "${CLIP_PORT:-51000}" ;; cnclip) echo "${CNCLIP_PORT:-51000}" ;; *) echo "" ;; esac @@ -41,7 +39,6 @@ get_port() { pid_file() { local service="$1" case "${service}" in - clip) echo "${LOG_DIR}/clip_service.pid" ;; cnclip) echo "${LOG_DIR}/cnclip_service.pid" ;; *) echo "${LOG_DIR}/${service}.pid" ;; esac @@ -62,7 +59,6 @@ service_start_cmd() { translator) echo "./scripts/start_translator.sh" ;; reranker) echo "./scripts/start_reranker.sh" ;; tei) echo "./scripts/start_tei_service.sh" ;; - clip) echo "./scripts/start_clip_service.sh" ;; cnclip) echo "./scripts/start_cnclip_service.sh" ;; *) return 1 ;; esac @@ -158,7 +154,10 @@ start_one() { local service="$1" cd "${PROJECT_ROOT}" local cmd - cmd="$(service_start_cmd "${service}")" + if ! cmd="$(service_start_cmd "${service}")"; then + echo "[error] unknown service: ${service}" >&2 + return 1 + fi local pf lf pf="$(pid_file "${service}")" lf="$(log_file "${service}")" @@ -186,7 +185,7 @@ start_one() { fi case "${service}" in - clip|cnclip|tei) + cnclip|tei) echo "[start] ${service} (managed by native script)" if [ "${service}" = "cnclip" ]; then CNCLIP_DEVICE="${CNCLIP_DEVICE:-cuda}" bash -lc "${cmd}" >> "${lf}" 2>&1 @@ -248,11 +247,6 @@ start_one() { stop_one() { local service="$1" cd "${PROJECT_ROOT}" - if [ "${service}" = "clip" ]; then - echo "[stop] clip (managed by native script)" - bash -lc "./scripts/stop_clip_service.sh" || true - return 0 - fi if [ "${service}" = "cnclip" ]; then echo "[stop] cnclip (managed by native script)" bash -lc "./scripts/stop_cnclip_service.sh" || true @@ -347,17 +341,9 @@ resolve_targets() { case "${scope}" in start) local targets=("${CORE_SERVICES[@]}") - # Start TEI before embedding when both are enabled, because embedding - # tei backend performs strict startup health checks against TEI. if [ "${START_TEI:-0}" = "1" ]; then targets+=("tei"); fi - if [ "${START_EMBEDDING:-0}" = "1" ]; then - local use_clip="${USE_CLIP_AS_SERVICE:-true}" - use_clip="$(echo "${use_clip}" | tr '[:upper:]' '[:lower:]')" - if [[ "${use_clip}" == "1" || "${use_clip}" == "true" || "${use_clip}" == "yes" ]]; then - targets+=("cnclip") - fi - targets+=("embedding") - fi + if [ "${START_CNCLIP:-0}" = "1" ]; then targets+=("cnclip"); fi + if [ "${START_EMBEDDING:-0}" = "1" ]; then targets+=("embedding"); fi if [ "${START_TRANSLATOR:-0}" = "1" ]; then targets+=("translator"); fi if [ "${START_RERANKER:-0}" = "1" ]; then targets+=("reranker"); fi echo "${targets[@]}" @@ -366,8 +352,7 @@ resolve_targets() { echo "$(all_services)" ;; restart) - # Restart with no explicit services should preserve start-order dependency - # behavior (e.g. tei/cnclip before embedding). + # Restart with no explicit services uses the same explicit start target order. echo "$(resolve_targets start)" ;; *) @@ -391,9 +376,8 @@ Default target set (when no service provided): status -> all known services Optional startup flags: - START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh - START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./scripts/service_ctl.sh start - # when USE_CLIP_AS_SERVICE=true (default), START_EMBEDDING=1 will auto-start cnclip + START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh + START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./scripts/service_ctl.sh start CNCLIP_DEVICE=cuda|cpu ./scripts/service_ctl.sh start cnclip EOF } @@ -411,7 +395,7 @@ main() { local stop_targets="" local targets # For restart without explicit services, stop everything first, then start - # with dependency-aware start targets. + # with the default start target order. if [ "${action}" = "restart" ] && [ "$#" -eq 0 ]; then stop_targets="$(resolve_targets stop)" fi diff --git a/scripts/start.sh b/scripts/start.sh index a1c4158..e7184cf 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -12,7 +12,7 @@ echo "saas-search 服务启动" echo "========================================" echo "默认启动核心服务: backend/indexer/frontend" echo "可选服务通过环境变量开启:" -echo " START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 ./run.sh" +echo " START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 START_TEI=1 START_CNCLIP=1 ./run.sh" echo ./scripts/service_ctl.sh start diff --git a/scripts/start_clip_service.sh b/scripts/start_clip_service.sh deleted file mode 100755 index 45c1786..0000000 --- a/scripts/start_clip_service.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash -# -# Start CLIP vector service (clip-server) in an independent environment. -# -# This service is designed to be a drop-in alternative to the local -# `embeddings` service, but runs in its own Python environment and depends -# on `jina` via `clip-server`. -# -set -e - -cd "$(dirname "$0")/.." - -LOG_DIR="$(pwd)/logs" -mkdir -p "${LOG_DIR}" -PID_FILE="${LOG_DIR}/clip_service.pid" -LOG_FILE="${LOG_DIR}/clip_service.log" - -echo "========================================" -echo "Starting CLIP vector service (clip-server)" -echo "========================================" - -# Force isolated CN-CLIP runtime env -CLIP_VENV="$(pwd)/.venv-cnclip" -if [ ! -x "${CLIP_VENV}/bin/python" ]; then - echo "Error: isolated clip runtime not found: ${CLIP_VENV}" >&2 - echo "Please run: ./scripts/setup_cnclip_venv.sh" >&2 - exit 1 -fi -PYTHON_BIN="${CLIP_VENV}/bin/python" - -if [ -f "${PID_FILE}" ]; then - EXISTING_PID="$(cat "${PID_FILE}")" - if ps -p "${EXISTING_PID}" > /dev/null 2>&1; then - echo "clip-server already appears to be running with PID ${EXISTING_PID}." - echo "If this is incorrect, remove ${PID_FILE} and try again." - exit 0 - else - echo "Stale PID file found at ${PID_FILE}, removing..." - rm -f "${PID_FILE}" - fi -fi - -echo "Log file: ${LOG_FILE}" -echo "PID file: ${PID_FILE}" -echo -echo "Starting clip-server in background..." - -nohup "${PYTHON_BIN}" -m clip_server > "${LOG_FILE}" 2>&1 & -SERVICE_PID=$! -echo "${SERVICE_PID}" > "${PID_FILE}" - -echo "clip-server started with PID ${SERVICE_PID}." -echo "You can check logs with:" -echo " tail -f ${LOG_FILE}" - - - diff --git a/scripts/start_embedding_service.sh b/scripts/start_embedding_service.sh index 56a3d34..67d49f3 100755 --- a/scripts/start_embedding_service.sh +++ b/scripts/start_embedding_service.sh @@ -21,32 +21,10 @@ if [[ ! -x "${PYTHON_BIN}" ]]; then exit 1 fi -# Load .env if present (same behavior as activate.sh, without activating main venv) -ENV_FILE="${PROJECT_ROOT}/.env" -if [ -f "${ENV_FILE}" ]; then - while IFS= read -r line || [ -n "${line}" ]; do - line="${line%$'\r'}" - [[ -z "${line//[[:space:]]/}" ]] && continue - [[ "${line}" =~ ^[[:space:]]*# ]] && continue - [[ "${line}" != *=* ]] && continue - - key="${line%%=*}" - value="${line#*=}" - key="${key#"${key%%[![:space:]]*}"}" - key="${key%"${key##*[![:space:]]}"}" - value="${value#"${value%%[![:space:]]*}"}" - - if [[ ${#value} -ge 2 ]]; then - first="${value:0:1}" - last="${value: -1}" - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then - value="${value:1:${#value}-2}" - fi - fi - - export "${key}=${value}" - done < "${ENV_FILE}" -fi +# Load .env without activating main venv. +# shellcheck source=scripts/lib/load_env.sh +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" +load_env_file "${PROJECT_ROOT}/.env" DEFAULT_EMBEDDING_SERVICE_HOST=$("${PYTHON_BIN}" -c "from embeddings.config import CONFIG; print(CONFIG.HOST)") DEFAULT_EMBEDDING_SERVICE_PORT=$("${PYTHON_BIN}" -c "from embeddings.config import CONFIG; print(CONFIG.PORT)") diff --git a/scripts/start_frontend.sh b/scripts/start_frontend.sh index a5ce8c0..5abe75f 100755 --- a/scripts/start_frontend.sh +++ b/scripts/start_frontend.sh @@ -15,13 +15,14 @@ echo -e "${GREEN}========================================${NC}" echo -e "${GREEN}Starting Frontend Server${NC}" echo -e "${GREEN}========================================${NC}" -PORT=6003 +FRONTEND_PORT="${FRONTEND_PORT:-6003}" +API_PORT="${API_PORT:-6002}" echo -e "\n${YELLOW}Frontend will be available at:${NC}" -echo -e " ${GREEN}http://localhost:$PORT${NC}" +echo -e " ${GREEN}http://localhost:${FRONTEND_PORT}${NC}" echo "" echo -e "${YELLOW}Make sure the backend API is running at:${NC}" -echo -e " ${GREEN}http://localhost:6002${NC}" +echo -e " ${GREEN}http://localhost:${API_PORT}${NC}" echo "" python scripts/frontend_server.py diff --git a/scripts/start_reranker.sh b/scripts/start_reranker.sh index 4748ce4..9d58998 100755 --- a/scripts/start_reranker.sh +++ b/scripts/start_reranker.sh @@ -16,32 +16,10 @@ if [[ ! -x "${PYTHON_BIN}" ]]; then exit 1 fi -# Load .env if present (without activating main venv) -ENV_FILE="${PROJECT_ROOT}/.env" -if [ -f "${ENV_FILE}" ]; then - while IFS= read -r line || [ -n "${line}" ]; do - line="${line%$'\r'}" - [[ -z "${line//[[:space:]]/}" ]] && continue - [[ "${line}" =~ ^[[:space:]]*# ]] && continue - [[ "${line}" != *=* ]] && continue - - key="${line%%=*}" - value="${line#*=}" - key="${key#"${key%%[![:space:]]*}"}" - key="${key%"${key##*[![:space:]]}"}" - value="${value#"${value%%[![:space:]]*}"}" - - if [[ ${#value} -ge 2 ]]; then - first="${value:0:1}" - last="${value: -1}" - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then - value="${value:1:${#value}-2}" - fi - fi - - export "${key}=${value}" - done < "${ENV_FILE}" -fi +# Load .env without activating main venv. +# shellcheck source=scripts/lib/load_env.sh +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" +load_env_file "${PROJECT_ROOT}/.env" RERANKER_HOST="${RERANKER_HOST:-0.0.0.0}" RERANKER_PORT="${RERANKER_PORT:-6007}" diff --git a/scripts/start_tei_service.sh b/scripts/start_tei_service.sh index ea3124e..aaea508 100755 --- a/scripts/start_tei_service.sh +++ b/scripts/start_tei_service.sh @@ -7,32 +7,10 @@ set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "${PROJECT_ROOT}" -# Load .env if present -ENV_FILE="${PROJECT_ROOT}/.env" -if [ -f "${ENV_FILE}" ]; then - while IFS= read -r line || [ -n "${line}" ]; do - line="${line%$'\r'}" - [[ -z "${line//[[:space:]]/}" ]] && continue - [[ "${line}" =~ ^[[:space:]]*# ]] && continue - [[ "${line}" != *=* ]] && continue - - key="${line%%=*}" - value="${line#*=}" - key="${key#"${key%%[![:space:]]*}"}" - key="${key%"${key##*[![:space:]]}"}" - value="${value#"${value%%[![:space:]]*}"}" - - if [[ ${#value} -ge 2 ]]; then - first="${value:0:1}" - last="${value: -1}" - if [[ ("${first}" == '"' && "${last}" == '"') || ("${first}" == "'" && "${last}" == "'") ]]; then - value="${value:1:${#value}-2}" - fi - fi - - export "${key}=${value}" - done < "${ENV_FILE}" -fi +# Load .env. +# shellcheck source=scripts/lib/load_env.sh +source "${PROJECT_ROOT}/scripts/lib/load_env.sh" +load_env_file "${PROJECT_ROOT}/.env" if ! command -v docker >/dev/null 2>&1; then echo "ERROR: docker is required to run TEI service." >&2 diff --git a/scripts/stop_clip_service.sh b/scripts/stop_clip_service.sh deleted file mode 100755 index 3110f99..0000000 --- a/scripts/stop_clip_service.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# -# Stop CLIP vector service (clip-as-service) started by start_clip_service.sh -# -set -e - -PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -LOG_DIR="${PROJECT_ROOT}/logs" -PID_FILE="${LOG_DIR}/clip_service.pid" - -echo "========================================" -echo "Stopping CLIP vector service (clip-as-service)" -echo "========================================" - -if [ ! -f "${PID_FILE}" ]; then - echo "No PID file found at ${PID_FILE}." - echo "clip-as-service may not be running (or was not started via start_clip_service.sh)." - exit 0 -fi - -PID="$(cat "${PID_FILE}")" - -if [ -z "${PID}" ]; then - echo "PID file exists but is empty. Removing it." - rm -f "${PID_FILE}" - exit 0 -fi - -if ps -p "${PID}" > /dev/null 2>&1; then - echo "Sending SIGTERM to clip-as-service (PID ${PID})..." - kill "${PID}" || true - sleep 1 - - if ps -p "${PID}" > /dev/null 2>&1; then - echo "Process still alive, sending SIGKILL..." - kill -9 "${PID}" || true - fi - - echo "clip-as-service (PID ${PID}) has been stopped." -else - echo "No process with PID ${PID} found. Assuming it's already stopped." -fi - -rm -f "${PID_FILE}" -echo "PID file removed: ${PID_FILE}" - - - - diff --git a/search/es_query_builder.py b/search/es_query_builder.py index ae8d0ad..8a5edd8 100644 --- a/search/es_query_builder.py +++ b/search/es_query_builder.py @@ -277,6 +277,17 @@ class ESQueryBuilder: "num_candidates": knn_num_candidates, "boost": knn_boost } + # Top-level knn does not inherit query.bool.filter automatically. + # Apply conjunctive + range filters here so vector recall respects hard filters. + if filter_clauses: + if len(filter_clauses) == 1: + knn_clause["filter"] = filter_clauses[0] + else: + knn_clause["filter"] = { + "bool": { + "filter": filter_clauses + } + } es_query["knn"] = knn_clause # 5. Add post_filter for disjunctive (multi-select) filters diff --git a/search/searcher.py b/search/searcher.py index 718a42e..1d7a1ac 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -593,11 +593,14 @@ class Searcher: if filters or range_filters: filter_clauses = self.query_builder._build_filters(filters, range_filters) if filter_clauses: - es_query["query"] = { - "bool": { - "filter": filter_clauses + if len(filter_clauses) == 1: + es_query["knn"]["filter"] = filter_clauses[0] + else: + es_query["knn"]["filter"] = { + "bool": { + "filter": filter_clauses + } } - } # Execute search es_response = self.es_client.search( diff --git a/tests/test_es_query_builder.py b/tests/test_es_query_builder.py new file mode 100644 index 0000000..82d940b --- /dev/null +++ b/tests/test_es_query_builder.py @@ -0,0 +1,64 @@ +from types import SimpleNamespace + +import numpy as np + +from search.es_query_builder import ESQueryBuilder + + +def _builder() -> ESQueryBuilder: + return ESQueryBuilder( + match_fields=["title.en^3.0", "brief.en^1.0"], + text_embedding_field="title_embedding", + default_language="en", + ) + + +def test_knn_prefilter_includes_range_filters(): + qb = _builder() + q = qb.build_query( + query_text="bags", + query_vector=np.array([0.1, 0.2, 0.3]), + range_filters={"min_price": {"gte": 50, "lt": 100}}, + enable_knn=True, + ) + + assert "knn" in q + assert q["knn"]["filter"] == {"range": {"min_price": {"gte": 50, "lt": 100}}} + + +def test_knn_prefilter_uses_only_conjunctive_filters_when_disjunctive_present(): + qb = _builder() + facets = [SimpleNamespace(field="category_name", disjunctive=True)] + q = qb.build_query( + query_text="bags", + query_vector=np.array([0.1, 0.2, 0.3]), + filters={"category_name": ["A", "B"], "vendor": "Nike"}, + range_filters={"min_price": {"gte": 50, "lt": 100}}, + facet_configs=facets, + enable_knn=True, + ) + + assert "knn" in q + assert "filter" in q["knn"] + knn_filter = q["knn"]["filter"] + assert knn_filter == { + "bool": { + "filter": [ + {"term": {"vendor": "Nike"}}, + {"range": {"min_price": {"gte": 50, "lt": 100}}}, + ] + } + } + assert q["post_filter"] == {"terms": {"category_name": ["A", "B"]}} + + +def test_knn_prefilter_not_added_without_filters(): + qb = _builder() + q = qb.build_query( + query_text="bags", + query_vector=np.array([0.1, 0.2, 0.3]), + enable_knn=True, + ) + + assert "knn" in q + assert "filter" not in q["knn"] -- libgit2 0.21.2