Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions backend/services/vectordatabase_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
):
"""
Expand All @@ -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")
Expand All @@ -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)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion frontend/services/knowledgeBaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
: {}),
}),
});

Expand Down
15 changes: 13 additions & 2 deletions sdk/nexent/vector_database/elasticsearch_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
"""
Expand All @@ -1147,14 +1147,25 @@ 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.

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)
Expand Down
46 changes: 46 additions & 0 deletions test/sdk/vector_database/test_elasticsearch_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down
Loading