From 2e3670ab2404580769ceedac7154de7144b2ebde Mon Sep 17 00:00:00 2001 From: tangwang Date: Sun, 8 Mar 2026 22:41:44 +0800 Subject: [PATCH] index services --- .env.example | 19 ++++++++++--------- CLAUDE.md | 2 +- README.md | 2 +- docs/DEVELOPER_GUIDE.md | 6 +++--- docs/QUICKSTART.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- docs/Usage-Guide.md | 2 +- docs/搜索API对接指南.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- docs/环境配置说明.md | 187 ++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tests/ci/test_service_api_contracts.py | 195 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 400 insertions(+), 209 deletions(-) diff --git a/.env.example b/.env.example index f5db7c2..a2d6ea2 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ # Environment Configuration Template -# Copy this file to .env and update with your actual values +# Copy this file to .env and update with your actual values. +# 生产/测试凭证与远程登录方式见 docs/QUICKSTART.md §1.6 -# Elasticsearch Configuration (v8.18) +# Elasticsearch (生产默认 10.200.16.14:9200,本地可用 localhost) ES_HOST=http://localhost:9200 -ES_USERNAME= +ES_USERNAME=saas ES_PASSWORD= -# Redis Configuration (for caching) +# Redis (生产默认 10.200.16.14:6479) REDIS_HOST=localhost REDIS_PORT=6479 REDIS_PASSWORD= @@ -43,9 +44,9 @@ IMAGE_MODEL_DIR=/data/tw/models/cn-clip # 已经改为web请求了,不使用 # Cache Directory CACHE_DIR=.cache -# MySQL Database Configuration (Shoplazza) -DB_HOST= -DB_PORT=3306 -DB_DATABASE= -DB_USERNAME= +# MySQL (Shoplazza,生产默认 10.200.16.14:3316) +DB_HOST=localhost +DB_PORT=3316 +DB_DATABASE=saas +DB_USERNAME=saas DB_PASSWORD= diff --git a/CLAUDE.md b/CLAUDE.md index a60aa3d..c5e6f3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ This is a **production-ready Multi-Tenant E-Commerce Search SaaS** platform spec # Optional on new machine: if conda is ~/anaconda3/bin/conda → export CONDA_ROOT=$HOME/anaconda3 source activate.sh ``` -See `docs/环境配置说明.md` for first-time env creation (`conda env create -f environment.yml` or `pip install -r requirements.txt`). +See `docs/QUICKSTART.md` §1.4–1.8 for first-time env creation and production credentials (venv: `./scripts/create_venv.sh`; conda: `conda env create -f environment.yml` or `pip install -r requirements.txt`). **Database Configuration:** ```yaml diff --git a/README.md b/README.md index 46acc12..f7d6eb3 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ START_EMBEDDING=1 START_TRANSLATOR=1 START_RERANKER=1 ./run.sh | 2. 运行与排障 | `docs/Usage-Guide.md` | | 3. API 详细说明 | `docs/搜索API对接指南.md` | | 4. 快速参数速查 | `docs/搜索API速查表.md` | -| 5. 首次环境搭建 | `docs/环境配置说明.md` | +| 5. 首次环境搭建、生产凭证 | `docs/QUICKSTART.md` §1.4–1.8 | --- diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 41e47c9..be9c7b7 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -48,7 +48,7 @@ - [系统设计文档.md](./系统设计文档.md) — 索引结构、数据流、通用化设计 - [基础配置指南.md](./基础配置指南.md) — 索引与查询配置说明 - [搜索API对接指南.md](./搜索API对接指南.md) — 搜索/索引/管理接口完整说明 -- [环境配置说明.md](./环境配置说明.md) — 首次部署、新机器环境 +- [QUICKSTART.md](./QUICKSTART.md) §1.4–1.8 — 系统要求、Python 环境、外部服务与生产凭证、店匠数据源(原环境配置说明已并入) - [Usage-Guide.md](./Usage-Guide.md) — 运维、日志、多环境、故障排查 --- @@ -258,7 +258,7 @@ services: - 能力选择:`TRANSLATION_PROVIDER`、`EMBEDDING_PROVIDER`、`RERANK_PROVIDER`、`RERANK_BACKEND` - 环境与索引:`ES_HOST`、`ES_INDEX_NAMESPACE`、`RUNTIME_ENV`、DB 与 Redis 等 -详见 [环境配置说明.md](./环境配置说明.md)、[Usage-Guide.md](./Usage-Guide.md)。 +详见 [QUICKSTART.md](./QUICKSTART.md) §1.6(.env 与生产凭证)、[Usage-Guide.md](./Usage-Guide.md)。 --- @@ -370,7 +370,7 @@ services: | 索引与查询配置说明 | [基础配置指南.md](./基础配置指南.md) | | 搜索/索引 API 完整说明 | [搜索API对接指南.md](./搜索API对接指南.md) | | 搜索 API 参数速查 | [搜索API速查表.md](./搜索API速查表.md) | -| 首次部署、新机器环境 | [环境配置说明.md](./环境配置说明.md) | +| 首次部署、新机器环境、生产凭证 | [QUICKSTART.md](./QUICKSTART.md) §1.4–1.8 | | 运维、日志、多环境、故障 | [Usage-Guide.md](./Usage-Guide.md) | | 索引模块职责与 Java 对接 | [indexer/README.md](../indexer/README.md) | | 向量模块与 clip-as-service | [embeddings/README.md](../embeddings/README.md) | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ca0fe10..fa77acc 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -22,6 +22,7 @@ ## 目录 1. [快速上手](#1-快速上手) + - [1.1 环境准备](#11-环境准备) / [1.2 服务与端口](#12-服务与端口) / [1.3 常用 API](#13-常用-api-请求示例) / [1.4 系统要求](#14-系统要求) / [1.5 Python 运行环境](#15-python-运行环境详细) / [1.6 外部服务与 .env](#16-外部服务与-env含生产凭证) / [1.7 店匠数据源](#17-店匠数据源说明) / [1.8 相关脚本](#18-相关脚本) / [1.9 配置入口总览](#19-配置入口总览) 2. [基础配置与搜索行为](#2-基础配置与搜索行为) 3. [Provider 架构](#3-provider-架构) 4. [模块扩展规范(Embedding / Rerank)](#4-模块扩展规范embedding--rerank) @@ -37,10 +38,8 @@ ```bash source activate.sh -# 首次推荐: +# 首次创建环境: ./scripts/create_venv.sh -# 或使用 conda: -# conda env create -f environment.yml ``` 依赖:Python 3.8+、Elasticsearch 8.x、MySQL、Redis(可选,缓存用途)。 @@ -52,7 +51,7 @@ INSTALL_ML=1 ./scripts/create_venv.sh source activate.sh ``` -详细环境说明见 `docs/环境配置说明.md`。 +更详细的系统要求、Python 环境、外部服务与生产凭证见 [1.4–1.8](#14-系统要求)。 ### 1.2 服务与端口 @@ -162,7 +161,134 @@ curl -X POST http://localhost:6007/rerank \ -d '{"query":"wireless mouse","docs":["logitech mx master","usb cable"]}' ``` -### 1.4 配置入口总览 +### 1.4 系统要求 + +- **操作系统**:Linux(推荐 Ubuntu 18.04+) +- **Python**:3.10(由 venv 提供) +- **内存**:建议 8GB+(含模型与 ES) +- **磁盘**:10GB+(含模型与索引) + +### 1.5 Python 运行环境(详细) + +项目根目录的 `activate.sh` 激活 **`.venv`** 并加载当前目录 `.env`。 + +```bash +cd /data/saas-search +./scripts/create_venv.sh +source activate.sh +``` + +如需运行本地 embedding / 图像编码(torch/transformers 等): + +```bash +INSTALL_ML=1 ./scripts/create_venv.sh +source activate.sh +``` + +### 1.6 外部服务与 .env(含生产凭证) + +以下为 **AI 生产环境** 统一使用的地址与凭证(Redis / ES / MySQL 均以此为准)。本地开发可将 `DB_HOST`/`ES_HOST`/`REDIS_HOST` 改为 `localhost`(服务在本机时)。 + +| 服务 | 地址(生产) | 端口 | 说明 | +|------|--------------|------|------| +| **MySQL** | 10.200.16.14 / localhost | 3316 | 店匠 SPU/SKU 数据 | +| **Redis** | 10.200.16.14 / localhost | 6479 | Embedding/翻译缓存 | +| **Elasticsearch** | 10.200.16.14 / localhost | 9200 | 搜索索引 | + +**MySQL**(3 个用户均可远程登录): + +| 用户 | 密码 | +|------|------| +| root | qY8tgodLoA&KT#yQ | +| saas | 6dlpco6dVGuqzt^l | +| sa | C#HU!GPps7ck8tsM | + +创建远程用户(如尚未创建): + +```sql +mysql -uroot -p'qY8tgodLoA&KT#yQ' +CREATE USER 'saas'@'%' IDENTIFIED BY '6dlpco6dVGuqzt^l'; +CREATE USER 'sa'@'%' IDENTIFIED BY 'C#HU!GPps7ck8tsM'; +``` + +**Redis**: + +- 密码:`dxEkegEZ@C5SXWKv` +- 远程登录示例:`redis-cli -h 43.166.252.75 -p 6479`(需带 `-a` 传密码时按 redis-cli 文档操作) + +**Elasticsearch**: + +- 用户名/密码:`saas` / `4hOaLaf41y2VuI8y` +- 访问示例: + +```bash +curl -u 'saas:4hOaLaf41y2VuI8y' \ + -X GET 'http://localhost:9200/search_products_tenant_111/_search?pretty' \ + -H 'Content-Type: application/json' \ + -d '{ + "size": 11, + "_source": ["title"], + "query": { + "bool": { + "filter": [ + { "term": {"spu_id" : 206150} } + ] + } + } + }' +``` + +在项目根目录创建 `.env`(可复制 `.env.example` 后按环境修改): + +```env +# MySQL(生产以 10.200.16.14 或 localhost 为准) +DB_HOST=10.200.16.14 +DB_PORT=3316 +DB_DATABASE=saas +DB_USERNAME=saas +DB_PASSWORD=6dlpco6dVGuqzt^l + +# Elasticsearch +ES_HOST=http://10.200.16.14:9200 +ES_USERNAME=saas +ES_PASSWORD=4hOaLaf41y2VuI8y + +# Redis(可选) +REDIS_HOST=10.200.16.14 +REDIS_PORT=6479 +REDIS_PASSWORD=dxEkegEZ@C5SXWKv + +# DeepL 翻译(按需) +DEEPL_AUTH_KEY=your-key + +# API +API_HOST=0.0.0.0 +API_PORT=6002 +``` + +> 生产环境请妥善保管凭证;本地/测试可改用上述值或自建实例。 + +### 1.7 店匠数据源说明 + +saas-search 以 MySQL 中的店匠标准表为权威数据源: + +- `shoplazza_product_spu`:SPU 商品主表 +- `shoplazza_product_sku`:SKU 变体表 + +**shoplazza_product_sku 字段节选**:`id`, `spu_id`, `shop_id`, `title`, `sku`, `price`, `compare_at_price`, `option1/2/3`, `inventory_quantity`, `image_src`, `tenant_id`, `create_time`, `update_time`, `deleted` 等。完整字段与 ES 对应关系见 `INDEX_FIELDS_DOCUMENTATION.md`(若有)。 + +### 1.8 相关脚本 + +- **`activate.sh`**(项目根目录):激活 Python 环境并加载 `.env`,日常开发/部署以本脚本为准。 +- `scripts/create_venv.sh`:创建 venv(可选 `INSTALL_ML=1` 安装 ML 依赖) +- `scripts/mock_data.sh`:生成 Tenant1 Mock + Tenant2 CSV 并导入 MySQL +- `scripts/create_tenant_index.sh `:创建租户 ES 索引结构 +- `POST /indexer/reindex`:从 MySQL 全量导入到 ES +- `run.sh` / `scripts/stop.sh`:服务启停;`scripts/service_ctl.sh`:start/stop/restart/status + +更多脚本与验证命令见 `docs/Usage-Guide.md`。 + +### 1.9 配置入口总览 - **搜索行为配置**:`config/config.yaml` - **索引结构定义**:`mappings/search_products.json` diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 8b985e4..0d10365 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -29,7 +29,7 @@ #### 1. 安装 Python 依赖与激活环境 -**推荐**:使用项目根目录的 `activate.sh` 激活环境(会加载 `.env`)。目前推荐 venv(`.venv`);Conda 仅作为兼容回退(需要 `CONDA_ROOT`)。详见 `docs/环境配置说明.md`。 +**推荐**:使用项目根目录的 `activate.sh` 激活环境(会加载 `.env`)。目前推荐 venv(`.venv`);Conda 仅作为兼容回退(需要 `CONDA_ROOT`)。系统要求、Python 环境、生产凭证与 `.env` 模板见 [QUICKSTART.md](./QUICKSTART.md) §1.4–1.8。 ```bash cd /data/saas-search diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md index a588cec..a2440d8 100644 --- a/docs/搜索API对接指南.md +++ b/docs/搜索API对接指南.md @@ -37,6 +37,8 @@ - 5.2 [增量索引接口](#52-增量索引接口) - 5.3 [查询文档接口](#53-查询文档接口) - 5.4 [索引健康检查接口](#54-索引健康检查接口) + - 5.5 [文档构建接口(正式对接)](#55-文档构建接口正式对接推荐) + - 5.6 [文档构建接口(测试/自测)](#56-文档构建接口测试--自测) 6. [管理接口](#管理接口) - 6.1 [健康检查](#61-健康检查) @@ -863,6 +865,17 @@ curl "http://localhost:6002/search/12345" -H "X-Tenant-ID: 162" ## 索引接口 +本节内容与 `api/routes/indexer.py` 中的索引相关服务一致,包含以下接口: + +| 接口 | 方法 | 路径 | 说明 | +|------|------|------|------| +| 全量重建索引 | POST | `/indexer/reindex` | 将指定租户所有 SPU 导入 ES(不删现有索引) | +| 增量索引 | POST | `/indexer/index` | 按 SPU ID 列表索引/删除,支持自动检测删除与显式删除 | +| 查询文档 | POST | `/indexer/documents` | 按 SPU ID 列表查询 ES 文档,不写入 ES | +| 构建 ES 文档(正式) | POST | `/indexer/build-docs` | 由上游提供 MySQL 行数据,返回 ES-ready 文档,不写 ES | +| 构建 ES 文档(测试) | POST | `/indexer/build-docs-from-db` | 由本服务查库并构建文档,仅测试/调试用 | +| 索引健康检查 | GET | `/indexer/health` | 检查索引服务与数据库连接状态 | + ### 5.0 为租户创建索引 为租户创建索引需要两个步骤: @@ -1259,7 +1272,7 @@ curl -X POST "http://localhost:6004/indexer/documents" \ ### 5.4 索引健康检查接口 - **端点**: `GET /indexer/health` -- **描述**: 检查索引服务的健康状态 +- **描述**: 检查索引服务健康状态(与 `api/routes/indexer.py` 中 `indexer_health_check` 一致) #### 响应格式 @@ -1273,6 +1286,18 @@ curl -X POST "http://localhost:6004/indexer/documents" \ } ``` +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | string | `available`(服务可用)、`unavailable`(未初始化)、`error`(异常) | +| `database` | string | 数据库连接状态,如 `connected` 或 `disconnected: ...` | +| `preloaded_data.category_mappings` | integer | 已加载的分类映射数量 | + +#### 请求示例 + +```bash +curl -X GET "http://localhost:6004/indexer/health" +``` + ### 5.5 文档构建接口(正式对接推荐) #### 5.5.1 `POST /indexer/build-docs` @@ -1298,6 +1323,11 @@ curl -X POST "http://localhost:6004/indexer/documents" \ } ``` +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `items` | array | Y | 需构建 doc 的 SPU 列表(每项含 `spu`、`skus`、`options`),**单次最多 200 条** | + > `spu` / `skus` / `options` 字段应当直接使用从 `shoplazza_product_spu` / `shoplazza_product_sku` / `shoplazza_product_option` 查询出的行字段。 #### 响应示例(节选) @@ -1334,6 +1364,15 @@ curl -X POST "http://localhost:6004/indexer/documents" \ } ``` +| 字段 | 类型 | 说明 | +|------|------|------| +| `tenant_id` | string | 租户 ID | +| `docs` | array | 构建成功的 ES 文档列表,与 `mappings/search_products.json` 一致 | +| `total` | integer | 请求的 items 总数 | +| `success_count` | integer | 成功构建数量 | +| `failed_count` | integer | 失败数量 | +| `failed` | array | 失败项列表,每项含 `spu_id`、`error` | + #### 使用建议 - **生产环境推荐流程**: @@ -1347,17 +1386,26 @@ curl -X POST "http://localhost:6004/indexer/documents" \ #### 5.6.1 `POST /indexer/build-docs-from-db` - **描述**: - 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。 + 仅用于测试/调试:调用方只提供 `tenant_id` 和 `spu_ids`,由 indexer 服务内部从 MySQL 查询 SPU/SKU/Option,然后调用与 `/indexer/build-docs` 相同的文档构建逻辑,返回 ES-ready doc。**生产环境请使用 `/indexer/build-docs`,由上游查库并写 ES。** #### 请求参数 ```json { "tenant_id": "170", - "spu_ids": ["223167"] + "spu_ids": ["223167", "223168"] } ``` +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `tenant_id` | string | Y | 租户 ID | +| `spu_ids` | array[string] | Y | SPU ID 列表,**单次最多 200 个** | + +#### 响应格式 + +与 `/indexer/build-docs` 相同:`tenant_id`、`docs`、`total`、`success_count`、`failed_count`、`failed`。 + #### 请求示例 ```bash @@ -1368,12 +1416,6 @@ curl -X POST "http://127.0.0.1:6004/indexer/build-docs-from-db" \ 返回结构与 `/indexer/build-docs` 相同,可直接用于对比 ES 实际文档或调试字段映射问题。 -#### 请求示例 - -```bash -curl -X GET "http://localhost:6004/indexer/health" -``` - --- ## 管理接口 diff --git a/docs/环境配置说明.md b/docs/环境配置说明.md index 5b3a6b2..98301e7 100644 --- a/docs/环境配置说明.md +++ b/docs/环境配置说明.md @@ -1,182 +1,11 @@ +# 环境配置说明(已并入) -## 1. 系统要求 - -- **操作系统**:Linux(推荐 Ubuntu 18.04+) -- **Conda**:Miniconda3 或 Anaconda(用于 Python 环境隔离) -- **Python**:3.10(由 Conda 环境提供) -- **内存**:建议 8GB+(含模型与 ES) -- **磁盘**:10GB+(含模型与索引) - ---- - -## 2. Python 运行环境 - -本项目历史上使用 Conda 管理环境;目前推荐使用 **venv**(更轻量、对 CI/容器更友好)。项目根目录下的 `activate.sh` 已升级为 **优先激活 `.venv`,并兼容 Conda 回退**,且会自动加载当前目录下的 `.env`(忽略注释与空行)。 - -### 2.1 venv(推荐) - -首次创建 venv: - -```bash -cd /data/saas-search -./scripts/create_venv.sh -source activate.sh -``` - -如需运行本地 embedding / 图像编码服务(torch/transformers 等依赖较重): - -```bash -cd /data/saas-search -INSTALL_ML=1 ./scripts/create_venv.sh -source activate.sh -``` - -日常使用: - -```bash -cd /data/saas-search -source activate.sh -``` - -### 2.2 Conda(兼容旧流程) - -`activate.sh` 会在未发现 `.venv` 时回退激活 Conda 环境 `searchengine`。若在新机器上部署,请先设置本机 Conda 路径再执行: - -```bash -# 你的 conda 在 ~/anaconda3/bin/conda,则 CONDA_ROOT=~/anaconda3 -export CONDA_ROOT=$HOME/anaconda3 # 或你的 Conda 安装路径(如 /home/ubuntu/anaconda3) -source activate.sh -``` - -**新机器首次部署(创建 Conda 环境)**:若本机尚未创建 `searchengine` 环境,任选其一: - -- **方式 A(推荐,与 environment.yml 一致)**: - ```bash - cd /data/saas-search - export CONDA_ROOT=$HOME/anaconda3 # 或你的 Conda 安装路径 - conda env create -f environment.yml - source activate.sh - ``` -- **方式 B(仅 pip)**: - ```bash - conda create -n searchengine python=3.10 -y - conda activate searchengine - cd /data/saas-search - pip install -r requirements.txt - ``` - -之后日常使用执行 `source activate.sh` 即可(如需可先 `export CONDA_ROOT=...`)。 - ---- - -## 3. 外部服务与端口 - -| 服务 | 默认地址 | 说明 | -|------|----------|------| -| Elasticsearch | `http://localhost:9200` | 可通过 Docker 单节点启动 | -| MySQL | `120.79.247.228:3316` | 存放店匠 SPU/SKU 数据 | -| Redis(可选) | `localhost:6479` | Embedding/翻译缓存 | - -示例:使用 Docker 启动 Elasticsearch - -```bash -docker run -d \ - --name elasticsearch \ - -p 9200:9200 \ - -e "discovery.type=single-node" \ - -e "ES_JAVA_OPTS=-Xms2g -Xmx2g" \ - elasticsearch:8.11.0 -``` - ---- - -## 4. 环境变量与 `.env` 模板 - -在项目根目录创建 `.env`,并根据环境替换敏感信息: - -```env -# MySQL -DB_HOST=120.79.247.228 -DB_PORT=3316 -DB_DATABASE=saas -DB_USERNAME=saas -DB_PASSWORD=P89cZHS5d7dFyc9R - -# Elasticsearch -ES_HOST=http://localhost:9200 -ES_USERNAME=saas -ES_PASSWORD=4hOaLaf41y2VuI8y - -# Redis(可选) -REDIS_HOST=localhost -REDIS_PORT=6479 -REDIS_PASSWORD=BMfv5aI31kgHWtlx - -# DeepL 翻译 -DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed - -# API -API_HOST=0.0.0.0 -API_PORT=6002 -``` - ---- - -## 5. 服务凭证速查 - -| 项目 | 值 | -|------|----| -| **MySQL** | host `120.79.247.228`, port `3316`, user `saas`, password `P89cZHS5d7dFyc9R` | -| **Elasticsearch** | host `http://localhost:9200`, user `saas`, password `4hOaLaf41y2VuI8y` | -| **Redis(可选)** | host `localhost`, port `6479`, password `BMfv5aI31kgHWtlx` | -| **DeepL** | `c9293ab4-ad25-479b-919f-ab4e63b429ed` | - -> 所有凭证仅用于本地/测试环境,生产环境需替换并妥善保管。 - ---- - -## 6. 店匠数据源说明 - -saas-search 以 MySQL 中的店匠标准表为权威数据源: - -- `shoplazza_product_spu`:SPU 商品主表 -- `shoplazza_product_sku`:SKU 变体表 - -### `shoplazza_product_sku` 字段节选 - -| 字段 | 类型 | 描述 | -|------|------|------| -| `id` | bigint(20) | SKU 主键 | -| `spu_id` | bigint(20) | 对应 SPU | -| `shop_id` | bigint(20) | 店铺 ID | -| `shoplazza_product_id` | varchar(64) | 店匠商品 ID | -| `title` | varchar(500) | 变体标题 | -| `sku` | varchar(100) | SKU 编码 | -| `price` | decimal(10,2) | 售价 | -| `compare_at_price` | decimal(10,2) | 原价 | -| `option1/2/3` | varchar(255) | 颜色/尺码等选项 | -| `inventory_quantity` | int(11) | 库存 | -| `image_src` | varchar(500) | 图片 | -| `tenant_id` | bigint(20) | 租户 | -| `create_time` | datetime | 创建时间 | -| `update_time` | datetime | 更新时间 | -| `deleted` | bit(1) | 逻辑删除标记 | - -> 完整字段、索引映射与 ES 对应关系详见 `INDEX_FIELDS_DOCUMENTATION.md`。 - ---- - -## 7. 相关脚本 - -- **`activate.sh`**(项目根目录):激活 Conda 环境 `searchengine` 并加载 `.env`,**日常开发/部署以本脚本为准**。 -- `scripts/mock_data.sh`:一次性生成 Tenant1 Mock + Tenant2 CSV 数据并导入 MySQL -- `scripts/create_tenant_index.sh `:创建租户索引结构 -- `POST /indexer/reindex`:从 MySQL 导入到 Elasticsearch(推荐) -- `run.sh` / `restart.sh`:服务启动/重启(统一走 `scripts/service_ctl.sh`) -- `scripts/service_ctl.sh`:统一服务管理(start/stop/restart/status) - -**新机器部署**:若 Conda 未安装在默认路径(如 `/home/tw/miniconda3`),请在执行上述脚本前设置 `CONDA_ROOT`。例如你的 conda 是 `~/anaconda3/bin/conda`(即 `/home/ubuntu/anaconda3/bin/conda`),则设置:`export CONDA_ROOT=$HOME/anaconda3`。可将该行写入 `~/.bashrc` 或部署说明。 - -更多脚本参数、日志与验证命令参见 `Usage-Guide.md` 与 `测试数据指南.md`。 +**本文内容已合并至 [QUICKSTART.md](./QUICKSTART.md) 第 1 节「快速上手」**,包括: +- **1.4 系统要求**:操作系统、Python、内存与磁盘 +- **1.5 Python 运行环境(详细)**:venv / Conda 创建与日常使用 +- **1.6 外部服务与 .env(含生产凭证)**:MySQL / Redis / Elasticsearch 地址、端口、凭证及远程登录方式、`.env` 模板 +- **1.7 店匠数据源说明**:SPU/SKU 表与字段节选 +- **1.8 相关脚本**:activate.sh、create_venv、mock_data、create_tenant_index、run.sh 等 +请直接查阅 [QUICKSTART.md §1.4–1.8](./QUICKSTART.md#14-系统要求)。 diff --git a/tests/ci/test_service_api_contracts.py b/tests/ci/test_service_api_contracts.py index 7dbe5df..bd78df3 100644 --- a/tests/ci/test_service_api_contracts.py +++ b/tests/ci/test_service_api_contracts.py @@ -4,6 +4,7 @@ from types import SimpleNamespace from typing import Any, Dict, List import numpy as np +import pandas as pd import pytest from fastapi.testclient import TestClient @@ -111,9 +112,31 @@ class _FakeTransformer: } +class _FakeDbConnection: + """Minimal fake for indexer health check: connect().execute(text('SELECT 1')).""" + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def execute(self, stmt): + pass + + +class _FakeDbEngine: + def connect(self): + return _FakeDbConnection() + + class _FakeIncrementalService: + def __init__(self): + self.db_engine = _FakeDbEngine() + self.category_id_to_name = {} + def index_spus_to_es(self, es_client, tenant_id: str, spu_ids: List[str], delete_spu_ids=None): - return { + out = { "tenant_id": tenant_id, "spu_ids": [{"spu_id": s, "status": "indexed"} for s in spu_ids], "delete_spu_ids": [], @@ -121,10 +144,33 @@ class _FakeIncrementalService: "success_count": len(spu_ids), "failed_count": 0, } + if delete_spu_ids: + out["delete_spu_ids"] = [{"spu_id": s, "status": "deleted"} for s in delete_spu_ids] + out["total"] += len(delete_spu_ids) + out["success_count"] += len(delete_spu_ids) + return out + + def get_spu_document(self, tenant_id: str, spu_id: str): + return { + "tenant_id": tenant_id, + "spu_id": spu_id, + "title": {"zh": "Fake doc"}, + } def _get_transformer_bundle(self, tenant_id: str): return _FakeTransformer(), None, False + def _load_spus_for_spu_ids(self, tenant_id: str, spu_ids: List[str], include_deleted: bool = False): + if not spu_ids: + return pd.DataFrame() + return pd.DataFrame([{"id": int(s), "title": "Fake", "tenant_id": tenant_id} for s in spu_ids]) + + def _load_skus_for_spu_ids(self, tenant_id: str, spu_ids: List[str]): + return pd.DataFrame() + + def _load_options_for_spu_ids(self, tenant_id: str, spu_ids: List[str]): + return pd.DataFrame() + @pytest.fixture def indexer_client(monkeypatch): @@ -173,6 +219,153 @@ def test_indexer_build_docs_contract(indexer_client: TestClient): assert data["docs"][0]["spu_id"] == "1" +def test_indexer_build_docs_from_db_contract(indexer_client: TestClient): + """POST /indexer/build-docs-from-db: tenant_id + spu_ids, returns same shape as build-docs.""" + response = indexer_client.post( + "/indexer/build-docs-from-db", + json={"tenant_id": "162", "spu_ids": ["1001", "1002"]}, + ) + assert response.status_code == 200 + data = response.json() + assert data["tenant_id"] == "162" + assert "docs" in data + assert data["success_count"] == 2 + assert len(data["docs"]) == 2 + assert data["docs"][0]["spu_id"] == "1001" + + +def test_indexer_documents_contract(indexer_client: TestClient): + """POST /indexer/documents: tenant_id + spu_ids, returns success/failed lists (no ES write).""" + response = indexer_client.post( + "/indexer/documents", + json={"tenant_id": "162", "spu_ids": ["1001", "1002"]}, + ) + assert response.status_code == 200 + data = response.json() + assert "success" in data and "failed" in data + assert data["total"] == 2 + assert data["success_count"] == 2 + assert data["failed_count"] == 0 + assert len(data["success"]) == 2 + assert data["success"][0]["spu_id"] == "1001" + assert "document" in data["success"][0] + assert data["success"][0]["document"]["title"]["zh"] == "Fake doc" + + +def test_indexer_health_contract(indexer_client: TestClient): + """GET /indexer/health: returns status and database/preloaded_data.""" + response = indexer_client.get("/indexer/health") + assert response.status_code == 200 + data = response.json() + assert "status" in data + assert data["status"] in ("available", "unavailable", "error") + assert "database" in data or "message" in data + if "preloaded_data" in data: + assert "category_mappings" in data["preloaded_data"] + + +def test_indexer_incremental_with_delete_spu_ids(indexer_client: TestClient): + """POST /indexer/index with delete_spu_ids: explicit delete path.""" + response = indexer_client.post( + "/indexer/index", + json={ + "tenant_id": "162", + "spu_ids": ["1001"], + "delete_spu_ids": ["2001", "2002"], + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["success_count"] == 3 + assert len(data["spu_ids"]) == 1 + assert len(data["delete_spu_ids"]) == 2 + assert data["delete_spu_ids"][0]["status"] == "deleted" + + +def test_indexer_index_validation_both_empty(indexer_client: TestClient): + """POST /indexer/index: 400 when spu_ids and delete_spu_ids both empty.""" + response = indexer_client.post( + "/indexer/index", + json={"tenant_id": "162", "spu_ids": [], "delete_spu_ids": []}, + ) + assert response.status_code == 400 + + +def test_indexer_index_validation_max_spu_ids(indexer_client: TestClient): + """POST /indexer/index: 400 when spu_ids > 100.""" + response = indexer_client.post( + "/indexer/index", + json={"tenant_id": "162", "spu_ids": [str(i) for i in range(101)], "delete_spu_ids": []}, + ) + assert response.status_code == 400 + + +def test_indexer_build_docs_validation_empty_items(indexer_client: TestClient): + """POST /indexer/build-docs: 400 when items empty.""" + response = indexer_client.post( + "/indexer/build-docs", + json={"tenant_id": "162", "items": []}, + ) + assert response.status_code == 400 + + +def test_indexer_documents_validation_empty_spu_ids(indexer_client: TestClient): + """POST /indexer/documents: 400 when spu_ids empty.""" + response = indexer_client.post( + "/indexer/documents", + json={"tenant_id": "162", "spu_ids": []}, + ) + assert response.status_code == 400 + + +def test_indexer_build_docs_from_db_validation_empty_spu_ids(indexer_client: TestClient): + """POST /indexer/build-docs-from-db: 400 when spu_ids empty.""" + response = indexer_client.post( + "/indexer/build-docs-from-db", + json={"tenant_id": "162", "spu_ids": []}, + ) + assert response.status_code == 400 + + +def test_indexer_build_docs_validation_max_items(indexer_client: TestClient): + """POST /indexer/build-docs: 400 when items > 200.""" + response = indexer_client.post( + "/indexer/build-docs", + json={ + "tenant_id": "162", + "items": [{"spu": {"id": i, "title": "x"}, "skus": [], "options": []} for i in range(201)], + }, + ) + assert response.status_code == 400 + + +def test_indexer_build_docs_from_db_validation_max_spu_ids(indexer_client: TestClient): + """POST /indexer/build-docs-from-db: 400 when spu_ids > 200.""" + response = indexer_client.post( + "/indexer/build-docs-from-db", + json={"tenant_id": "162", "spu_ids": [str(i) for i in range(201)]}, + ) + assert response.status_code == 400 + + +def test_indexer_documents_validation_max_spu_ids(indexer_client: TestClient): + """POST /indexer/documents: 400 when spu_ids > 100.""" + response = indexer_client.post( + "/indexer/documents", + json={"tenant_id": "162", "spu_ids": [str(i) for i in range(101)]}, + ) + assert response.status_code == 400 + + +def test_indexer_index_validation_max_delete_spu_ids(indexer_client: TestClient): + """POST /indexer/index: 400 when delete_spu_ids > 100.""" + response = indexer_client.post( + "/indexer/index", + json={"tenant_id": "162", "spu_ids": [], "delete_spu_ids": [str(i) for i in range(101)]}, + ) + assert response.status_code == 400 + + class _FakeTextModel: def encode_batch(self, texts, batch_size=32, device="cpu"): return [np.array([0.1, 0.2, 0.3], dtype=np.float32) for _ in texts] -- libgit2 0.21.2