Skip to content

Commit 10ef831

Browse files
feat: add more advanced fusion strategies (#492)
1 parent 4dd581a commit 10ef831

File tree

13 files changed

+432
-57
lines changed

13 files changed

+432
-57
lines changed

docs/api_reference/core/hybrid.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Hybrid Vector Store & Fusion Strategies
2+
3+
::: ragbits.core.vector_stores.hybrid.HybridSearchVectorStore
4+
5+
::: ragbits.core.vector_stores.hybrid_strategies.OrderedHybridRetrivalStrategy
6+
7+
::: ragbits.core.vector_stores.hybrid_strategies.ReciprocalRankFusion
8+
9+
::: ragbits.core.vector_stores.hybrid_strategies.DistributionBasedScoreFusion

docs/api_reference/core/vector-stores.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
::: ragbits.core.vector_stores.base.VectorStore
88

9-
::: ragbits.core.vector_stores.hybrid.HybridSearchVectorStore
10-
119
::: ragbits.core.vector_stores.in_memory.InMemoryVectorStore
1210

1311
::: ragbits.core.vector_stores.chroma.ChromaVectorStore

docs/how-to/document_search/search-documents.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@ Searching for elements is performed using a vector store. [`DocumentSearch`][rag
2222

2323
One of the simplest vector search strategies used in Ragbits is dense search. This approach leverages an embedding model to generate vector representations of search queries and compares them against the dense vector representations of ingested elements. It is a straightforward method and often serves as a good starting point for developing a retrieval pipeline.
2424

25+
=== "Hybrid search"
26+
27+
```python
28+
from ragbits.core.embeddings import LiteLLMEmbedder
29+
from ragbits.core.vector_stores.qdrant import QdrantVectorStore
30+
from ragbits.core.vector_stores.hybrid import HybridSearchVectorStore
31+
from ragbits.document_search import DocumentSearch
32+
33+
embedder = LiteLLMEmbedder(model="text-embedding-3-small", ...)
34+
vector_store_text = InMemoryVectorStore(embedder=embedder, index_name="text_index", embedding_type=EmbeddingType.TEXT)
35+
vector_store_image = InMemoryVectorStore(embedder=embedder, index_name="image_index", embedding_type=EmbeddingType.IMAGE)
36+
vector_store = HybridSearchVectorStore(vector_store_text, vector_store_image)
37+
38+
document_search = DocumentSearch(vector_store=vector_store, ...)
39+
40+
elements = await document_search.search("What is the capital of Poland?")
41+
```
42+
43+
Hybrid search is a more advanced strategy that combines multiple vector stores, each optimized for different types of data or embedding models. This approach allows for more flexible and efficient retrieval, as it can leverage the strengths of different vector stores to improve search results. For example, you can combine dense and sparse vector stores or use different embedding models for different data types, or like in this example, use one store for text embeddings and another for image embeddings of the same entry.
44+
45+
To learn more about using Hybrid Search, refer to [How to Perform Hybrid Search with Multiple Vector Stores](../vector_stores/hybrid.md).
46+
2547
## Rephrase query
2648

2749
By default, the input query is provided directly to the embedding model. However, there is an option to add an additional step before vector search. Ragbits offers several common rephrasing techniques that can be utilized to refine the query and generate better embeddings for retrieval.

docs/how-to/prompts/prompts_lab.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Prompts Lab is a GUI tool that automatically detects prompts in your project and allows you to interact with them. You can use it to test your prompts with Large Language Models and see how the model responds to different prompts.
44

55
!!! note
6-
To follow this guide, ensure that you have installed the `ragbits` package and are in a directory with Python files that define some ragbits prompts (usually, this would be the root directory of your project) in your command line terminal. If you haven't defined any prompts yet, you can use the `SongPrompt` example from [Ragbit's Quickstart Guide](../../quickstart/quickstart1_prompts.md) and save it in a Python file with a name starting with "prompt_" in your project directory.
6+
To follow this guide, ensure that you have installed the `ragbits` package and are in a directory with Python files that define some ragbits prompts (usually, this would be the root directory of your project) in your command line terminal. If you haven't defined any prompts yet, you can use the `SongPrompt` example from [Ragbits' Quickstart Guide](../../quickstart/quickstart1_prompts.md) and save it in a Python file with a name starting with "prompt_" in your project directory.
77

88
## Starting Prompts Lab
99

@@ -35,7 +35,7 @@ Then, click "Render prompt" to view the final prompt content, with all placehold
3535
!!! note
3636
If there is no [preferred LLM configured for your project](../project/component_preferences.md), Prompts Lab will use OpenAI's gpt-3.5-turbo. Ensure that the OPENAI_API_KEY environment variable is set and contains your OpenAI API key.
3737

38-
Alternatively, you can use your own custom LLM factory (a function that creates an instance of [ragbit's LLM class][ragbits.core.llms.LLM]) by specifying the path to the factory function using the `--llm-factory` option with the `ragbits prompts lab` command.
38+
Alternatively, you can use your own custom LLM factory (a function that creates an instance of [Ragbits' LLM class][ragbits.core.llms.LLM]) by specifying the path to the factory function using the `--llm-factory` option with the `ragbits prompts lab` command.
3939

4040

4141
## Conclusion
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# How to Perform Hybrid Search with Multiple Vector Stores
2+
3+
Ragbits comes with a special type of vector store called [`HybridSearchVectorStore`][ragbits.core.vector_stores.hybrid.HybridSearchVectorStore], which allows you to combine multiple vector stores into a single search index. It acts as a single vector store but internally manages querying and updating multiple vector stores during operations like storing, searching, and deleting entries.
4+
5+
The main use cases for using a hybrid vector store are:
6+
7+
* **Combining Different Modalities**: You can combine multiple vector stores that store different types of data, like text and images. This allows you to store multiple modality-specific vectors for the same entry (for example, an image embedding and a text embedding of a description of the image) and search them together.
8+
* **Combining Different Types of Embeddings**: You can combine multiple vector stores that store different types of embeddings, like dense and sparse embeddings. This allows you to store multiple embeddings for the same entry and search them simultaneously.
9+
10+
!!! info
11+
<!-- TODO: Remove this once sparse embedding support in Vector Stores is implemented -->
12+
Sparse embeddings support in Vector Stores is an upcoming feature of Ragbits. The examples below will be updated to show how to use them with hybrid search once they are available.
13+
14+
## Using a Hybrid Vector Store with Different Modalities
15+
16+
To create a hybrid vector store, you need to pass a list of vector stores to the constructor of the [`HybridSearchVectorStore`][ragbits.core.vector_stores.hybrid.HybridSearchVectorStore] class. For example, this creates two in-memory vector stores—one for text and one for images:
17+
18+
```python
19+
from ragbits.core.vector_stores.hybrid import HybridSearchVectorStore
20+
from ragbits.core.vector_stores.in_memory import InMemoryVectorStore
21+
from ragbits.core.embeddings.vertex_multimodal import VertexAIMultimodelEmbedder
22+
23+
embedder = VertexAIMultimodelEmbedder()
24+
25+
vector_store_text = InMemoryVectorStore(embedder=embedder, embedding_type=EmbeddingType.TEXT)
26+
vector_store_image = InMemoryVectorStore(embedder=embedder, embedding_type=EmbeddingType.IMAGE)
27+
28+
vector_store_hybrid = HybridSearchVectorStore(vector_store_text, vector_store_image)
29+
```
30+
31+
You can then use the `vector_store_hybrid` object to store, search, and delete entries, just as you would use a regular vector store, or pass it to [Ragbits' Document Search](../document_search/ingest-documents.md). When you store an entry in the hybrid vector store, it will be stored in all the vector stores it contains. In this case, one will store the text embedding and the other will store the image embedding.
32+
33+
## Using a Hybrid Vector Store with Different Types of Embeddings
34+
35+
<!-- TODO: Change this example to dense and sparse embeddings once sparse embedding support in Vector Stores is implemented -->
36+
Similarly, you can create a hybrid vector store with different types of embeddings. For example, this creates two in-memory vector stores—one using an embedding model from OpenAI and one using an embedding model from Mistral:
37+
38+
```python
39+
from ragbits.core.vector_stores.hybrid import HybridSearchVectorStore
40+
from ragbits.core.vector_stores.in_memory import InMemoryVectorStore
41+
from ragbits.core.embeddings.litellm import LiteLLMEmbedder
42+
43+
vector_store_openai = InMemoryVectorStore(embedder=LiteLLMEmbedder(model="text-embedding-ada-002"))
44+
vector_store_mistral = InMemoryVectorStore(embedder=LiteLLMEmbedder(model="mistral/mistral-embed"))
45+
46+
vector_store_hybrid = HybridSearchVectorStore(vector_store_openai, vector_store_mistral)
47+
```
48+
49+
You can then use the `vector_store_hybrid` object to store, search, and delete entries, just as you would use a regular vector store, or pass it to [Ragbits' Document Search](../document_search/ingest-documents.md). When you store an entry in the hybrid vector store, it will be stored in all the vector stores it contains. In this case, one will store the embedding using the OpenAI model and the other will store the embedding using the Mistral model.
50+
51+
Note that you can pass an arbitrary number of vector stores to the `HybridSearchVectorStore` constructor, and they can be of any type as long as they implement the `VectorStore` interface. For example, this combines three vector stores—one Chroma vector store, one Qdrant vector store, and one PgVector vector store:
52+
53+
```python
54+
import asyncpg
55+
from chromadb import EphemeralClient
56+
from qdrant_client import AsyncQdrantClient
57+
58+
from ragbits.core.vector_stores.hybrid import HybridSearchVectorStore
59+
from ragbits.core.vector_stores.chroma import ChromaVectorStore
60+
from ragbits.core.vector_stores.qdrant import QdrantVectorStore
61+
from ragbits.core.vector_stores.pgvector import PgVectorStore
62+
from ragbits.core.embeddings.litellm import LiteLLMEmbedder
63+
64+
postgres_pool = await asyncpg.create_pool("postgresql://user:password@localhost/db")
65+
66+
vector_store_hybrid = HybridSearchVectorStore(
67+
ChromaVectorStore(
68+
client=EphemeralClient(),
69+
index_name="chroma_example",
70+
embedder=LiteLLMEmbedder(),
71+
),
72+
QdrantVectorStore(
73+
client=AsyncQdrantClient(location=":memory:"),
74+
index_name="qdrant_example",
75+
embedder=LiteLLMEmbedder(),
76+
),
77+
PgVectorStore(
78+
client=pool,
79+
table_name="postgres_example",
80+
vector_size=1536,
81+
embedder=LiteLLMEmbedder(),
82+
),
83+
)
84+
85+
# The entry will be stored in all three vector stores
86+
await vector_store_hybrid.store([VectorStoreEntry(id=uuid.uuid4(), text="Example entry")])
87+
```
88+
89+
## Specifying the Retrieval Strategy for a Hybrid Vector Store
90+
91+
When you search a hybrid vector store, you can specify a retrieval strategy to determine how the results from the different vector stores are combined. Ragbits comes with the following retrieval strategies:
92+
93+
* [`OrderedHybridRetrivalStrategy`][ragbits.core.vector_stores.hybrid_strategies.OrderedHybridRetrivalStrategy]: This strategy returns the results from the vector stores ordered by their score. If the same entry is found in multiple vector stores, either the highest score is used or if the `sum_scores` parameter is set to `True`, the scores are summed. This is the default strategy.
94+
* [`ReciprocalRankFusion`][ragbits.core.vector_stores.hybrid_strategies.ReciprocalRankFusion]: This strategy combines the results from the vector stores using the [Reciprocal Rank Fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) algorithm, which prioritizes entries that appear at the top of the results from individual vector stores. If the same entry is found in multiple vector stores, the scores are summed by default, or if the `sum_scores` parameter is set to `False`, the highest score is used.
95+
* [`DistributionBasedScoreFusion`][ragbits.core.vector_stores.hybrid_strategies.DistributionBasedScoreFusion]: This strategy combines the results from the vector stores using the [Distribution-Based Score Fusion](https://medium.com/plain-simple-software/distribution-based-score-fusion-dbsf-a-new-approach-to-vector-search-ranking-f87c37488b18) algorithm, which normalizes the scores from the individual vector stores so they can be compared and combined sensibly. If the same entry is found in multiple vector stores, either the highest score is used or if the `sum_scores` parameter is set to `True`, the scores are summed.
96+
97+
Note that summing the scores from individual stores boosts the entries found in multiple stores. This can be useful when searching through multiple types of embeddings but may not be desirable when searching through multiple modalities since entries containing both text and image embeddings would have an advantage over those containing only one.
98+
99+
To specify a retrieval strategy when searching a hybrid vector store, you can pass it as the `retrieval_strategy` parameter to the constructor of the [`HybridSearchVectorStore`][ragbits.core.vector_stores.hybrid.HybridSearchVectorStore] class. For example, this creates a hybrid vector store with the `DistributionBasedScoreFusion` retrieval strategy:
100+
101+
```python
102+
from ragbits.core.vector_stores.hybrid import HybridSearchVectorStore
103+
from ragbits.core.vector_stores.in_memory import InMemoryVectorStore
104+
from ragbits.core.vector_stores.hybrid_strategies import DistributionBasedScoreFusion
105+
from ragbits.core.embeddings.litellm import LiteLLMEmbedder
106+
107+
embedder = LiteLLMEmbedder()
108+
109+
vector_store_text = InMemoryVectorStore(embedder=embedder, embedding_type=EmbeddingType.TEXT)
110+
vector_store_image = InMemoryVectorStore(embedder=embedder, embedding_type=EmbeddingType.IMAGE)
111+
112+
vector_store_hybrid = HybridSearchVectorStore(
113+
vector_store_text,
114+
vector_store_image,
115+
retrieval_strategy=DistributionBasedScoreFusion(),
116+
)
117+
```

docs/quickstart/quickstart2_rag.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Quickstart 2: Adding RAG Capabilities
22

3-
In this chapter, we will explore how to use Ragbit's Document Search capabilities to retrieve relevant documents for your prompts. This technique is based on the Retrieval Augmented Generation (RAG) architecture, which allows the LLM to generate responses informed by relevant information from your documents.
3+
In this chapter, we will explore how to use Ragbits' Document Search capabilities to retrieve relevant documents for your prompts. This technique is based on the Retrieval Augmented Generation (RAG) architecture, which allows the LLM to generate responses informed by relevant information from your documents.
44

55
To work with document content, we first need to "ingest" them (i.e., process, embed, and store them in a vector database). Afterwards, we can search for relevant documents based on the user's input and use the retrieved information to enhance the LLM's response.
66

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ nav:
1919
- "Interact with LLMs": how-to/llms/use_llms.md
2020
- "Use local or self-hosted LLMs": how-to/llms/use_local_llms.md
2121
- Vector Stores:
22+
- "Perform hybrid search": how-to/vector_stores/hybrid.md
2223
- "Use PostgreSQL as a vector store with pgvector": how-to/vector_stores/use_pgVector_store.md
2324
- Project configuration:
2425
- "Set preferred components in project": how-to/project/component_preferences.md
@@ -43,6 +44,7 @@ nav:
4344
- api_reference/core/llms.md
4445
- api_reference/core/embeddings.md
4546
- api_reference/core/vector-stores.md
47+
- api_reference/core/hybrid.md
4648
- Document Search:
4749
- api_reference/document_search/index.md
4850
- api_reference/document_search/documents.md

packages/ragbits-core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CHANGELOG
22

33
## Unreleased
4+
- Add new fusion strategies for the hybrid vector store: RRF and DBSF (#413)
45

56
## 0.13.0 (2025-04-02)
67
- Make the score in VectorStoreResult consistent (always bigger is better)

packages/ragbits-core/src/ragbits/core/vector_stores/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class VectorStoreResult(BaseModel):
5555
vector: list[float]
5656
score: float
5757

58+
# If the results were created by combining multiple results, this field will contain the subresults.
59+
subresults: list["VectorStoreResult"] = []
60+
5861

5962
class VectorStoreOptions(Options):
6063
"""

packages/ragbits-core/src/ragbits/core/vector_stores/hybrid.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import abc
21
import asyncio
32
from uuid import UUID
43

@@ -10,46 +9,7 @@
109
VectorStoreResult,
1110
WhereQuery,
1211
)
13-
14-
15-
class HybridRetrivalStrategy(abc.ABC):
16-
"""
17-
A class that can join vectors retrieved from different vector stores into a single list,
18-
allowing for different strategies for combining results.
19-
"""
20-
21-
@abc.abstractmethod
22-
def join(self, results: list[list[VectorStoreResult]]) -> list[VectorStoreResult]:
23-
"""
24-
Joins the multiple lists of results into a single list.
25-
26-
Args:
27-
results: The lists of results to join.
28-
29-
Returns:
30-
The joined list of results.
31-
"""
32-
33-
34-
class OrderedHybridRetrivalStrategy(HybridRetrivalStrategy):
35-
"""
36-
A class that orders the results by score and deduplicates them by choosing the first occurrence of each entry.
37-
"""
38-
39-
def join(self, results: list[list[VectorStoreResult]]) -> list[VectorStoreResult]: # noqa: PLR6301
40-
"""
41-
Joins the multiple lists of results into a single list.
42-
43-
Args:
44-
results: The lists of results to join.
45-
46-
Returns:
47-
The joined list of results.
48-
"""
49-
all_results = [result for sublist in results for result in sublist]
50-
all_results.sort(key=lambda result: result.score, reverse=True)
51-
52-
return list({result.entry.id: result for result in all_results}.values())
12+
from ragbits.core.vector_stores.hybrid_strategies import HybridRetrivalStrategy, OrderedHybridRetrivalStrategy
5313

5414

5515
class HybridSearchVectorStore(VectorStore):

0 commit comments

Comments
 (0)