From ff32d8945e2d83e4937b40241a902da6a4018820 Mon Sep 17 00:00:00 2001 From: tangwang Date: Thu, 5 Feb 2026 16:13:46 +0800 Subject: [PATCH] rerank --- api/models.py | 18 ++++++++++++++++-- api/routes/search.py | 8 ++++++-- config/config.yaml | 9 +++++++-- config/config_loader.py | 15 ++++++++++++--- docs/搜索API对接指南.md | 8 ++++++-- query/test_translation.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------- reranker/bge_reranker.py | 20 ++++++++++++++++++++ search/rerank_client.py | 40 +++++++++++++++++++++++++++++----------- search/searcher.py | 24 ++++++++++++++++-------- 9 files changed, 170 insertions(+), 88 deletions(-) diff --git a/api/models.py b/api/models.py index f4c9a0f..f22e888 100644 --- a/api/models.py +++ b/api/models.py @@ -151,9 +151,23 @@ class SearchRequest(BaseModel): min_score: Optional[float] = Field(None, ge=0, description="最小相关性分数阈值") highlight: bool = Field(False, description="是否高亮搜索关键词(暂不实现)") debug: bool = Field(False, description="是否返回调试信息") - ai_search: bool = Field( + enable_rerank: bool = Field( False, - description="是否开启 AI 搜索(调用本地重排服务对 ES 结果进行二次排序)" + description="是否开启重排(调用外部重排服务对 ES 结果进行二次排序)" + ) + rerank_query_template: Optional[str] = Field( + None, + description=( + "重排 query 模板(可选)。支持 {query} 占位符。" + "不传则使用服务端配置的 rerank_query_template。" + ), + ) + rerank_doc_template: Optional[str] = Field( + None, + description=( + "重排 doc 模板(可选)。支持 {title} {brief} {vendor} {description} {category_path} 占位符。" + "不传则使用服务端配置的 rerank_doc_template。" + ), ) # SKU筛选参数 diff --git a/api/routes/search.py b/api/routes/search.py index b1cf4cf..a1f7b05 100644 --- a/api/routes/search.py +++ b/api/routes/search.py @@ -84,7 +84,9 @@ async def search(request: SearchRequest, http_request: Request): f"min_score: {request.min_score} | " f"language: {request.language} | " f"debug: {request.debug} | " - f"ai_search: {request.ai_search} | " + f"enable_rerank: {request.enable_rerank} | " + f"rerank_query_template: {request.rerank_query_template} | " + f"rerank_doc_template: {request.rerank_doc_template} | " f"sku_filter_dimension: {request.sku_filter_dimension} | " f"filters: {request.filters} | " f"range_filters: {request.range_filters} | " @@ -112,7 +114,9 @@ async def search(request: SearchRequest, http_request: Request): debug=request.debug, language=request.language, sku_filter_dimension=request.sku_filter_dimension, - ai_search=request.ai_search, + enable_rerank=request.enable_rerank, + rerank_query_template=request.rerank_query_template, + rerank_doc_template=request.rerank_doc_template, ) # Include performance summary in response diff --git a/config/config.yaml b/config/config.yaml index 7c041da..b28b801 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -133,14 +133,19 @@ function_score: boost_mode: "multiply" functions: [] -# 重排配置(唯一实现:外部 BGE 重排服务,由请求参数 ai_search 控制是否执行) -# ai_search 且 from+size<=rerank_window 时:从 ES 取前 rerank_window 条、重排后再按 from/size 分页 +# 重排配置(唯一实现:外部 BGE 重排服务,由请求参数 enable_rerank 控制是否执行) +# enable_rerank 且 from+size<=rerank_window 时:从 ES 取前 rerank_window 条、重排后再按 from/size 分页 rerank: rerank_window: 1000 # service_url: "http://127.0.0.1:6007/rerank" # 可选,不填则用默认端口 6007 timeout_sec: 15.0 # 文档多时重排耗时长,可按需调大 weight_es: 0.4 weight_ai: 0.6 + # 模板:用于将搜索请求/文档字段组装成重排服务输入 + # - rerank_query_template:支持 {query} + # - rerank_doc_template:支持 {title} {brief} {vendor} {description} {category_path} + rerank_query_template: "{query}" + rerank_doc_template: "{title}" # SPU配置(已启用,使用嵌套skus) spu_config: diff --git a/config/config_loader.py b/config/config_loader.py index 72109f4..47fa38e 100644 --- a/config/config_loader.py +++ b/config/config_loader.py @@ -88,14 +88,19 @@ class RankingConfig: @dataclass class RerankConfig: - """重排配置(唯一实现:调用外部 BGE 重排服务,由请求参数 ai_search 控制是否执行)""" - # 重排窗口:ai_search 且 from+size<=rerank_window 时,从 ES 取前 rerank_window 条重排后再分页 + """重排配置(唯一实现:调用外部 BGE 重排服务,由请求参数 enable_rerank 控制是否执行)""" + # 重排窗口:enable_rerank 且 from+size<=rerank_window 时,从 ES 取前 rerank_window 条重排后再分页 rerank_window: int = 1000 # 可选:重排服务 URL,为空时使用 reranker 模块默认端口 6007 service_url: Optional[str] = None timeout_sec: float = 15.0 weight_es: float = 0.4 weight_ai: float = 0.6 + # 模板:用于将搜索请求/文档字段组装成重排服务输入 + # - rerank_query_template:支持 {query} + # - rerank_doc_template:支持 {title} {brief} {vendor} {description} {category_path} + rerank_query_template: str = "{query}" + rerank_doc_template: str = "{title}" @dataclass @@ -267,7 +272,7 @@ class ConfigLoader: functions=fs_data.get("functions") or [] ) - # Parse Rerank configuration(唯一实现:外部重排服务,由 ai_search 控制) + # Parse Rerank configuration(唯一实现:外部重排服务,由 enable_rerank 控制) rerank_data = config_data.get("rerank", {}) rerank = RerankConfig( rerank_window=int(rerank_data.get("rerank_window", 1000)), @@ -275,6 +280,8 @@ class ConfigLoader: timeout_sec=float(rerank_data.get("timeout_sec", 15.0)), weight_es=float(rerank_data.get("weight_es", 0.4)), weight_ai=float(rerank_data.get("weight_ai", 0.6)), + rerank_query_template=str(rerank_data.get("rerank_query_template") or "{query}"), + rerank_doc_template=str(rerank_data.get("rerank_doc_template") or "{title}"), ) # Parse SPU config @@ -410,6 +417,8 @@ class ConfigLoader: "timeout_sec": config.rerank.timeout_sec, "weight_es": config.rerank.weight_es, "weight_ai": config.rerank.weight_ai, + "rerank_query_template": config.rerank.rerank_query_template, + "rerank_doc_template": config.rerank.rerank_doc_template, }, "spu_config": { "enabled": config.spu_config.enabled, diff --git a/docs/搜索API对接指南.md b/docs/搜索API对接指南.md index 2a07055..4da4c47 100644 --- a/docs/搜索API对接指南.md +++ b/docs/搜索API对接指南.md @@ -167,7 +167,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ "min_score": 0.0, "sku_filter_dimension": ["string"], "debug": false, - "ai_search": false, + "enable_rerank": false, + "rerank_query_template": "{query}", + "rerank_doc_template": "{title}", "user_id": "string", "session_id": "string" } @@ -189,7 +191,9 @@ curl -X POST "http://120.76.41.98:6002/search/" \ | `min_score` | float | N | null | 最小相关性分数阈值 | | `sku_filter_dimension` | array[string] | N | null | 子SKU筛选维度列表(见[SKU筛选维度](#35-sku筛选维度)) | | `debug` | boolean | N | false | 是否返回调试信息 | -| `ai_search` | boolean | N | false | 是否开启 AI 搜索(调用本地重排服务对 ES 结果进行二次排序) | +| `enable_rerank` | boolean | N | false | 是否开启重排(调用外部重排服务对 ES 结果进行二次排序)。开启后若 `from+size<=rerank_window` 才会触发重排 | +| `rerank_query_template` | string | N | null | 重排 query 模板(可选)。支持 `{query}` 占位符;不传则使用服务端配置 | +| `rerank_doc_template` | string | N | null | 重排 doc 模板(可选)。支持 `{title} {brief} {vendor} {description} {category_path}`;不传则使用服务端配置 | | `user_id` | string | N | null | 用户ID(用于个性化,预留) | | `session_id` | string | N | null | 会话ID(用于分析,预留) | diff --git a/query/test_translation.py b/query/test_translation.py index 8422b27..ff38e61 100755 --- a/query/test_translation.py +++ b/query/test_translation.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 """ -翻译功能测试脚本。 +Translation function test script. -测试内容: -1. 翻译提示词配置加载 -2. 同步翻译(索引场景) -3. 异步翻译(查询场景) -4. 不同提示词的使用 -5. 缓存功能 -6. DeepL Context参数使用 +Test content: +1. Translation prompt configuration loading +2. Synchronous translation (indexing scenario) +3. Asynchronous translation (query scenario) +4. Usage of different prompts +5. Cache functionality +6. DeepL Context parameter usage """ import sys @@ -31,37 +31,37 @@ logger = logging.getLogger(__name__) def test_config_loading(): - """测试配置加载""" + """Test configuration loading""" print("\n" + "="*60) - print("测试1: 配置加载") + print("Test 1: Configuration loading") print("="*60) try: config_loader = ConfigLoader() config = config_loader.load_config() - print(f"✓ 配置加载成功") - print(f" 翻译服务: {config.query_config.translation_service}") - print(f" 翻译提示词配置:") + print(f"✓ Configuration loaded successfully") + print(f" Translation service: {config.query_config.translation_service}") + print(f" Translation prompt configuration:") for key, value in config.query_config.translation_prompts.items(): print(f" {key}: {value[:60]}..." if len(value) > 60 else f" {key}: {value}") return config except Exception as e: - print(f"✗ 配置加载失败: {e}") + print(f"✗ Configuration loading failed: {e}") import traceback traceback.print_exc() return None def test_translator_sync(config): - """测试同步翻译(索引场景)""" + """Test synchronous translation (indexing scenario)""" print("\n" + "="*60) - print("测试2: 同步翻译(索引场景)") + print("Test 2: Synchronous translation (indexing scenario)") print("="*60) if not config: - print("✗ 跳过:配置未加载") + print("✗ Skipped: Configuration not loaded") return None try: @@ -90,10 +90,10 @@ def test_translator_sync(config): else: prompt = config.query_config.translation_prompts.get('default_en') - print(f"\n翻译测试:") - print(f" 原文 ({source_lang}): {text}") - print(f" 目标语言: {target_lang}") - print(f" 提示词: {prompt[:50] if prompt else 'None'}...") + print(f"\nTranslation test:") + print(f" Original text ({source_lang}): {text}") + print(f" Target language: {target_lang}") + print(f" Prompt: {prompt[:50] if prompt else 'None'}...") result = translator.translate( text, @@ -103,28 +103,28 @@ def test_translator_sync(config): ) if result: - print(f" 结果: {result}") - print(f" ✓ 翻译成功") + print(f" Result: {result}") + print(f" ✓ Translation successful") else: - print(f" ⚠ 翻译返回None(可能是mock模式或无API key)") + print(f" ⚠ Translation returned None (possibly mock mode or no API key)") return translator except Exception as e: - print(f"✗ 同步翻译测试失败: {e}") + print(f"✗ Synchronous translation test failed: {e}") import traceback traceback.print_exc() return None def test_translator_async(config, translator): - """测试异步翻译(查询场景)""" + """Test asynchronous translation (query scenario)""" print("\n" + "="*60) - print("测试3: 异步翻译(查询场景)") + print("Test 3: Asynchronous translation (query scenario)") print("="*60) if not config or not translator: - print("✗ 跳过:配置或翻译器未初始化") + print("✗ Skipped: Configuration or translator not initialized") return try: @@ -134,9 +134,9 @@ def test_translator_async(config, translator): query_prompt = config.query_config.translation_prompts.get('query_zh') - print(f"查询文本: {query_text}") - print(f"目标语言: {target_langs}") - print(f"提示词: {query_prompt}") + print(f"Query text: {query_text}") + print(f"Target languages: {target_langs}") + print(f"Prompt: {query_prompt}") # 异步模式(立即返回,后台翻译) results = translator.translate_multi( @@ -148,15 +148,15 @@ def test_translator_async(config, translator): prompt=query_prompt ) - print(f"\n异步翻译结果:") + print(f"\nAsynchronous translation results:") for lang, translation in results.items(): if translation: - print(f" {lang}: {translation} (缓存命中)") + print(f" {lang}: {translation} (cache hit)") else: - print(f" {lang}: None (后台翻译中...)") + print(f" {lang}: None (translating in background...)") # 同步模式(等待完成) - print(f"\n同步翻译(等待完成):") + print(f"\nSynchronous translation (waiting for completion):") results_sync = translator.translate_multi( query_text, target_langs, @@ -170,7 +170,7 @@ def test_translator_async(config, translator): print(f" {lang}: {translation}") except Exception as e: - print(f"✗ 异步翻译测试失败: {e}") + print(f"✗ Asynchronous translation test failed: {e}") import traceback traceback.print_exc() @@ -178,7 +178,7 @@ def test_translator_async(config, translator): def test_cache(): """测试缓存功能""" print("\n" + "="*60) - print("测试4: 缓存功能") + print("Test 4: Cache functionality") print("="*60) try: @@ -195,29 +195,29 @@ def test_cache(): source_lang = "zh" prompt = config.query_config.translation_prompts.get('default_zh') - print(f"第一次翻译(应该调用API或返回mock):") + print(f"First translation (should call API or return mock):") result1 = translator.translate(test_text, target_lang, source_lang, prompt=prompt) - print(f" 结果: {result1}") - - print(f"\n第二次翻译(应该使用缓存):") + print(f" Result: {result1}") + + print(f"\nSecond translation (should use cache):") result2 = translator.translate(test_text, target_lang, source_lang, prompt=prompt) - print(f" 结果: {result2}") - + print(f" Result: {result2}") + if result1 == result2: - print(f" ✓ 缓存功能正常") + print(f" ✓ Cache functionality working properly") else: - print(f" ⚠ 缓存可能有问题") + print(f" ⚠ Cache might have issues") except Exception as e: - print(f"✗ 缓存测试失败: {e}") + print(f"✗ Cache test failed: {e}") import traceback traceback.print_exc() def test_context_parameter(): - """测试DeepL Context参数使用""" + """Test DeepL Context parameter usage""" print("\n" + "="*60) - print("测试5: DeepL Context参数") + print("Test 5: DeepL Context parameter") print("="*60) try: @@ -233,8 +233,8 @@ def test_context_parameter(): text = "手机" prompt = config.query_config.translation_prompts.get('query_zh') - print(f"测试文本: {text}") - print(f"提示词(作为context): {prompt}") + print(f"Test text: {text}") + print(f"Prompt (as context): {prompt}") # 带context的翻译 result_with_context = translator.translate( @@ -243,7 +243,7 @@ def test_context_parameter(): source_lang='zh', prompt=prompt ) - print(f"\n带context翻译结果: {result_with_context}") + print(f"\nTranslation result with context: {result_with_context}") # 不带context的翻译 result_without_context = translator.translate( @@ -252,21 +252,21 @@ def test_context_parameter(): source_lang='zh', prompt=None ) - print(f"不带context翻译结果: {result_without_context}") + print(f"Translation result without context: {result_without_context}") - print(f"\n✓ Context参数测试完成") - print(f" 注意:根据DeepL API,context参数影响翻译但不参与翻译本身") + print(f"\n✓ Context parameter test completed") + print(f" Note: According to DeepL API, context parameter affects translation but does not participate in translation itself") except Exception as e: - print(f"✗ Context参数测试失败: {e}") + print(f"✗ Context parameter test failed: {e}") import traceback traceback.print_exc() def main(): - """主测试函数""" + """Main test function""" print("="*60) - print("翻译功能测试") + print("Translation function test") print("="*60) # 测试1: 配置加载 @@ -285,7 +285,7 @@ def main(): test_context_parameter() print("\n" + "="*60) - print("测试完成") + print("Test completed") print("="*60) diff --git a/reranker/bge_reranker.py b/reranker/bge_reranker.py index 5385463..26a3590 100644 --- a/reranker/bge_reranker.py +++ b/reranker/bge_reranker.py @@ -112,6 +112,17 @@ class BGEReranker: total_docs = len(docs) output_scores: List[float] = [0.0] * total_docs + # Log request summary (query + first 3 docs preview) + preview_docs: List[str] = [] + for d in docs[:3]: + preview_docs.append("" if d is None else str(d)) + logger.info( + "[BGE_RERANKER] Request | query=%r | docs=%d | docs_preview=%s", + query, + total_docs, + preview_docs, + ) + indexed_docs: List[Tuple[int, str]] = [] for i, doc in enumerate(docs): if doc is None: @@ -158,6 +169,15 @@ class BGEReranker: for (orig_idx, _text), unique_idx in zip(indexed_docs, position_to_unique): output_scores[orig_idx] = float(unique_scores[unique_idx]) + # Log per-doc scores (aligned to original docs order) + try: + lines = [] + for i, d in enumerate(docs[:100]): + lines.append(f"{output_scores[i]},{'' if d is None else str(d)}") + logger.info("[BGE_RERANKER] query:%s Scores (score,doc):\n%s", query, "\n".join(lines)) + except Exception: + pass + elapsed_ms = (time.time() - start_ts) * 1000.0 dedup_ratio = 0.0 if indexed_docs: diff --git a/search/rerank_client.py b/search/rerank_client.py index 8f11820..0962db0 100644 --- a/search/rerank_client.py +++ b/search/rerank_client.py @@ -22,12 +22,13 @@ DEFAULT_TIMEOUT_SEC = 15.0 def build_docs_from_hits( es_hits: List[Dict[str, Any]], language: str = "zh", + doc_template: str = "{title}", ) -> List[str]: """ 从 ES 命中结果构造重排服务所需的文档文本列表(与 hits 一一对应)。 - 文本由 title、brief、description、vendor、category_path 等多语言字段拼接, - 按 language 优先选取对应语言;若无内容则用 spu_id 兜底。 + 使用 doc_template 将文档字段组装为重排服务输入。 + 支持占位符:{title} {brief} {vendor} {description} {category_path} Args: es_hits: ES 返回的 hits 列表,每项含 _source @@ -47,16 +48,29 @@ def build_docs_from_hits( return str(obj.get(lang) or obj.get("zh") or obj.get("en") or "").strip() return str(obj).strip() + class _SafeDict(dict): + def __missing__(self, key: str) -> str: + return "" + docs: List[str] = [] + only_title = "{title}" == doc_template + need_brief = "{brief}" in doc_template + need_vendor = "{vendor}" in doc_template + need_description = "{description}" in doc_template + need_category_path = "{category_path}" in doc_template for hit in es_hits: src = hit.get("_source") or {} - parts: List[str] = [] - for key in ("title", "brief", "description", "vendor", "category_path"): - parts.append(pick_lang_text(src.get(key))) - text = " ".join(p for p in parts if p).strip() - if not text: - text = str(src.get("spu_id", "")) - docs.append(text) + if only_title: + docs.append(pick_lang_text(src.get("title"))) + else: + values = _SafeDict( + title=pick_lang_text(src.get("title")), + brief=pick_lang_text(src.get("brief")) if need_brief else "", + vendor=pick_lang_text(src.get("vendor")) if need_vendor else "", + description=pick_lang_text(src.get("description")) if need_description else "", + category_path=pick_lang_text(src.get("category_path")) if need_category_path else "", + ) + docs.append(str(doc_template).format_map(values)) return docs @@ -188,6 +202,8 @@ def run_rerank( timeout_sec: float = DEFAULT_TIMEOUT_SEC, weight_es: float = DEFAULT_WEIGHT_ES, weight_ai: float = DEFAULT_WEIGHT_AI, + rerank_query_template: str = "{query}", + rerank_doc_template: str = "{title}", ) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]], List[Dict[str, Any]]]: """ 完整重排流程:从 es_response 取 hits -> 构造 docs -> 调服务 -> 融合分数并重排 -> 更新 max_score。 @@ -222,8 +238,10 @@ def run_rerank( if not hits: return es_response, None, [] - docs = build_docs_from_hits(hits, language=language) - scores, meta = call_rerank_service(query, docs, url, timeout_sec=timeout_sec) + # Apply query template (supports {query}) + query_text = str(rerank_query_template).format_map({"query": query}) + docs = build_docs_from_hits(hits, language=language, doc_template=rerank_doc_template) + scores, meta = call_rerank_service(query_text, docs, url, timeout_sec=timeout_sec) if scores is None or len(scores) != len(hits): return es_response, None, [] diff --git a/search/searcher.py b/search/searcher.py index a0a85ff..38beb1c 100644 --- a/search/searcher.py +++ b/search/searcher.py @@ -135,7 +135,9 @@ class Searcher: debug: bool = False, language: str = "en", sku_filter_dimension: Optional[List[str]] = None, - ai_search: bool = False, + enable_rerank: bool = False, + rerank_query_template: Optional[str] = None, + rerank_doc_template: Optional[str] = None, ) -> SearchResult: """ Execute search query (外部友好格式). @@ -167,11 +169,11 @@ class Searcher: index_langs = tenant_cfg.get("index_languages") or [] enable_translation = len(index_langs) > 0 enable_embedding = self.config.query_config.enable_text_embedding - # 重排仅由请求参数 ai_search 控制,唯一实现为调用外部 BGE 重排服务 - enable_rerank = bool(ai_search) + # 重排仅由请求参数 enable_rerank 控制,唯一实现为调用外部 BGE 重排服务 + do_rerank = bool(enable_rerank) rerank_window = self.config.rerank.rerank_window or 1000 # 若开启重排且请求范围在窗口内:从 ES 取前 rerank_window 条、重排后再按 from/size 分页;否则不重排,按原 from/size 查 ES - in_rerank_window = enable_rerank and (from_ + size) <= rerank_window + in_rerank_window = do_rerank and (from_ + size) <= rerank_window es_fetch_from = 0 if in_rerank_window else from_ es_fetch_size = rerank_window if in_rerank_window else size @@ -180,7 +182,7 @@ class Searcher: context.logger.info( f"开始搜索请求 | 查询: '{query}' | 参数: size={size}, from_={from_}, " - f"enable_rerank={enable_rerank}, in_rerank_window={in_rerank_window}, es_fetch=({es_fetch_from},{es_fetch_size}) | " + f"enable_rerank={do_rerank}, in_rerank_window={in_rerank_window}, es_fetch=({es_fetch_from},{es_fetch_size}) | " f"enable_translation={enable_translation}, enable_embedding={enable_embedding}, min_score={min_score}", extra={'reqid': context.reqid, 'uid': context.uid} ) @@ -192,12 +194,14 @@ class Searcher: 'es_fetch_from': es_fetch_from, 'es_fetch_size': es_fetch_size, 'in_rerank_window': in_rerank_window, + 'rerank_query_template': rerank_query_template, + 'rerank_doc_template': rerank_doc_template, 'filters': filters, 'range_filters': range_filters, 'facets': facets, 'enable_translation': enable_translation, 'enable_embedding': enable_embedding, - 'enable_rerank': enable_rerank, + 'enable_rerank': do_rerank, 'min_score': min_score, 'sort_by': sort_by, 'sort_order': sort_order @@ -206,7 +210,7 @@ class Searcher: context.metadata['feature_flags'] = { 'translation_enabled': enable_translation, 'embedding_enabled': enable_embedding, - 'rerank_enabled': enable_rerank + 'rerank_enabled': do_rerank } # Step 1: Parse query @@ -374,13 +378,15 @@ class Searcher: context.end_stage(RequestContextStage.ELASTICSEARCH_SEARCH) # Optional Step 4.5: AI reranking(仅当请求范围在重排窗口内时执行) - if enable_rerank and in_rerank_window: + if do_rerank and in_rerank_window: context.start_stage(RequestContextStage.RERANKING) try: from .rerank_client import run_rerank rerank_query = parsed_query.original_query if parsed_query else query rc = self.config.rerank + effective_query_template = rerank_query_template or rc.rerank_query_template + effective_doc_template = rerank_doc_template or rc.rerank_doc_template es_response, rerank_meta, fused_debug = run_rerank( query=rerank_query, es_response=es_response, @@ -389,6 +395,8 @@ class Searcher: timeout_sec=rc.timeout_sec, weight_es=rc.weight_es, weight_ai=rc.weight_ai, + rerank_query_template=effective_query_template, + rerank_doc_template=effective_doc_template, ) if rerank_meta is not None: -- libgit2 0.21.2