Merge pull request #1068 from qdrant/fastembed

Add FastEmbed Documentation, Pt. 1
This commit is contained in:
David Myriel
2024-08-06 15:46:03 -07:00
committed by GitHub
5 changed files with 512 additions and 0 deletions
@@ -0,0 +1,26 @@
---
title: "FastEmbed"
weight: 6
---
# What is FastEmbed?
FastEmbed is a lightweight Python library built for embedding generation. It supports popular embedding models and offers a user-friendly experience for embedding data into vector space.
By using FastEmbed, you can ensure that your embedding generation process is not only fast and efficient but also highly accurate, meeting the needs of various machine learning and natural language processing applications.
FastEmbed easily integrates with Qdrant for a variety of multimodal search purposes.
## How to get started with FastEmbed
|Beginner|Advanced|
|:-:|:-:|
|[Generate Text Embedings with FastEmbed](fastembed-quickstart/)|[Combine FastEmbed with Qdrant for Vector Search](fastembed-semantic-search/)|
## Why is FastEmbed useful?
- Light: Unlike other inference frameworks, such as PyTorch, FastEmbed requires very little external dependencies. Because it uses the ONNX runtime, it is perfect for serverless environments like AWS Lambda.
- Fast: By using ONNX, FastEmbed ensures high-performance inference across various hardware platforms.
- Accurate: FastEmbed aims for better accuracy and recall than models like OpenAI’s `Ada-002`. It always uses model which demonstrate strong results on the MTEB leaderboard.
- Support: FastEmbed supports a wide range of models, including multilingual ones, to meet diverse use case needs.
@@ -0,0 +1,146 @@
---
title: Working with ColBERT
weight: 6
---
# How to Generate ColBERT Multivectors with FastEmbed
With FastEmbed, you can use ColBERT to generate multivector embeddings. ColBERT is a powerful retrieval model that combines the strength of BERT embeddings with efficient late interaction techniques. FastEmbed will provide you with an optimized pipeline to utilize these embeddings in your search tasks.
Please note that ColBERT requires more resources than other no-interaction models. We recommend you use ColBERT as a re-ranker instead of a first-stage retriever.
The first-stage retriever can retrieve 100-500 examples. This task would be done by a simpler model. Then, you can rank the leftover results with ColBERT.
## Setup
This command imports all late interaction models for text embedding.
```python
from fastembed import LateInteractionTextEmbedding
```
You can list which models are supported in your version of FastEmbed.
```python
LateInteractionTextEmbedding.list_supported_models()
```
This command displays the available models. The output shows details about the ColBERT model, including its dimensions, description, size, sources, and model file.
```python
[{'model': 'colbert-ir/colbertv2.0',
'dim': 128,
'description': 'Late interaction model',
'size_in_GB': 0.44,
'sources': {'hf': 'colbert-ir/colbertv2.0'},
'model_file': 'model.onnx'}]
```
Now, load the model.
```python
embedding_model = LateInteractionTextEmbedding("colbert-ir/colbertv2.0")
```
The model files will be fetched and downloaded, with progress showing.
## Embed data
First, you need to define both documents and queries.
```python
documents = [
"ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.",
"On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process",
]
queries = [
"Are there any other late interaction text embedding models except ColBERT?",
"What is the difference between late interaction and early interaction text embedding models?",
]
```
**Note:** ColBERT computes document and query embeddings differently. Make sure to use the corresponding methods.
Now, create embeddings from both documents and queries.
```python
document_embeddings = list(
embedding_model.embed(documents)
) # embed and qury_embed return generators,
# which we need to evaluate by writing them to a list
query_embeddings = list(embedding_model.query_embed(queries))
```
Display the shapes of document and query embeddings.
```python
document_embeddings[0].shape, query_embeddings[0].shape
```
You should get something like this:
```python
((26, 128), (32, 128))
```
Don't worry about query embeddings having the bigger shape in this case. ColBERT authors recommend to pad queries with [MASK] tokens to 32 tokens. They also recommend truncating queries to 32 tokens, however, we don't do that in FastEmbed so that you can put some straight into the queries.
## Compute similarity
This function calculates the relevance scores using the MaxSim operator, sorts the documents based on these scores, and returns the indices of the top-k documents.
```python
import numpy as np
def compute_relevance_scores(query_embedding: np.array, document_embeddings: np.array, k: int):
"""
Compute relevance scores for top-k documents given a query.
:param query_embedding: Numpy array representing the query embedding, shape: [num_query_terms, embedding_dim]
:param document_embeddings: Numpy array representing embeddings for documents, shape: [num_documents, max_doc_length, embedding_dim]
:param k: Number of top documents to return
:return: Indices of the top-k documents based on their relevance scores
"""
# Compute batch dot-product of query_embedding and document_embeddings
# Resulting shape: [num_documents, num_query_terms, max_doc_length]
scores = np.matmul(query_embedding, document_embeddings.transpose(0, 2, 1))
# Apply max-pooling across document terms (axis=2) to find the max similarity per query term
# Shape after max-pool: [num_documents, num_query_terms]
max_scores_per_query_term = np.max(scores, axis=2)
# Sum the scores across query terms to get the total score for each document
# Shape after sum: [num_documents]
total_scores = np.sum(max_scores_per_query_term, axis=1)
# Sort the documents based on their total scores and get the indices of the top-k documents
sorted_indices = np.argsort(total_scores)[::-1][:k]
return sorted_indices
```
Calculate sorted indices.
```python
sorted_indices = compute_relevance_scores(
np.array(query_embeddings[0]), np.array(document_embeddings), k=3
)
print("Sorted document indices:", sorted_indices)
```
The output shows the sorted document indices based on the relevance to the query.
```python
Sorted document indices: [0 1]
```
## Show results
```python
print(f"Query: {queries[0]}")
for index in sorted_indices:
print(f"Document: {documents[index]}")
```
The query and corresponding sorted documents are displayed, showing the relevance of each document to the query.
```bash
Query: Are there any other late interaction text embedding models except ColBERT?
Document: ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.
Document: On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process
```
@@ -0,0 +1,74 @@
---
title: "Quickstart"
weight: 2
---
# How to Generate Text Embedings with FastEmbed
## Install FastEmbed
```python
pip install fastembed
```
Just for demo purposes, you will use Lists and NumPy to work with sample data.
```python
from typing import List
import numpy as np
```
## Load default model
In this example, you will use the default text embedding model, `BAAI/bge-small-en-v1.5`.
```python
from fastembed import TextEmbedding
```
## Add sample data
Now, add two sample documents. Your documents must be in a list, and each document must be a string
```python
documents: List[str] = [
"FastEmbed is lighter than Transformers & Sentence-Transformers.",
"FastEmbed is supported by and maintained by Qdrant.",
]
```
Download and initialize the model. Print a message to verify the process.
```python
embedding_model = TextEmbedding()
print("The model BAAI/bge-small-en-v1.5 is ready to use.")
```
## Embed data
Generate embeddings for both documents.
```python
embeddings_generator = embedding_model.embed(documents)
embeddings_list = list(embeddings_generator)
len(embeddings_list[0])
```
Here is the sample document list. The default model creates vectors with 384 dimensions.
```bash
Document: This is built to be faster and lighter than other embedding libraries e.g. Transformers, Sentence-Transformers, etc.
Vector of type: <class 'numpy.ndarray'> with shape: (384,)
Document: fastembed is supported by and maintained by Qdrant.
Vector of type: <class 'numpy.ndarray'> with shape: (384,)
```
## Visualize embeddings
```python
print("Embeddings:\n", embeddings_list)
```
The embeddings don't look too interesting, but here is a visual.
```bash
Embeddings:
[[-0.11154681 0.00976555 0.00524559 0.01951888 -0.01934952 0.02943449
-0.10519084 -0.00890122 0.01831438 0.01486796 -0.05642502 0.02561352
-0.00120165 0.00637456 0.02633459 0.0089221 0.05313658 0.03955453
-0.04400245 -0.02929407 0.04691846 -0.02515868 0.00778646 -0.05410657
...
-0.00243012 -0.01820582 0.02938612 0.02108984 -0.02178085 0.02971899
-0.00790564 0.03561783 0.0652488 -0.04371546 -0.05550042 0.02651665
-0.01116153 -0.01682246 -0.05976734 -0.03143916 0.06522726 0.01801389
-0.02611006 0.01627177 -0.0368538 0.03968835 0.027597 0.03305927]]
```
@@ -0,0 +1,70 @@
---
title: "FastEmbed & Qdrant"
weight: 3
---
# Using FastEmbed with Qdrant for Vector Search
## Install Qdrant Client
```python
pip install qdrant-client
```
## Install FastEmbed
Installing FastEmbed will let you quickly turn data to vectors, so that Qdrant can search over them.
```python
pip install fastembed
```
## Initialize the client
Qdrant Client has a simple in-memory mode that lets you try semantic search locally.
```python
from qdrant_client import QdrantClient
client = QdrantClient(":memory:") # Qdrant is running from RAM.
```
## Add data
Now you can add two sample documents, their associated metadata, and a point `id` for each.
```python
docs = ["Qdrant has a LangChain integration for chatbots.", "Qdrant has a LlamaIndex integration for agents."]
metadata = [
{"source": "langchain-docs"},
{"source": "llamaindex-docs"},
]
ids = [42, 2]
```
## Load data to a collection
Create a test collection and upsert your two documents to it.
```python
client.add(
collection_name="test_collection",
documents=docs,
metadata=metadata,
ids=ids
)
```
## Run vector search
Here, you will ask a dummy question that will allow you to retrieve a semantically relevant result.
```python
search_result = client.query(
collection_name="test_collection",
query_text="Which integration is best for agents?"
)
print(search_result)
```
The semantic search engine will retrieve the most similar result in order of relevance. In this case, the second statement about LlamaIndex is more relevant.
```bash
[QueryResponse(id=2, embedding=None, sparse_embedding=None,
metadata={'document': 'Qdrant has a LlamaIndex integration for agents',
'source': 'llamaindex-docs'}, document='Qdrant has a LlamaIndex integration for agents.',
score=0.8749180370667156),
QueryResponse(id=42, embedding=None, sparse_embedding=None,
metadata={'document': 'Qdrant has a LangChain integration for chatbots.',
'source': 'langchain-docs'}, document='Qdrant has a LangChain integration for chatbots.',
score=0.8351846822959111)]
```
@@ -0,0 +1,196 @@
---
title: Working with SPLADE
weight: 5
---
# How to Generate Sparse Vectors with SPLADE
SPLADE is a novel method for learning sparse text representation vectors, outperforming BM25 in tasks like information retrieval and document classification. Its main advantage is generating efficient and interpretable sparse vectors, making it effective for large-scale text data.
## Setup
First, install FastEmbed.
```python
pip install -q fastembed
```
Next, import the required modules for sparse embeddings and Python’s typing module.
```python
from fastembed import SparseTextEmbedding, SparseEmbedding
from typing import List
```
You may always check the list of all supported sparse embedding models.
```python
SparseTextEmbedding.list_supported_models()
```
This will return a list of models, each with its details such as model name, vocabulary size, description, and sources.
```python
[{'model': 'prithivida/Splade_PP_en_v1',
'vocab_size': 30522,
'description': 'Independent Implementation of SPLADE++ Model for English',
'size_in_GB': 0.532,
'sources': {'hf': 'Qdrant/SPLADE_PP_en_v1'}}]
```
Now, load the model.
```python
model_name = "prithvida/Splade_PP_en_v1"
# This triggers the model download
model = SparseTextEmbedding(model_name=model_name)
```
## Embed data
You need to define a list of documents to be embedded.
```python
documents: List[str] = [
"Chandrayaan-3 is India's third lunar mission",
"It aimed to land a rover on the Moon's surface - joining the US, China and Russia",
"The mission is a follow-up to Chandrayaan-2, which had partial success",
"Chandrayaan-3 will be launched by the Indian Space Research Organisation (ISRO)",
"The estimated cost of the mission is around $35 million",
"It will carry instruments to study the lunar surface and atmosphere",
"Chandrayaan-3 landed on the Moon's surface on 23rd August 2023",
"It consists of a lander named Vikram and a rover named Pragyan similar to Chandrayaan-2. Its propulsion module would act like an orbiter.",
"The propulsion module carries the lander and rover configuration until the spacecraft is in a 100-kilometre (62 mi) lunar orbit",
"The mission used GSLV Mk III rocket for its launch",
"Chandrayaan-3 was launched from the Satish Dhawan Space Centre in Sriharikota",
"Chandrayaan-3 was launched earlier in the year 2023",
]
```
Then, generate sparse embeddings for each document.
Here,`batch_size` is optional and helps to process documents in batches.
```python
sparse_embeddings_list: List[SparseEmbedding] = list(
model.embed(documents, batch_size=6)
)
```
## Retrieve embeddings
`sparse_embeddings_list` contains sparse embeddings for the documents provided earlier. Each element in this list is a `SparseEmbedding` object that contains the sparse vector representation of a document.
```python
index = 0
sparse_embeddings_list[index]
```
This output is a `SparseEmbedding` object for the first document in our list. It contains two arrays: `values` and `indices`. - The `values` array represents the weights of the features (tokens) in the document. - The `indices` array represents the indices of these features in the model's vocabulary.
Each pair of corresponding `values` and `indices` represents a token and its weight in the document.
```python
SparseEmbedding(values=array([0.05297208, 0.01963477, 0.36459631, 1.38508618, 0.71776593,
0.12667948, 0.46230844, 0.446771 , 0.26897505, 1.01519883,
1.5655334 , 0.29412213, 1.53102326, 0.59785569, 1.1001817 ,
0.02079751, 0.09955651, 0.44249091, 0.09747757, 1.53519952,
1.36765671, 0.15740395, 0.49882549, 0.38629025, 0.76612782,
1.25805044, 0.39058095, 0.27236196, 0.45152301, 0.48262018,
0.26085234, 1.35912788, 0.70710695, 1.71639752]), indices=array([ 1010, 1011, 1016, 1017, 2001, 2018, 2034, 2093, 2117,
2319, 2353, 2509, 2634, 2686, 2796, 2817, 2922, 2959,
3003, 3148, 3260, 3390, 3462, 3523, 3822, 4231, 4316,
4774, 5590, 5871, 6416, 11926, 12076, 16469]))
```
## Examine weights
Now, print the first 5 features and their weights for better understanding.
```python
for i in range(5):
print(f"Token at index {sparse_embeddings_list[0].indices[i]} has weight {sparse_embeddings_list[0].values[i]}")
```
The output will display the token indices and their corresponding weights for the first document.
```python
Token at index 1010 has weight 0.05297207832336426
Token at index 1011 has weight 0.01963476650416851
Token at index 1016 has weight 0.36459630727767944
Token at index 1017 has weight 1.385086178779602
Token at index 2001 has weight 0.7177659273147583
```
## Analyze results
Let's use the tokenizer vocab to make sense of these indices.
```python
import json
from tokenizers import Tokenizer
tokenizer = Tokenizer.from_pretrained(SparseTextEmbedding.list_supported_models()[0]["sources"]["hf"])
```
The `get_tokens_and_weights` function takes a `SparseEmbedding` object and a `tokenizer` as input. It will construct a dictionary where the keys are the decoded tokens, and the values are their corresponding weights.
```python
def get_tokens_and_weights(sparse_embedding, tokenizer):
token_weight_dict = {}
for i in range(len(sparse_embedding.indices)):
token = tokenizer.decode([sparse_embedding.indices[i]])
weight = sparse_embedding.values[i]
token_weight_dict[token] = weight
# Sort the dictionary by weights
token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))
return token_weight_dict
# Test the function with the first SparseEmbedding
print(json.dumps(get_tokens_and_weights(sparse_embeddings_list[index], tokenizer), indent=4))
```
## Dictionary output
The dictionary is then sorted by weights in descending order.
```python
{
"chandra": 1.7163975238800049,
"third": 1.5655333995819092,
"##ya": 1.535199522972107,
"india": 1.5310232639312744,
"3": 1.385086178779602,
"mission": 1.3676567077636719,
"lunar": 1.3591278791427612,
"moon": 1.2580504417419434,
"indian": 1.1001816987991333,
"##an": 1.015198826789856,
"3rd": 0.7661278247833252,
"was": 0.7177659273147583,
"spacecraft": 0.7071069478988647,
"space": 0.5978556871414185,
"flight": 0.4988254904747009,
"satellite": 0.4826201796531677,
"first": 0.46230843663215637,
"expedition": 0.4515230059623718,
"three": 0.4467709958553314,
"fourth": 0.44249090552330017,
"vehicle": 0.390580952167511,
"iii": 0.3862902522087097,
"2": 0.36459630727767944,
"##3": 0.2941221296787262,
"planet": 0.27236196398735046,
"second": 0.26897504925727844,
"missions": 0.2608523368835449,
"launched": 0.15740394592285156,
"had": 0.12667948007583618,
"largest": 0.09955651313066483,
"leader": 0.09747757017612457,
",": 0.05297207832336426,
"study": 0.02079751156270504,
"-": 0.01963476650416851
}
```
## Observations
- The relative order of importance is quite useful. The most important tokens in the sentence have the highest weights.
- **Term Expansion:** The model can expand the terms in the document. This means that the model can generate weights for tokens that are not present in the document but are related to the tokens in the document. This is a powerful feature that allows the model to capture the context of the document. Here, you'll see that the model has added the tokens '3' from 'third' and 'moon' from 'lunar' to the sparse vector.
## Design choices
- The weights are not normalized. This means that the sum of the weights is not 1 or 100. This is a common practice in sparse embeddings, as it allows the model to capture the importance of each token in the document.
- Tokens are included in the sparse vector only if they are present in the model's vocabulary. This means that the model will not generate a weight for tokens that it has not seen during training.
- Tokens do not map to words directly -- allowing you to gracefully handle typo errors and out-of-vocabulary tokens.