diff --git a/backend/consts/model.py b/backend/consts/model.py index 650537bd4d..84a2d340c6 100644 --- a/backend/consts/model.py +++ b/backend/consts/model.py @@ -448,8 +448,12 @@ class HybridSearchRequest(BaseModel): description="List of index names to search") top_k: int = Field(10, ge=1, le=100, description="Number of results to return") - weight_accurate: float = Field(0.5, ge=0.0, le=1.0, - description="Weight applied to accurate search scores") + weight_accurate: Optional[float] = Field( + None, + ge=0.0, + le=1.0, + description="Optional caller-specified weight applied to accurate search scores", + ) # Request models diff --git a/backend/services/vectordatabase_service.py b/backend/services/vectordatabase_service.py index 3d1f07a9a5..05fc4451d7 100644 --- a/backend/services/vectordatabase_service.py +++ b/backend/services/vectordatabase_service.py @@ -2264,7 +2264,7 @@ def search_hybrid( query: str, tenant_id: str, top_k: int = 10, - weight_accurate: float = 0.5, + weight_accurate: Optional[float] = None, vdb_core: VectorDatabaseCore = Depends(get_vector_db_core), ): """ @@ -2279,9 +2279,20 @@ def search_hybrid( raise ValueError("At least one index name is required") if top_k <= 0: raise ValueError("top_k must be greater than 0") - if weight_accurate < 0 or weight_accurate > 1: + if weight_accurate and ( + weight_accurate < 0 or weight_accurate > 1 + ): raise ValueError("weight_accurate must be between 0 and 1") + # Preserve the REST API's historical 0.5 default for ordinary + # queries. When the caller has not supplied a preference, give + # digit-containing identifiers more accurate-search influence. + effective_weight_accurate = weight_accurate + if effective_weight_accurate is None: + effective_weight_accurate = ( + 0.7 if any(char.isdigit() for char in query) else 0.5 + ) + # Get embedding model from the first index's knowledge base record if not index_names: raise ValueError("At least one index name is required") @@ -2306,7 +2317,7 @@ def search_hybrid( query_text=query, embedding_model=embedding_model, top_k=top_k, - weight_accurate=weight_accurate, + weight_accurate=effective_weight_accurate, ) elapsed_ms = int((time.perf_counter() - start_time) * 1000) diff --git a/doc/bug2/screenshots/after-01999-correct-result.png b/doc/bug2/screenshots/after-01999-correct-result.png new file mode 100644 index 0000000000..1908302e9b Binary files /dev/null and b/doc/bug2/screenshots/after-01999-correct-result.png differ diff --git a/doc/bug2/screenshots/before-01999-wrong-result.png b/doc/bug2/screenshots/before-01999-wrong-result.png new file mode 100644 index 0000000000..25377c2033 Binary files /dev/null and b/doc/bug2/screenshots/before-01999-wrong-result.png differ diff --git a/frontend/services/knowledgeBaseService.ts b/frontend/services/knowledgeBaseService.ts index 4878746e50..1326d680b0 100644 --- a/frontend/services/knowledgeBaseService.ts +++ b/frontend/services/knowledgeBaseService.ts @@ -1706,7 +1706,9 @@ class KnowledgeBaseService { query, index_names: [indexName], top_k: options?.topK ?? 10, - weight_accurate: options?.weightAccurate ?? 0.5, + ...(options?.weightAccurate !== undefined + ? { weight_accurate: options.weightAccurate } + : {}), }), }); diff --git a/sdk/nexent/vector_database/elasticsearch_core.py b/sdk/nexent/vector_database/elasticsearch_core.py index e1def70ddc..ce522499e1 100644 --- a/sdk/nexent/vector_database/elasticsearch_core.py +++ b/sdk/nexent/vector_database/elasticsearch_core.py @@ -1136,7 +1136,7 @@ def hybrid_search( query_text: str, embedding_model: BaseEmbedding, top_k: int = 5, - weight_accurate: float = 0.3, + weight_accurate: Optional[float] = None, filter: Optional[Any] = None, ) -> List[Dict[str, Any]]: """ @@ -1147,7 +1147,10 @@ def hybrid_search( query_text: The text query to search for embedding_model: The embedding model to use top_k: Number of results to return - weight_accurate: The weight of the accurate matching score (0-1), the semantic search weight is 1-weight_accurate + weight_accurate: The weight of the accurate matching score (0-1), + with semantic weight ``1 - weight_accurate``. When omitted, + queries containing digits prefer accurate matching (0.7); + all other queries retain the SDK default (0.3). filter: Optional Elasticsearch filter clause applied to both the accurate and semantic sub-queries. When ``None`` (the default), no extra filter is applied and legacy behaviour is preserved. @@ -1155,6 +1158,14 @@ def hybrid_search( Returns: List of search results sorted by combined score """ + if weight_accurate is None: + # Identifiers such as alert numbers and IPs are poorly served by a + # semantic-heavy ranking. Keep the existing retrieval requests and + # only adjust their fusion weight when no caller preference exists. + weight_accurate = ( + 0.7 if any(char.isdigit() for char in query_text) else 0.3 + ) + # Get results from both searches accurate_results = self.accurate_search( index_names, query_text, top_k=top_k, filter=filter) diff --git a/test/sdk/vector_database/test_elasticsearch_core.py b/test/sdk/vector_database/test_elasticsearch_core.py index d8df2806ff..6dd30dea90 100644 --- a/test/sdk/vector_database/test_elasticsearch_core.py +++ b/test/sdk/vector_database/test_elasticsearch_core.py @@ -1608,6 +1608,52 @@ def test_hybrid_search_success(elasticsearch_core_instance): mock_semantic.assert_called_once() +@pytest.mark.parametrize( + ("query_text", "weight_accurate", "expected_first_id"), + [ + ("记录01999", None, "accurate_doc"), + ("记录01999", 0.3, "semantic_doc"), + ("显示全部告警", None, "semantic_doc"), + ], +) +def test_hybrid_search_adapts_only_unspecified_numeric_weights( + elasticsearch_core_instance, + query_text, + weight_accurate, + expected_first_id, +): + """Numeric queries only prefer accurate results when callers omit a weight.""" + mock_embedding_model = MagicMock() + mock_embedding_model.model_type = "text" + + with patch.object(elasticsearch_core_instance, "accurate_search") as mock_accurate, \ + patch.object(elasticsearch_core_instance, "semantic_search") as mock_semantic: + mock_accurate.return_value = [ + { + "score": 1.0, + "document": {"id": "accurate_doc", "content": "记录01999"}, + "index": "test_index", + } + ] + mock_semantic.return_value = [ + { + "score": 1.0, + "document": {"id": "semantic_doc", "content": "相关告警"}, + "index": "test_index", + } + ] + + results = elasticsearch_core_instance.hybrid_search( + ["test_index"], + query_text, + mock_embedding_model, + top_k=2, + weight_accurate=weight_accurate, + ) + + assert results[0]["document"]["id"] == expected_first_id + + def test_get_indices_detail_success(elasticsearch_core_instance): """Test getting index statistics.""" with patch.object(elasticsearch_core_instance.client.indices, 'stats') as mock_stats, \