[BUG] Improvements in HybridSearchConfig — Modifications in Score Fusion Logic and Configuration #81

Closed
opened 2026-02-16 05:16:29 -05:00 by yindo · 3 comments
Owner

Originally created by @alberto-agudo on GitHub (Jul 8, 2025).

Originally assigned to: @dishaprakash on GitHub.

Hello @vishwarajanand. mentioning you here as you've been the contributor of the Hybrid Search solution. First, thank you for working on it.

I have recently refactored my code to include the HybridSearchConfig, and in doing so, I think I've encountered several critical issues that need addressing to ensure correctness in hybrid search behavior.


1. Major Issue: Incorrect Score Fusion Logic

The current default implementation linearly combines the semantic search distance (dense vector):
https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L618-L622

with the full-text search score from ts_rank_cd (sparse):
https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L663

This is fundamentally flawed because:

  • Dense distance: Lower is better (e.g., 0.1 is a strong match).
  • Sparse score: Higher is better (e.g., 0.9 is a strong match).

Current logic (weighted_sum_ranking):
https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/hybrid_search_config.py#L36-L64

# Example of the logic being used
final_score =  dense_distance * dense_weight + sparse_score * sparse_weight

This leads to an inconsistent score, where worse semantic matches can be ranked higher than better ones.

Note that reciprocal rank fusion overcomes these problems, but relies on each of the retrieved documents to be ordered in a ranking where the first element is the highest ranked and the last element is the lowest. This is the case for the inputs given to the reciprocal rank fusion function. However, both are sorted in descending order, which invalidates the assumption that lower distances are better (since the first element in the list will be the one with the highest distance).
https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/hybrid_search_config.py#L93-L95

Hence, both alternatives for retrieving the top k documents from hybrid search are flawed.

Recommendations:

  1. Invert one of the metrics so that they follow the same shape (higher=better / lower=better).
  2. Normalize the metrics so that both scores fall under the same range.

You could also use a modified reciprocal rank fusion function that doesn't sort the inputs again.


2. Configuration and Querying Issues

  1. Inconsistent use of secondary_search_top_k

  2. HybridSearchConfig placement in the __query_collection method


Best,
Alberto.

Originally created by @alberto-agudo on GitHub (Jul 8, 2025). Originally assigned to: @dishaprakash on GitHub. Hello @vishwarajanand. mentioning you here as you've been the contributor of the Hybrid Search solution. First, thank you for working on it. I have recently refactored my code to include the `HybridSearchConfig`, and in doing so, I think I've encountered several critical issues that need addressing to ensure correctness in hybrid search behavior. --- ### 1. Major Issue: Incorrect Score Fusion Logic The current default implementation **linearly combines** the semantic search **distance** (dense vector): https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L618-L622 with the full-text search [**score**](https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING) from `ts_rank_cd` (sparse): https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L663 This is fundamentally flawed because: - **Dense distance**: Lower is better (e.g., 0.1 is a strong match). - **Sparse score**: Higher is better (e.g., 0.9 is a strong match). **Current logic (`weighted_sum_ranking`):** https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/hybrid_search_config.py#L36-L64 ```python # Example of the logic being used final_score = dense_distance * dense_weight + sparse_score * sparse_weight ``` This leads to an inconsistent score, where worse semantic matches can be ranked higher than better ones. Note that reciprocal rank fusion overcomes these problems, but relies on each of the retrieved documents to be ordered in a ranking where the first element is the highest ranked and the last element is the lowest. This is the case for the inputs given to the reciprocal rank fusion function. However, both are sorted in descending order, which invalidates the assumption that lower distances are better (since the first element in the list will be the one with the highest distance). https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/hybrid_search_config.py#L93-L95 Hence, both alternatives for retrieving the top k documents from hybrid search are flawed. #### Recommendations: 1. Invert one of the metrics so that they follow the same shape (higher=better / lower=better). 2. Normalize the metrics so that both scores fall under the same range. You could also use a modified reciprocal rank fusion function that doesn't sort the inputs again. --- ### 2. Configuration and Querying Issues 1. **Inconsistent use of `secondary_search_top_k`** - For the dense search, k is used https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L622 but later the SQL query for sparse search uses `hybrid_search_config.secondary_top_k` instead. - **Fix**: I would use `k` for both queries. 2. **HybridSearchConfig placement** in the `__query_collection` method - Currently initialized too late in the call stack https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L640-L642 - **Fix**: Move initialization before `k` is defined https://github.com/langchain-ai/langchain-postgres/blob/18b1bcdb75ed152da717e3d624e1ed822d17d60f/langchain_postgres/v2/async_vectorstore.py#L583-L592 to ensure proper parameter propagation. --- Best, Alberto.
yindo added the bug label 2026-02-16 05:16:29 -05:00
yindo closed this issue 2026-02-16 05:16:29 -05:00
Author
Owner

@alberto-agudo commented on GitHub (Aug 26, 2025):

Note, I've edited the original comment to indicate that, while reciprocal rank fusion usually solves the mismatch between different metrics, its assumption that distances from primary and secondary search should be sorted before calculation is also flawed. The rest of the reciprocal rank fusion function is ok

@alberto-agudo commented on GitHub (Aug 26, 2025): Note, I've edited the original comment to indicate that, while reciprocal rank fusion usually solves the mismatch between different metrics, its assumption that distances from primary and secondary search should be sorted before calculation is also flawed. The rest of the reciprocal rank fusion function is ok
Author
Owner

@dishaprakash commented on GitHub (Sep 17, 2025):

Hi @alberto-agudo Thank you for opening this detailed issue!
We've logged this as a bug and we will review it. We'll update this thread with our findings or any follow-up questions. We appreciate you taking the time to help improve the project!

@dishaprakash commented on GitHub (Sep 17, 2025): Hi @alberto-agudo Thank you for opening this detailed issue! We've logged this as a bug and we will review it. We'll update this thread with our findings or any follow-up questions. We appreciate you taking the time to help improve the project!
Author
Owner

@dishaprakash commented on GitHub (Oct 12, 2025):

@alberto-agudo This issue has been resolved in #255, #256 & #257. The fix will be available in the next release. Thanks again for the contribution! Closing this issue.

@dishaprakash commented on GitHub (Oct 12, 2025): @alberto-agudo This issue has been resolved in #255, #256 & #257. The fix will be available in the next release. Thanks again for the contribution! Closing this issue.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langchain-postgres#81