Commit fb973d19cdee6ce7db1611c9b7a306e3fab08628

Authored by tangwang
1 parent 00c8ddb9

configs

@@ -76,3 +76,4 @@ logs_*/ @@ -76,3 +76,4 @@ logs_*/
76 .pytest_cache 76 .pytest_cache
77 77
78 models/ 78 models/
  79 +model_cache/
@ deleted
@@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
1 -  
2 -# Please enter the commit message for your changes. Lines starting  
3 -# with '#' will be ignored, and an empty message aborts the commit.  
4 -#  
5 -# On branch master  
6 -# Your branch is ahead of 'origin/master' by 3 commits.  
7 -# (use "git push" to publish your local commits)  
8 -#  
9 -# Changes to be committed:  
10 -# modified: README.md  
11 -# modified: docs/Usage-Guide.md  
12 -# modified: scripts/service_ctl.sh  
13 -# new file: status.sh  
14 -#  
15 -# Changes not staged for commit:  
16 -# modified: third-party/clip-as-service (untracked content)  
17 -#  
] deleted
@@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
1 -docs  
2 -# Please enter the commit message for your changes. Lines starting  
3 -# with '#' will be ignored, and an empty message aborts the commit.  
4 -#  
5 -# On branch master  
6 -# Your branch is ahead of 'origin/master' by 5 commits.  
7 -# (use "git push" to publish your local commits)  
8 -#  
9 -# Changes to be committed:  
10 -# modified: config/config.yaml  
11 -# modified: docs/TODO.txt  
12 -# modified: "docs/\346\220\234\347\264\242API\345\257\271\346\216\245\346\214\207\345\215\227-07-\345\276\256\346\234\215\345\212\241\346\216\245\345\217\243\357\274\210Embedding-Reranker-Translation\357\274\211.md"  
13 -# modified: "docs/\347\233\270\345\205\263\346\200\247\346\243\200\347\264\242\344\274\230\345\214\226\350\257\264\346\230\216.md"  
14 -#  
15 -# Changes not staged for commit:  
16 -# modified: third-party/clip-as-service (untracked content)  
17 -#  
config/config.yaml
@@ -131,7 +131,7 @@ function_score: @@ -131,7 +131,7 @@ function_score:
131 # 重排配置(provider/URL 在 services.rerank) 131 # 重排配置(provider/URL 在 services.rerank)
132 rerank: 132 rerank:
133 enabled: true 133 enabled: true
134 - rerank_window: 384 134 + rerank_window: 400
135 timeout_sec: 15.0 135 timeout_sec: 15.0
136 weight_es: 0.4 136 weight_es: 0.4
137 weight_ai: 0.6 137 weight_ai: 0.6
@@ -275,7 +275,7 @@ services: @@ -275,7 +275,7 @@ services:
275 max_docs: 1000 275 max_docs: 1000
276 normalize: true 276 normalize: true
277 # 服务内后端(reranker 进程启动时读取) 277 # 服务内后端(reranker 进程启动时读取)
278 - backend: "qwen3_transformers" # bge | qwen3_vllm | qwen3_transformers | dashscope_rerank 278 + backend: "qwen3_vllm" # bge | qwen3_vllm | qwen3_transformers | dashscope_rerank
279 backends: 279 backends:
280 bge: 280 bge:
281 model_name: "BAAI/bge-reranker-v2-m3" 281 model_name: "BAAI/bge-reranker-v2-m3"
@@ -296,15 +296,18 @@ services: @@ -296,15 +296,18 @@ services:
296 enforce_eager: false 296 enforce_eager: false
297 infer_batch_size: 100 297 infer_batch_size: 100
298 sort_by_doc_length: true 298 sort_by_doc_length: true
299 - length_sort_mode: "char" # char | token  
300 - instruction: "rank products by given query" 299 + # "rank products by given query" 比 “Given a query, score the product for relevance” 更好点
  300 + instruction: "rank products by given query"
  301 + # instruction: "Given a query, score the product for relevance"
301 qwen3_transformers: 302 qwen3_transformers:
302 model_name: "Qwen/Qwen3-Reranker-0.6B" 303 model_name: "Qwen/Qwen3-Reranker-0.6B"
303 instruction: "rank products by given query" 304 instruction: "rank products by given query"
  305 + # instruction: "Score the product’s relevance to the given query"
304 max_length: 8192 306 max_length: 8192
305 batch_size: 64 307 batch_size: 64
306 use_fp16: true 308 use_fp16: true
307 - attn_implementation: "flash_attention_2" 309 + # sdpa:默认无需 flash-attn;若已安装 flash_attn 可改为 flash_attention_2
  310 + attn_implementation: "sdpa"
308 dashscope_rerank: 311 dashscope_rerank:
309 model_name: "qwen3-rerank" 312 model_name: "qwen3-rerank"
310 # 按地域选择 endpoint: 313 # 按地域选择 endpoint:
docs/DEVELOPER_GUIDE.md
@@ -360,7 +360,7 @@ services: @@ -360,7 +360,7 @@ services:
360 ### 7.6 新增后端清单(以 Qwen3-Reranker 为例) 360 ### 7.6 新增后端清单(以 Qwen3-Reranker 为例)
361 361
362 1. **实现协议**:在 `reranker/backends/qwen3_vllm.py` 中实现类,提供 `score_with_meta(query, docs, normalize) -> (scores, meta)`,输出与 docs 等长且顺序一致。 362 1. **实现协议**:在 `reranker/backends/qwen3_vllm.py` 中实现类,提供 `score_with_meta(query, docs, normalize) -> (scores, meta)`,输出与 docs 等长且顺序一致。
363 -2. **配置**:在 `config/config.yaml` 的 `services.rerank.backends` 下增加 `qwen3_vllm` 块(model_name、engine、max_model_len、gpu_memory_utilization、`infer_batch_size`、`sort_by_doc_length`、`length_sort_mode` 等);支持环境变量 `RERANK_BACKEND=qwen3_vllm`。 363 +2. **配置**:在 `config/config.yaml` 的 `services.rerank.backends` 下增加 `qwen3_vllm` 块(model_name、engine、max_model_len、gpu_memory_utilization、`infer_batch_size`、`sort_by_doc_length`等);支持环境变量 `RERANK_BACKEND=qwen3_vllm`。
364 3. **注册**:在 `reranker/backends/__init__.py` 的 `get_rerank_backend(name, config)` 中增加 `qwen3_vllm` 分支。 364 3. **注册**:在 `reranker/backends/__init__.py` 的 `get_rerank_backend(name, config)` 中增加 `qwen3_vllm` 分支。
365 4. **服务启动**:`reranker/server.py` 启动时根据配置调用 `get_rerank_backend(backend_name, backend_cfg)` 得到实例。 365 4. **服务启动**:`reranker/server.py` 启动时根据配置调用 `get_rerank_backend(backend_name, backend_cfg)` 得到实例。
366 5. **调用方**:无需修改;仅部署时启动使用新后端的 reranker 服务即可。 366 5. **调用方**:无需修改;仅部署时启动使用新后端的 reranker 服务即可。
@@ -21,6 +21,15 @@ @@ -21,6 +21,15 @@
21 如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。 21 如果有: 先做sku筛选,然后把最优的拼接到名称中,参与reranker。
22 22
23 23
  24 +现在在reranker、分页之后、做填充的时候,已经有做sku的筛选。
  25 +需要优化:
  26 +现在是,先做包含的判断,找到第一个 option_value被query包含的,则直接认为匹配。改为
  27 +1. 第一轮:遍历完,如果有且仅有一个才这样。
  28 +2. 第二轮:如果有多个,跳到3。如果没有,对每个词都走泛化词表进行匹配。
  29 +3. 第三轮:如果有多个,那么对这多个,走embedding相关性取最高的。如果一个也没有,则对所有的走embedding相关性取最高的
  30 +这个sku筛选也需要提取为一个独立的模块
  31 +
  32 +
24 33
25 2026-03-21 10:29:23,698 - elastic_transport.transport - INFO - POST http://localhost:9200/search_products_tenant_163/_search?include_named_queries_score=false [status:200 duration:0.009s] 34 2026-03-21 10:29:23,698 - elastic_transport.transport - INFO - POST http://localhost:9200/search_products_tenant_163/_search?include_named_queries_score=false [status:200 duration:0.009s]
26 2026-03-21 10:29:23,700 - request_context - INFO - 分页详情回填 | ids=20 | filled=20 | took=7ms 35 2026-03-21 10:29:23,700 - request_context - INFO - 分页详情回填 | ids=20 | filled=20 | took=7ms
docs/工作总结-微服务性能优化与架构.md
@@ -41,7 +41,7 @@ @@ -41,7 +41,7 @@
41 - **精度**:`dtype: "float16"`,降低显存与计算量。 41 - **精度**:`dtype: "float16"`,降低显存与计算量。
42 - **Prefix Caching**:`enable_prefix_caching: true`,对重复前缀(如相同 query)做缓存,减少重复计算。 42 - **Prefix Caching**:`enable_prefix_caching: true`,对重复前缀(如相同 query)做缓存,减少重复计算。
43 - **CUDA 图**:`enforce_eager: false`(默认),利用 vLLM 的 CUDA graph 降低 kernel 启动开销。 43 - **CUDA 图**:`enforce_eager: false`(默认),利用 vLLM 的 CUDA graph 降低 kernel 启动开销。
44 -- **按文档长度分批**:`sort_by_doc_length: true`,请求内先按文档长度排序再按 `infer_batch_size` 分批推理,减少 padding 浪费;`length_sort_mode: "char"`(更快,短文本推荐)或 `"token"`(更精确) 44 +- **按文档长度分批**:`sort_by_doc_length: true`,请求内先按文档长度排序再按 `infer_batch_size` 分批推理,减少 padding 浪费
45 - **参数搜索结论**:在 T4、1000-doc 口径下对 `infer_batch_size` 做了 24/32/48/64 对比;**单请求延迟(c=1)** 上 `infer_batch_size=64` 最优,故当前默认 `infer_batch_size: 64`;`max_model_len: 256` 满足 query+doc 短文本场景;`gpu_memory_utilization: 0.36` 与 T4 16GB 匹配。 45 - **参数搜索结论**:在 T4、1000-doc 口径下对 `infer_batch_size` 做了 24/32/48/64 对比;**单请求延迟(c=1)** 上 `infer_batch_size=64` 最优,故当前默认 `infer_batch_size: 64`;`max_model_len: 256` 满足 query+doc 短文本场景;`gpu_memory_utilization: 0.36` 与 T4 16GB 匹配。
46 46
47 **具体配置**(`config/config.yaml` → `services.rerank.backends.qwen3_vllm`): 47 **具体配置**(`config/config.yaml` → `services.rerank.backends.qwen3_vllm`):
@@ -56,7 +56,6 @@ enable_prefix_caching: true @@ -56,7 +56,6 @@ enable_prefix_caching: true
56 enforce_eager: false 56 enforce_eager: false
57 infer_batch_size: 64 57 infer_batch_size: 64
58 sort_by_doc_length: true 58 sort_by_doc_length: true
59 -length_sort_mode: "char"  
60 instruction: "Given a shopping query, rank product titles by relevance" 59 instruction: "Given a shopping query, rank product titles by relevance"
61 ``` 60 ```
62 环境变量覆盖:`RERANK_BACKEND`、`RERANKER_SERVICE_URL`、`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH` 等。启停:`./scripts/service_ctl.sh start reranker`,健康:`curl -sS http://127.0.0.1:6007/health`。 61 环境变量覆盖:`RERANK_BACKEND`、`RERANKER_SERVICE_URL`、`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH` 等。启停:`./scripts/service_ctl.sh start reranker`,健康:`curl -sS http://127.0.0.1:6007/health`。
docs/搜索API对接指南-07-微服务接口(Embedding-Reranker-Translation).md
@@ -161,7 +161,7 @@ curl "http://localhost:6008/ready" @@ -161,7 +161,7 @@ curl "http://localhost:6008/ready"
161 161
162 说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。 162 说明:默认后端为 `qwen3_vllm`(`Qwen/Qwen3-Reranker-0.6B`),需要可用 GPU 显存。
163 163
164 -补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分;若 `sort_by_doc_length=true`,会先按文档长度排序后分批,减少 padding,再按原输入顺序返回分数。`length_sort_mode` 可选 `char`(更快)或 `token`(更精确) 164 +补充:`docs` 的请求大小与模型推理 `batch size` 解耦。即使一次传入 1000 条文档,服务端也会按 `services.rerank.backends.qwen3_vllm.infer_batch_size` 自动拆分
165 165
166 #### 7.2.1 `POST /rerank` — 结果重排 166 #### 7.2.1 `POST /rerank` — 结果重排
167 167
docs/相关性检索优化说明.md
@@ -267,3 +267,17 @@ python ./scripts/eval_search_quality.py @@ -267,3 +267,17 @@ python ./scripts/eval_search_quality.py
267 3. 源语种不在索引语言,翻译全部失败(验证多目标 fallback) 267 3. 源语种不在索引语言,翻译全部失败(验证多目标 fallback)
268 4. 自定义 `original_query_fallback_boost_when_translation_missing` 生效 268 4. 自定义 `original_query_fallback_boost_when_translation_missing` 生效
269 5. 非 `zh/en` 语种字段动态拼接(如 `de/fr/es`) 269 5. 非 `zh/en` 语种字段动态拼接(如 `de/fr/es`)
  270 +
  271 +
  272 +
  273 +## reranker方面:
  274 +BAAI/bge-reranker-v2-m3的一个严重badcase:
  275 +q=黑色中长半身裙
  276 +
  277 +Rerank score: 0.0785
  278 +title.zh: 2026款韩版高腰显瘦雪尼尔包臀裙灯芯绒开叉中长款咖啡色半身裙女
  279 +title.en: 2026 Korean-style High-waisted Slimming Corduroy Skirt with Slit, Mid-Length Coffee-colored Skirt for Women
  280 +
  281 +Rerank score: 0.9643
  282 +title.en: Black Half-high Collar Base Shirt Women's Autumn and Winter fleece-lined Contrast Color Pure Desire Design Sense Horn Sleeve Ruffled Inner Top
  283 +title.zh: 黑色高领半高领女士秋冬内搭加绒拼色纯欲设计荷叶边袖内衬上衣
reranker/DEPLOYMENT_AND_TUNING.md
@@ -93,19 +93,14 @@ curl -sS http://127.0.0.1:6007/health @@ -93,19 +93,14 @@ curl -sS http://127.0.0.1:6007/health
93 93
94 - 调用方一次可传入 1000 docs(业务需求) 94 - 调用方一次可传入 1000 docs(业务需求)
95 - 服务端按 `infer_batch_size` 自动拆批推理(模型效率需求) 95 - 服务端按 `infer_batch_size` 自动拆批推理(模型效率需求)
96 -  
97 -### 4.2 先排序再分批,降低 padding 浪费  
98 -  
99 - `sort_by_doc_length: true`:按长度排序后再分批 96 - `sort_by_doc_length: true`:按长度排序后再分批
100 -- `length_sort_mode: "char"`:短文本场景下开销更低,默认推荐  
101 -- `length_sort_mode: "token"`:长度估计更精确,但有额外 tokenizer 开销  
102 97
103 -### 4.3 全局去重后回填 98 +### 4.2 全局去重后回填
104 99
105 - 对 docs 进行全局去重(非“仅相邻去重”) 100 - 对 docs 进行全局去重(非“仅相邻去重”)
106 - 推理后按原请求顺序回填 scores,保证接口契约稳定 101 - 推理后按原请求顺序回填 scores,保证接口契约稳定
107 102
108 -### 4.4 启动稳定性修复 103 +### 4.3 启动稳定性修复
109 104
110 - `service_ctl.sh` 对 reranker 使用独立启动路径 105 - `service_ctl.sh` 对 reranker 使用独立启动路径
111 - 增加“稳定健康检查”(连续健康探测)避免“刚 healthy 即退出”的假阳性 106 - 增加“稳定健康检查”(连续健康探测)避免“刚 healthy 即退出”的假阳性
@@ -159,7 +154,7 @@ curl -sS http://127.0.0.1:6007/health @@ -159,7 +154,7 @@ curl -sS http://127.0.0.1:6007/health
159 154
160 ## 7. 生产建议 155 ## 7. 生产建议
161 156
162 -- 默认保持:`infer_batch_size: 64`、`sort_by_doc_length: true`、`length_sort_mode: "char"` 157 +- 默认保持:`infer_batch_size: 64`、`sort_by_doc_length: true`
163 - 满足以下条件时可考虑提高到 `96`:业务以吞吐优先、可接受更高单请求延迟、已通过同机同数据压测验证收益 158 - 满足以下条件时可考虑提高到 `96`:业务以吞吐优先、可接受更高单请求延迟、已通过同机同数据压测验证收益
164 - 每次改动后都必须复跑 `benchmark_reranker_1000docs.sh` 并归档结果 159 - 每次改动后都必须复跑 `benchmark_reranker_1000docs.sh` 并归档结果
165 160
reranker/README.md
@@ -24,7 +24,7 @@ Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwe @@ -24,7 +24,7 @@ Reranker 服务提供统一的 `/rerank` API,支持可插拔后端(BGE、Qwe
24 - `reranker/config.py`:服务端口、MAX_DOCS、NORMALIZE 等(后端参数在 config.yaml) 24 - `reranker/config.py`:服务端口、MAX_DOCS、NORMALIZE 等(后端参数在 config.yaml)
25 25
26 ## 依赖 26 ## 依赖
27 -- 通用:`torch`、`modelscope`、`fastapi`、`uvicorn`(见项目 `requirements.txt` / `requirements_ml.txt`) 27 +- 通用:`torch`、`transformers`、`fastapi`、`uvicorn`(隔离环境见 `requirements_reranker_service.txt`;全量 ML 环境另见 `requirements_ml.txt`)
28 - **Qwen3-vLLM 后端**:`vllm>=0.8.5`、`transformers>=4.51.0`(仅当使用 `backend: qwen3_vllm` 时需 vLLM) 28 - **Qwen3-vLLM 后端**:`vllm>=0.8.5`、`transformers>=4.51.0`(仅当使用 `backend: qwen3_vllm` 时需 vLLM)
29 - **Qwen3-Transformers 后端**:`transformers>=4.51.0`、`torch`(无需 vLLM,适合 CPU 或小显存) 29 - **Qwen3-Transformers 后端**:`transformers>=4.51.0`、`torch`(无需 vLLM,适合 CPU 或小显存)
30 ```bash 30 ```bash
@@ -53,7 +53,6 @@ services: @@ -53,7 +53,6 @@ services:
53 max_model_len: 256 53 max_model_len: 256
54 infer_batch_size: 64 54 infer_batch_size: 64
55 sort_by_doc_length: true 55 sort_by_doc_length: true
56 - length_sort_mode: "char" # char | token  
57 enable_prefix_caching: true 56 enable_prefix_caching: true
58 enforce_eager: false 57 enforce_eager: false
59 instruction: "Given a shopping query, rank product titles by relevance" 58 instruction: "Given a shopping query, rank product titles by relevance"
@@ -157,7 +156,7 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info @@ -157,7 +156,7 @@ uvicorn reranker.server:app --host 0.0.0.0 --port 6007 --log-level info
157 ## Notes 156 ## Notes
158 - 无请求级缓存;输入按字符串去重后推理,再按原始顺序回填分数。 157 - 无请求级缓存;输入按字符串去重后推理,再按原始顺序回填分数。
159 - 空或 null 的 doc 跳过并计为 0。 158 - 空或 null 的 doc 跳过并计为 0。
160 -- **Qwen3-vLLM 分批策略**:`docs` 请求体可为 1000+,服务端会按 `infer_batch_size` 拆分;当 `sort_by_doc_length=true` 时,会先按文档长度排序后分批,减少 padding 开销,最终再按输入顺序回填分数。`length_sort_mode` 支持 `char`(默认,更快)与 `token`(更精确)。 159 +- **Qwen3-vLLM 分批策略**:`docs` 请求体可为 1000+,服务端会按 `infer_batch_size` 拆分;当 `sort_by_doc_length=true` 时,会先按文档长度排序后分批,减少 padding 开销,最终再按输入顺序回填分数。
161 - 运行时可用环境变量临时覆盖批量参数:`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH`。 160 - 运行时可用环境变量临时覆盖批量参数:`RERANK_VLLM_INFER_BATCH_SIZE`、`RERANK_VLLM_SORT_BY_DOC_LENGTH`。
162 - **Qwen3-vLLM**:参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B),需 GPU 与较多显存;与 BGE 相比适合长文本、高吞吐场景(vLLM 前缀缓存)。 161 - **Qwen3-vLLM**:参考 [Qwen3-Reranker-0.6B](https://huggingface.co/Qwen/Qwen3-Reranker-0.6B),需 GPU 与较多显存;与 BGE 相比适合长文本、高吞吐场景(vLLM 前缀缓存)。
163 -- **Qwen3-Transformers**:官方 Transformers Usage 方式,无需 vLLM;适合 CPU 或小显存,可选 `attn_implementation: "flash_attention_2"` 加速。 162 +- **Qwen3-Transformers**:官方 Transformers Usage 方式,无需 vLLM;适合 CPU 或小显存。默认 `attn_implementation: "sdpa"`;若已安装 `flash_attn` 可设 `flash_attention_2`(未安装时服务会自动回退到 sdpa)。
reranker/backends/qwen3_vllm.py
@@ -76,7 +76,7 @@ class Qwen3VLLMRerankerBackend: @@ -76,7 +76,7 @@ class Qwen3VLLMRerankerBackend:
76 dtype = str(self._config.get("dtype", "float16")).strip().lower() 76 dtype = str(self._config.get("dtype", "float16")).strip().lower()
77 self._instruction = str( 77 self._instruction = str(
78 self._config.get("instruction") 78 self._config.get("instruction")
79 - or "Given a shopping query, rank product titles by relevance" 79 + or "Given a query, score the product for relevance"
80 ) 80 )
81 infer_batch_size = os.getenv("RERANK_VLLM_INFER_BATCH_SIZE") or self._config.get("infer_batch_size", 64) 81 infer_batch_size = os.getenv("RERANK_VLLM_INFER_BATCH_SIZE") or self._config.get("infer_batch_size", 64)
82 sort_by_doc_length = os.getenv("RERANK_VLLM_SORT_BY_DOC_LENGTH") 82 sort_by_doc_length = os.getenv("RERANK_VLLM_SORT_BY_DOC_LENGTH")
reranker/bge_reranker.py
@@ -170,13 +170,14 @@ class BGEReranker: @@ -170,13 +170,14 @@ class BGEReranker:
170 output_scores[orig_idx] = float(unique_scores[unique_idx]) 170 output_scores[orig_idx] = float(unique_scores[unique_idx])
171 171
172 # Log per-doc scores (aligned to original docs order) 172 # Log per-doc scores (aligned to original docs order)
173 - try:  
174 - lines = []  
175 - for i, d in enumerate(docs[:100]):  
176 - lines.append(f"{output_scores[i]},{'' if d is None else str(d)}")  
177 - logger.info("[BGE_RERANKER] query:%s Scores (score,doc):\n%s", query, "\n".join(lines))  
178 - except Exception:  
179 - pass 173 + if 0:
  174 + try:
  175 + lines = []
  176 + for i, d in enumerate(docs[:100]):
  177 + lines.append(f"{output_scores[i]},{'' if d is None else str(d)}")
  178 + logger.info("[BGE_RERANKER] query:%s Scores (score,doc):\n%s", query, "\n".join(lines))
  179 + except Exception:
  180 + pass
180 181
181 elapsed_ms = (time.time() - start_ts) * 1000.0 182 elapsed_ms = (time.time() - start_ts) * 1000.0
182 dedup_ratio = 0.0 183 dedup_ratio = 0.0