mirror of
https://github.com/qdrant/fastembed.git
synced 2026-09-22 05:57:51 -05:00
Compare commits
157
Commits
dev-workflow
...
v0.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba1f6053bd | ||
|
|
4dc76e3859 | ||
|
|
6efe06b172 | ||
|
|
887239239b | ||
|
|
ca023be0c0 | ||
|
|
d1ddc8142d | ||
|
|
faf3d9fe18 | ||
|
|
acec31277b | ||
|
|
cb902149c8 | ||
|
|
a260022ae6 | ||
|
|
d5da56299a | ||
|
|
4e5575f4a7 | ||
|
|
4736f46548 | ||
|
|
c85e8c278f | ||
|
|
0df605fc92 | ||
|
|
04bc7a3039 | ||
|
|
b785640bd5 | ||
|
|
5568a62c2f | ||
|
|
b34209dcfb | ||
|
|
c91d42dda7 | ||
|
|
aa0c475a1f | ||
|
|
4c239b11d5 | ||
|
|
6acfb001fb | ||
|
|
1729aab1ec | ||
|
|
42fca3b467 | ||
|
|
2082108baf | ||
|
|
6cda2ce7f0 | ||
|
|
5bd5c0a0f0 | ||
|
|
58ee7cc95c | ||
|
|
27eeb39473 | ||
|
|
4e527b1c63 | ||
|
|
8d04b81782 | ||
|
|
b389798d8e | ||
|
|
6f43572373 | ||
|
|
de4ecb48c7 | ||
|
|
b9d605138f | ||
|
|
a931f143ef | ||
|
|
105ff19035 | ||
|
|
4599b933ca | ||
|
|
0fa1596c3d | ||
|
|
2fe33c5a62 | ||
|
|
969ea29923 | ||
|
|
a5b266e018 | ||
|
|
877d963bd1 | ||
|
|
37a66d9e16 | ||
|
|
b08febbf93 | ||
|
|
6dbdd6d171 | ||
|
|
f1a3a6d082 | ||
|
|
993dcd5f68 | ||
|
|
73e1e5ecb9 | ||
|
|
105d6cfb97 | ||
|
|
bb815405aa | ||
|
|
314842121d | ||
|
|
c2f6fd1c90 | ||
|
|
b05877de93 | ||
|
|
54f6cd9cbc | ||
|
|
ae37da3bd4 | ||
|
|
fa11d0f0c7 | ||
|
|
50289c62ed | ||
|
|
3c10b6625b | ||
|
|
e89654d435 | ||
|
|
cec8d54502 | ||
|
|
55b985c1ad | ||
|
|
c8b1a18cfc | ||
|
|
3b5e4c8722 | ||
|
|
516170cbaf | ||
|
|
0f79d3f9d8 | ||
|
|
2ef9c38b8b | ||
|
|
da30f934d8 | ||
|
|
adfc03ed0d | ||
|
|
e9dc3b1060 | ||
|
|
1343e55076 | ||
|
|
9841666bd5 | ||
|
|
860e2ad691 | ||
|
|
d141e29784 | ||
|
|
7c935717d2 | ||
|
|
8413066f7b | ||
|
|
faed7f1320 | ||
|
|
5b895420b1 | ||
|
|
e868bbaebc | ||
|
|
a5f3f11829 | ||
|
|
3fc0e2b382 | ||
|
|
12b06ece51 | ||
|
|
1deb830328 | ||
|
|
aba8fb43cf | ||
|
|
69ffaa0f48 | ||
|
|
cf67a80ff7 | ||
|
|
a21e925000 | ||
|
|
0638c011dc | ||
|
|
eaecf7d471 | ||
|
|
58b5a8ed9a | ||
|
|
519b310f22 | ||
|
|
e2e1f93685 | ||
|
|
dab4dcc99a | ||
|
|
40a03740ff | ||
|
|
65c2efd6f1 | ||
|
|
97f2fb278e | ||
|
|
fa2205115d | ||
|
|
9445f95a32 | ||
|
|
ab9ab73278 | ||
|
|
08925c9cfc | ||
|
|
3a1f468ef1 | ||
|
|
bfeeb28721 | ||
|
|
a6841a8bde | ||
|
|
62607c237b | ||
|
|
49762a6d19 | ||
|
|
782273f851 | ||
|
|
9c72d2f59f | ||
|
|
0e258ab875 | ||
|
|
e49789c129 | ||
|
|
1bf72922ce | ||
|
|
70566dff99 | ||
|
|
fd116dd507 | ||
|
|
0315c3b8c6 | ||
|
|
54e0f38914 | ||
|
|
3a8985b35c | ||
|
|
f0ff09c546 | ||
|
|
d09af55edd | ||
|
|
9387ca3205 | ||
|
|
9c74fb3cfb | ||
|
|
1fe42d8d18 | ||
|
|
f820c36656 | ||
|
|
35c535aae3 | ||
|
|
e071c84f22 | ||
|
|
e1ecfe9c2f | ||
|
|
331207976e | ||
|
|
fd0b26f009 | ||
|
|
5461012ab1 | ||
|
|
29cfcda056 | ||
|
|
615d6ee2b6 | ||
|
|
bf4ef9d513 | ||
|
|
48c59dc7b3 | ||
|
|
7099dec962 | ||
|
|
a2660f8a3a | ||
|
|
e7d9abaee1 | ||
|
|
01097708fe | ||
|
|
d725974fc4 | ||
|
|
14c067e15e | ||
|
|
c8fff66b18 | ||
|
|
85aaae4c08 | ||
|
|
dfd25d41c9 | ||
|
|
316c33634b | ||
|
|
490c340a76 | ||
|
|
ec8f06978f | ||
|
|
cbe00107ec | ||
|
|
99164c9050 | ||
|
|
6ecab6d40d | ||
|
|
d8c592032b | ||
|
|
da603b8b7d | ||
|
|
562d604375 | ||
|
|
8b1a98a6a3 | ||
|
|
3c9b147e0a | ||
|
|
6abd415f4a | ||
|
|
8184acbb39 | ||
|
|
47cf7f9f92 | ||
|
|
f7896c81f3 | ||
|
|
4a59d09248 |
@@ -1,6 +1,6 @@
|
||||
name: Bug/New Model Request
|
||||
description: File a bug report/Request a new Model
|
||||
title: "[Bug/Model Request]: "
|
||||
name: Bug
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -10,33 +10,36 @@ body:
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
description: Describe the error you encountered.
|
||||
placeholder: <Description>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: Python version
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected behaviour?
|
||||
description: Describe the way you expected the code to behave.
|
||||
placeholder: <Description>
|
||||
- type: textarea
|
||||
id: code-snippet
|
||||
attributes:
|
||||
label: A minimal reproducible example
|
||||
description: It would really help us to fix the problem if you could provide a code snippet that reproduces the issue.
|
||||
placeholder: <Code snippet>
|
||||
- type: textarea
|
||||
id: python-version
|
||||
attributes:
|
||||
label: What Python version are you on? e.g. python --version
|
||||
description: Also tell us, what package manager are you using e.g. conda, pip, poetry?
|
||||
placeholder: Python3.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
label: FastEmbed version
|
||||
description: What version of FastEmbed are you running? python -c "import fastembed; print(fastembed.__version__)". If you're not on the latest, please upgrade and see if the problem persists.
|
||||
options:
|
||||
- 0.2.6 (Latest)
|
||||
- 0.2.5
|
||||
- 0.2.4
|
||||
- 0.2.3
|
||||
- 0.2.2
|
||||
- 0.2.1
|
||||
- 0.1.x
|
||||
default: 0
|
||||
placeholder: v0.5.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: GitHub Community Support
|
||||
url: https://github.com/qdrant/fastembed/discussions
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Feature
|
||||
description: New functionality request
|
||||
title: "[Feature]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this report!
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: What feature would you like to request?
|
||||
description: Please provide the description of the feature you would like to request.
|
||||
placeholder: <Description>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Is there any additional information you would like to provide?
|
||||
description: Please provide any additional information that you think might be useful.
|
||||
placeholder: <Info>
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Model
|
||||
description: Request a new model
|
||||
title: "[Model]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this report!
|
||||
- type: textarea
|
||||
id: model-name
|
||||
attributes:
|
||||
label: Which model would you like to support?
|
||||
description: Please provide the name of the model you would like to see supported.
|
||||
placeholder: Link to the model (e.g. on HuggingFace)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: motivation
|
||||
attributes:
|
||||
label: What are the main advantages of this model?
|
||||
description: Please describe the main advantages of this model comparing to the existing ones and provide links to benchmarks if there are any.
|
||||
placeholder: <Description>
|
||||
@@ -0,0 +1,19 @@
|
||||
### All Submissions:
|
||||
|
||||
* [ ] Have you followed the guidelines in our Contributing document?
|
||||
* [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change?
|
||||
|
||||
<!-- You can erase any parts of this template not applicable to your Pull Request. -->
|
||||
|
||||
### New Feature Submissions:
|
||||
|
||||
* [ ] Does your submission pass the existing tests?
|
||||
* [ ] Have you added tests for your feature?
|
||||
* [ ] Have you installed `pre-commit` with `pip3 install pre-commit` and set up hooks with `pre-commit install`?
|
||||
|
||||
### New models submission:
|
||||
|
||||
* [ ] Have you added an explanation of why it's important to include this model?
|
||||
* [ ] Have you added tests for the new model? Were canonical values for tests computed via the original model?
|
||||
* [ ] Have you added the code snippet for how canonical values were computed?
|
||||
* [ ] Have you successfully ran tests with your changes locally?
|
||||
@@ -21,5 +21,5 @@ jobs:
|
||||
path: .cache
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
- run: pip install mkdocs-material mkdocstrings pillow cairosvg mknotebooks
|
||||
- run: pip install mkdocs-material mkdocstrings==0.27.0 pillow cairosvg mknotebooks
|
||||
- run: mkdocs gh-deploy --force
|
||||
|
||||
@@ -15,7 +15,6 @@ on:
|
||||
tags:
|
||||
- 'v*' # Push events to every version tag
|
||||
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
schedule:
|
||||
- cron: 0 0 * * *
|
||||
pull_request:
|
||||
branches: [ master, main, gpu ]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -16,11 +15,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- '3.8.x'
|
||||
- '3.9.x'
|
||||
- '3.10.x'
|
||||
- '3.11.x'
|
||||
- '3.12.x'
|
||||
- '3.13.x'
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
@@ -40,15 +39,10 @@ jobs:
|
||||
run: |
|
||||
python -m pip install poetry
|
||||
poetry config virtualenvs.create false
|
||||
poetry install --no-interaction --no-ansi --without docs
|
||||
|
||||
- name: Install Test Dependencies
|
||||
run: pip install pytest pytest-md pytest-emoji
|
||||
poetry install --no-interaction --no-ansi --without dev,docs
|
||||
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
with:
|
||||
verbose: true
|
||||
emoji: true
|
||||
job-summary: true
|
||||
report-title: 'FastEmbed Test Report'
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
run: |
|
||||
poetry run pytest
|
||||
@@ -0,0 +1,38 @@
|
||||
name: type-checkers
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
name: Python ${{ matrix.python-version }} test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip poetry
|
||||
poetry install --no-interaction --no-ansi --without dev,docs,test
|
||||
|
||||
- name: mypy
|
||||
run: |
|
||||
poetry run mypy fastembed \
|
||||
--disallow-incomplete-defs \
|
||||
--disallow-untyped-defs \
|
||||
--disable-error-code=import-untyped
|
||||
|
||||
- name: pyright
|
||||
run: |
|
||||
poetry run pyright tests/type_stub.py
|
||||
@@ -0,0 +1,22 @@
|
||||
Copyright 2024 Qdrant
|
||||
|
||||
This product includes software developed by Qdrant
|
||||
|
||||
This distribution includes the following Jina AI models, each with its respective license:
|
||||
- jinaai/jina-colbert-v2
|
||||
- License: cc-by-nc-4.0
|
||||
- jinaai/jina-reranker-v2-base-multilingual
|
||||
- License: cc-by-nc-4.0
|
||||
- jinaai/jina-embeddings-v3
|
||||
- License: cc-by-nc-4.0
|
||||
|
||||
These models are developed by Jina (https://jina.ai/) and are subject to Jina AI's licensing terms.
|
||||
|
||||
This distribution includes the following Google models, each with its respective license:
|
||||
- vidore/colpali-v1.3
|
||||
- License: gemma
|
||||
|
||||
Gemma is provided under and subject to the Gemma Terms of Use found at ai.google.dev/gemma/terms
|
||||
|
||||
Additional Notes:
|
||||
This project also includes third-party libraries with their respective licenses. Please refer to the documentation of each library for details regarding its usage and licensing terms.
|
||||
@@ -8,26 +8,30 @@ The default text embedding (`TextEmbedding`) model is Flag Embedding, presented
|
||||
|
||||
1. Light: FastEmbed is a lightweight library with few external dependencies. We don't require a GPU and don't download GBs of PyTorch dependencies, and instead use the ONNX Runtime. This makes it a great candidate for serverless runtimes like AWS Lambda.
|
||||
|
||||
2. Fast: FastEmbed is designed for speed. We use the ONNX Runtime, which is faster than PyTorch. We also use data-parallelism for encoding large datasets.
|
||||
2. Fast: FastEmbed is designed for speed. We use the ONNX Runtime, which is faster than PyTorch. We also use data parallelism for encoding large datasets.
|
||||
|
||||
3. Accurate: FastEmbed is better than OpenAI Ada-002. We also [supported](https://qdrant.github.io/fastembed/examples/Supported_Models/) an ever expanding set of models, including a few multilingual models.
|
||||
3. Accurate: FastEmbed is better than OpenAI Ada-002. We also [support](https://qdrant.github.io/fastembed/examples/Supported_Models/) an ever-expanding set of models, including a few multilingual models.
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
To install the FastEmbed library, pip works:
|
||||
To install the FastEmbed library, pip works best. You can install it with or without GPU support:
|
||||
|
||||
```bash
|
||||
pip install fastembed
|
||||
|
||||
# or with GPU support
|
||||
|
||||
pip install fastembed-gpu
|
||||
```
|
||||
|
||||
## 📖 Quickstart
|
||||
|
||||
```python
|
||||
from fastembed import TextEmbedding
|
||||
from typing import List
|
||||
|
||||
|
||||
# Example list of documents
|
||||
documents: List[str] = [
|
||||
documents: list[str] = [
|
||||
"This is built to be faster and lighter than other embedding libraries e.g. Transformers, Sentence-Transformers, etc.",
|
||||
"fastembed is supported by and maintained by Qdrant.",
|
||||
]
|
||||
@@ -42,6 +46,189 @@ embeddings_list = list(embedding_model.embed(documents))
|
||||
len(embeddings_list[0]) # Vector of 384 dimensions
|
||||
```
|
||||
|
||||
Fastembed supports a variety of models for different tasks and modalities.
|
||||
The list of all the available models can be found [here](https://qdrant.github.io/fastembed/examples/Supported_Models/)
|
||||
### 🎒 Dense text embeddings
|
||||
|
||||
```python
|
||||
from fastembed import TextEmbedding
|
||||
|
||||
model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
|
||||
embeddings = list(model.embed(documents))
|
||||
|
||||
# [
|
||||
# array([-0.1115, 0.0097, 0.0052, 0.0195, ...], dtype=float32),
|
||||
# array([-0.1019, 0.0635, -0.0332, 0.0522, ...], dtype=float32)
|
||||
# ]
|
||||
|
||||
```
|
||||
|
||||
Dense text embedding can also be extended with models which are not in the list of supported models.
|
||||
|
||||
```python
|
||||
from fastembed import TextEmbedding
|
||||
from fastembed.common.model_description import PoolingType, ModelSource
|
||||
|
||||
TextEmbedding.add_custom_model(
|
||||
model="intfloat/multilingual-e5-small",
|
||||
pooling=PoolingType.MEAN,
|
||||
normalization=True,
|
||||
sources=ModelSource(hf="intfloat/multilingual-e5-small"), # can be used with an `url` to load files from a private storage
|
||||
dim=384,
|
||||
model_file="onnx/model.onnx", # can be used to load an already supported model with another optimization or quantization, e.g. onnx/model_O4.onnx
|
||||
)
|
||||
model = TextEmbedding(model_name="intfloat/multilingual-e5-small")
|
||||
embeddings = list(model.embed(documents))
|
||||
```
|
||||
|
||||
|
||||
### 🔱 Sparse text embeddings
|
||||
|
||||
* SPLADE++
|
||||
|
||||
```python
|
||||
from fastembed import SparseTextEmbedding
|
||||
|
||||
model = SparseTextEmbedding(model_name="prithivida/Splade_PP_en_v1")
|
||||
embeddings = list(model.embed(documents))
|
||||
|
||||
# [
|
||||
# SparseEmbedding(indices=[ 17, 123, 919, ... ], values=[0.71, 0.22, 0.39, ...]),
|
||||
# SparseEmbedding(indices=[ 38, 12, 91, ... ], values=[0.11, 0.22, 0.39, ...])
|
||||
# ]
|
||||
```
|
||||
|
||||
<!--
|
||||
* BM42 - ([link](ToDo))
|
||||
|
||||
```
|
||||
from fastembed import SparseTextEmbedding
|
||||
|
||||
model = SparseTextEmbedding(model_name="Qdrant/bm42-all-minilm-l6-v2-attentions")
|
||||
embeddings = list(model.embed(documents))
|
||||
|
||||
# [
|
||||
# SparseEmbedding(indices=[ 17, 123, 919, ... ], values=[0.71, 0.22, 0.39, ...]),
|
||||
# SparseEmbedding(indices=[ 38, 12, 91, ... ], values=[0.11, 0.22, 0.39, ...])
|
||||
# ]
|
||||
```
|
||||
-->
|
||||
|
||||
### 🦥 Late interaction models (aka ColBERT)
|
||||
|
||||
|
||||
```python
|
||||
from fastembed import LateInteractionTextEmbedding
|
||||
|
||||
model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
|
||||
embeddings = list(model.embed(documents))
|
||||
|
||||
# [
|
||||
# array([
|
||||
# [-0.1115, 0.0097, 0.0052, 0.0195, ...],
|
||||
# [-0.1019, 0.0635, -0.0332, 0.0522, ...],
|
||||
# ]),
|
||||
# array([
|
||||
# [-0.9019, 0.0335, -0.0032, 0.0991, ...],
|
||||
# [-0.2115, 0.8097, 0.1052, 0.0195, ...],
|
||||
# ]),
|
||||
# ]
|
||||
```
|
||||
|
||||
### 🖼️ Image embeddings
|
||||
|
||||
```python
|
||||
from fastembed import ImageEmbedding
|
||||
|
||||
images = [
|
||||
"./path/to/image1.jpg",
|
||||
"./path/to/image2.jpg",
|
||||
]
|
||||
|
||||
model = ImageEmbedding(model_name="Qdrant/clip-ViT-B-32-vision")
|
||||
embeddings = list(model.embed(images))
|
||||
|
||||
# [
|
||||
# array([-0.1115, 0.0097, 0.0052, 0.0195, ...], dtype=float32),
|
||||
# array([-0.1019, 0.0635, -0.0332, 0.0522, ...], dtype=float32)
|
||||
# ]
|
||||
```
|
||||
|
||||
### Late interaction multimodal models (ColPali)
|
||||
|
||||
```python
|
||||
from fastembed import LateInteractionMultimodalEmbedding
|
||||
|
||||
doc_images = [
|
||||
"./path/to/qdrant_pdf_doc_1_screenshot.jpg",
|
||||
"./path/to/colpali_pdf_doc_2_screenshot.jpg",
|
||||
]
|
||||
|
||||
query = "What is Qdrant?"
|
||||
|
||||
model = LateInteractionMultimodalEmbedding(model_name="Qdrant/colpali-v1.3-fp16")
|
||||
doc_images_embeddings = list(model.embed_image(doc_images))
|
||||
# shape (2, 1030, 128)
|
||||
# [array([[-0.03353882, -0.02090454, ..., -0.15576172, -0.07678223]], dtype=float32)]
|
||||
query_embedding = model.embed_text(query)
|
||||
# shape (1, 20, 128)
|
||||
# [array([[-0.00218201, 0.14758301, ..., -0.02207947, 0.16833496]], dtype=float32)]
|
||||
```
|
||||
|
||||
### 🔄 Rerankers
|
||||
```python
|
||||
from fastembed.rerank.cross_encoder import TextCrossEncoder
|
||||
|
||||
query = "Who is maintaining Qdrant?"
|
||||
documents: list[str] = [
|
||||
"This is built to be faster and lighter than other embedding libraries e.g. Transformers, Sentence-Transformers, etc.",
|
||||
"fastembed is supported by and maintained by Qdrant.",
|
||||
]
|
||||
encoder = TextCrossEncoder(model_name="Xenova/ms-marco-MiniLM-L-6-v2")
|
||||
scores = list(encoder.rerank(query, documents))
|
||||
|
||||
# [-11.48061752319336, 5.472434997558594]
|
||||
```
|
||||
|
||||
Text cross encoders can also be extended with models which are not in the list of supported models.
|
||||
|
||||
```python
|
||||
from fastembed.rerank.cross_encoder import TextCrossEncoder
|
||||
from fastembed.common.model_description import ModelSource
|
||||
|
||||
TextCrossEncoder.add_custom_model(
|
||||
model="Xenova/ms-marco-MiniLM-L-4-v2",
|
||||
model_file="onnx/model.onnx",
|
||||
sources=ModelSource(hf="Xenova/ms-marco-MiniLM-L-4-v2"),
|
||||
)
|
||||
model = TextCrossEncoder(model_name="Xenova/ms-marco-MiniLM-L-4-v2")
|
||||
scores = list(model.rerank_pairs(
|
||||
[("What is AI?", "Artificial intelligence is ..."), ("What is ML?", "Machine learning is ..."),]
|
||||
))
|
||||
```
|
||||
|
||||
## ⚡️ FastEmbed on a GPU
|
||||
|
||||
FastEmbed supports running on GPU devices.
|
||||
It requires installation of the `fastembed-gpu` package.
|
||||
|
||||
```bash
|
||||
pip install fastembed-gpu
|
||||
```
|
||||
|
||||
Check our [example](https://qdrant.github.io/fastembed/examples/FastEmbed_GPU/) for detailed instructions, CUDA 12.x support and troubleshooting of the common issues.
|
||||
|
||||
```python
|
||||
from fastembed import TextEmbedding
|
||||
|
||||
embedding_model = TextEmbedding(
|
||||
model_name="BAAI/bge-small-en-v1.5",
|
||||
providers=["CUDAExecutionProvider"]
|
||||
)
|
||||
print("The model BAAI/bge-small-en-v1.5 is ready to use on a GPU.")
|
||||
|
||||
```
|
||||
|
||||
## Usage with Qdrant
|
||||
|
||||
Installation with Qdrant Client in Python:
|
||||
@@ -50,43 +237,45 @@ Installation with Qdrant Client in Python:
|
||||
pip install qdrant-client[fastembed]
|
||||
```
|
||||
|
||||
You might have to use ```pip install 'qdrant-client[fastembed]'``` on zsh.
|
||||
or
|
||||
|
||||
```bash
|
||||
pip install qdrant-client[fastembed-gpu]
|
||||
```
|
||||
|
||||
You might have to use quotes ```pip install 'qdrant-client[fastembed]'``` on zsh.
|
||||
|
||||
```python
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client import QdrantClient, models
|
||||
|
||||
# Initialize the client
|
||||
client = QdrantClient("localhost", port=6333) # For production
|
||||
# client = QdrantClient(":memory:") # For small experiments
|
||||
# client = QdrantClient(":memory:") # For experimentation
|
||||
|
||||
# Prepare your documents, metadata, and IDs
|
||||
docs = ["Qdrant has Langchain integrations", "Qdrant also has Llama Index integrations"]
|
||||
metadata = [
|
||||
{"source": "Langchain-docs"},
|
||||
{"source": "Llama-index-docs"},
|
||||
model_name = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
payload = [
|
||||
{"document": "Qdrant has Langchain integrations", "source": "Langchain-docs", },
|
||||
{"document": "Qdrant also has Llama Index integrations", "source": "LlamaIndex-docs"},
|
||||
]
|
||||
docs = [models.Document(text=data["document"], model=model_name) for data in payload]
|
||||
ids = [42, 2]
|
||||
|
||||
# If you want to change the model:
|
||||
# client.set_model("sentence-transformers/all-MiniLM-L6-v2")
|
||||
# List of supported models: https://qdrant.github.io/fastembed/examples/Supported_Models
|
||||
|
||||
# Use the new add() instead of upsert()
|
||||
# This internally calls embed() of the configured embedding model
|
||||
client.add(
|
||||
collection_name="demo_collection",
|
||||
documents=docs,
|
||||
metadata=metadata,
|
||||
ids=ids
|
||||
client.create_collection(
|
||||
"demo_collection",
|
||||
vectors_config=models.VectorParams(
|
||||
size=client.get_embedding_size(model_name), distance=models.Distance.COSINE)
|
||||
)
|
||||
|
||||
search_result = client.query(
|
||||
client.upload_collection(
|
||||
collection_name="demo_collection",
|
||||
query_text="This is a query document"
|
||||
vectors=docs,
|
||||
ids=ids,
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
search_result = client.query_points(
|
||||
collection_name="demo_collection",
|
||||
query=models.Document(text="This is a query document", model=model_name)
|
||||
).points
|
||||
print(search_result)
|
||||
```
|
||||
|
||||
#### Similar Work
|
||||
|
||||
Ilyas M. wrote about using [FlagEmbeddings with Optimum](https://twitter.com/IlysMoutawwakil/status/1705215192425288017) over CUDA.
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Releasing FastEmbed
|
||||
|
||||
This is a guide how to release `fastembed` and `fastembed-gpu` packages.
|
||||
|
||||
## How to
|
||||
|
||||
1. Accumulate changes in the `main` branch.
|
||||
2. Bump the version in `pyproject.toml`
|
||||
|
||||
3. Rebase the `gpu` branch on `main` and resolve conflicts if occurred:
|
||||
|
||||
```bash
|
||||
git checkout gpu
|
||||
git rebase main
|
||||
git push -f origin gpu
|
||||
```
|
||||
|
||||
4. Draft release notes
|
||||
5. Checkout to `main` and create a tag, e.g.:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
```
|
||||
|
||||
6. Checkout `gpu` and create a tag, e.g.:
|
||||
|
||||
```bash
|
||||
git checkout gpu
|
||||
git tag -a v0.1.0-gpu -m "Release v0.1.0"
|
||||
```
|
||||
|
||||
7. Push tags:
|
||||
|
||||
```bash
|
||||
git push --tags
|
||||
```
|
||||
|
||||
8. Verify that both packages have been published successfully on PyPI. Try installing them and verify imports.
|
||||
9. Create a release on GitHub with the written release notes.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"\n",
|
||||
"## Quick Start\n",
|
||||
"\n",
|
||||
"The fastembed package is designed to be easy to use. We'll be using `TextEmbedding` class. It takes a list of strings as input and returns an generator of vectors. If you're seeing generators for the first time, don't worry, you can convert it to a list using `list()`.\n",
|
||||
"The fastembed package is designed to be easy to use. We'll be using `TextEmbedding` class. It takes a list of strings as input and returns a generator of vectors.\n",
|
||||
"\n",
|
||||
"> 💡 You can learn more about generators from [Python Wiki](https://wiki.python.org/moin/Generators)"
|
||||
]
|
||||
@@ -23,7 +23,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -Uqq fastembed # Install fastembed"
|
||||
"!pip install -Uqq fastembed"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -66,11 +66,12 @@
|
||||
],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"from fastembed import TextEmbedding\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Example list of documents\n",
|
||||
"documents: List[str] = [\n",
|
||||
"documents: list[str] = [\n",
|
||||
" \"This is built to be faster and lighter than other embedding libraries e.g. Transformers, Sentence-Transformers, etc.\",\n",
|
||||
" \"fastembed is supported by and maintained by Qdrant.\",\n",
|
||||
"]\n",
|
||||
@@ -79,9 +80,8 @@
|
||||
"embedding_model = TextEmbedding()\n",
|
||||
"print(\"The model BAAI/bge-small-en-v1.5 is ready to use.\")\n",
|
||||
"\n",
|
||||
"embeddings_generator = embedding_model.embed(documents) # reminder this is a generator\n",
|
||||
"embeddings_generator = embedding_model.embed(documents)\n",
|
||||
"embeddings_list = list(embeddings_generator)\n",
|
||||
"# you can also convert the generator to a list, and that to a numpy array\n",
|
||||
"len(embeddings_list[0]) # Vector of 384 dimensions"
|
||||
]
|
||||
},
|
||||
@@ -113,7 +113,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"embeddings_generator = embedding_model.embed(documents) # reminder this is a generator\n",
|
||||
"embeddings_generator = embedding_model.embed(documents)\n",
|
||||
"\n",
|
||||
"for doc, vector in zip(documents, embeddings_generator):\n",
|
||||
" print(\"Document:\", doc)\n",
|
||||
@@ -138,9 +138,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"embeddings_list = np.array(\n",
|
||||
" list(embedding_model.embed(documents))\n",
|
||||
") # you can also convert the generator to a list, and that to a numpy array\n",
|
||||
"embeddings_list = np.array(list(embedding_model.embed(documents)))\n",
|
||||
"embeddings_list.shape"
|
||||
]
|
||||
},
|
||||
@@ -185,7 +183,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"multilingual_large_model = TextEmbedding(\"intfloat/multilingual-e5-large\") # This can take a few minutes to download"
|
||||
"multilingual_large_model = TextEmbedding(\"intfloat/multilingual-e5-large\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d14d29ebd3592ecb",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"# Late Interaction Text Embedding Models\n",
|
||||
"\n",
|
||||
"As of version 0.3.0 FastEmbed supports Late Interaction Text Embedding Models and currently available with one of the most popular embedding model of the family - ColBERT.\n",
|
||||
"\n",
|
||||
"## What is a Late Interaction Text Embedding Model?\n",
|
||||
"\n",
|
||||
"Late Interaction Text Embedding Model is a kind of information retrieval model which performs query and documents interactions at the scoring stage.\n",
|
||||
"In order to better understand it, we can compare it to the models without interaction. \n",
|
||||
"For instance, if you take a sentence-transformer model, compute embeddings for your documents, compute embeddings for your queries, and just compare them by cosine similarity, then you're retrieving points without interaction.\n",
|
||||
"\n",
|
||||
"It is a pretty much easy and straightforward approach, however we might be sacrificing some precision due to its simplicity. It is caused by several facts: \n",
|
||||
"- there is no interaction between queries and documents at the early stage (embedding generation) nor at the late stage (during scoring). \n",
|
||||
"- we are trying to encapsulate all the document information in only one pooled embedding, and obviously, some information might be lost.\n",
|
||||
"\n",
|
||||
"Late Interaction Text Embedding models are trying to address it by computing embeddings for each token in queries and documents, and then finding the most similar ones via model specific operation, e.g. ColBERT (Contextual Late Interaction over BERT) uses MaxSim operation.\n",
|
||||
"With this approach we can have not only a better representation of the documents, but also make queries and documents more aware one of another.\n",
|
||||
"\n",
|
||||
"For more information on ColBERT and MaxSim operation, you can check out [this blogpost](https://jina.ai/news/what-is-colbert-and-late-interaction-and-why-they-matter-in-search/) by Jina AI.\n",
|
||||
"\n",
|
||||
"## ColBERT in FastEmbed\n",
|
||||
"\n",
|
||||
"FastEmbed provides a simple way to use ColBERT model, similar to the ones it has with `TextEmbedding`.\n",
|
||||
" "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "7f1053b17c810be5",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:20:26.927643Z",
|
||||
"start_time": "2024-06-03T17:20:25.128994Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/Users/joein/work/qdrant/fastembed/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
|
||||
" from .autonotebook import tqdm as notebook_tqdm\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'model': 'colbert-ir/colbertv2.0',\n",
|
||||
" 'dim': 128,\n",
|
||||
" 'description': 'Late interaction model',\n",
|
||||
" 'size_in_GB': 0.44,\n",
|
||||
" 'sources': {'hf': 'colbert-ir/colbertv2.0'},\n",
|
||||
" 'model_file': 'model.onnx'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from fastembed import LateInteractionTextEmbedding\n",
|
||||
"\n",
|
||||
"LateInteractionTextEmbedding.list_supported_models()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "c2c15893df422631",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:23:35.764183Z",
|
||||
"start_time": "2024-06-03T17:23:21.630277Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]\n",
|
||||
"config.json: 100%|██████████| 743/743 [00:00<00:00, 4.56MB/s]\n",
|
||||
"\n",
|
||||
"tokenizer_config.json: 100%|██████████| 405/405 [00:00<00:00, 3.34MB/s]\n",
|
||||
"Fetching 5 files: 20%|██ | 1/5 [00:00<00:01, 3.64it/s]\n",
|
||||
"tokenizer.json: 0%| | 0.00/466k [00:00<?, ?B/s]\u001b[A\n",
|
||||
"\n",
|
||||
"special_tokens_map.json: 100%|██████████| 112/112 [00:00<00:00, 727kB/s]\n",
|
||||
"\n",
|
||||
"tokenizer.json: 100%|██████████| 466k/466k [00:00<00:00, 1.48MB/s]\u001b[A\n",
|
||||
"\n",
|
||||
"model.onnx: 0%| | 0.00/436M [00:00<?, ?B/s]\u001b[A\n",
|
||||
"model.onnx: 2%|▏ | 10.5M/436M [00:00<00:34, 12.2MB/s]\u001b[A\n",
|
||||
"model.onnx: 5%|▍ | 21.0M/436M [00:01<00:20, 20.3MB/s]\u001b[A\n",
|
||||
"model.onnx: 7%|▋ | 31.5M/436M [00:01<00:15, 25.7MB/s]\u001b[A\n",
|
||||
"model.onnx: 10%|▉ | 41.9M/436M [00:01<00:13, 29.4MB/s]\u001b[A\n",
|
||||
"model.onnx: 12%|█▏ | 52.4M/436M [00:01<00:12, 31.9MB/s]\u001b[A\n",
|
||||
"model.onnx: 14%|█▍ | 62.9M/436M [00:02<00:11, 33.7MB/s]\u001b[A\n",
|
||||
"model.onnx: 17%|█▋ | 73.4M/436M [00:02<00:10, 34.9MB/s]\u001b[A\n",
|
||||
"model.onnx: 19%|█▉ | 83.9M/436M [00:02<00:09, 35.5MB/s]\u001b[A\n",
|
||||
"model.onnx: 22%|██▏ | 94.4M/436M [00:03<00:09, 36.1MB/s]\u001b[A\n",
|
||||
"model.onnx: 24%|██▍ | 105M/436M [00:03<00:09, 36.6MB/s] \u001b[A\n",
|
||||
"model.onnx: 26%|██▋ | 115M/436M [00:03<00:08, 36.9MB/s]\u001b[A\n",
|
||||
"model.onnx: 29%|██▉ | 126M/436M [00:03<00:08, 37.1MB/s]\u001b[A\n",
|
||||
"model.onnx: 31%|███▏ | 136M/436M [00:04<00:08, 37.3MB/s]\u001b[A\n",
|
||||
"model.onnx: 34%|███▎ | 147M/436M [00:04<00:07, 37.4MB/s]\u001b[A\n",
|
||||
"model.onnx: 36%|███▌ | 157M/436M [00:04<00:07, 37.4MB/s]\u001b[A\n",
|
||||
"model.onnx: 38%|███▊ | 168M/436M [00:05<00:07, 37.5MB/s]\u001b[A\n",
|
||||
"model.onnx: 41%|████ | 178M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 43%|████▎ | 189M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 46%|████▌ | 199M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 48%|████▊ | 210M/436M [00:06<00:06, 37.5MB/s]\u001b[A\n",
|
||||
"model.onnx: 50%|█████ | 220M/436M [00:06<00:05, 37.5MB/s]\u001b[A\n",
|
||||
"model.onnx: 53%|█████▎ | 231M/436M [00:06<00:05, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 55%|█████▌ | 241M/436M [00:06<00:05, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 58%|█████▊ | 252M/436M [00:07<00:04, 37.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 60%|██████ | 262M/436M [00:07<00:04, 37.7MB/s]\u001b[A\n",
|
||||
"model.onnx: 63%|██████▎ | 273M/436M [00:07<00:04, 37.7MB/s]\u001b[A\n",
|
||||
"model.onnx: 65%|██████▍ | 283M/436M [00:08<00:04, 36.0MB/s]\u001b[A\n",
|
||||
"model.onnx: 67%|██████▋ | 294M/436M [00:08<00:03, 36.4MB/s]\u001b[A\n",
|
||||
"model.onnx: 70%|██████▉ | 304M/436M [00:08<00:03, 36.8MB/s]\u001b[A\n",
|
||||
"model.onnx: 72%|███████▏ | 315M/436M [00:08<00:03, 37.0MB/s]\u001b[A\n",
|
||||
"model.onnx: 75%|███████▍ | 325M/436M [00:09<00:02, 37.3MB/s]\u001b[A\n",
|
||||
"model.onnx: 77%|███████▋ | 336M/436M [00:09<00:03, 30.8MB/s]\u001b[A\n",
|
||||
"model.onnx: 79%|███████▉ | 346M/436M [00:10<00:02, 32.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 82%|████████▏ | 357M/436M [00:10<00:02, 33.9MB/s]\u001b[A\n",
|
||||
"model.onnx: 84%|████████▍ | 367M/436M [00:10<00:01, 34.8MB/s]\u001b[A\n",
|
||||
"model.onnx: 87%|████████▋ | 377M/436M [00:10<00:01, 35.7MB/s]\u001b[A\n",
|
||||
"model.onnx: 89%|████████▉ | 388M/436M [00:11<00:01, 36.2MB/s]\u001b[A\n",
|
||||
"model.onnx: 91%|█████████▏| 398M/436M [00:11<00:01, 36.6MB/s]\u001b[A\n",
|
||||
"model.onnx: 94%|█████████▍| 409M/436M [00:11<00:00, 36.9MB/s]\u001b[A\n",
|
||||
"model.onnx: 96%|█████████▌| 419M/436M [00:11<00:00, 37.1MB/s]\u001b[A\n",
|
||||
"model.onnx: 99%|█████████▊| 430M/436M [00:12<00:00, 37.3MB/s]\u001b[A\n",
|
||||
"model.onnx: 100%|██████████| 436M/436M [00:12<00:00, 35.1MB/s]\u001b[A\n",
|
||||
"Fetching 5 files: 100%|██████████| 5/5 [00:13<00:00, 2.68s/it]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"embedding_model = LateInteractionTextEmbedding(\"colbert-ir/colbertv2.0\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"id": "e560b5fa7d63bea3",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:39:33.400876Z",
|
||||
"start_time": "2024-06-03T17:39:33.397431Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"documents = [\n",
|
||||
" \"ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.\",\n",
|
||||
" \"On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process\",\n",
|
||||
"]\n",
|
||||
"queries = [\n",
|
||||
" \"Are there any other late interaction text embedding models except ColBERT?\",\n",
|
||||
" \"What is the difference between late interaction and early interaction text embedding models?\",\n",
|
||||
"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "347ad924a3449743",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"*NOTE*: ColBERT computes query and documents embeddings differently, make sure to use the corresponding methods."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"id": "496fbf51e4eaaae",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:39:34.379885Z",
|
||||
"start_time": "2024-06-03T17:39:34.316257Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"document_embeddings = list(\n",
|
||||
" embedding_model.embed(documents)\n",
|
||||
") # embed and qury_embed return generators,\n",
|
||||
"# which we need to evaluate by writing them to a list\n",
|
||||
"query_embeddings = list(embedding_model.query_embed(queries))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "50595bb0498f0c7c",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:39:34.793528Z",
|
||||
"start_time": "2024-06-03T17:39:34.788545Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"((26, 128), (32, 128))"
|
||||
]
|
||||
},
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"document_embeddings[0].shape, query_embeddings[0].shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "13e43f2c24a7d5fc",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"Don't worry about query embeddings having the bigger shape in this case. \n",
|
||||
"ColBERT authors recommend to pad queries with [MASK] tokens to 32 tokens.\n",
|
||||
"They also recommends to truncate queries to 32 tokens, however we don't do that in FastEmbed, so you can put some straight into the queries."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bb1a4011effd3699",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"## MaxSim operator"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e9ea4cf82521f2de",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"Qdrant will support ColBERT as of the next version (v1.10), however, at the moment, you can compute embedding similarities manually. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"id": "f84392f63d2c6076",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:39:36.431622Z",
|
||||
"start_time": "2024-06-03T17:39:36.427363Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def compute_relevance_scores(\n",
|
||||
" query_embedding: np.array, document_embeddings: np.array, k: int\n",
|
||||
") -> list[int]:\n",
|
||||
" \"\"\"\n",
|
||||
" Compute relevance scores for top-k documents given a query.\n",
|
||||
"\n",
|
||||
" :param query_embedding: Numpy array representing the query embedding, shape: [num_query_terms, embedding_dim]\n",
|
||||
" :param document_embeddings: Numpy array representing embeddings for documents, shape: [num_documents, max_doc_length, embedding_dim]\n",
|
||||
" :param k: Number of top documents to return\n",
|
||||
" :return: Indices of the top-k documents based on their relevance scores\n",
|
||||
" \"\"\"\n",
|
||||
" # Compute batch dot-product of query_embedding and document_embeddings\n",
|
||||
" # Resulting shape: [num_documents, num_query_terms, max_doc_length]\n",
|
||||
" scores = np.matmul(query_embedding, document_embeddings.transpose(0, 2, 1))\n",
|
||||
"\n",
|
||||
" # Apply max-pooling across document terms (axis=2) to find the max similarity per query term\n",
|
||||
" # Shape after max-pool: [num_documents, num_query_terms]\n",
|
||||
" max_scores_per_query_term = np.max(scores, axis=2)\n",
|
||||
"\n",
|
||||
" # Sum the scores across query terms to get the total score for each document\n",
|
||||
" # Shape after sum: [num_documents]\n",
|
||||
" total_scores = np.sum(max_scores_per_query_term, axis=1)\n",
|
||||
"\n",
|
||||
" # Sort the documents based on their total scores and get the indices of the top-k documents\n",
|
||||
" sorted_indices = np.argsort(total_scores)[::-1][:k]\n",
|
||||
"\n",
|
||||
" return sorted_indices"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"id": "c61d07bed7b60e35",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:39:37.053383Z",
|
||||
"start_time": "2024-06-03T17:39:37.050926Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Sorted document indices: [0 1]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"sorted_indices = compute_relevance_scores(\n",
|
||||
" np.array(query_embeddings[0]), np.array(document_embeddings), k=3\n",
|
||||
")\n",
|
||||
"print(\"Sorted document indices:\", sorted_indices)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"id": "b24df2569970d9e8",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-03T17:40:52.276846Z",
|
||||
"start_time": "2024-06-03T17:40:52.273789Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Query: Are there any other late interaction text embedding models except ColBERT?\n",
|
||||
"Document: ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.\n",
|
||||
"Document: On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(f\"Query: {queries[0]}\")\n",
|
||||
"for index in sorted_indices:\n",
|
||||
" print(f\"Document: {documents[index]}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6de537c37aff3927",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"## Use-case recommendation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "37e3525d3259cd2b",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"Despite ColBERT allows to compute embeddings independently and spare some workload offline, it still computes more resources than no interaction models. Due to this, it might be more reasonable to use ColBERT not as a first-stage retriever, but as a re-ranker.\n",
|
||||
"\n",
|
||||
"The first-stage retriever would then be a no-interaction model, which e.g. retrieves first 100 or 500 examples, and leave the final ranking to the ColBERT model."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cfa922793454b4ad",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "ntGNDuSCeAR2"
|
||||
},
|
||||
"source": [
|
||||
"# FastEmbed on GPU\n",
|
||||
"\n",
|
||||
"As of version 0.2.7 FastEmbed supports GPU acceleration.\n",
|
||||
"\n",
|
||||
"This notebook covers the installation process and usage of fastembed on GPU.\n",
|
||||
"\n",
|
||||
"## Installation\n",
|
||||
"\n",
|
||||
"Fastembed depends on `onnxruntime` and inherits its scheme of GPU support.\n",
|
||||
"\n",
|
||||
"In order to use GPU with onnx models, you would need to have `onnxruntime-gpu` package, which substitutes all the `onnxruntime` functionality.\n",
|
||||
"Fastembed mimics this behavior and requires `fastembed-gpu` package to be installed."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "GK2XADwUeEK7"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install fastembed-gpu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "3aiGPqjCeGzo"
|
||||
},
|
||||
"source": [
|
||||
"**NOTE**: `onnxruntime-gpu` and `onnxruntime` can't be installed in the same environment. If you have `onnxruntime` installed, you would need to uninstall it before installing `onnxruntime-gpu`. Same is true for `fastembed` and `fastembed-gpu`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "3xx3r-9jgAMi"
|
||||
},
|
||||
"source": [
|
||||
"### CUDA 12.x support\n",
|
||||
"You can check your CUDA version using such commands as `nvidia-smi` or `nvcc --version`\n",
|
||||
"\n",
|
||||
"Starting from version 1.19.0, onnxruntime-gpu ships with support for CUDA 12.x by default.\n",
|
||||
"\n",
|
||||
"Google Colab notebooks have by default CUDA 12.x and CuDNN 8.x.\n",
|
||||
"\n",
|
||||
"Latest version of `onnxruntime-gpu` requires CuDNN 9.x, in order to install it you can run the following command: "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!sudo apt install cudnn9\n",
|
||||
"!pip install fastembed-gpu -qqq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If it necessary to work with CuDNN 8, you can consider locking `onnxruntime-gpu` to 1.18.0 with CUDA 12.x by this command:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install onnxruntime-gpu==1.18.0 -i https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/ -qq\n",
|
||||
"!pip install fastembed-gpu -qqq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### CUDA 11.x support\n",
|
||||
"To use latest version of `onnxruntime-gpu` with CUDA 11.x, you can run the following command:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install onnxruntime-gpu -i https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-11/pypi/simple/ -qq"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**NOTE**: Ensure that CuDNN 9.x is installed when working with the latest `onnxruntime-gpu`, whether using CUDA 11.x or 12.x."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "Igv5RXhSeO68"
|
||||
},
|
||||
"source": [
|
||||
"### CUDA drivers\n",
|
||||
"\n",
|
||||
"FastEmbed does not include CUDA drivers and CuDNN libraries.\n",
|
||||
"You would need to take care of the environment setup on your own.\n",
|
||||
"The dependencies required for the chosen onnxruntime version are listed in the [CUDA Execution Provider requirements](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Setting up fastembed-gpu on GCP\n",
|
||||
"\n",
|
||||
"#### CUDA drivers\n",
|
||||
"[CUDA 11.8 toolkit](https://developer.nvidia.com/cuda-11-8-0-download-archive) or [CUDA 12.x toolkit](https://developer.nvidia.com/cuda-downloads) has to be installed if they haven't yet been set up.\n",
|
||||
"\n",
|
||||
"#### Example of setting up CUDA 12.x on Ubuntu 22.04\n",
|
||||
"Make sure to download an archive which has been created for your particular platform, CPU architecture and OS distribution.\n",
|
||||
"\n",
|
||||
"For Ubuntu 22.04 with x86_64 CPU architecture the following [archive](https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=22.04&target_type=deb_network) has to be downloaded.\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb\n",
|
||||
"sudo dpkg -i cuda-keyring_1.1-1_all.deb\n",
|
||||
"sudo apt-get update\n",
|
||||
"sudo apt-get -y install cuda\n",
|
||||
"```\n",
|
||||
"**NOTE**: Specific CUDA libraries can be found in the [meta packages section](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/#meta-packages) in the CUDA installation guide.\n",
|
||||
"\n",
|
||||
"**NOTE**: When installing CUDA, the environment variable might not be set by default. Make sure to add the following line to your environment variables:\n",
|
||||
"```bash\n",
|
||||
"LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH\n",
|
||||
"```\n",
|
||||
"This will ensure that the CUDA libraries are properly linked.\n",
|
||||
"\n",
|
||||
"#### CuDNN 9.x\n",
|
||||
"CuDNN 9.x library can be installed via the following [archive](https://developer.nvidia.com/rdp/cudnn-archive).\n",
|
||||
"\n",
|
||||
"#### Example of setting up CuDNN 9.x on Ubuntu 22.04\n",
|
||||
"CuDNN 9.x for Ubuntu 22.04 x86_64 [archive](https://developer.nvidia.com/cudnn-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=22.04&target_type=deb_network) can be downloaded and installed in the following way:\n",
|
||||
"```bash\n",
|
||||
"wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb\n",
|
||||
"sudo dpkg -i cuda-keyring_1.1-1_all.deb\n",
|
||||
"sudo apt-get update\n",
|
||||
"sudo apt-get -y install cudnn\n",
|
||||
"```\n",
|
||||
"**NOTE**: When installing CuDNN, you can choose specific version, cudnn-cuda-11 or cudnn-cuda-12"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Common issues\n",
|
||||
"\n",
|
||||
"The following are some common issues that may arise while using `fastembed-gpu` if not installed properly:\n",
|
||||
"\n",
|
||||
"CUDA library is not installed:\n",
|
||||
"```bash\n",
|
||||
"FAIL : Failed to load library libonnxruntime_providers_cuda.so with error: libcublasLt.so.x: cannot open shared object file: No such file or directory\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"CuDNN library is not installed:\n",
|
||||
"```bash\n",
|
||||
"FAIL : Failed to load library libonnxruntime_providers_cuda.so with error: libcudnn.so.x: cannot open shared object file: No such file or directory\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"CUDA library path is not set:\n",
|
||||
"```bash\n",
|
||||
"FAIL : Failed to load library libonnxruntime_providers_cuda.so with error: libcufft.so.x: failed to map segment from shared object\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"Make sure to add the following line to your environment variables:\n",
|
||||
"```bash\n",
|
||||
"LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Usage"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 334,
|
||||
"referenced_widgets": [
|
||||
"aacf08a7aa444b64a2efad1967d28a53",
|
||||
"5606aa785de74d65a9928b31c0be8a53",
|
||||
"d4ec9d3b74ec4412894da2161ed2bddf",
|
||||
"8edd544c3e074ec1813e5b9d1aef43d9",
|
||||
"9898890f8a75468ea20e3ce319d0b6e2",
|
||||
"da3b18abb16241a0a7191ee9afcb0510",
|
||||
"258a619168824253a6a329efdc51ebe6",
|
||||
"53c7cdc967d24faba0b5c659c94c50b8",
|
||||
"e9348d8be28d408e8e760c71b21ab294",
|
||||
"0ba06e0816714f2fbdec8260f160abc0",
|
||||
"0b96563334964d449dd34f35b6b3e715",
|
||||
"11c2eec490e8479b944eec7f30cb1ca2",
|
||||
"91463da0d1c5466795e06ab586002259",
|
||||
"30f4f7833406474f89ef0700b00a33aa",
|
||||
"4302c304ec6a4b5985797e300bd7e353",
|
||||
"2605640c7b824ed7aa137d404e14b774",
|
||||
"b02efe3a33d04f06aa8938719ab35671",
|
||||
"50408e5d052343b1a1b44a0fae0f801d",
|
||||
"1be01c95d9e84f8ea88367c987a72fdc",
|
||||
"a109c13bc93a449186424542dc330be8",
|
||||
"4adce304ce1947b5a01dde10bbb3bb8c",
|
||||
"a761366a37e44837a25e0f25b18efed2",
|
||||
"94512b9055e546389471197b76ad5449",
|
||||
"072dca00bd7b4918a178f90ccabf698a",
|
||||
"48a856c59ef74cc3834521b1bf616541",
|
||||
"c020c503aeaa464cad643ade5ee3ae24",
|
||||
"a4e7e40c0bbd4f878c20a9f65fe3a048",
|
||||
"e8c0a1c339fd47668d944a9defad79d4",
|
||||
"cd782d35c6bd40c0a60d57b1828a7251",
|
||||
"04f638ab08da4d20928644c4ba03f8ef",
|
||||
"17f20477fc79475f97adf1c1f64a4192",
|
||||
"96f7b5a2e224462e9fcffd03f906a593",
|
||||
"755cd32d9fc9407c80a160f45c802d1e",
|
||||
"a886258e7cd14c048b58391d7b772901",
|
||||
"bc3e48f826a74840867a6209e622b75e",
|
||||
"125b2ac0f78043bba7eca53474ca44c4",
|
||||
"82f186d1ffb4435d94a6c7e9025242ef",
|
||||
"77000333e5ca4094be291ad82d4a627a",
|
||||
"7fe64fb53055431488d002c76c8e331e",
|
||||
"7de59ae9919f4a5bb2b6e601a3c02412",
|
||||
"97a69423a6644eab87fc636e182f23a4",
|
||||
"4df936d1065b41f4bf02ed394fdf7b7e",
|
||||
"3918bd1affa3454e8e9044a418a056ea",
|
||||
"163b27ae0bce41e5b48efcb4b3fd780d",
|
||||
"94631fd6e0744085bc79c3121de4a9f7",
|
||||
"31cd98d66bc54418b35e70fbbc0fa3c0",
|
||||
"6d21627a638b4ddca6fe7bfb80a621b5",
|
||||
"b37bed9dc4fe45c08b8397288fe5b1a9",
|
||||
"164fef95d1414177a40d563f5682f6a3",
|
||||
"1a9a0ea53448413a8e4b360b7bb69e26",
|
||||
"dd1a4483b4b045c6929e3d2cf1338f63",
|
||||
"496ddd8e05f949cd8cbba8e677f476ac",
|
||||
"2813be951d7f48b2aad1dd4a444ce3eb",
|
||||
"8e9a2c2dd21942edbdfecb3b7dffc70b",
|
||||
"08a10fe247f1425db044cfc13f2fb384",
|
||||
"b8786aded92d421592bc7623c5c7899e",
|
||||
"c91a20a9433d4016ba2db69fa50e0b4d",
|
||||
"e997820738594c6dadb061908d7afdc1",
|
||||
"a5fc751f81ae498f9aa55ece0e6853b2",
|
||||
"2aee4fc8cda64c5eb8722be81e48e0ca",
|
||||
"3a53e8624dff48b3959875ef58ee99ce",
|
||||
"50a70044f77542108fe188598e70797e",
|
||||
"13cf998b35ae4507a63e797f6fa3eada",
|
||||
"6209eb6a68cf4a378767ef34d0d9216d",
|
||||
"7395db766b944af9b41d6b56c9ada0b1",
|
||||
"42122c317ec648688f0164a1adb5df28"
|
||||
]
|
||||
},
|
||||
"id": "Ttf4YggPeQQK",
|
||||
"outputId": "aa75129d-9e2d-4c88-cf03-251dd43a11b1"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_token.py:88: UserWarning: \n",
|
||||
"The secret `HF_TOKEN` does not exist in your Colab secrets.\n",
|
||||
"To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n",
|
||||
"You will be able to reuse this secret in all of your notebooks.\n",
|
||||
"Please note that authentication is recommended but still optional to access public models or datasets.\n",
|
||||
" warnings.warn(\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "aacf08a7aa444b64a2efad1967d28a53",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "11c2eec490e8479b944eec7f30cb1ca2",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"tokenizer_config.json: 0%| | 0.00/1.24k [00:00<?, ?B/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "94512b9055e546389471197b76ad5449",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"config.json: 0%| | 0.00/706 [00:00<?, ?B/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "a886258e7cd14c048b58391d7b772901",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"special_tokens_map.json: 0%| | 0.00/695 [00:00<?, ?B/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "94631fd6e0744085bc79c3121de4a9f7",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"tokenizer.json: 0%| | 0.00/711k [00:00<?, ?B/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "b8786aded92d421592bc7623c5c7899e",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"model_optimized.onnx: 0%| | 0.00/66.5M [00:00<?, ?B/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['CUDAExecutionProvider', 'CPUExecutionProvider']"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"from fastembed import TextEmbedding\n",
|
||||
"\n",
|
||||
"embedding_model_gpu = TextEmbedding(\n",
|
||||
" model_name=\"BAAI/bge-small-en-v1.5\", providers=[\"CUDAExecutionProvider\"]\n",
|
||||
")\n",
|
||||
"embedding_model_gpu.model.model.get_providers()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"id": "iPtoHf7GeV-i"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": "documents: list[str] = list(np.repeat(\"Demonstrating GPU acceleration in fastembed\", 500))"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "islhyLf4ed-H",
|
||||
"outputId": "8c8ed09b-9eac-438f-97bc-578751975148"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"43.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%%timeit\n",
|
||||
"list(embedding_model_gpu.embed(documents))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 67,
|
||||
"referenced_widgets": [
|
||||
"9c306ce5188c45feb8dfb9089592591c",
|
||||
"296ff54c6e61441f978084df59626598",
|
||||
"d6d42b4f245a49b7ba7769e23a3202fc",
|
||||
"39ce7754480147759c16a3089d8105af",
|
||||
"8253960a069d4106863a75faae54b90d",
|
||||
"7ccf959452af4c0b873c7567747f0816",
|
||||
"ac9d0b5a5b1f401e90a1cc9ffe6d4b4c",
|
||||
"0aada067dec3472f9aba1772d6b775a5",
|
||||
"07597b1287e04653b80c47a771549376",
|
||||
"054be1dd9f084cae911745b692ccd929",
|
||||
"ab19e8e831694e308a4b79f05aff728e"
|
||||
]
|
||||
},
|
||||
"id": "bOKVUvWJegYJ",
|
||||
"outputId": "dde74917-08b0-4ce2-9a2b-cc31e02cafb2"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "9c306ce5188c45feb8dfb9089592591c",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['CPUExecutionProvider']"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"embedding_model_cpu = TextEmbedding(model_name=\"BAAI/bge-small-en-v1.5\")\n",
|
||||
"embedding_model_cpu.model.model.get_providers()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
"id": "0NJj9RvSfASP",
|
||||
"outputId": "526f5280-99bd-454e-8af8-6a860ad96e54"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"4.33 s ± 591 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%%timeit\n",
|
||||
"list(embedding_model_cpu.embed(documents))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"gpuType": "T4",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 1
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Fastembed Multi-GPU Tutorial\n",
|
||||
"This tutorial demonstrates how to leverage multi-GPU support in Fastembed. Fastembed supports embedding text and images utilizing modern GPUs for acceleration. Let's explore how to use Fastembed with multiple GPUs step by step."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Prerequisites\n",
|
||||
"To get started, ensure you have the following installed:\n",
|
||||
"- Python 3.9 or later\n",
|
||||
"- Fastembed (`pip install fastembed-gpu`)\n",
|
||||
"- Refer to [this](https://github.com/qdrant/fastembed/blob/main/docs/examples/FastEmbed_GPU.ipynb) tutorial if you have issues with GPU dependencies\n",
|
||||
"- Access to a multi-GPU server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Multi-GPU using cuda argument with TextEmbedding Model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from fastembed import TextEmbedding\n",
|
||||
"\n",
|
||||
"# define the documents to embed\n",
|
||||
"docs = [\"hello world\", \"flag embedding\"] * 100\n",
|
||||
"\n",
|
||||
"# define gpu ids\n",
|
||||
"device_ids = [0, 1]\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" # initialize a TextEmbedding model using CUDA\n",
|
||||
" text_model = TextEmbedding(\n",
|
||||
" model_name=\"sentence-transformers/all-MiniLM-L6-v2\",\n",
|
||||
" cuda=True,\n",
|
||||
" device_ids=device_ids,\n",
|
||||
" lazy_load=True,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # generate embeddings\n",
|
||||
" text_embeddings = list(text_model.embed(docs, batch_size=2, parallel=len(device_ids)))\n",
|
||||
" print(text_embeddings)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this snippet:\n",
|
||||
"- `cuda=True` enables GPU acceleration.\n",
|
||||
"- `device_ids=[0, 1]` specifies GPUs to use. Replace `[0, 1]` with available GPU IDs.\n",
|
||||
"- `lazy_load=True`\n",
|
||||
"\n",
|
||||
"**NOTE**: When using multi-GPU settings, it is important to configure `parallel` and `lazy_load` properly to avoid inefficiencies:\n",
|
||||
"\n",
|
||||
"`parallel`: This parameter enables multi-GPU support by spawning child processes for each GPU specified in device_ids. To ensure proper utilization, the value of `parallel` must match the number of GPUs in device_ids. If using a single GPU, this parameter is not necessary.\n",
|
||||
"\n",
|
||||
"`lazy_load`: Enabling `lazy_load` prevents redundant memory usage. Without `lazy_load`, the model is initially loaded into the memory of the first GPU by the main process. When child processes are spawned for each GPU, the model is reloaded on the first GPU, causing redundant memory consumption and inefficiencies."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.15"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"from typing import Callable, List, Tuple\n",
|
||||
"from typing import Callable\n",
|
||||
"\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"import torch.nn.functional as F\n",
|
||||
@@ -64,6 +64,7 @@
|
||||
],
|
||||
"source": [
|
||||
"import fastembed\n",
|
||||
"\n",
|
||||
"fastembed.__version__"
|
||||
]
|
||||
},
|
||||
@@ -98,7 +99,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"documents: List[str] = [\n",
|
||||
"documents: list[str] = [\n",
|
||||
" \"Chandrayaan-3 is India's third lunar mission\",\n",
|
||||
" \"It aimed to land a rover on the Moon's surface - joining the US, China and Russia\",\n",
|
||||
" \"The mission is a follow-up to Chandrayaan-2, which had partial success\",\n",
|
||||
@@ -151,11 +152,11 @@
|
||||
" HuggingFace Transformer implementation of FlagEmbedding\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, model_id: str):\n",
|
||||
" def __init__(self, model_id: str) -> None:\n",
|
||||
" self.model = AutoModel.from_pretrained(model_id)\n",
|
||||
" self.tokenizer = AutoTokenizer.from_pretrained(model_id)\n",
|
||||
"\n",
|
||||
" def embed(self, texts: List[str]):\n",
|
||||
" def embed(self, texts: list[str]):\n",
|
||||
" encoded_input = self.tokenizer(\n",
|
||||
" texts, max_length=512, padding=True, truncation=True, return_tensors=\"pt\"\n",
|
||||
" )\n",
|
||||
@@ -254,7 +255,7 @@
|
||||
"\n",
|
||||
"def calculate_time_stats(\n",
|
||||
" embed_func: Callable, documents: list, k: int\n",
|
||||
") -> Tuple[float, float, float]:\n",
|
||||
") -> tuple[float, float, float]:\n",
|
||||
" times = []\n",
|
||||
" for _ in range(k):\n",
|
||||
" # Timing the embed_func call\n",
|
||||
@@ -309,7 +310,7 @@
|
||||
],
|
||||
"source": [
|
||||
"def plot_character_per_second_comparison(\n",
|
||||
" hf_stats: Tuple[float, float, float], fst_stats: Tuple[float, float, float], documents: list\n",
|
||||
" hf_stats: tuple[float, float, float], fst_stats: tuple[float, float, float], documents: list\n",
|
||||
"):\n",
|
||||
" # Calculating total characters in documents\n",
|
||||
" total_characters = sum(len(doc) for doc in documents)\n",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-03-30T00:45:24.814968Z",
|
||||
@@ -58,8 +58,6 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"from datasets import load_dataset\n",
|
||||
"from peft import AutoPeftModelForCausalLM\n",
|
||||
@@ -72,11 +70,11 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 23,
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"hf_token = <YOUR_HF_TOKEN_HERE> # Get your token from https://huggingface.co/settings/token, needed for Gemma weights"
|
||||
"hf_token = \"<YOUR_HF_TOKEN_HERE>\" # Get your token from https://huggingface.co/settings/token, needed for Gemma weights"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -246,7 +244,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"context_embeddings: List[np.ndarray] = list(\n",
|
||||
"context_embeddings: list[np.ndarray] = list(\n",
|
||||
" embedding_model.embed(contexts)\n",
|
||||
") # Note the list() call - this is a generator"
|
||||
]
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from typing import List, Tuple\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
@@ -328,7 +327,9 @@
|
||||
],
|
||||
"source": [
|
||||
"source_df = dataset.to_pandas()\n",
|
||||
"df = source_df.drop_duplicates(subset=[\"product_text\", \"product_title\", \"product_bullet_point\", \"product_brand\"])\n",
|
||||
"df = source_df.drop_duplicates(\n",
|
||||
" subset=[\"product_text\", \"product_title\", \"product_bullet_point\", \"product_brand\"]\n",
|
||||
")\n",
|
||||
"df = df.dropna(subset=[\"product_text\", \"product_title\", \"product_bullet_point\", \"product_brand\"])\n",
|
||||
"df.head()"
|
||||
]
|
||||
@@ -367,7 +368,9 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df[\"combined_text\"] = df[\"product_title\"] + \"\\n\" + df[\"product_text\"] + \"\\n\" + df[\"product_bullet_point\"]"
|
||||
"df[\"combined_text\"] = (\n",
|
||||
" df[\"product_title\"] + \"\\n\" + df[\"product_text\"] + \"\\n\" + df[\"product_bullet_point\"]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -485,11 +488,13 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def make_sparse_embedding(texts: List[str]):\n",
|
||||
"def make_sparse_embedding(texts: list[str]) -> list[SparseEmbedding]:\n",
|
||||
" return list(sparse_model.embed(texts, batch_size=32))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"sparse_embedding: List[SparseEmbedding] = make_sparse_embedding([\"Fastembed is a great library for text embeddings!\"])\n",
|
||||
"sparse_embedding: list[SparseEmbedding] = make_sparse_embedding(\n",
|
||||
" [\"Fastembed is a great library for text embeddings!\"]\n",
|
||||
")\n",
|
||||
"sparse_embedding"
|
||||
]
|
||||
},
|
||||
@@ -610,7 +615,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def get_tokens_and_weights(sparse_embedding, model_name):\n",
|
||||
"def get_tokens_and_weights(sparse_embedding, model_name) -> dict[str, float]:\n",
|
||||
" # Find the tokenizer for the model\n",
|
||||
" tokenizer_source = None\n",
|
||||
" for model_info in SparseTextEmbedding.list_supported_models():\n",
|
||||
@@ -621,14 +626,16 @@
|
||||
" raise ValueError(f\"Model {model_name} not found in the supported models.\")\n",
|
||||
"\n",
|
||||
" tokenizer = AutoTokenizer.from_pretrained(tokenizer_source)\n",
|
||||
" token_weight_dict = {}\n",
|
||||
" token_weight_dict: dict[str, float] = {}\n",
|
||||
" for i in range(len(sparse_embedding.indices)):\n",
|
||||
" token = tokenizer.decode([sparse_embedding.indices[i]])\n",
|
||||
" weight = sparse_embedding.values[i]\n",
|
||||
" token_weight_dict[token] = weight\n",
|
||||
"\n",
|
||||
" # Sort the dictionary by weights\n",
|
||||
" token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))\n",
|
||||
" token_weight_dict = dict(\n",
|
||||
" sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True)\n",
|
||||
" )\n",
|
||||
" return token_weight_dict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -654,7 +661,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def make_dense_embedding(texts: List[str]):\n",
|
||||
"def make_dense_embedding(texts: list[str]):\n",
|
||||
" return list(dense_model.embed(texts))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -864,27 +871,34 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def make_points(df: pd.DataFrame) -> List[PointStruct]:\n",
|
||||
"def make_points(df: pd.DataFrame) -> list[PointStruct]:\n",
|
||||
" sparse_vectors = df[\"sparse_embedding\"].tolist()\n",
|
||||
" product_texts = df[\"combined_text\"].tolist()\n",
|
||||
" dense_vectors = df[\"dense_embedding\"].tolist()\n",
|
||||
" rows = df.to_dict(orient=\"records\")\n",
|
||||
" points = []\n",
|
||||
" for idx, (text, sparse_vector, dense_vector) in enumerate(zip(product_texts, sparse_vectors, dense_vectors)):\n",
|
||||
" sparse_vector = SparseVector(indices=sparse_vector.indices.tolist(), values=sparse_vector.values.tolist())\n",
|
||||
" for idx, (text, sparse_vector, dense_vector) in enumerate(\n",
|
||||
" zip(product_texts, sparse_vectors, dense_vectors)\n",
|
||||
" ):\n",
|
||||
" sparse_vector = SparseVector(\n",
|
||||
" indices=sparse_vector.indices.tolist(), values=sparse_vector.values.tolist()\n",
|
||||
" )\n",
|
||||
" point = PointStruct(\n",
|
||||
" id=idx,\n",
|
||||
" payload={\"text\": text, \"product_id\": rows[idx][\"product_id\"]}, # Add any additional payload if necessary\n",
|
||||
" payload={\n",
|
||||
" \"text\": text,\n",
|
||||
" \"product_id\": rows[idx][\"product_id\"],\n",
|
||||
" }, # Add any additional payload if necessary\n",
|
||||
" vector={\n",
|
||||
" \"text-sparse\": sparse_vector,\n",
|
||||
" \"text-dense\": dense_vector,\n",
|
||||
" \"text-dense\": dense_vector.tolist(),\n",
|
||||
" },\n",
|
||||
" )\n",
|
||||
" points.append(point)\n",
|
||||
" return points\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"points: List[PointStruct] = make_points(df)"
|
||||
"points: list[PointStruct] = make_points(df)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -927,8 +941,8 @@
|
||||
"source": [
|
||||
"def search(query_text: str):\n",
|
||||
" # # Compute sparse and dense vectors\n",
|
||||
" query_sparse_vectors: List[SparseEmbedding] = make_sparse_embedding([query_text])\n",
|
||||
" query_dense_vector: List[np.ndarray] = make_dense_embedding([query_text])\n",
|
||||
" query_sparse_vectors: list[SparseEmbedding] = make_sparse_embedding([query_text])\n",
|
||||
" query_dense_vector: list[np.ndarray] = make_dense_embedding([query_text])\n",
|
||||
"\n",
|
||||
" search_results = client.search_batch(\n",
|
||||
" collection_name=collection_name,\n",
|
||||
@@ -936,7 +950,7 @@
|
||||
" SearchRequest(\n",
|
||||
" vector=NamedVector(\n",
|
||||
" name=\"text-dense\",\n",
|
||||
" vector=query_dense_vector[0],\n",
|
||||
" vector=query_dense_vector[0].tolist(),\n",
|
||||
" ),\n",
|
||||
" limit=10,\n",
|
||||
" with_payload=True,\n",
|
||||
@@ -1060,7 +1074,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def rank_list(search_result: List[ScoredPoint]):\n",
|
||||
"def rank_list(search_result: list[ScoredPoint]):\n",
|
||||
" return [(point.id, rank + 1) for rank, point in enumerate(search_result)]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -1133,8 +1147,12 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def find_point_by_id(client: QdrantClient, collection_name: str, rrf_rank_list: List[Tuple[int, float]]):\n",
|
||||
" return client.retrieve(collection_name=collection_name, ids=[item[0] for item in rrf_rank_list])\n",
|
||||
"def find_point_by_id(\n",
|
||||
" client: QdrantClient, collection_name: str, rrf_rank_list: list[tuple[int, float]]\n",
|
||||
"):\n",
|
||||
" return client.retrieve(\n",
|
||||
" collection_name=collection_name, ids=[item[0] for item in rrf_rank_list]\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"find_point_by_id(client, collection_name, rrf_rank_list)"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "aa0a86859809102",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"# Image Embedding\n",
|
||||
"As of version 0.3.0 fastembed supports computation of image embeddings.\n",
|
||||
"\n",
|
||||
"The process is as easy and straightforward as with text embeddings. Let's see how it works."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "cea8fd5c019571fe",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-02T11:35:40.126023Z",
|
||||
"start_time": "2024-06-02T11:35:39.864701Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Fetching 3 files: 100%|██████████| 3/3 [00:00<00:00, 47482.69it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": "[array([0. , 0. , 0. , ..., 0. , 0.01139933,\n 0. ], dtype=float32),\n array([0.02169187, 0. , 0. , ..., 0. , 0.00848291,\n 0. ], dtype=float32)]"
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from fastembed import ImageEmbedding\n",
|
||||
"\n",
|
||||
"model = ImageEmbedding(\"Qdrant/resnet50-onnx\")\n",
|
||||
"\n",
|
||||
"embeddings_generator = model.embed(\n",
|
||||
" [\"../../tests/misc/image.jpeg\", \"../../tests/misc/small_image.jpeg\"]\n",
|
||||
")\n",
|
||||
"embeddings_list = list(embeddings_generator)\n",
|
||||
"embeddings_list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3f838f18523ad1e0",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"## Preprocessing\n",
|
||||
"\n",
|
||||
"Preprocessing is encapsulated in the ImageEmbedding class, applied operations are identical to the ones provided by [Hugging Face Transformers](https://huggingface.co/docs/transformers/en/index).\n",
|
||||
"You don't need to think about batching, opening/closing files, resizing images, etc., Fastembed will take care of it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "894b33ff9b385d72",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"## Supported models\n",
|
||||
"\n",
|
||||
"List of supported image embedding models can either be found [here](https://qdrant.github.io/fastembed/examples/Supported_Models/#supported-image-embedding-models) or by calling the `ImageEmbedding.list_supported_models()` method."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "6d6a4cbbd2200d14",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-02T11:40:19.313226Z",
|
||||
"start_time": "2024-06-02T11:40:19.309845Z"
|
||||
},
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": "[{'model': 'Qdrant/clip-ViT-B-32-vision',\n 'dim': 512,\n 'description': 'CLIP vision encoder based on ViT-B/32',\n 'size_in_GB': 0.34,\n 'sources': {'hf': 'Qdrant/clip-ViT-B-32-vision'},\n 'model_file': 'model.onnx'},\n {'model': 'Qdrant/resnet50-onnx',\n 'dim': 2048,\n 'description': 'ResNet-50 from `Deep Residual Learning for Image Recognition <https://arxiv.org/abs/1512.03385>`__.',\n 'size_in_GB': 0.1,\n 'sources': {'hf': 'Qdrant/resnet50-onnx'},\n 'model_file': 'model.onnx'}]"
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"ImageEmbedding.list_supported_models()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-03-30T00:49:20.516644Z",
|
||||
@@ -56,8 +56,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from fastembed import SparseTextEmbedding, SparseEmbedding\n",
|
||||
"from typing import List"
|
||||
"from fastembed import SparseTextEmbedding, SparseEmbedding"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -134,7 +133,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-03-30T00:49:28.624109Z",
|
||||
@@ -143,7 +142,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"documents: List[str] = [\n",
|
||||
"documents: list[str] = [\n",
|
||||
" \"Chandrayaan-3 is India's third lunar mission\",\n",
|
||||
" \"It aimed to land a rover on the Moon's surface - joining the US, China and Russia\",\n",
|
||||
" \"The mission is a follow-up to Chandrayaan-2, which had partial success\",\n",
|
||||
@@ -157,7 +156,7 @@
|
||||
" \"Chandrayaan-3 was launched from the Satish Dhawan Space Centre in Sriharikota\",\n",
|
||||
" \"Chandrayaan-3 was launched earlier in the year 2023\",\n",
|
||||
"]\n",
|
||||
"sparse_embeddings_list: List[SparseEmbedding] = list(\n",
|
||||
"sparse_embeddings_list: list[SparseEmbedding] = list(\n",
|
||||
" model.embed(documents, batch_size=6)\n",
|
||||
") # batch_size is optional, notice the generator"
|
||||
]
|
||||
@@ -235,7 +234,9 @@
|
||||
"source": [
|
||||
"# Let's print the first 5 features and their weights for better understanding.\n",
|
||||
"for i in range(5):\n",
|
||||
" print(f\"Token at index {sparse_embeddings_list[0].indices[i]} has weight {sparse_embeddings_list[0].values[i]}\")"
|
||||
" print(\n",
|
||||
" f\"Token at index {sparse_embeddings_list[0].indices[i]} has weight {sparse_embeddings_list[0].values[i]}\"\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -261,7 +262,9 @@
|
||||
"import json\n",
|
||||
"from transformers import AutoTokenizer\n",
|
||||
"\n",
|
||||
"tokenizer = AutoTokenizer.from_pretrained(SparseTextEmbedding.list_supported_models()[0][\"sources\"][\"hf\"])"
|
||||
"tokenizer = AutoTokenizer.from_pretrained(\n",
|
||||
" SparseTextEmbedding.list_supported_models()[0][\"sources\"][\"hf\"]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -326,7 +329,9 @@
|
||||
" token_weight_dict[token] = weight\n",
|
||||
"\n",
|
||||
" # Sort the dictionary by weights\n",
|
||||
" token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))\n",
|
||||
" token_weight_dict = dict(\n",
|
||||
" sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True)\n",
|
||||
" )\n",
|
||||
" return token_weight_dict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
|
||||
@@ -2,29 +2,49 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-03-30T11:18:52.052764Z",
|
||||
"start_time": "2024-03-30T11:18:52.039616Z"
|
||||
"end_time": "2024-11-13T09:01:03.324551Z",
|
||||
"start_time": "2024-11-13T09:01:03.234711Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%load_ext autoreload\n",
|
||||
"%autoreload 2"
|
||||
]
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"The autoreload extension is already loaded. To reload it, use:\n",
|
||||
" %reload_ext autoreload\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 10
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-11-13T09:01:04.505772Z",
|
||||
"start_time": "2024-11-13T09:01:04.493296Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"from fastembed import SparseTextEmbedding, TextEmbedding"
|
||||
]
|
||||
"from fastembed import (\n",
|
||||
" SparseTextEmbedding,\n",
|
||||
" TextEmbedding,\n",
|
||||
" LateInteractionTextEmbedding,\n",
|
||||
" ImageEmbedding,\n",
|
||||
")\n",
|
||||
"from fastembed.rerank.cross_encoder import TextCrossEncoder"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": 11
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -35,11 +55,79 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-11-13T09:01:05.812271Z",
|
||||
"start_time": "2024-11-13T09:01:05.795846Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"supported_models = (\n",
|
||||
" pd.DataFrame(TextEmbedding.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=[\"sources\", \"model_file\", \"additional_files\"])\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")\n",
|
||||
"supported_models"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" model dim \\\n",
|
||||
"0 BAAI/bge-small-en-v1.5 384 \n",
|
||||
"1 BAAI/bge-small-zh-v1.5 512 \n",
|
||||
"2 snowflake/snowflake-arctic-embed-xs 384 \n",
|
||||
"3 sentence-transformers/all-MiniLM-L6-v2 384 \n",
|
||||
"4 jinaai/jina-embeddings-v2-small-en 512 \n",
|
||||
"5 BAAI/bge-small-en 384 \n",
|
||||
"6 snowflake/snowflake-arctic-embed-s 384 \n",
|
||||
"7 nomic-ai/nomic-embed-text-v1.5-Q 768 \n",
|
||||
"8 BAAI/bge-base-en-v1.5 768 \n",
|
||||
"9 sentence-transformers/paraphrase-multilingual-... 384 \n",
|
||||
"10 Qdrant/clip-ViT-B-32-text 512 \n",
|
||||
"11 jinaai/jina-embeddings-v2-base-de 768 \n",
|
||||
"12 BAAI/bge-base-en 768 \n",
|
||||
"13 snowflake/snowflake-arctic-embed-m 768 \n",
|
||||
"14 nomic-ai/nomic-embed-text-v1.5 768 \n",
|
||||
"15 jinaai/jina-embeddings-v2-base-en 768 \n",
|
||||
"16 nomic-ai/nomic-embed-text-v1 768 \n",
|
||||
"17 snowflake/snowflake-arctic-embed-m-long 768 \n",
|
||||
"18 mixedbread-ai/mxbai-embed-large-v1 1024 \n",
|
||||
"19 jinaai/jina-embeddings-v2-base-code 768 \n",
|
||||
"20 sentence-transformers/paraphrase-multilingual-... 768 \n",
|
||||
"21 snowflake/snowflake-arctic-embed-l 1024 \n",
|
||||
"22 thenlper/gte-large 1024 \n",
|
||||
"23 BAAI/bge-large-en-v1.5 1024 \n",
|
||||
"24 intfloat/multilingual-e5-large 1024 \n",
|
||||
"\n",
|
||||
" description license size_in_GB \n",
|
||||
"0 Text embeddings, Unimodal (text), English, 512... mit 0.067 \n",
|
||||
"1 Text embeddings, Unimodal (text), Chinese, 512... mit 0.090 \n",
|
||||
"2 Text embeddings, Unimodal (text), English, 512... apache-2.0 0.090 \n",
|
||||
"3 Text embeddings, Unimodal (text), English, 256... apache-2.0 0.090 \n",
|
||||
"4 Text embeddings, Unimodal (text), English, 819... apache-2.0 0.120 \n",
|
||||
"5 Text embeddings, Unimodal (text), English, 512... mit 0.130 \n",
|
||||
"6 Text embeddings, Unimodal (text), English, 512... apache-2.0 0.130 \n",
|
||||
"7 Text embeddings, Multimodal (text, image), Eng... apache-2.0 0.130 \n",
|
||||
"8 Text embeddings, Unimodal (text), English, 512... mit 0.210 \n",
|
||||
"9 Text embeddings, Unimodal (text), Multilingual... apache-2.0 0.220 \n",
|
||||
"10 Text embeddings, Multimodal (text&image), Engl... mit 0.250 \n",
|
||||
"11 Text embeddings, Unimodal (text), Multilingual... apache-2.0 0.320 \n",
|
||||
"12 Text embeddings, Unimodal (text), English, 512... mit 0.420 \n",
|
||||
"13 Text embeddings, Unimodal (text), English, 512... apache-2.0 0.430 \n",
|
||||
"14 Text embeddings, Multimodal (text, image), Eng... apache-2.0 0.520 \n",
|
||||
"15 Text embeddings, Unimodal (text), English, 819... apache-2.0 0.520 \n",
|
||||
"16 Text embeddings, Multimodal (text, image), Eng... apache-2.0 0.520 \n",
|
||||
"17 Text embeddings, Unimodal (text), English, 204... apache-2.0 0.540 \n",
|
||||
"18 Text embeddings, Unimodal (text), English, 512... apache-2.0 0.640 \n",
|
||||
"19 Text embeddings, Unimodal (text), Multilingual... apache-2.0 0.640 \n",
|
||||
"20 Text embeddings, Unimodal (text), Multilingual... apache-2.0 1.000 \n",
|
||||
"21 Text embeddings, Unimodal (text), English, 512... apache-2.0 1.020 \n",
|
||||
"22 Text embeddings, Unimodal (text), English, 512... mit 1.200 \n",
|
||||
"23 Text embeddings, Unimodal (text), English, 512... mit 1.200 \n",
|
||||
"24 Text embeddings, Unimodal (text), Multilingual... mit 2.240 "
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
@@ -62,6 +150,7 @@
|
||||
" <th>model</th>\n",
|
||||
" <th>dim</th>\n",
|
||||
" <th>description</th>\n",
|
||||
" <th>license</th>\n",
|
||||
" <th>size_in_GB</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
@@ -70,215 +159,213 @@
|
||||
" <th>0</th>\n",
|
||||
" <td>BAAI/bge-small-en-v1.5</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Fast and Default English model</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.067</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>BAAI/bge-small-zh-v1.5</td>\n",
|
||||
" <td>512</td>\n",
|
||||
" <td>Fast and recommended Chinese model</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Chinese, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.090</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>sentence-transformers/all-MiniLM-L6-v2</td>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-xs</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Sentence Transformer model, MiniLM-L6-v2</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.090</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-xs</td>\n",
|
||||
" <td>sentence-transformers/all-MiniLM-L6-v2</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Based on all-MiniLM-L6-v2 model with only 22m ...</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 256...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.090</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>jinaai/jina-embeddings-v2-small-en</td>\n",
|
||||
" <td>512</td>\n",
|
||||
" <td>English embedding model supporting 8192 sequen...</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 819...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.120</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-s</td>\n",
|
||||
" <td>BAAI/bge-small-en</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Based on infloat/e5-small-unsupervised, does n...</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.130</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>6</th>\n",
|
||||
" <td>BAAI/bge-small-en</td>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-s</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Fast English model</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.130</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>7</th>\n",
|
||||
" <td>BAAI/bge-base-en-v1.5</td>\n",
|
||||
" <td>nomic-ai/nomic-embed-text-v1.5-Q</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Base English model, v1.5</td>\n",
|
||||
" <td>0.210</td>\n",
|
||||
" <td>Text embeddings, Multimodal (text, image), Eng...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.130</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>8</th>\n",
|
||||
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Sentence Transformer model, paraphrase-multili...</td>\n",
|
||||
" <td>0.220</td>\n",
|
||||
" <td>BAAI/bge-base-en-v1.5</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.210</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>9</th>\n",
|
||||
" <td>BAAI/bge-base-en</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Base English model</td>\n",
|
||||
" <td>0.420</td>\n",
|
||||
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
|
||||
" <td>384</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.220</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>10</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-m</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Based on intfloat/e5-base-unsupervised model, ...</td>\n",
|
||||
" <td>0.430</td>\n",
|
||||
" <td>Qdrant/clip-ViT-B-32-text</td>\n",
|
||||
" <td>512</td>\n",
|
||||
" <td>Text embeddings, Multimodal (text&image), Engl...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.250</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>11</th>\n",
|
||||
" <td>jinaai/jina-embeddings-v2-base-en</td>\n",
|
||||
" <td>jinaai/jina-embeddings-v2-base-de</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>English embedding model supporting 8192 sequen...</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.320</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>12</th>\n",
|
||||
" <td>nomic-ai/nomic-embed-text-v1</td>\n",
|
||||
" <td>BAAI/bge-base-en</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>8192 context length english model</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.420</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>13</th>\n",
|
||||
" <td>nomic-ai/nomic-embed-text-v1.5</td>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-m</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>8192 context length english model</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.430</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>14</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-m-long</td>\n",
|
||||
" <td>nomic-ai/nomic-embed-text-v1.5</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Based on nomic-ai/nomic-embed-text-v1-unsuperv...</td>\n",
|
||||
" <td>0.540</td>\n",
|
||||
" <td>Text embeddings, Multimodal (text, image), Eng...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>15</th>\n",
|
||||
" <td>mixedbread-ai/mxbai-embed-large-v1</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>MixedBread Base sentence embedding model, does...</td>\n",
|
||||
" <td>0.640</td>\n",
|
||||
" <td>jinaai/jina-embeddings-v2-base-en</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 819...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>16</th>\n",
|
||||
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
|
||||
" <td>nomic-ai/nomic-embed-text-v1</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Sentence-transformers model for tasks like clu...</td>\n",
|
||||
" <td>1.000</td>\n",
|
||||
" <td>Text embeddings, Multimodal (text, image), Eng...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.520</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>17</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-l</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Based on intfloat/e5-large-unsupervised, large...</td>\n",
|
||||
" <td>1.020</td>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-m-long</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 204...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.540</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>18</th>\n",
|
||||
" <td>BAAI/bge-large-en-v1.5</td>\n",
|
||||
" <td>mixedbread-ai/mxbai-embed-large-v1</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Large English model, v1.5</td>\n",
|
||||
" <td>1.200</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.640</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>19</th>\n",
|
||||
" <td>thenlper/gte-large</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Large general text embeddings model</td>\n",
|
||||
" <td>1.200</td>\n",
|
||||
" <td>jinaai/jina-embeddings-v2-base-code</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.640</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>20</th>\n",
|
||||
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>1.000</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>21</th>\n",
|
||||
" <td>snowflake/snowflake-arctic-embed-l</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>1.020</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>22</th>\n",
|
||||
" <td>thenlper/gte-large</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>1.200</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>23</th>\n",
|
||||
" <td>BAAI/bge-large-en-v1.5</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), English, 512...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>1.200</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>24</th>\n",
|
||||
" <td>intfloat/multilingual-e5-large</td>\n",
|
||||
" <td>1024</td>\n",
|
||||
" <td>Multilingual model, e5-large. Recommend using ...</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>2.240</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" model dim \\\n",
|
||||
"0 BAAI/bge-small-en-v1.5 384 \n",
|
||||
"1 BAAI/bge-small-zh-v1.5 512 \n",
|
||||
"2 sentence-transformers/all-MiniLM-L6-v2 384 \n",
|
||||
"3 snowflake/snowflake-arctic-embed-xs 384 \n",
|
||||
"4 jinaai/jina-embeddings-v2-small-en 512 \n",
|
||||
"5 snowflake/snowflake-arctic-embed-s 384 \n",
|
||||
"6 BAAI/bge-small-en 384 \n",
|
||||
"7 BAAI/bge-base-en-v1.5 768 \n",
|
||||
"8 sentence-transformers/paraphrase-multilingual-... 384 \n",
|
||||
"9 BAAI/bge-base-en 768 \n",
|
||||
"10 snowflake/snowflake-arctic-embed-m 768 \n",
|
||||
"11 jinaai/jina-embeddings-v2-base-en 768 \n",
|
||||
"12 nomic-ai/nomic-embed-text-v1 768 \n",
|
||||
"13 nomic-ai/nomic-embed-text-v1.5 768 \n",
|
||||
"14 snowflake/snowflake-arctic-embed-m-long 768 \n",
|
||||
"15 mixedbread-ai/mxbai-embed-large-v1 1024 \n",
|
||||
"16 sentence-transformers/paraphrase-multilingual-... 768 \n",
|
||||
"17 snowflake/snowflake-arctic-embed-l 1024 \n",
|
||||
"18 BAAI/bge-large-en-v1.5 1024 \n",
|
||||
"19 thenlper/gte-large 1024 \n",
|
||||
"20 intfloat/multilingual-e5-large 1024 \n",
|
||||
"\n",
|
||||
" description size_in_GB \n",
|
||||
"0 Fast and Default English model 0.067 \n",
|
||||
"1 Fast and recommended Chinese model 0.090 \n",
|
||||
"2 Sentence Transformer model, MiniLM-L6-v2 0.090 \n",
|
||||
"3 Based on all-MiniLM-L6-v2 model with only 22m ... 0.090 \n",
|
||||
"4 English embedding model supporting 8192 sequen... 0.120 \n",
|
||||
"5 Based on infloat/e5-small-unsupervised, does n... 0.130 \n",
|
||||
"6 Fast English model 0.130 \n",
|
||||
"7 Base English model, v1.5 0.210 \n",
|
||||
"8 Sentence Transformer model, paraphrase-multili... 0.220 \n",
|
||||
"9 Base English model 0.420 \n",
|
||||
"10 Based on intfloat/e5-base-unsupervised model, ... 0.430 \n",
|
||||
"11 English embedding model supporting 8192 sequen... 0.520 \n",
|
||||
"12 8192 context length english model 0.520 \n",
|
||||
"13 8192 context length english model 0.520 \n",
|
||||
"14 Based on nomic-ai/nomic-embed-text-v1-unsuperv... 0.540 \n",
|
||||
"15 MixedBread Base sentence embedding model, does... 0.640 \n",
|
||||
"16 Sentence-transformers model for tasks like clu... 1.000 \n",
|
||||
"17 Based on intfloat/e5-large-unsupervised, large... 1.020 \n",
|
||||
"18 Large English model, v1.5 1.200 \n",
|
||||
"19 Large general text embeddings model 1.200 \n",
|
||||
"20 Multilingual model, e5-large. Recommend using ... 2.240 "
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"supported_models = (\n",
|
||||
" pd.DataFrame(TextEmbedding.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=\"sources\")\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")\n",
|
||||
"supported_models"
|
||||
]
|
||||
"execution_count": 12
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -289,16 +376,42 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-03-30T11:19:01.564291Z",
|
||||
"start_time": "2024-03-30T11:19:01.538768Z"
|
||||
"end_time": "2024-11-13T09:01:07.038954Z",
|
||||
"start_time": "2024-11-13T09:01:07.019656Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"(\n",
|
||||
" pd.DataFrame(SparseTextEmbedding.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=[\"sources\", \"model_file\", \"additional_files\"])\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" model vocab_size \\\n",
|
||||
"0 Qdrant/bm25 NaN \n",
|
||||
"1 Qdrant/bm42-all-minilm-l6-v2-attentions 30522.0 \n",
|
||||
"2 prithivida/Splade_PP_en_v1 30522.0 \n",
|
||||
"3 prithvida/Splade_PP_en_v1 30522.0 \n",
|
||||
"\n",
|
||||
" description license size_in_GB \\\n",
|
||||
"0 BM25 as sparse embeddings meant to be used wit... apache-2.0 0.010 \n",
|
||||
"1 Light sparse embedding model, which assigns an... apache-2.0 0.090 \n",
|
||||
"2 Independent Implementation of SPLADE++ Model f... apache-2.0 0.532 \n",
|
||||
"3 Independent Implementation of SPLADE++ Model f... apache-2.0 0.532 \n",
|
||||
"\n",
|
||||
" requires_idf \n",
|
||||
"0 True \n",
|
||||
"1 True \n",
|
||||
"2 NaN \n",
|
||||
"3 NaN "
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
@@ -321,53 +434,414 @@
|
||||
" <th>model</th>\n",
|
||||
" <th>vocab_size</th>\n",
|
||||
" <th>description</th>\n",
|
||||
" <th>license</th>\n",
|
||||
" <th>size_in_GB</th>\n",
|
||||
" <th>sources</th>\n",
|
||||
" <th>requires_idf</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>prithvida/Splade_PP_en_v1</td>\n",
|
||||
" <td>30522</td>\n",
|
||||
" <td>Misspelled version of the model. Retained for ...</td>\n",
|
||||
" <td>0.532</td>\n",
|
||||
" <td>{'hf': 'Qdrant/SPLADE_PP_en_v1'}</td>\n",
|
||||
" <td>Qdrant/bm25</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>BM25 as sparse embeddings meant to be used wit...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.010</td>\n",
|
||||
" <td>True</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>Qdrant/bm42-all-minilm-l6-v2-attentions</td>\n",
|
||||
" <td>30522.0</td>\n",
|
||||
" <td>Light sparse embedding model, which assigns an...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.090</td>\n",
|
||||
" <td>True</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>prithivida/Splade_PP_en_v1</td>\n",
|
||||
" <td>30522</td>\n",
|
||||
" <td>30522.0</td>\n",
|
||||
" <td>Independent Implementation of SPLADE++ Model f...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.532</td>\n",
|
||||
" <td>{'hf': 'Qdrant/SPLADE_PP_en_v1'}</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>prithvida/Splade_PP_en_v1</td>\n",
|
||||
" <td>30522.0</td>\n",
|
||||
" <td>Independent Implementation of SPLADE++ Model f...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.532</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" model vocab_size \\\n",
|
||||
"0 prithvida/Splade_PP_en_v1 30522 \n",
|
||||
"1 prithivida/Splade_PP_en_v1 30522 \n",
|
||||
"\n",
|
||||
" description size_in_GB \\\n",
|
||||
"0 Misspelled version of the model. Retained for ... 0.532 \n",
|
||||
"1 Independent Implementation of SPLADE++ Model f... 0.532 \n",
|
||||
"\n",
|
||||
" sources \n",
|
||||
"0 {'hf': 'Qdrant/SPLADE_PP_en_v1'} \n",
|
||||
"1 {'hf': 'Qdrant/SPLADE_PP_en_v1'} "
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 13
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"pd.DataFrame(SparseTextEmbedding.list_supported_models())"
|
||||
"## Supported Late Interaction Text Embedding Models"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-11-13T09:01:08.074442Z",
|
||||
"start_time": "2024-11-13T09:01:08.056138Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"(\n",
|
||||
" pd.DataFrame(LateInteractionTextEmbedding.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=[\"sources\", \"model_file\"])\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" model dim \\\n",
|
||||
"0 answerdotai/answerai-colbert-small-v1 96 \n",
|
||||
"1 colbert-ir/colbertv2.0 128 \n",
|
||||
"2 jinaai/jina-colbert-v2 128 \n",
|
||||
"\n",
|
||||
" description license \\\n",
|
||||
"0 Text embeddings, Unimodal (text), Multilingual... apache-2.0 \n",
|
||||
"1 Late interaction model mit \n",
|
||||
"2 New model that expands capabilities of colbert... cc-by-nc-4.0 \n",
|
||||
"\n",
|
||||
" size_in_GB additional_files \n",
|
||||
"0 0.13 NaN \n",
|
||||
"1 0.44 NaN \n",
|
||||
"2 2.24 [onnx/model.onnx_data] "
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>model</th>\n",
|
||||
" <th>dim</th>\n",
|
||||
" <th>description</th>\n",
|
||||
" <th>license</th>\n",
|
||||
" <th>size_in_GB</th>\n",
|
||||
" <th>additional_files</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>answerdotai/answerai-colbert-small-v1</td>\n",
|
||||
" <td>96</td>\n",
|
||||
" <td>Text embeddings, Unimodal (text), Multilingual...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.13</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>colbert-ir/colbertv2.0</td>\n",
|
||||
" <td>128</td>\n",
|
||||
" <td>Late interaction model</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.44</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>jinaai/jina-colbert-v2</td>\n",
|
||||
" <td>128</td>\n",
|
||||
" <td>New model that expands capabilities of colbert...</td>\n",
|
||||
" <td>cc-by-nc-4.0</td>\n",
|
||||
" <td>2.24</td>\n",
|
||||
" <td>[onnx/model.onnx_data]</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 14
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"source": [
|
||||
"## Supported Image Embedding Models"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-11-13T09:01:09.171647Z",
|
||||
"start_time": "2024-11-13T09:01:09.150940Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"(\n",
|
||||
" pd.DataFrame(ImageEmbedding.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=[\"sources\", \"model_file\"])\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" model dim \\\n",
|
||||
"0 Qdrant/resnet50-onnx 2048 \n",
|
||||
"1 Qdrant/clip-ViT-B-32-vision 512 \n",
|
||||
"2 Qdrant/Unicom-ViT-B-32 512 \n",
|
||||
"3 Qdrant/Unicom-ViT-B-16 768 \n",
|
||||
"\n",
|
||||
" description license size_in_GB \n",
|
||||
"0 Image embeddings, Unimodal (image), 2016 year apache-2.0 0.10 \n",
|
||||
"1 Image embeddings, Multimodal (text&image), 202... mit 0.34 \n",
|
||||
"2 Image embeddings, Multimodal (text&image), 202... apache-2.0 0.48 \n",
|
||||
"3 Image embeddings (more detailed than Unicom-Vi... apache-2.0 0.82 "
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>model</th>\n",
|
||||
" <th>dim</th>\n",
|
||||
" <th>description</th>\n",
|
||||
" <th>license</th>\n",
|
||||
" <th>size_in_GB</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>Qdrant/resnet50-onnx</td>\n",
|
||||
" <td>2048</td>\n",
|
||||
" <td>Image embeddings, Unimodal (image), 2016 year</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.10</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>Qdrant/clip-ViT-B-32-vision</td>\n",
|
||||
" <td>512</td>\n",
|
||||
" <td>Image embeddings, Multimodal (text&image), 202...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" <td>0.34</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>Qdrant/Unicom-ViT-B-32</td>\n",
|
||||
" <td>512</td>\n",
|
||||
" <td>Image embeddings, Multimodal (text&image), 202...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.48</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>Qdrant/Unicom-ViT-B-16</td>\n",
|
||||
" <td>768</td>\n",
|
||||
" <td>Image embeddings (more detailed than Unicom-Vi...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" <td>0.82</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
"execution_count": 15,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 15
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Supported Rerank Cross Encoder Models"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-11-13T09:01:10.313943Z",
|
||||
"start_time": "2024-11-13T09:01:10.298428Z"
|
||||
}
|
||||
},
|
||||
"source": [
|
||||
"(\n",
|
||||
" pd.DataFrame(TextCrossEncoder.list_supported_models())\n",
|
||||
" .sort_values(\"size_in_GB\")\n",
|
||||
" .drop(columns=[\"sources\", \"model_file\"])\n",
|
||||
" .reset_index(drop=True)\n",
|
||||
")"
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
" model size_in_GB \\\n",
|
||||
"0 Xenova/ms-marco-MiniLM-L-6-v2 0.08 \n",
|
||||
"1 Xenova/ms-marco-MiniLM-L-12-v2 0.12 \n",
|
||||
"2 jinaai/jina-reranker-v1-tiny-en 0.13 \n",
|
||||
"3 jinaai/jina-reranker-v1-turbo-en 0.15 \n",
|
||||
"4 BAAI/bge-reranker-base 1.04 \n",
|
||||
"5 jinaai/jina-reranker-v2-base-multilingual 1.11 \n",
|
||||
"\n",
|
||||
" description license \n",
|
||||
"0 MiniLM-L-6-v2 model optimized for re-ranking t... apache-2.0 \n",
|
||||
"1 MiniLM-L-12-v2 model optimized for re-ranking ... apache-2.0 \n",
|
||||
"2 Designed for blazing-fast re-ranking with 8K c... apache-2.0 \n",
|
||||
"3 Designed for blazing-fast re-ranking with 8K c... apache-2.0 \n",
|
||||
"4 BGE reranker base model for cross-encoder re-r... mit \n",
|
||||
"5 A multi-lingual reranker model for cross-encod... cc-by-nc-4.0 "
|
||||
],
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>model</th>\n",
|
||||
" <th>size_in_GB</th>\n",
|
||||
" <th>description</th>\n",
|
||||
" <th>license</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>Xenova/ms-marco-MiniLM-L-6-v2</td>\n",
|
||||
" <td>0.08</td>\n",
|
||||
" <td>MiniLM-L-6-v2 model optimized for re-ranking t...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>Xenova/ms-marco-MiniLM-L-12-v2</td>\n",
|
||||
" <td>0.12</td>\n",
|
||||
" <td>MiniLM-L-12-v2 model optimized for re-ranking ...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>jinaai/jina-reranker-v1-tiny-en</td>\n",
|
||||
" <td>0.13</td>\n",
|
||||
" <td>Designed for blazing-fast re-ranking with 8K c...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>jinaai/jina-reranker-v1-turbo-en</td>\n",
|
||||
" <td>0.15</td>\n",
|
||||
" <td>Designed for blazing-fast re-ranking with 8K c...</td>\n",
|
||||
" <td>apache-2.0</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>BAAI/bge-reranker-base</td>\n",
|
||||
" <td>1.04</td>\n",
|
||||
" <td>BGE reranker base model for cross-encoder re-r...</td>\n",
|
||||
" <td>mit</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>jinaai/jina-reranker-v2-base-multilingual</td>\n",
|
||||
" <td>1.11</td>\n",
|
||||
" <td>A multi-lingual reranker model for cross-encod...</td>\n",
|
||||
" <td>cc-by-nc-4.0</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"execution_count": 16
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": ""
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -386,7 +860,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.13"
|
||||
"version": "3.11.8"
|
||||
},
|
||||
"orig_nbformat": 4,
|
||||
"vscode": {
|
||||
|
||||
@@ -14,23 +14,33 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:00:06.460001Z",
|
||||
"start_time": "2024-06-06T17:00:04.214098Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install matplotlib tqdm pandas numpy --quiet"
|
||||
"!pip install matplotlib tqdm pandas numpy datasets --quiet --upgrade"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"execution_count": 2,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:00:07.041784Z",
|
||||
"start_time": "2024-06-06T17:00:06.461658Z"
|
||||
},
|
||||
"id": "WBVTItUX4yyr"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"import pandas as pd\n",
|
||||
"from datasets import load_dataset\n",
|
||||
"from tqdm import tqdm"
|
||||
]
|
||||
},
|
||||
@@ -52,8 +62,12 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:09.343230Z",
|
||||
"start_time": "2024-06-06T17:00:07.042526Z"
|
||||
},
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/",
|
||||
"height": 250
|
||||
@@ -61,58 +75,24 @@
|
||||
"id": "REJpFqkG7EG2",
|
||||
"outputId": "7a43c0ae-fbcc-45fe-fd58-bfe691297b22"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 26/26 [00:10<00:00, 2.45it/s]\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(1000000, 1536)"
|
||||
]
|
||||
},
|
||||
"execution_count": 13,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def get_openai_vectors(force_download: bool = False):\n",
|
||||
" res = []\n",
|
||||
" for i in tqdm(range(26)):\n",
|
||||
" if force_download:\n",
|
||||
" !wget https://huggingface.co/api/datasets/KShivendu/dbpedia-entities-openai-1M/parquet/KShivendu--dbpedia-entities-openai-1M/train/{i}.parquet\n",
|
||||
" df = pd.read_parquet(f\"{i}.parquet\", engine=\"pyarrow\")\n",
|
||||
" res.append(np.stack(df.openai))\n",
|
||||
" del df\n",
|
||||
"\n",
|
||||
" openai_vectors = np.concatenate(res)\n",
|
||||
" del res\n",
|
||||
" return openai_vectors\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"openai_vectors = get_openai_vectors(force_download=False)\n",
|
||||
"openai_vectors.shape"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## ㆓ Binary Conversion\n",
|
||||
"\n",
|
||||
"Here, we will use 0 as the threshold for the binary conversion. All values greater than 0 will be set to 1, and others will remain 0. This is a simple and effective way to convert continuous values into binary values for OpenAI embeddings."
|
||||
"# Download from Huggingface Hub\n",
|
||||
"ds = load_dataset(\n",
|
||||
" \"Qdrant/dbpedia-entities-openai3-text-embedding-3-large-3072-100K\", split=\"train\"\n",
|
||||
")\n",
|
||||
"openai_vectors = np.array(ds[\"text-embedding-3-large-3072-embedding\"])\n",
|
||||
"del ds"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"id": "0JM2-Bj2Jkab"
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:10.900963Z",
|
||||
"start_time": "2024-06-06T17:01:09.344842Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -120,6 +100,30 @@
|
||||
"openai_bin[openai_vectors > 0] = 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:10.906827Z",
|
||||
"start_time": "2024-06-06T17:01:10.901820Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": "3072"
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"n_dim = openai_vectors.shape[1]\n",
|
||||
"n_dim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
@@ -131,8 +135,12 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:10.909730Z",
|
||||
"start_time": "2024-06-06T17:01:10.908166Z"
|
||||
},
|
||||
"id": "FqshI-GlIERd"
|
||||
},
|
||||
"outputs": [],
|
||||
@@ -141,7 +149,7 @@
|
||||
" scores = np.dot(openai_vectors, openai_vectors[idx])\n",
|
||||
" dot_results = np.argsort(scores)[-limit:][::-1]\n",
|
||||
"\n",
|
||||
" bin_scores = 1536 - np.logical_xor(openai_bin, openai_bin[idx]).sum(axis=1)\n",
|
||||
" bin_scores = n_dim - np.logical_xor(openai_bin, openai_bin[idx]).sum(axis=1)\n",
|
||||
" bin_results = np.argsort(bin_scores)[-(limit * oversampling) :][::-1]\n",
|
||||
"\n",
|
||||
" return len(set(dot_results).intersection(set(bin_results))) / limit"
|
||||
@@ -156,8 +164,12 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"execution_count": 7,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:25.206592Z",
|
||||
"start_time": "2024-06-06T17:01:10.911971Z"
|
||||
},
|
||||
"colab": {
|
||||
"base_uri": "https://localhost:8080/"
|
||||
},
|
||||
@@ -169,110 +181,128 @@
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" 0%| | 0/4 [00:00<?, ?it/s]"
|
||||
" 0%| | 0/4 [00:00<?, ?it/s]\n",
|
||||
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
|
||||
" 50%|█████ | 1/2 [00:02<00:02, 2.05s/it]\u001b[A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 1, 'limit': 10, 'recall': 0.8}\n"
|
||||
"{'sampling_rate': 1, 'limit': 3, 'mean_acc': 0.9}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 2/2 [00:33<00:00, 16.98s/it]\n",
|
||||
" 25%|██▌ | 1/4 [00:33<01:41, 33.96s/it]"
|
||||
"\n",
|
||||
"100%|██████████| 2/2 [00:04<00:00, 2.02s/it]\u001b[A\n",
|
||||
" 25%|██▌ | 1/4 [00:04<00:12, 4.05s/it]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 1, 'limit': 100, 'recall': 0.708}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": []
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 2, 'limit': 10, 'recall': 0.95}\n"
|
||||
"{'sampling_rate': 1, 'limit': 10, 'mean_acc': 0.8300000000000001}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 2/2 [00:32<00:00, 16.38s/it]\n",
|
||||
" 50%|█████ | 2/4 [01:06<01:06, 33.26s/it]"
|
||||
"\n",
|
||||
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
|
||||
" 50%|█████ | 1/2 [00:01<00:01, 1.72s/it]\u001b[A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 2, 'limit': 100, 'recall': 0.877}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": []
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 3, 'limit': 10, 'recall': 0.96}\n"
|
||||
"{'sampling_rate': 2, 'limit': 3, 'mean_acc': 1.0}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 2/2 [00:32<00:00, 16.49s/it]\n",
|
||||
" 75%|███████▌ | 3/4 [01:39<00:33, 33.13s/it]"
|
||||
"\n",
|
||||
"100%|██████████| 2/2 [00:03<00:00, 1.76s/it]\u001b[A\n",
|
||||
" 50%|█████ | 2/4 [00:07<00:07, 3.75s/it]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 3, 'limit': 100, 'recall': 0.937}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": []
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 5, 'limit': 10, 'recall': 0.9800000000000001}\n"
|
||||
"{'sampling_rate': 2, 'limit': 10, 'mean_acc': 0.9700000000000001}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"100%|██████████| 2/2 [00:32<00:00, 16.47s/it]\n",
|
||||
"100%|██████████| 4/4 [02:12<00:00, 33.17s/it]"
|
||||
"\n",
|
||||
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
|
||||
" 50%|█████ | 1/2 [00:01<00:01, 1.72s/it]\u001b[A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 5, 'limit': 100, 'recall': 0.977}\n"
|
||||
"{'sampling_rate': 3, 'limit': 3, 'mean_acc': 1.0}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"100%|██████████| 2/2 [00:03<00:00, 1.69s/it]\u001b[A\n",
|
||||
" 75%|███████▌ | 3/4 [00:10<00:03, 3.58s/it]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 3, 'limit': 10, 'mean_acc': 0.9800000000000001}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
|
||||
" 50%|█████ | 1/2 [00:01<00:01, 1.68s/it]\u001b[A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 5, 'limit': 3, 'mean_acc': 1.0}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"100%|██████████| 2/2 [00:03<00:00, 1.65s/it]\u001b[A\n",
|
||||
"100%|██████████| 4/4 [00:14<00:00, 3.57s/it]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'sampling_rate': 5, 'limit': 10, 'mean_acc': 0.99}\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -285,119 +315,53 @@
|
||||
],
|
||||
"source": [
|
||||
"number_of_samples = 10\n",
|
||||
"limits = [10, 100]\n",
|
||||
"limits = [3, 10]\n",
|
||||
"sampling_rate = [1, 2, 3, 5]\n",
|
||||
"results = []\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def mean_accuracy(number_of_samples, limit, sampling_rate):\n",
|
||||
" return np.mean([accuracy(i, limit=limit, oversampling=sampling_rate) for i in range(number_of_samples)])\n",
|
||||
" return np.mean(\n",
|
||||
" [accuracy(i, limit=limit, oversampling=sampling_rate) for i in range(number_of_samples)]\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"for i in tqdm(sampling_rate):\n",
|
||||
" for j in tqdm(limits):\n",
|
||||
" result = {\"sampling_rate\": i, \"limit\": j, \"recall\": mean_accuracy(number_of_samples, j, i)}\n",
|
||||
" result = {\n",
|
||||
" \"sampling_rate\": i,\n",
|
||||
" \"limit\": j,\n",
|
||||
" \"mean_acc\": mean_accuracy(number_of_samples, j, i),\n",
|
||||
" }\n",
|
||||
" print(result)\n",
|
||||
" results.append(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## ㆓ Binary Conversion\n",
|
||||
"\n",
|
||||
"Here, we will use 0 as the threshold for the binary conversion. All values greater than 0 will be set to 1, and others will remain 0. This is a simple and effective way to convert continuous values into binary values for OpenAI embeddings."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-06-06T17:01:25.247495Z",
|
||||
"start_time": "2024-06-06T17:01:25.213508Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>sampling_rate</th>\n",
|
||||
" <th>limit</th>\n",
|
||||
" <th>recall</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>10</td>\n",
|
||||
" <td>0.800</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>1</td>\n",
|
||||
" <td>100</td>\n",
|
||||
" <td>0.708</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>2</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>10</td>\n",
|
||||
" <td>0.950</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>3</th>\n",
|
||||
" <td>2</td>\n",
|
||||
" <td>100</td>\n",
|
||||
" <td>0.877</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>4</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>10</td>\n",
|
||||
" <td>0.960</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>5</th>\n",
|
||||
" <td>3</td>\n",
|
||||
" <td>100</td>\n",
|
||||
" <td>0.937</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>6</th>\n",
|
||||
" <td>5</td>\n",
|
||||
" <td>10</td>\n",
|
||||
" <td>0.980</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>7</th>\n",
|
||||
" <td>5</td>\n",
|
||||
" <td>100</td>\n",
|
||||
" <td>0.977</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" sampling_rate limit recall\n",
|
||||
"0 1 10 0.800\n",
|
||||
"1 1 100 0.708\n",
|
||||
"2 2 10 0.950\n",
|
||||
"3 2 100 0.877\n",
|
||||
"4 3 10 0.960\n",
|
||||
"5 3 100 0.937\n",
|
||||
"6 5 10 0.980\n",
|
||||
"7 5 100 0.977"
|
||||
]
|
||||
"text/html": "<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>sampling_rate</th>\n <th>limit</th>\n <th>mean_acc</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>1</td>\n <td>3</td>\n <td>0.90</td>\n </tr>\n <tr>\n <th>1</th>\n <td>1</td>\n <td>10</td>\n <td>0.83</td>\n </tr>\n <tr>\n <th>2</th>\n <td>2</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>3</th>\n <td>2</td>\n <td>10</td>\n <td>0.97</td>\n </tr>\n <tr>\n <th>4</th>\n <td>3</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>5</th>\n <td>3</td>\n <td>10</td>\n <td>0.98</td>\n </tr>\n <tr>\n <th>6</th>\n <td>5</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>7</th>\n <td>5</td>\n <td>10</td>\n <td>0.99</td>\n </tr>\n </tbody>\n</table>\n</div>",
|
||||
"text/plain": " sampling_rate limit mean_acc\n0 1 3 0.90\n1 1 10 0.83\n2 2 3 1.00\n3 2 10 0.97\n4 3 3 1.00\n5 3 10 0.98\n6 5 3 1.00\n7 5 10 0.99"
|
||||
},
|
||||
"execution_count": 19,
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -408,22 +372,13 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"| sampling_rate | limit | accuracy |\n",
|
||||
"|---------------|-------|----------|\n",
|
||||
"| 1 | 10 | 0.800 |\n",
|
||||
"| 1 | 100 | 0.708 |\n",
|
||||
"| 2 | 10 | 0.950 |\n",
|
||||
"| 2 | 100 | 0.877 |\n",
|
||||
"| 4 | 10 | 0.970 |\n",
|
||||
"| 4 | 100 | 0.956 |\n",
|
||||
"| 8 | 10 | 0.990 |\n",
|
||||
"| 8 | 100 | 0.990 |\n",
|
||||
"| 16 | 10 | 1.000 |\n",
|
||||
"| 16 | 100 | 0.998 |"
|
||||
]
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
@@ -432,7 +387,8 @@
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
@@ -445,7 +401,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.17"
|
||||
"version": "3.10.13"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+12
-13
@@ -2,17 +2,17 @@
|
||||
|
||||
FastEmbed is a lightweight, fast, Python library built for embedding generation. We [support popular text models](https://qdrant.github.io/fastembed/examples/Supported_Models/). Please [open a Github issue](https://github.com/qdrant/fastembed/issues/new) if you want us to add a new model.
|
||||
|
||||
The default embedding supports "query" and "passage" prefixes for the input text. The default model is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard. Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/examples/Retrieval%20with%20FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/).
|
||||
|
||||
1. Light & Fast
|
||||
- Quantized model weights
|
||||
- ONNX Runtime for inference via [Optimum](https://github.com/huggingface/optimum)
|
||||
- ONNX Runtime for inference
|
||||
|
||||
2. Accuracy/Recall
|
||||
- Better than OpenAI Ada-002
|
||||
- Default is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard
|
||||
- Default is Flag Embedding, which has shown good results on the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard
|
||||
- List of [supported models](https://qdrant.github.io/fastembed/examples/Supported_Models/) - including multilingual models
|
||||
|
||||
Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/examples/Retrieval%20with%20FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/).
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
To install the FastEmbed library, pip works:
|
||||
@@ -24,16 +24,16 @@ pip install fastembed
|
||||
## 📖 Usage
|
||||
|
||||
```python
|
||||
from fastembed.embedding import FlagEmbedding as Embedding
|
||||
from fastembed import TextEmbedding
|
||||
|
||||
documents: List[str] = [
|
||||
documents: list[str] = [
|
||||
"passage: Hello, World!",
|
||||
"query: Hello, World!", # these are two different embedding
|
||||
"query: Hello, World!",
|
||||
"passage: This is an example passage.",
|
||||
"fastembed is supported by and maintained by Qdrant." # You can leave out the prefix but it's recommended
|
||||
"fastembed is supported by and maintained by Qdrant."
|
||||
]
|
||||
embedding_model = Embedding(model_name="BAAI/bge-base-en", max_length=512)
|
||||
embeddings: List[np.ndarray] = embedding_model.embed(documents) # If you use
|
||||
embedding_model = TextEmbedding()
|
||||
embeddings: list[np.ndarray] = embedding_model.embed(documents)
|
||||
```
|
||||
|
||||
## Usage with Qdrant
|
||||
@@ -50,17 +50,16 @@ Might have to use ```pip install 'qdrant-client[fastembed]'``` on zsh.
|
||||
from qdrant_client import QdrantClient
|
||||
|
||||
# Initialize the client
|
||||
client = QdrantClient(":memory:") # or QdrantClient(path="path/to/db")
|
||||
client = QdrantClient(":memory:") # Using an in-process Qdrant
|
||||
|
||||
# Prepare your documents, metadata, and IDs
|
||||
docs = ["Qdrant has Langchain integrations", "Qdrant also has Llama Index integrations"]
|
||||
metadata = [
|
||||
{"source": "Langchain-docs"},
|
||||
{"source": "Linkedin-docs"},
|
||||
{"source": "Llama-index-docs"},
|
||||
]
|
||||
ids = [42, 2]
|
||||
|
||||
# Use the new add method
|
||||
client.add(
|
||||
collection_name="demo_collection",
|
||||
documents=docs,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -41,7 +41,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List\n",
|
||||
"import numpy as np\n",
|
||||
"from fastembed import TextEmbedding"
|
||||
]
|
||||
@@ -71,7 +70,7 @@
|
||||
],
|
||||
"source": [
|
||||
"# Example list of documents\n",
|
||||
"documents: List[str] = [\n",
|
||||
"documents: list[str] = [\n",
|
||||
" \"Maharana Pratap was a Rajput warrior king from Mewar\",\n",
|
||||
" \"He fought against the Mughal Empire led by Akbar\",\n",
|
||||
" \"The Battle of Haldighati in 1576 was his most famous battle\",\n",
|
||||
@@ -87,7 +86,7 @@
|
||||
"embedding_model = TextEmbedding(model_name=\"BAAI/bge-small-en\")\n",
|
||||
"\n",
|
||||
"# We'll use the passage_embed method to get the embeddings for the documents\n",
|
||||
"embeddings: List[np.ndarray] = list(\n",
|
||||
"embeddings: list[np.ndarray] = list(\n",
|
||||
" embedding_model.passage_embed(documents)\n",
|
||||
") # notice that we are casting the generator to a list\n",
|
||||
"\n",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List\n",
|
||||
"from qdrant_client import QdrantClient"
|
||||
]
|
||||
},
|
||||
@@ -67,7 +66,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Example list of documents\n",
|
||||
"documents: List[str] = [\n",
|
||||
"documents: list[str] = [\n",
|
||||
" \"Maharana Pratap was a Rajput warrior king from Mewar\",\n",
|
||||
" \"He fought against the Mughal Empire led by Akbar\",\n",
|
||||
" \"The Battle of Haldighati in 1576 was his most famous battle\",\n",
|
||||
@@ -199,7 +198,9 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"search_result = client.query(collection_name=\"demo_collection\", query_text=\"This is a query document\")\n",
|
||||
"search_result = client.query(\n",
|
||||
" collection_name=\"demo_collection\", query_text=\"This is a query document\"\n",
|
||||
")\n",
|
||||
"print(search_result)"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pathlib import Path\n",
|
||||
"from typing import List, Tuple, Any\n",
|
||||
"from typing import Any\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"import time\n",
|
||||
@@ -91,9 +91,11 @@
|
||||
" return last_hidden.sum(dim=1) / attention_mask.sum(dim=1)[..., None]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def hf_embed(model_id: str, inputs: List[str]):\n",
|
||||
"def hf_embed(model_id: str, inputs: list[str]):\n",
|
||||
" # Tokenize the input texts\n",
|
||||
" batch_dict = hf_tokenizer(inputs, max_length=512, padding=True, truncation=True, return_tensors=\"pt\")\n",
|
||||
" batch_dict = hf_tokenizer(\n",
|
||||
" inputs, max_length=512, padding=True, truncation=True, return_tensors=\"pt\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" outputs = hf_model(**batch_dict)\n",
|
||||
" embeddings = average_pool(outputs.last_hidden_state, batch_dict[\"attention_mask\"])\n",
|
||||
@@ -133,7 +135,9 @@
|
||||
"optimization_config = AutoOptimizationConfig.O4()\n",
|
||||
"optimizer = ORTOptimizer.from_pretrained(model)\n",
|
||||
"\n",
|
||||
"optimizer.optimize(save_dir=save_dir, optimization_config=optimization_config, use_external_data_format=True)\n",
|
||||
"optimizer.optimize(\n",
|
||||
" save_dir=save_dir, optimization_config=optimization_config, use_external_data_format=True\n",
|
||||
")\n",
|
||||
"model = ORTModelForFeatureExtraction.from_pretrained(save_dir)\n",
|
||||
"\n",
|
||||
"tokenizer.save_pretrained(save_dir)\n",
|
||||
@@ -171,7 +175,9 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def measure_pipeline_time(pipeline, input_texts: List[str], num_runs=10, **kwargs: Any) -> Tuple[float, float]:\n",
|
||||
"def measure_pipeline_time(\n",
|
||||
" pipeline, input_texts: list[str], num_runs=10, **kwargs: Any\n",
|
||||
") -> tuple[float, float]:\n",
|
||||
" \"\"\"Measures the time it takes to run the pipeline on the input texts.\"\"\"\n",
|
||||
" times = []\n",
|
||||
" total_chars = sum(len(text) for text in input_texts)\n",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "4bdb2a91-fa2a-4cee-ad5a-176cc957394d",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2024-05-23T12:15:28.171586Z",
|
||||
"start_time": "2024-05-23T12:15:28.076314Z"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ModuleNotFoundError",
|
||||
"evalue": "No module named 'torch'",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
|
||||
"\u001B[0;31mModuleNotFoundError\u001B[0m Traceback (most recent call last)",
|
||||
"Cell \u001B[0;32mIn[1], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorch\u001B[39;00m\n\u001B[1;32m 2\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorch\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01monnx\u001B[39;00m\n\u001B[1;32m 3\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorchvision\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mmodels\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01mmodels\u001B[39;00m\n",
|
||||
"\u001B[0;31mModuleNotFoundError\u001B[0m: No module named 'torch'"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import torch\n",
|
||||
"import torch.onnx\n",
|
||||
"import torchvision.models as models\n",
|
||||
"import torchvision.transforms as transforms\n",
|
||||
"from PIL import Image\n",
|
||||
"import numpy as np\n",
|
||||
"from tests.config import TEST_MISC_DIR\n",
|
||||
"\n",
|
||||
"# Load pre-trained ResNet-50 model\n",
|
||||
"resnet = models.resnet50(pretrained=True)\n",
|
||||
"resnet = torch.nn.Sequential(*(list(resnet.children())[:-1])) # Remove the last fully connected layer\n",
|
||||
"resnet.eval()\n",
|
||||
"\n",
|
||||
"# Define preprocessing transform\n",
|
||||
"preprocess = transforms.Compose([\n",
|
||||
" transforms.Resize(256),\n",
|
||||
" transforms.CenterCrop(224),\n",
|
||||
" transforms.ToTensor(),\n",
|
||||
" transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n",
|
||||
"])\n",
|
||||
"\n",
|
||||
"# Load and preprocess the image\n",
|
||||
"def preprocess_image(image_path):\n",
|
||||
" input_image = Image.open(image_path)\n",
|
||||
" input_tensor = preprocess(input_image)\n",
|
||||
" input_batch = input_tensor.unsqueeze(0) # Add batch dimension\n",
|
||||
" return input_batch\n",
|
||||
"\n",
|
||||
"# Example input for exporting\n",
|
||||
"input_image = preprocess_image('example.jpg')\n",
|
||||
"\n",
|
||||
"# Export the model to ONNX with dynamic axes\n",
|
||||
"torch.onnx.export(\n",
|
||||
" resnet, \n",
|
||||
" input_image, \n",
|
||||
" \"model.onnx\", \n",
|
||||
" export_params=True, \n",
|
||||
" opset_version=9, \n",
|
||||
" input_names=['input'], \n",
|
||||
" output_names=['output'],\n",
|
||||
" dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Load ONNX model\n",
|
||||
"import onnx\n",
|
||||
"import onnxruntime as ort\n",
|
||||
"\n",
|
||||
"onnx_model = onnx.load(\"model.onnx\")\n",
|
||||
"ort_session = ort.InferenceSession(\"model.onnx\")\n",
|
||||
"\n",
|
||||
"# Run inference and extract feature vectors\n",
|
||||
"def extract_feature_vectors(image_paths):\n",
|
||||
" input_images = [preprocess_image(image_path) for image_path in image_paths]\n",
|
||||
" input_batch = torch.cat(input_images, dim=0) # Combine images into a single batch\n",
|
||||
" ort_inputs = {ort_session.get_inputs()[0].name: input_batch.numpy()}\n",
|
||||
" ort_outs = ort_session.run(None, ort_inputs)\n",
|
||||
" return ort_outs[0]\n",
|
||||
"\n",
|
||||
"# Example usage\n",
|
||||
"images = [TEST_MISC_DIR / \"image.jpeg\", str(TEST_MISC_DIR / \"small_image.jpeg\")] # Replace with your image paths\n",
|
||||
"feature_vectors = extract_feature_vectors(images)\n",
|
||||
"print(\"Feature vector shape:\", feature_vectors.shape)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"source": [],
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"id": "baa650c4cb3e0e6d"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -12,4 +12,6 @@ tokenizer = AutoTokenizer.from_pretrained(model_id)
|
||||
# print("Model already exported")
|
||||
# except FileNotFoundError:
|
||||
print(f"Exporting model to {output_dir}")
|
||||
main_export(model_id, output=output_dir, no_post_process=True, model_kwargs=model_kwargs)
|
||||
main_export(
|
||||
model_id, output=output_dir, no_post_process=True, model_kwargs=model_kwargs
|
||||
)
|
||||
|
||||
@@ -17,10 +17,14 @@ input_ids = tokenizer_output["input_ids"]
|
||||
attention_mask = tokenizer_output["attention_mask"]
|
||||
print(attention_mask)
|
||||
# Prepare the input
|
||||
input_ids = np.array(input_ids).astype(np.int64) # Replace your_input_ids with actual input data
|
||||
input_ids = np.array(input_ids).astype(
|
||||
np.int64
|
||||
) # Replace your_input_ids with actual input data
|
||||
|
||||
# Run the ONNX model
|
||||
outputs = ort_session.run(None, {"input_ids": input_ids, "attention_mask": attention_mask})
|
||||
outputs = ort_session.run(
|
||||
None, {"input_ids": input_ids, "attention_mask": attention_mask}
|
||||
)
|
||||
|
||||
# Get the attention weights
|
||||
attentions = outputs[-1]
|
||||
|
||||
+18
-3
@@ -1,7 +1,22 @@
|
||||
import importlib.metadata
|
||||
|
||||
from fastembed.image import ImageEmbedding
|
||||
from fastembed.late_interaction import LateInteractionTextEmbedding
|
||||
from fastembed.late_interaction_multimodal import LateInteractionMultimodalEmbedding
|
||||
from fastembed.sparse import SparseEmbedding, SparseTextEmbedding
|
||||
from fastembed.text import TextEmbedding
|
||||
from fastembed.sparse import SparseTextEmbedding, SparseEmbedding
|
||||
|
||||
__version__ = importlib.metadata.version("fastembed")
|
||||
__all__ = ["TextEmbedding", "SparseTextEmbedding", "SparseEmbedding"]
|
||||
try:
|
||||
version = importlib.metadata.version("fastembed")
|
||||
except importlib.metadata.PackageNotFoundError as _:
|
||||
version = importlib.metadata.version("fastembed-gpu")
|
||||
|
||||
__version__ = version
|
||||
__all__ = [
|
||||
"TextEmbedding",
|
||||
"SparseTextEmbedding",
|
||||
"SparseEmbedding",
|
||||
"ImageEmbedding",
|
||||
"LateInteractionTextEmbedding",
|
||||
"LateInteractionMultimodalEmbedding",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastembed.common.types import ImageInput, OnnxProvider, PathInput
|
||||
|
||||
__all__ = ["OnnxProvider", "ImageInput", "PathInput"]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional, Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ModelSource:
|
||||
hf: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
_deprecated_tar_struct: bool = False
|
||||
|
||||
@property
|
||||
def deprecated_tar_struct(self) -> bool:
|
||||
return self._deprecated_tar_struct
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.hf is None and self.url is None:
|
||||
raise ValueError(
|
||||
f"At least one source should be set, current sources: hf={self.hf}, url={self.url}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BaseModelDescription:
|
||||
model: str
|
||||
sources: ModelSource
|
||||
model_file: str
|
||||
description: str
|
||||
license: str
|
||||
size_in_GB: float
|
||||
additional_files: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DenseModelDescription(BaseModelDescription):
|
||||
dim: Optional[int] = None
|
||||
tasks: Optional[dict[str, Any]] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert self.dim is not None, "dim is required for dense model description"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SparseModelDescription(BaseModelDescription):
|
||||
requires_idf: Optional[bool] = None
|
||||
vocab_size: Optional[int] = None
|
||||
|
||||
|
||||
class PoolingType(str, Enum):
|
||||
CLS = "CLS"
|
||||
MEAN = "MEAN"
|
||||
DISABLED = "DISABLED"
|
||||
@@ -1,28 +1,69 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import shutil
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
from typing import Any, Optional, Union, TypeVar, Generic
|
||||
|
||||
import requests
|
||||
from huggingface_hub import snapshot_download
|
||||
from huggingface_hub.utils import RepositoryNotFoundError
|
||||
from tqdm import tqdm
|
||||
from huggingface_hub import snapshot_download, model_info, list_repo_tree
|
||||
from huggingface_hub.hf_api import RepoFile
|
||||
from huggingface_hub.utils import (
|
||||
RepositoryNotFoundError,
|
||||
disable_progress_bars,
|
||||
enable_progress_bars,
|
||||
)
|
||||
from loguru import logger
|
||||
from tqdm import tqdm
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
|
||||
T = TypeVar("T", bound=BaseModelDescription)
|
||||
|
||||
|
||||
class ModelManagement:
|
||||
class ModelManagement(Generic[T]):
|
||||
METADATA_FILE = "files_metadata.json"
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
list[T]: A list of dictionaries containing the model information.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def _get_model_description(cls, model_name: str) -> Dict[str, Any]:
|
||||
def add_custom_model(
|
||||
cls,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Add a custom model to the existing embedding classes based on the passed model descriptions
|
||||
|
||||
Model description dict should contain the fields same as in one of the model descriptions presented
|
||||
in fastembed.common.model_description
|
||||
|
||||
E.g. for BaseModelDescription:
|
||||
model: str
|
||||
sources: ModelSource
|
||||
model_file: str
|
||||
description: str
|
||||
license: str
|
||||
size_in_GB: float
|
||||
additional_files: list[str]
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[T]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def _get_model_description(cls, model_name: str) -> T:
|
||||
"""
|
||||
Gets the model description from the model_name.
|
||||
|
||||
@@ -33,10 +74,10 @@ class ModelManagement:
|
||||
ValueError: If the model_name is not supported.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The model description.
|
||||
T: The model description.
|
||||
"""
|
||||
for model in cls.list_supported_models():
|
||||
if model_name.lower() == model["model"].lower():
|
||||
for model in cls._list_supported_models():
|
||||
if model_name.lower() == model.model.lower():
|
||||
return model
|
||||
|
||||
raise ValueError(f"Model {model_name} is not supported in {cls.__name__}.")
|
||||
@@ -73,10 +114,13 @@ class ModelManagement:
|
||||
if total_size_in_bytes == 0:
|
||||
print(f"Warning: Content-length header is missing or zero in the response from {url}.")
|
||||
|
||||
show_progress = total_size_in_bytes and show_progress
|
||||
show_progress = bool(total_size_in_bytes and show_progress)
|
||||
|
||||
with tqdm(
|
||||
total=total_size_in_bytes, unit="iB", unit_scale=True, disable=not show_progress
|
||||
total=total_size_in_bytes,
|
||||
unit="iB",
|
||||
unit_scale=True,
|
||||
disable=not show_progress,
|
||||
) as progress_bar:
|
||||
with open(output_path, "wb") as file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
@@ -89,36 +133,162 @@ class ModelManagement:
|
||||
def download_files_from_huggingface(
|
||||
cls,
|
||||
hf_source_repo: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
extra_patterns: Optional[List[str]] = None,
|
||||
cache_dir: str,
|
||||
extra_patterns: list[str],
|
||||
local_files_only: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""
|
||||
Downloads a model from HuggingFace Hub.
|
||||
Args:
|
||||
hf_source_repo (str): Name of the model on HuggingFace Hub, e.g. "qdrant/all-MiniLM-L6-v2-onnx".
|
||||
cache_dir (Optional[str]): The path to the cache directory.
|
||||
extra_patterns (Optional[List[str]]): extra patterns to allow in the snapshot download, typically
|
||||
extra_patterns (list[str]): extra patterns to allow in the snapshot download, typically
|
||||
includes the required model files.
|
||||
local_files_only (bool, optional): Whether to only use local files. Defaults to False.
|
||||
Returns:
|
||||
Path: The path to the model directory.
|
||||
"""
|
||||
|
||||
def _verify_files_from_metadata(
|
||||
model_dir: Path, stored_metadata: dict[str, Any], repo_files: list[RepoFile]
|
||||
) -> bool:
|
||||
try:
|
||||
for rel_path, meta in stored_metadata.items():
|
||||
file_path = model_dir / rel_path
|
||||
|
||||
if not file_path.exists():
|
||||
return False
|
||||
|
||||
if repo_files: # online verification
|
||||
file_info = next((f for f in repo_files if f.path == file_path.name), None)
|
||||
if (
|
||||
not file_info
|
||||
or file_info.size != meta["size"]
|
||||
or file_info.blob_id != meta["blob_id"]
|
||||
):
|
||||
return False
|
||||
|
||||
else: # offline verification
|
||||
if file_path.stat().st_size != meta["size"]:
|
||||
return False
|
||||
return True
|
||||
except (OSError, KeyError) as e:
|
||||
logger.error(f"Error verifying files: {str(e)}")
|
||||
return False
|
||||
|
||||
def _collect_file_metadata(
|
||||
model_dir: Path, repo_files: list[RepoFile]
|
||||
) -> dict[str, dict[str, Union[int, str]]]:
|
||||
meta: dict[str, dict[str, Union[int, str]]] = {}
|
||||
file_info_map = {f.path: f for f in repo_files}
|
||||
for file_path in model_dir.rglob("*"):
|
||||
if file_path.is_file() and file_path.name != cls.METADATA_FILE:
|
||||
repo_file = file_info_map.get(file_path.name)
|
||||
if repo_file:
|
||||
meta[str(file_path.relative_to(model_dir))] = {
|
||||
"size": repo_file.size,
|
||||
"blob_id": repo_file.blob_id,
|
||||
}
|
||||
return meta
|
||||
|
||||
def _save_file_metadata(
|
||||
model_dir: Path, meta: dict[str, dict[str, Union[int, str]]]
|
||||
) -> None:
|
||||
try:
|
||||
if not model_dir.exists():
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
(model_dir / cls.METADATA_FILE).write_text(json.dumps(meta))
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning(f"Error saving metadata: {str(e)}")
|
||||
|
||||
allow_patterns = [
|
||||
"config.json",
|
||||
"tokenizer.json",
|
||||
"tokenizer_config.json",
|
||||
"special_tokens_map.json",
|
||||
"preprocessor_config.json",
|
||||
]
|
||||
if extra_patterns is not None:
|
||||
allow_patterns.extend(extra_patterns)
|
||||
|
||||
return snapshot_download(
|
||||
allow_patterns.extend(extra_patterns)
|
||||
|
||||
snapshot_dir = Path(cache_dir) / f"models--{hf_source_repo.replace('/', '--')}"
|
||||
metadata_file = snapshot_dir / cls.METADATA_FILE
|
||||
|
||||
if local_files_only:
|
||||
disable_progress_bars()
|
||||
if metadata_file.exists():
|
||||
metadata = json.loads(metadata_file.read_text())
|
||||
verified = _verify_files_from_metadata(snapshot_dir, metadata, repo_files=[])
|
||||
if not verified:
|
||||
logger.warning(
|
||||
"Local file sizes do not match the metadata."
|
||||
) # do not raise, still make an attempt to load the model
|
||||
else:
|
||||
logger.warning(
|
||||
"Metadata file not found. Proceeding without checking local files."
|
||||
) # if users have downloaded models from hf manually, or they're updating from previous versions of
|
||||
# fastembed
|
||||
result = snapshot_download(
|
||||
repo_id=hf_source_repo,
|
||||
allow_patterns=allow_patterns,
|
||||
cache_dir=cache_dir,
|
||||
local_files_only=local_files_only,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
repo_revision = model_info(hf_source_repo).sha
|
||||
repo_tree = list(list_repo_tree(hf_source_repo, revision=repo_revision, repo_type="model"))
|
||||
|
||||
allowed_extensions = {".json", ".onnx", ".txt"}
|
||||
repo_files = (
|
||||
[
|
||||
f
|
||||
for f in repo_tree
|
||||
if isinstance(f, RepoFile) and Path(f.path).suffix in allowed_extensions
|
||||
]
|
||||
if repo_tree
|
||||
else []
|
||||
)
|
||||
|
||||
verified_metadata = False
|
||||
|
||||
if snapshot_dir.exists() and metadata_file.exists():
|
||||
metadata = json.loads(metadata_file.read_text())
|
||||
verified_metadata = _verify_files_from_metadata(snapshot_dir, metadata, repo_files)
|
||||
|
||||
if verified_metadata:
|
||||
disable_progress_bars()
|
||||
|
||||
result = snapshot_download(
|
||||
repo_id=hf_source_repo,
|
||||
allow_patterns=allow_patterns,
|
||||
cache_dir=cache_dir,
|
||||
local_files_only=local_files_only,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if (
|
||||
not verified_metadata
|
||||
): # metadata is not up-to-date, update it and check whether the files have been
|
||||
# downloaded correctly
|
||||
metadata = _collect_file_metadata(snapshot_dir, repo_files)
|
||||
|
||||
download_successful = _verify_files_from_metadata(
|
||||
snapshot_dir, metadata, repo_files=[]
|
||||
) # offline verification
|
||||
if not download_successful:
|
||||
raise ValueError(
|
||||
"Files have been corrupted during downloading process. "
|
||||
"Please check your internet connection and try again."
|
||||
)
|
||||
_save_file_metadata(snapshot_dir, metadata)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def decompress_to_cache(cls, targz_path: str, cache_dir: str):
|
||||
def decompress_to_cache(cls, targz_path: str, cache_dir: str) -> str:
|
||||
"""
|
||||
Decompresses a .tar.gz file to a cache directory.
|
||||
|
||||
@@ -141,7 +311,9 @@ class ModelManagement:
|
||||
# Open the tar.gz file
|
||||
with tarfile.open(targz_path, "r:gz") as tar:
|
||||
# Extract all files into the cache directory
|
||||
tar.extractall(path=cache_dir)
|
||||
tar.extractall(
|
||||
path=cache_dir,
|
||||
)
|
||||
except tarfile.TarError as e:
|
||||
# If any error occurs while opening or extracting the tar.gz file,
|
||||
# delete the cache directory (if it was created in this function)
|
||||
@@ -153,9 +325,15 @@ class ModelManagement:
|
||||
return cache_dir
|
||||
|
||||
@classmethod
|
||||
def retrieve_model_gcs(cls, model_name: str, source_url: str, cache_dir: str) -> Path:
|
||||
fast_model_name = f"fast-{model_name.split('/')[-1]}"
|
||||
|
||||
def retrieve_model_gcs(
|
||||
cls,
|
||||
model_name: str,
|
||||
source_url: str,
|
||||
cache_dir: str,
|
||||
deprecated_tar_struct: bool = False,
|
||||
local_files_only: bool = False,
|
||||
) -> Path:
|
||||
fast_model_name = f"{'fast-' if deprecated_tar_struct else ''}{model_name.split('/')[-1]}"
|
||||
cache_tmp_dir = Path(cache_dir) / "tmp"
|
||||
model_tmp_dir = cache_tmp_dir / fast_model_name
|
||||
model_dir = Path(cache_dir) / fast_model_name
|
||||
@@ -174,27 +352,35 @@ class ModelManagement:
|
||||
if model_tar_gz.exists():
|
||||
model_tar_gz.unlink()
|
||||
|
||||
cls.download_file_from_gcs(
|
||||
source_url,
|
||||
output_path=str(model_tar_gz),
|
||||
)
|
||||
if not local_files_only:
|
||||
cls.download_file_from_gcs(
|
||||
source_url,
|
||||
output_path=str(model_tar_gz),
|
||||
)
|
||||
|
||||
cls.decompress_to_cache(targz_path=str(model_tar_gz), cache_dir=str(cache_tmp_dir))
|
||||
assert model_tmp_dir.exists(), f"Could not find {model_tmp_dir} in {cache_tmp_dir}"
|
||||
cls.decompress_to_cache(targz_path=str(model_tar_gz), cache_dir=str(cache_tmp_dir))
|
||||
assert model_tmp_dir.exists(), f"Could not find {model_tmp_dir} in {cache_tmp_dir}"
|
||||
|
||||
model_tar_gz.unlink()
|
||||
# Rename from tmp to final name is atomic
|
||||
model_tmp_dir.rename(model_dir)
|
||||
model_tar_gz.unlink()
|
||||
# Rename from tmp to final name is atomic
|
||||
model_tmp_dir.rename(model_dir)
|
||||
else:
|
||||
logger.error(
|
||||
f"Could not find the model tar.gz file at {model_dir} and local_files_only=True."
|
||||
)
|
||||
raise ValueError(
|
||||
f"Could not find the model tar.gz file at {model_dir} and local_files_only=True."
|
||||
)
|
||||
|
||||
return model_dir
|
||||
|
||||
@classmethod
|
||||
def download_model(cls, model: Dict[str, Any], cache_dir: Path) -> Path:
|
||||
def download_model(cls, model: T, cache_dir: str, retries: int = 3, **kwargs: Any) -> Path:
|
||||
"""
|
||||
Downloads a model from HuggingFace Hub or Google Cloud Storage.
|
||||
|
||||
Args:
|
||||
model (Dict[str, Any]): The model description.
|
||||
model (T): The model description.
|
||||
Example:
|
||||
```
|
||||
{
|
||||
@@ -209,31 +395,64 @@ class ModelManagement:
|
||||
}
|
||||
```
|
||||
cache_dir (str): The path to the cache directory.
|
||||
retries: (int): The number of times to retry (including the first attempt)
|
||||
|
||||
Returns:
|
||||
Path: The path to the downloaded model directory.
|
||||
"""
|
||||
local_files_only = kwargs.get("local_files_only", False)
|
||||
specific_model_path: Optional[str] = kwargs.pop("specific_model_path", None)
|
||||
if specific_model_path:
|
||||
return Path(specific_model_path)
|
||||
retries = 1 if local_files_only else retries
|
||||
hf_source = model.sources.hf
|
||||
url_source = model.sources.url
|
||||
|
||||
hf_source = model.get("sources", {}).get("hf")
|
||||
url_source = model.get("sources", {}).get("url")
|
||||
sleep = 3.0
|
||||
while retries > 0:
|
||||
retries -= 1
|
||||
|
||||
if hf_source:
|
||||
extra_patterns = [model["model_file"]]
|
||||
extra_patterns.extend(model.get("additional_files", []))
|
||||
if hf_source:
|
||||
extra_patterns = [model.model_file]
|
||||
extra_patterns.extend(model.additional_files)
|
||||
|
||||
try:
|
||||
return Path(
|
||||
cls.download_files_from_huggingface(
|
||||
hf_source, cache_dir=str(cache_dir), extra_patterns=extra_patterns
|
||||
try:
|
||||
return Path(
|
||||
cls.download_files_from_huggingface(
|
||||
hf_source,
|
||||
cache_dir=cache_dir,
|
||||
extra_patterns=extra_patterns,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
)
|
||||
except (EnvironmentError, RepositoryNotFoundError, ValueError) as e:
|
||||
except (EnvironmentError, RepositoryNotFoundError, ValueError) as e:
|
||||
if not local_files_only:
|
||||
logger.error(
|
||||
f"Could not download model from HuggingFace: {e} "
|
||||
"Falling back to other sources."
|
||||
)
|
||||
finally:
|
||||
enable_progress_bars()
|
||||
if url_source or local_files_only:
|
||||
try:
|
||||
return cls.retrieve_model_gcs(
|
||||
model.model,
|
||||
str(url_source),
|
||||
str(cache_dir),
|
||||
deprecated_tar_struct=model.sources.deprecated_tar_struct,
|
||||
local_files_only=local_files_only,
|
||||
)
|
||||
except Exception:
|
||||
if not local_files_only:
|
||||
logger.error(f"Could not download model from url: {url_source}")
|
||||
|
||||
if local_files_only:
|
||||
logger.error("Could not find model in cache_dir")
|
||||
else:
|
||||
logger.error(
|
||||
f"Could not download model from HuggingFace: {e}"
|
||||
"Falling back to other sources."
|
||||
f"Could not download model from either source, sleeping for {sleep} seconds, {retries} retries left."
|
||||
)
|
||||
time.sleep(sleep)
|
||||
sleep *= 3
|
||||
|
||||
if url_source:
|
||||
return cls.retrieve_model_gcs(model["model"], url_source, str(cache_dir))
|
||||
|
||||
raise ValueError(f"Could not download model {model['model']} from any source.")
|
||||
raise ValueError(f"Could not load model {model.model} from any source.")
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from tokenizers import Tokenizer, AddedToken
|
||||
|
||||
|
||||
def load_tokenizer(model_dir: Path, max_length: int = 512) -> Tokenizer:
|
||||
config_path = model_dir / "config.json"
|
||||
if not config_path.exists():
|
||||
raise ValueError(f"Could not find config.json in {model_dir}")
|
||||
|
||||
tokenizer_path = model_dir / "tokenizer.json"
|
||||
if not tokenizer_path.exists():
|
||||
raise ValueError(f"Could not find tokenizer.json in {model_dir}")
|
||||
|
||||
tokenizer_config_path = model_dir / "tokenizer_config.json"
|
||||
if not tokenizer_config_path.exists():
|
||||
raise ValueError(f"Could not find tokenizer_config.json in {model_dir}")
|
||||
|
||||
tokens_map_path = model_dir / "special_tokens_map.json"
|
||||
if not tokens_map_path.exists():
|
||||
raise ValueError(f"Could not find special_tokens_map.json in {model_dir}")
|
||||
|
||||
with open(str(config_path)) as config_file:
|
||||
config = json.load(config_file)
|
||||
|
||||
with open(str(tokenizer_config_path)) as tokenizer_config_file:
|
||||
tokenizer_config = json.load(tokenizer_config_file)
|
||||
|
||||
with open(str(tokens_map_path)) as tokens_map_file:
|
||||
tokens_map = json.load(tokens_map_file)
|
||||
|
||||
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
||||
tokenizer.enable_truncation(max_length=min(tokenizer_config["model_max_length"], max_length))
|
||||
tokenizer.enable_padding(
|
||||
pad_id=config.get("pad_token_id", 0), pad_token=tokenizer_config["pad_token"]
|
||||
)
|
||||
|
||||
for token in tokens_map.values():
|
||||
if isinstance(token, str):
|
||||
tokenizer.add_special_tokens([token])
|
||||
elif isinstance(token, dict):
|
||||
tokenizer.add_special_tokens([AddedToken(**token)])
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
def normalize(input_array, p=2, dim=1, eps=1e-12) -> np.ndarray:
|
||||
# Calculate the Lp norm along the specified dimension
|
||||
norm = np.linalg.norm(input_array, ord=p, axis=dim, keepdims=True)
|
||||
norm = np.maximum(norm, eps) # Avoid division by zero
|
||||
normalized_array = input_array / norm
|
||||
return normalized_array
|
||||
@@ -1,49 +1,96 @@
|
||||
import os
|
||||
from multiprocessing import get_all_start_methods
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Generic, Iterable, List, Optional, Tuple, Type, TypeVar, Union
|
||||
from typing import Any, Generic, Iterable, Optional, Sequence, Type, TypeVar
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
from fastembed.common.models import load_tokenizer
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.parallel_processor import ParallelWorkerPool, Worker
|
||||
from numpy.typing import NDArray
|
||||
from tokenizers import Tokenizer
|
||||
|
||||
from fastembed.common.types import OnnxProvider, NumpyArray
|
||||
from fastembed.parallel_processor import Worker
|
||||
|
||||
# Holds type of the embedding result
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass
|
||||
class OnnxOutputContext:
|
||||
model_output: NumpyArray
|
||||
attention_mask: Optional[NDArray[np.int64]] = None
|
||||
input_ids: Optional[NDArray[np.int64]] = None
|
||||
|
||||
|
||||
class OnnxModel(Generic[T]):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["EmbeddingWorker"]:
|
||||
def _get_worker_class(cls) -> Type["EmbeddingWorker[T]"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@classmethod
|
||||
def _post_process_onnx_output(cls, output: Tuple[np.ndarray, np.ndarray]) -> Iterable[T]:
|
||||
def _post_process_onnx_output(self, output: OnnxOutputContext, **kwargs: Any) -> Iterable[T]:
|
||||
"""Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
**kwargs: Additional keyword arguments that may be needed by specific implementations.
|
||||
|
||||
Returns:
|
||||
Iterable[T]: Post-processed output as an iterable of type T.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.model = None
|
||||
self.tokenizer = None
|
||||
self.model: Optional[ort.InferenceSession] = None
|
||||
self.tokenizer: Optional[Tokenizer] = None
|
||||
|
||||
def _preprocess_onnx_input(self, onnx_input: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
def load_onnx_model(
|
||||
def _load_onnx_model(
|
||||
self,
|
||||
model_dir: Path,
|
||||
model_file: str,
|
||||
threads: Optional[int],
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
) -> None:
|
||||
model_path = model_dir / model_file
|
||||
|
||||
# List of Execution Providers: https://onnxruntime.ai/docs/execution-providers
|
||||
onnx_providers = ["CPUExecutionProvider"]
|
||||
|
||||
if cuda and providers is not None:
|
||||
warnings.warn(
|
||||
f"`cuda` and `providers` are mutually exclusive parameters, cuda: {cuda}, providers: {providers}",
|
||||
category=UserWarning,
|
||||
stacklevel=6,
|
||||
)
|
||||
|
||||
if providers is not None:
|
||||
onnx_providers = list(providers)
|
||||
elif cuda:
|
||||
if device_id is None:
|
||||
onnx_providers = ["CUDAExecutionProvider"]
|
||||
else:
|
||||
onnx_providers = [("CUDAExecutionProvider", {"device_id": device_id})]
|
||||
else:
|
||||
onnx_providers = ["CPUExecutionProvider"]
|
||||
|
||||
available_providers = ort.get_available_providers()
|
||||
requested_provider_names: list[str] = []
|
||||
for provider in onnx_providers:
|
||||
# check providers available
|
||||
provider_name = provider if isinstance(provider, str) else provider[0]
|
||||
requested_provider_names.append(provider_name)
|
||||
if provider_name not in available_providers:
|
||||
raise ValueError(
|
||||
f"Provider {provider_name} is not available. Available providers: {available_providers}"
|
||||
)
|
||||
|
||||
so = ort.SessionOptions()
|
||||
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
@@ -52,90 +99,47 @@ class OnnxModel(Generic[T]):
|
||||
so.intra_op_num_threads = threads
|
||||
so.inter_op_num_threads = threads
|
||||
|
||||
self.tokenizer = load_tokenizer(model_dir=model_dir)
|
||||
self.model = ort.InferenceSession(
|
||||
str(model_path), providers=onnx_providers, sess_options=so
|
||||
)
|
||||
if "CUDAExecutionProvider" in requested_provider_names:
|
||||
assert self.model is not None
|
||||
current_providers = self.model.get_providers()
|
||||
if "CUDAExecutionProvider" not in current_providers:
|
||||
warnings.warn(
|
||||
f"Attempt to set CUDAExecutionProvider failed. Current providers: {current_providers}."
|
||||
"If you are using CUDA 12.x, install onnxruntime-gpu via "
|
||||
"`pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/`",
|
||||
RuntimeWarning,
|
||||
)
|
||||
|
||||
def onnx_embed(self, documents: List[str]) -> Tuple[np.ndarray, np.ndarray]:
|
||||
encoded = self.tokenizer.encode_batch(documents)
|
||||
input_ids = np.array([e.ids for e in encoded])
|
||||
attention_mask = np.array([e.attention_mask for e in encoded])
|
||||
def load_onnx_model(self) -> None:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
onnx_input = {
|
||||
"input_ids": np.array(input_ids, dtype=np.int64),
|
||||
"attention_mask": np.array(attention_mask, dtype=np.int64),
|
||||
"token_type_ids": np.array(
|
||||
[np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
|
||||
),
|
||||
}
|
||||
|
||||
onnx_input = self._preprocess_onnx_input(onnx_input)
|
||||
|
||||
model_output = self.model.run(None, onnx_input)
|
||||
embeddings = model_output[0]
|
||||
return embeddings, attention_mask
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
) -> Iterable[T]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(documents, str):
|
||||
documents = [documents]
|
||||
is_small = True
|
||||
|
||||
if isinstance(documents, list):
|
||||
if len(documents) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
if parallel is None or is_small:
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed(batch))
|
||||
else:
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
}
|
||||
pool = ParallelWorkerPool(
|
||||
parallel, self._get_worker_class(), start_method=start_method
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
|
||||
yield from self._post_process_onnx_output(batch)
|
||||
def onnx_embed(self, *args: Any, **kwargs: Any) -> OnnxOutputContext:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
|
||||
class EmbeddingWorker(Worker):
|
||||
class EmbeddingWorker(Worker, Generic[T]):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
) -> OnnxModel:
|
||||
**kwargs: Any,
|
||||
) -> OnnxModel[T]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model = self.init_embedding(model_name, cache_dir)
|
||||
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker":
|
||||
return cls(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
)
|
||||
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker[T]":
|
||||
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
|
||||
|
||||
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
embeddings, attn_mask = self.model.onnx_embed(batch)
|
||||
yield idx, (embeddings, attn_mask)
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import json
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
|
||||
from tokenizers import AddedToken, Tokenizer
|
||||
|
||||
from fastembed.image.transform.operators import Compose
|
||||
|
||||
|
||||
def load_special_tokens(model_dir: Path) -> dict[str, Any]:
|
||||
tokens_map_path = model_dir / "special_tokens_map.json"
|
||||
if not tokens_map_path.exists():
|
||||
raise ValueError(f"Could not find special_tokens_map.json in {model_dir}")
|
||||
|
||||
with open(str(tokens_map_path)) as tokens_map_file:
|
||||
tokens_map = json.load(tokens_map_file)
|
||||
|
||||
return tokens_map
|
||||
|
||||
|
||||
def load_tokenizer(model_dir: Path) -> tuple[Tokenizer, dict[str, int]]:
|
||||
config_path = model_dir / "config.json"
|
||||
if not config_path.exists():
|
||||
raise ValueError(f"Could not find config.json in {model_dir}")
|
||||
|
||||
tokenizer_path = model_dir / "tokenizer.json"
|
||||
if not tokenizer_path.exists():
|
||||
raise ValueError(f"Could not find tokenizer.json in {model_dir}")
|
||||
|
||||
tokenizer_config_path = model_dir / "tokenizer_config.json"
|
||||
if not tokenizer_config_path.exists():
|
||||
raise ValueError(f"Could not find tokenizer_config.json in {model_dir}")
|
||||
|
||||
with open(str(config_path)) as config_file:
|
||||
config = json.load(config_file)
|
||||
|
||||
with open(str(tokenizer_config_path)) as tokenizer_config_file:
|
||||
tokenizer_config = json.load(tokenizer_config_file)
|
||||
assert "model_max_length" in tokenizer_config or "max_length" in tokenizer_config, (
|
||||
"Models without model_max_length or max_length are not supported."
|
||||
)
|
||||
if "model_max_length" not in tokenizer_config:
|
||||
max_context = tokenizer_config["max_length"]
|
||||
elif "max_length" not in tokenizer_config:
|
||||
max_context = tokenizer_config["model_max_length"]
|
||||
else:
|
||||
max_context = min(tokenizer_config["model_max_length"], tokenizer_config["max_length"])
|
||||
|
||||
tokens_map = load_special_tokens(model_dir)
|
||||
|
||||
tokenizer = Tokenizer.from_file(str(tokenizer_path))
|
||||
tokenizer.enable_truncation(max_length=max_context)
|
||||
tokenizer.enable_padding(
|
||||
pad_id=config.get("pad_token_id", 0), pad_token=tokenizer_config["pad_token"]
|
||||
)
|
||||
|
||||
for token in tokens_map.values():
|
||||
if isinstance(token, str):
|
||||
tokenizer.add_special_tokens([token])
|
||||
elif isinstance(token, dict):
|
||||
tokenizer.add_special_tokens([AddedToken(**token)])
|
||||
|
||||
special_token_to_id: dict[str, int] = {}
|
||||
|
||||
for token in tokens_map.values():
|
||||
if isinstance(token, str):
|
||||
special_token_to_id[token] = tokenizer.token_to_id(token)
|
||||
elif isinstance(token, dict):
|
||||
token_str = token.get("content", "")
|
||||
special_token_to_id[token_str] = tokenizer.token_to_id(token_str)
|
||||
|
||||
return tokenizer, special_token_to_id
|
||||
|
||||
|
||||
def load_preprocessor(model_dir: Path) -> Compose:
|
||||
preprocessor_config_path = model_dir / "preprocessor_config.json"
|
||||
if not preprocessor_config_path.exists():
|
||||
raise ValueError(f"Could not find preprocessor_config.json in {model_dir}")
|
||||
|
||||
with open(str(preprocessor_config_path)) as preprocessor_config_file:
|
||||
preprocessor_config = json.load(preprocessor_config_file)
|
||||
transforms = Compose.from_config(preprocessor_config)
|
||||
return transforms
|
||||
@@ -0,0 +1,25 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from PIL import Image
|
||||
from typing import Any, Union
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
PathInput: TypeAlias = Union[str, Path]
|
||||
ImageInput: TypeAlias = Union[PathInput, Image.Image]
|
||||
|
||||
OnnxProvider: TypeAlias = Union[str, tuple[str, dict[Any, Any]]]
|
||||
NumpyArray = Union[
|
||||
NDArray[np.float64],
|
||||
NDArray[np.float32],
|
||||
NDArray[np.float16],
|
||||
NDArray[np.int8],
|
||||
NDArray[np.int64],
|
||||
NDArray[np.int32],
|
||||
]
|
||||
@@ -1,11 +1,38 @@
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import tempfile
|
||||
from itertools import islice
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from typing import Union, Iterable, Generator, Optional
|
||||
from itertools import islice
|
||||
from typing import Iterable, Optional, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def iter_batch(iterable: Union[Iterable, Generator], size: int) -> Iterable:
|
||||
def normalize(input_array: NumpyArray, p: int = 2, dim: int = 1, eps: float = 1e-12) -> NumpyArray:
|
||||
# Calculate the Lp norm along the specified dimension
|
||||
norm = np.linalg.norm(input_array, ord=p, axis=dim, keepdims=True)
|
||||
norm = np.maximum(norm, eps) # Avoid division by zero
|
||||
normalized_array = input_array / norm
|
||||
return normalized_array
|
||||
|
||||
|
||||
def mean_pooling(input_array: NumpyArray, attention_mask: NDArray[np.int64]) -> NumpyArray:
|
||||
input_mask_expanded = np.expand_dims(attention_mask, axis=-1).astype(np.int64)
|
||||
input_mask_expanded = np.tile(input_mask_expanded, (1, 1, input_array.shape[-1]))
|
||||
sum_embeddings = np.sum(input_array * input_mask_expanded, axis=1)
|
||||
sum_mask = np.sum(input_mask_expanded, axis=1)
|
||||
pooled_embeddings = sum_embeddings / np.maximum(sum_mask, 1e-9)
|
||||
return pooled_embeddings
|
||||
|
||||
|
||||
def iter_batch(iterable: Iterable[T], size: int) -> Iterable[list[T]]:
|
||||
"""
|
||||
>>> list(iter_batch([1,2,3,4,5], 3))
|
||||
[[1, 2, 3], [4, 5]]
|
||||
@@ -27,7 +54,16 @@ def define_cache_dir(cache_dir: Optional[str] = None) -> Path:
|
||||
cache_path = Path(os.getenv("FASTEMBED_CACHE_PATH", default_cache_dir))
|
||||
else:
|
||||
cache_path = Path(cache_dir)
|
||||
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return cache_path
|
||||
|
||||
|
||||
def get_all_punctuation() -> set[str]:
|
||||
return set(
|
||||
chr(i) for i in range(sys.maxunicode) if unicodedata.category(chr(i)).startswith("P")
|
||||
)
|
||||
|
||||
|
||||
def remove_non_alphanumeric(text: str) -> str:
|
||||
return re.sub(r"[^\w\s]", " ", text, flags=re.UNICODE)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -19,6 +19,6 @@ class JinaEmbedding(TextEmbedding):
|
||||
model_name: str = "jinaai/jina-embeddings-v2-base-en",
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastembed.image.image_embedding import ImageEmbedding
|
||||
|
||||
__all__ = ["ImageEmbedding"]
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import ImageInput, OnnxProvider
|
||||
from fastembed.image.image_embedding_base import ImageEmbeddingBase
|
||||
from fastembed.image.onnx_embedding import OnnxImageEmbedding
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
|
||||
|
||||
class ImageEmbedding(ImageEmbeddingBase):
|
||||
EMBEDDINGS_REGISTRY: list[Type[ImageEmbeddingBase]] = [OnnxImageEmbedding]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"model": "Qdrant/clip-ViT-B-32-vision",
|
||||
"dim": 512,
|
||||
"description": "CLIP vision encoder based on ViT-B/32",
|
||||
"license": "mit",
|
||||
"size_in_GB": 0.33,
|
||||
"sources": {
|
||||
"hf": "Qdrant/clip-ViT-B-32-vision",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
}
|
||||
]
|
||||
```
|
||||
"""
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
result: list[DenseModelDescription] = []
|
||||
for embedding in cls.EMBEDDINGS_REGISTRY:
|
||||
result.extend(embedding._list_supported_models())
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
||||
supported_models = EMBEDDING_MODEL_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(
|
||||
model_name,
|
||||
cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in ImageEmbedding."
|
||||
"Please check the supported models using `ImageEmbedding.list_supported_models()`"
|
||||
)
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Get the embedding size of the current model"""
|
||||
if self._embedding_size is None:
|
||||
self._embedding_size = self.get_embedding_size(self.model_name)
|
||||
return self._embedding_size
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Get the embedding size of the passed model
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to get embedding size for.
|
||||
|
||||
Returns:
|
||||
int: The size of the embedding.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model name is not found in the supported models.
|
||||
"""
|
||||
descriptions = cls._list_supported_models()
|
||||
embedding_size: Optional[int] = None
|
||||
for description in descriptions:
|
||||
if description.model.lower() == model_name.lower():
|
||||
embedding_size = description.dim
|
||||
break
|
||||
if embedding_size is None:
|
||||
model_names = [description.model for description in descriptions]
|
||||
raise ValueError(
|
||||
f"Embedding size for model {model_name} was None. "
|
||||
f"Available model names: {model_names}"
|
||||
)
|
||||
return embedding_size
|
||||
|
||||
def embed(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of images into list of embeddings.
|
||||
|
||||
Args:
|
||||
images: Iterator of image paths or single image path to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self.model.embed(images, batch_size, parallel, **kwargs)
|
||||
@@ -0,0 +1,55 @@
|
||||
from typing import Iterable, Optional, Any, Union
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
from fastembed.common.types import ImageInput
|
||||
|
||||
|
||||
class ImageEmbeddingBase(ModelManagement[DenseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
self._embedding_size: Optional[int] = None
|
||||
|
||||
def embed(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds a list of images into a list of embeddings.
|
||||
|
||||
Args:
|
||||
images: The list of image paths to preprocess and embed.
|
||||
batch_size: Batch size for encoding
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[NdArray]: The embeddings.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Returns embedding size of the chosen model."""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Returns embedding size for the current model"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
@@ -0,0 +1,212 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import ImageInput, OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir, normalize
|
||||
from fastembed.image.image_embedding_base import ImageEmbeddingBase
|
||||
from fastembed.image.onnx_image_model import ImageEmbeddingWorker, OnnxImageModel
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_onnx_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="Qdrant/clip-ViT-B-32-vision",
|
||||
dim=512,
|
||||
description="Image embeddings, Multimodal (text&image), 2021 year",
|
||||
license="mit",
|
||||
size_in_GB=0.34,
|
||||
sources=ModelSource(hf="Qdrant/clip-ViT-B-32-vision"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="Qdrant/resnet50-onnx",
|
||||
dim=2048,
|
||||
description="Image embeddings, Unimodal (image), 2016 year",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.1,
|
||||
sources=ModelSource(hf="Qdrant/resnet50-onnx"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="Qdrant/Unicom-ViT-B-16",
|
||||
dim=768,
|
||||
description="Image embeddings (more detailed than Unicom-ViT-B-32), Multimodal (text&image), 2023 year",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.82,
|
||||
sources=ModelSource(hf="Qdrant/Unicom-ViT-B-16"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="Qdrant/Unicom-ViT-B-32",
|
||||
dim=512,
|
||||
description="Image embeddings, Multimodal (text&image), 2023 year",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.48,
|
||||
sources=ModelSource(hf="Qdrant/Unicom-ViT-B-32"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-clip-v1",
|
||||
dim=768,
|
||||
description="Image embeddings, Multimodal (text&image), 2024 year",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.34,
|
||||
sources=ModelSource(hf="jinaai/jina-clip-v1"),
|
||||
model_file="onnx/vision_model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[NumpyArray]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
"""
|
||||
Load the onnx model.
|
||||
"""
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_onnx_models
|
||||
|
||||
def embed(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of images into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
images: Iterator of image paths or single image path to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
|
||||
yield from self._embed_images(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
images=images,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["ImageEmbeddingWorker[NumpyArray]"]:
|
||||
return OnnxImageEmbeddingWorker
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
|
||||
return onnx_input
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
return normalize(output.model_output)
|
||||
|
||||
|
||||
class OnnxImageEmbeddingWorker(ImageEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> OnnxImageEmbedding:
|
||||
return OnnxImageEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
import contextlib
|
||||
import os
|
||||
from multiprocessing import get_all_start_methods
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from fastembed.image.transform.operators import Compose
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import ImageInput, OnnxProvider
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_preprocessor
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
# Holds type of the embedding result
|
||||
|
||||
|
||||
class OnnxImageModel(OnnxModel[T]):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["ImageEmbeddingWorker[T]"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _post_process_onnx_output(self, output: OnnxOutputContext, **kwargs: Any) -> Iterable[T]:
|
||||
"""Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
**kwargs: Additional keyword arguments that may be needed by specific implementations.
|
||||
|
||||
Returns:
|
||||
Iterable[T]: Post-processed output as an iterable of type T.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.processor: Optional[Compose] = None
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
def _load_onnx_model(
|
||||
self,
|
||||
model_dir: Path,
|
||||
model_file: str,
|
||||
threads: Optional[int],
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
) -> None:
|
||||
super()._load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_file,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_id=device_id,
|
||||
)
|
||||
self.processor = load_preprocessor(model_dir=model_dir)
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _build_onnx_input(self, encoded: NumpyArray) -> dict[str, NumpyArray]:
|
||||
input_name = self.model.get_inputs()[0].name # type: ignore[union-attr]
|
||||
return {input_name: encoded}
|
||||
|
||||
def onnx_embed(self, images: list[ImageInput], **kwargs: Any) -> OnnxOutputContext:
|
||||
with contextlib.ExitStack():
|
||||
image_files = [
|
||||
Image.open(image) if not isinstance(image, Image.Image) else image
|
||||
for image in images
|
||||
]
|
||||
assert self.processor is not None, "Processor is not initialized"
|
||||
encoded = np.array(self.processor(image_files))
|
||||
onnx_input = self._build_onnx_input(encoded)
|
||||
onnx_input = self._preprocess_onnx_input(onnx_input)
|
||||
model_output = self.model.run(None, onnx_input) # type: ignore[union-attr]
|
||||
embeddings = model_output[0].reshape(len(images), -1)
|
||||
return OnnxOutputContext(model_output=embeddings)
|
||||
|
||||
def _embed_images(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[T]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(images, (str, Path, Image.Image)):
|
||||
images = [images]
|
||||
is_small = True
|
||||
|
||||
if isinstance(images, list) and len(images) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
|
||||
for batch in iter_batch(images, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed(batch), **kwargs)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"providers": providers,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_worker_class(),
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(images, batch_size), **params):
|
||||
yield from self._post_process_onnx_output(batch, **kwargs) # type: ignore
|
||||
|
||||
|
||||
class ImageEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
embeddings = self.model.onnx_embed(batch)
|
||||
yield idx, embeddings
|
||||
@@ -0,0 +1,149 @@
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
|
||||
def convert_to_rgb(image: Image.Image) -> Image.Image:
|
||||
if image.mode == "RGB":
|
||||
return image
|
||||
|
||||
image = image.convert("RGB")
|
||||
return image
|
||||
|
||||
|
||||
def center_crop(
|
||||
image: Union[Image.Image, NumpyArray],
|
||||
size: tuple[int, int],
|
||||
) -> NumpyArray:
|
||||
if isinstance(image, np.ndarray):
|
||||
_, orig_height, orig_width = image.shape
|
||||
else:
|
||||
orig_height, orig_width = image.height, image.width
|
||||
# (H, W, C) -> (C, H, W)
|
||||
image = np.array(image).transpose((2, 0, 1))
|
||||
|
||||
crop_height, crop_width = size
|
||||
|
||||
# left upper corner (0, 0)
|
||||
top = (orig_height - crop_height) // 2
|
||||
bottom = top + crop_height
|
||||
left = (orig_width - crop_width) // 2
|
||||
right = left + crop_width
|
||||
|
||||
# Check if cropped area is within image boundaries
|
||||
if top >= 0 and bottom <= orig_height and left >= 0 and right <= orig_width:
|
||||
image = image[..., top:bottom, left:right]
|
||||
return image
|
||||
|
||||
# Padding with zeros
|
||||
new_height = max(crop_height, orig_height)
|
||||
new_width = max(crop_width, orig_width)
|
||||
new_shape = image.shape[:-2] + (new_height, new_width)
|
||||
new_image = np.zeros_like(image, shape=new_shape, dtype=np.float32)
|
||||
|
||||
top_pad = (new_height - orig_height) // 2
|
||||
bottom_pad = top_pad + orig_height
|
||||
left_pad = (new_width - orig_width) // 2
|
||||
right_pad = left_pad + orig_width
|
||||
new_image[..., top_pad:bottom_pad, left_pad:right_pad] = image
|
||||
|
||||
top += top_pad
|
||||
bottom += top_pad
|
||||
left += left_pad
|
||||
right += left_pad
|
||||
|
||||
new_image = new_image[
|
||||
..., max(0, top) : min(new_height, bottom), max(0, left) : min(new_width, right)
|
||||
]
|
||||
|
||||
return new_image
|
||||
|
||||
|
||||
def normalize(
|
||||
image: NumpyArray,
|
||||
mean: Union[float, list[float]],
|
||||
std: Union[float, list[float]],
|
||||
) -> NumpyArray:
|
||||
num_channels = image.shape[1] if len(image.shape) == 4 else image.shape[0]
|
||||
|
||||
if not np.issubdtype(image.dtype, np.floating):
|
||||
image = image.astype(np.float32)
|
||||
|
||||
mean_list = mean if isinstance(mean, list) else [mean] * num_channels
|
||||
|
||||
if len(mean_list) != num_channels:
|
||||
raise ValueError(
|
||||
f"mean must have the same number of channels as the image, image has {num_channels} channels, got "
|
||||
f"{len(mean_list)}"
|
||||
)
|
||||
|
||||
mean_arr = np.array(mean_list, dtype=np.float32)
|
||||
|
||||
std_list = std if isinstance(std, list) else [std] * num_channels
|
||||
if len(std_list) != num_channels:
|
||||
raise ValueError(
|
||||
f"std must have the same number of channels as the image, image has {num_channels} channels, got {len(std_list)}"
|
||||
)
|
||||
|
||||
std_arr = np.array(std_list, dtype=np.float32)
|
||||
|
||||
image_upd = ((image.T - mean_arr) / std_arr).T
|
||||
return image_upd
|
||||
|
||||
|
||||
def resize(
|
||||
image: Image.Image,
|
||||
size: Union[int, tuple[int, int]],
|
||||
resample: Union[int, Image.Resampling] = Image.Resampling.BILINEAR,
|
||||
) -> Image.Image:
|
||||
if isinstance(size, tuple):
|
||||
return image.resize(size, resample)
|
||||
|
||||
height, width = image.height, image.width
|
||||
short, long = (width, height) if width <= height else (height, width)
|
||||
|
||||
new_short, new_long = size, int(size * long / short)
|
||||
if width <= height:
|
||||
new_size = (new_short, new_long)
|
||||
else:
|
||||
new_size = (new_long, new_short)
|
||||
return image.resize(new_size, resample)
|
||||
|
||||
|
||||
def rescale(image: NumpyArray, scale: float, dtype: type = np.float32) -> NumpyArray:
|
||||
return (image * scale).astype(dtype)
|
||||
|
||||
|
||||
def pil2ndarray(image: Union[Image.Image, NumpyArray]) -> NumpyArray:
|
||||
if isinstance(image, Image.Image):
|
||||
return np.asarray(image).transpose((2, 0, 1))
|
||||
return image
|
||||
|
||||
|
||||
def pad2square(
|
||||
image: Image.Image,
|
||||
size: int,
|
||||
fill_color: Union[str, int, tuple[int, ...]] = 0,
|
||||
) -> Image.Image:
|
||||
height, width = image.height, image.width
|
||||
|
||||
left, right = 0, width
|
||||
top, bottom = 0, height
|
||||
|
||||
crop_required = False
|
||||
if width > size:
|
||||
left = (width - size) // 2
|
||||
right = left + size
|
||||
crop_required = True
|
||||
|
||||
if height > size:
|
||||
top = (height - size) // 2
|
||||
bottom = top + size
|
||||
crop_required = True
|
||||
|
||||
new_image = Image.new(mode="RGB", size=(size, size), color=fill_color)
|
||||
new_image.paste(image.crop((left, top, right, bottom)) if crop_required else image)
|
||||
return new_image
|
||||
@@ -0,0 +1,269 @@
|
||||
from typing import Any, Union, Optional
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.image.transform.functional import (
|
||||
center_crop,
|
||||
convert_to_rgb,
|
||||
normalize,
|
||||
pil2ndarray,
|
||||
rescale,
|
||||
resize,
|
||||
pad2square,
|
||||
)
|
||||
|
||||
|
||||
class Transform:
|
||||
def __call__(self, images: list[Any]) -> Union[list[Image.Image], list[NumpyArray]]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
|
||||
class ConvertToRGB(Transform):
|
||||
def __call__(self, images: list[Image.Image]) -> list[Image.Image]:
|
||||
return [convert_to_rgb(image=image) for image in images]
|
||||
|
||||
|
||||
class CenterCrop(Transform):
|
||||
def __init__(self, size: tuple[int, int]):
|
||||
self.size = size
|
||||
|
||||
def __call__(self, images: list[Image.Image]) -> list[NumpyArray]:
|
||||
return [center_crop(image=image, size=self.size) for image in images]
|
||||
|
||||
|
||||
class Normalize(Transform):
|
||||
def __init__(self, mean: Union[float, list[float]], std: Union[float, list[float]]):
|
||||
self.mean = mean
|
||||
self.std = std
|
||||
|
||||
def __call__(self, images: list[NumpyArray]) -> list[NumpyArray]:
|
||||
return [normalize(image, mean=self.mean, std=self.std) for image in images]
|
||||
|
||||
|
||||
class Resize(Transform):
|
||||
def __init__(
|
||||
self,
|
||||
size: Union[int, tuple[int, int]],
|
||||
resample: Image.Resampling = Image.Resampling.BICUBIC,
|
||||
):
|
||||
self.size = size
|
||||
self.resample = resample
|
||||
|
||||
def __call__(self, images: list[Image.Image]) -> list[Image.Image]:
|
||||
return [resize(image, size=self.size, resample=self.resample) for image in images]
|
||||
|
||||
|
||||
class Rescale(Transform):
|
||||
def __init__(self, scale: float = 1 / 255):
|
||||
self.scale = scale
|
||||
|
||||
def __call__(self, images: list[NumpyArray]) -> list[NumpyArray]:
|
||||
return [rescale(image, scale=self.scale) for image in images]
|
||||
|
||||
|
||||
class PILtoNDarray(Transform):
|
||||
def __call__(self, images: list[Union[Image.Image, NumpyArray]]) -> list[NumpyArray]:
|
||||
return [pil2ndarray(image) for image in images]
|
||||
|
||||
|
||||
class PadtoSquare(Transform):
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
fill_color: Union[str, int, tuple[int, ...]],
|
||||
):
|
||||
self.size = size
|
||||
self.fill_color = fill_color
|
||||
|
||||
def __call__(self, images: list[Image.Image]) -> list[Image.Image]:
|
||||
return [
|
||||
pad2square(image=image, size=self.size, fill_color=self.fill_color) for image in images
|
||||
]
|
||||
|
||||
|
||||
class Compose:
|
||||
def __init__(self, transforms: list[Transform]):
|
||||
self.transforms = transforms
|
||||
|
||||
def __call__(
|
||||
self, images: Union[list[Image.Image], list[NumpyArray]]
|
||||
) -> Union[list[NumpyArray], list[Image.Image]]:
|
||||
for transform in self.transforms:
|
||||
images = transform(images)
|
||||
return images
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict[str, Any]) -> "Compose":
|
||||
"""Creates processor from a config dict.
|
||||
Args:
|
||||
config (dict[str, Any]): Configuration dictionary.
|
||||
|
||||
Valid keys:
|
||||
- do_resize
|
||||
- resize_mode
|
||||
- size
|
||||
- fill_color
|
||||
- do_center_crop
|
||||
- crop_size
|
||||
- do_rescale
|
||||
- rescale_factor
|
||||
- do_normalize
|
||||
- image_mean
|
||||
- mean
|
||||
- image_std
|
||||
- std
|
||||
- resample
|
||||
- interpolation
|
||||
Valid size keys (nested):
|
||||
- {"height", "width"}
|
||||
- {"shortest_edge"}
|
||||
|
||||
Returns:
|
||||
Compose: Image processor.
|
||||
"""
|
||||
transforms: list[Transform] = []
|
||||
cls._get_convert_to_rgb(transforms, config)
|
||||
cls._get_resize(transforms, config)
|
||||
cls._get_pad2square(transforms, config)
|
||||
cls._get_center_crop(transforms, config)
|
||||
cls._get_pil2ndarray(transforms, config)
|
||||
cls._get_rescale(transforms, config)
|
||||
cls._get_normalize(transforms, config)
|
||||
return cls(transforms=transforms)
|
||||
|
||||
@staticmethod
|
||||
def _get_convert_to_rgb(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
transforms.append(ConvertToRGB())
|
||||
|
||||
@classmethod
|
||||
def _get_resize(cls, transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
mode = config.get("image_processor_type", "CLIPImageProcessor")
|
||||
if mode in ("CLIPImageProcessor", "SiglipImageProcessor"):
|
||||
if config.get("do_resize", False):
|
||||
size = config["size"]
|
||||
if "shortest_edge" in size:
|
||||
size = size["shortest_edge"]
|
||||
elif "height" in size and "width" in size:
|
||||
size = (size["height"], size["width"])
|
||||
else:
|
||||
raise ValueError(
|
||||
"Size must contain either 'shortest_edge' or 'height' and 'width'."
|
||||
)
|
||||
transforms.append(
|
||||
Resize(
|
||||
size=size,
|
||||
resample=config.get("resample", Image.Resampling.BICUBIC),
|
||||
)
|
||||
)
|
||||
elif mode == "ConvNextFeatureExtractor":
|
||||
if "size" in config and "shortest_edge" not in config["size"]:
|
||||
raise ValueError(
|
||||
f"Size dictionary must contain 'shortest_edge' key. Got {config['size'].keys()}"
|
||||
)
|
||||
shortest_edge = config["size"]["shortest_edge"]
|
||||
crop_pct = config.get("crop_pct", 0.875)
|
||||
if shortest_edge < 384:
|
||||
# maintain same ratio, resizing shortest edge to shortest_edge/crop_pct
|
||||
resize_shortest_edge = int(shortest_edge / crop_pct)
|
||||
transforms.append(
|
||||
Resize(
|
||||
size=resize_shortest_edge,
|
||||
resample=config.get("resample", Image.Resampling.BICUBIC),
|
||||
)
|
||||
)
|
||||
transforms.append(CenterCrop(size=(shortest_edge, shortest_edge)))
|
||||
else:
|
||||
transforms.append(
|
||||
Resize(
|
||||
size=(shortest_edge, shortest_edge),
|
||||
resample=config.get("resample", Image.Resampling.BICUBIC),
|
||||
)
|
||||
)
|
||||
elif mode == "JinaCLIPImageProcessor":
|
||||
interpolation = config.get("interpolation")
|
||||
if isinstance(interpolation, str):
|
||||
resample = cls._interpolation_resolver(interpolation)
|
||||
else:
|
||||
resample = interpolation or Image.Resampling.BICUBIC
|
||||
|
||||
if "size" in config:
|
||||
resize_mode = config.get("resize_mode", "shortest")
|
||||
if resize_mode == "shortest":
|
||||
transforms.append(
|
||||
Resize(
|
||||
size=config["size"],
|
||||
resample=resample,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Preprocessor {mode} is not supported")
|
||||
|
||||
@staticmethod
|
||||
def _get_center_crop(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
mode = config.get("image_processor_type", "CLIPImageProcessor")
|
||||
if mode in ("CLIPImageProcessor", "SiglipImageProcessor"):
|
||||
if config.get("do_center_crop", False):
|
||||
crop_size_raw = config["crop_size"]
|
||||
crop_size: tuple[int, int]
|
||||
if isinstance(crop_size_raw, int):
|
||||
crop_size = (crop_size_raw, crop_size_raw)
|
||||
elif isinstance(crop_size_raw, dict):
|
||||
crop_size = (crop_size_raw["height"], crop_size_raw["width"])
|
||||
else:
|
||||
raise ValueError(f"Invalid crop size: {crop_size_raw}")
|
||||
transforms.append(CenterCrop(size=crop_size))
|
||||
elif mode == "ConvNextFeatureExtractor":
|
||||
pass
|
||||
elif mode == "JinaCLIPImageProcessor":
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Preprocessor {mode} is not supported")
|
||||
|
||||
@staticmethod
|
||||
def _get_pil2ndarray(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
transforms.append(PILtoNDarray())
|
||||
|
||||
@staticmethod
|
||||
def _get_rescale(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
if config.get("do_rescale", True):
|
||||
rescale_factor = config.get("rescale_factor", 1 / 255)
|
||||
transforms.append(Rescale(scale=rescale_factor))
|
||||
|
||||
@staticmethod
|
||||
def _get_normalize(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
if config.get("do_normalize", False):
|
||||
transforms.append(Normalize(mean=config["image_mean"], std=config["image_std"]))
|
||||
elif "mean" in config and "std" in config:
|
||||
transforms.append(Normalize(mean=config["mean"], std=config["std"]))
|
||||
|
||||
@staticmethod
|
||||
def _get_pad2square(transforms: list[Transform], config: dict[str, Any]) -> None:
|
||||
mode = config.get("image_processor_type", "CLIPImageProcessor")
|
||||
if mode == "CLIPImageProcessor":
|
||||
pass
|
||||
elif mode == "ConvNextFeatureExtractor":
|
||||
pass
|
||||
elif mode == "JinaCLIPImageProcessor":
|
||||
transforms.append(
|
||||
PadtoSquare(
|
||||
size=config["size"],
|
||||
fill_color=config.get("fill_color", 0),
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _interpolation_resolver(resample: Optional[str] = None) -> Image.Resampling:
|
||||
interpolation_map = {
|
||||
"nearest": Image.Resampling.NEAREST,
|
||||
"lanczos": Image.Resampling.LANCZOS,
|
||||
"bilinear": Image.Resampling.BILINEAR,
|
||||
"bicubic": Image.Resampling.BICUBIC,
|
||||
"box": Image.Resampling.BOX,
|
||||
"hamming": Image.Resampling.HAMMING,
|
||||
}
|
||||
|
||||
if resample and (method := interpolation_map.get(resample.lower())):
|
||||
return method
|
||||
|
||||
raise ValueError(f"Unknown interpolation method: {resample}")
|
||||
@@ -0,0 +1,5 @@
|
||||
from fastembed.late_interaction.late_interaction_text_embedding import (
|
||||
LateInteractionTextEmbedding,
|
||||
)
|
||||
|
||||
__all__ = ["LateInteractionTextEmbedding"]
|
||||
@@ -0,0 +1,265 @@
|
||||
import string
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.late_interaction.late_interaction_embedding_base import (
|
||||
LateInteractionTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_colbert_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="colbert-ir/colbertv2.0",
|
||||
dim=128,
|
||||
description="Late interaction model",
|
||||
license="mit",
|
||||
size_in_GB=0.44,
|
||||
sources=ModelSource(hf="colbert-ir/colbertv2.0"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="answerdotai/answerai-colbert-small-v1",
|
||||
dim=96,
|
||||
description="Text embeddings, Unimodal (text), Multilingual (~100 languages), 512 input tokens truncation, 2024 year",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.13,
|
||||
sources=ModelSource(hf="answerdotai/answerai-colbert-small-v1"),
|
||||
model_file="vespa_colbert.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[NumpyArray]):
|
||||
QUERY_MARKER_TOKEN_ID = 1
|
||||
DOCUMENT_MARKER_TOKEN_ID = 2
|
||||
MIN_QUERY_LENGTH = 31 # it's 32, we add one additional special token in the beginning
|
||||
MASK_TOKEN = "[MASK]"
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, is_doc: bool = True, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
if not is_doc:
|
||||
for embedding in output.model_output:
|
||||
yield embedding
|
||||
else:
|
||||
if output.input_ids is None or output.attention_mask is None:
|
||||
raise ValueError(
|
||||
"input_ids and attention_mask must be provided for document post-processing"
|
||||
)
|
||||
|
||||
for i, token_sequence in enumerate(output.input_ids):
|
||||
for j, token_id in enumerate(token_sequence): # type: ignore
|
||||
if token_id in self.skip_list or token_id == self.pad_token_id:
|
||||
output.attention_mask[i, j] = 0
|
||||
|
||||
output.model_output *= np.expand_dims(output.attention_mask, 2)
|
||||
norm = np.linalg.norm(output.model_output, ord=2, axis=2, keepdims=True)
|
||||
norm_clamped = np.maximum(norm, 1e-12)
|
||||
output.model_output /= norm_clamped
|
||||
|
||||
for embedding, attention_mask in zip(output.model_output, output.attention_mask):
|
||||
yield embedding[attention_mask == 1]
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], is_doc: bool = True, **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
marker_token = self.DOCUMENT_MARKER_TOKEN_ID if is_doc else self.QUERY_MARKER_TOKEN_ID
|
||||
onnx_input["input_ids"] = np.insert(
|
||||
onnx_input["input_ids"].astype(np.int64), 1, marker_token, axis=1
|
||||
)
|
||||
onnx_input["attention_mask"] = np.insert(
|
||||
onnx_input["attention_mask"].astype(np.int64), 1, 1, axis=1
|
||||
)
|
||||
return onnx_input
|
||||
|
||||
def tokenize(self, documents: list[str], is_doc: bool = True, **kwargs: Any) -> list[Encoding]:
|
||||
return (
|
||||
self._tokenize_documents(documents=documents)
|
||||
if is_doc
|
||||
else self._tokenize_query(query=next(iter(documents)))
|
||||
)
|
||||
|
||||
def _tokenize_query(self, query: str) -> list[Encoding]:
|
||||
assert self.query_tokenizer is not None
|
||||
encoded = self.query_tokenizer.encode_batch([query])
|
||||
return encoded
|
||||
|
||||
def _tokenize_documents(self, documents: list[str]) -> list[Encoding]:
|
||||
encoded = self.tokenizer.encode_batch(documents) # type: ignore[union-attr]
|
||||
return encoded
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_colbert_models
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
self.mask_token_id: Optional[int] = None
|
||||
self.pad_token_id: Optional[int] = None
|
||||
self.skip_list: set[int] = set()
|
||||
|
||||
self.query_tokenizer: Optional[Tokenizer] = None
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
self.query_tokenizer, _ = load_tokenizer(model_dir=self._model_dir)
|
||||
|
||||
assert self.tokenizer is not None
|
||||
self.mask_token_id = self.special_token_to_id[self.MASK_TOKEN]
|
||||
self.pad_token_id = self.tokenizer.padding["pad_id"]
|
||||
self.skip_list = {
|
||||
self.tokenizer.encode(symbol, add_special_tokens=False).ids[0]
|
||||
for symbol in string.punctuation
|
||||
}
|
||||
current_max_length = self.tokenizer.truncation["max_length"]
|
||||
# ensure not to overflow after adding document-marker
|
||||
self.tokenizer.enable_truncation(max_length=current_max_length - 1)
|
||||
self.query_tokenizer.enable_truncation(max_length=current_max_length - 1)
|
||||
self.query_tokenizer.enable_padding(
|
||||
pad_token=self.MASK_TOKEN,
|
||||
pad_id=self.mask_token_id,
|
||||
length=self.MIN_QUERY_LENGTH,
|
||||
)
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
if isinstance(query, str):
|
||||
query = [query]
|
||||
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
|
||||
for text in query:
|
||||
yield from self._post_process_onnx_output(
|
||||
self.onnx_embed([text], is_doc=False), is_doc=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[TextEmbeddingWorker[NumpyArray]]:
|
||||
return ColbertEmbeddingWorker
|
||||
|
||||
|
||||
class ColbertEmbeddingWorker(TextEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> Colbert:
|
||||
return Colbert(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
from typing import Any, Type
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.late_interaction.colbert import Colbert, ColbertEmbeddingWorker
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_jina_colbert_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-colbert-v2",
|
||||
dim=128,
|
||||
description="New model that expands capabilities of colbert-v1 with multilingual and context length of 8192, 2024 year",
|
||||
license="cc-by-nc-4.0",
|
||||
size_in_GB=2.24,
|
||||
sources=ModelSource(hf="jinaai/jina-colbert-v2"),
|
||||
model_file="onnx/model.onnx",
|
||||
additional_files=["onnx/model.onnx_data"],
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class JinaColbert(Colbert):
|
||||
QUERY_MARKER_TOKEN_ID = 250002
|
||||
DOCUMENT_MARKER_TOKEN_ID = 250003
|
||||
MIN_QUERY_LENGTH = 31 # it's 32, we add one additional special token in the beginning
|
||||
MASK_TOKEN = "<mask>"
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[ColbertEmbeddingWorker]:
|
||||
return JinaColbertEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_jina_colbert_models
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], is_doc: bool = True, **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
onnx_input = super()._preprocess_onnx_input(onnx_input, is_doc)
|
||||
|
||||
# the attention mask for jina-colbert-v2 is always 1 in queries
|
||||
if not is_doc:
|
||||
onnx_input["attention_mask"][:] = 1
|
||||
return onnx_input
|
||||
|
||||
|
||||
class JinaColbertEmbeddingWorker(ColbertEmbeddingWorker):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> JinaColbert:
|
||||
return JinaColbert(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
from typing import Iterable, Optional, Union, Any
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
|
||||
|
||||
class LateInteractionTextEmbeddingBase(ModelManagement[DenseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
self._embedding_size: Optional[int] = None
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds a list of text passages into a list of embeddings.
|
||||
|
||||
Args:
|
||||
texts (Iterable[str]): The list of texts to embed.
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[NdArray]: The embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.embed(texts, **kwargs)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
Args:
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[NdArray]: The embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
if isinstance(query, str):
|
||||
yield from self.embed([query], **kwargs)
|
||||
else:
|
||||
yield from self.embed(query, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Returns embedding size of the chosen model."""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Returns embedding size for the current model"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
@@ -0,0 +1,153 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.late_interaction.colbert import Colbert
|
||||
from fastembed.late_interaction.jina_colbert import JinaColbert
|
||||
from fastembed.late_interaction.late_interaction_embedding_base import (
|
||||
LateInteractionTextEmbeddingBase,
|
||||
)
|
||||
|
||||
|
||||
class LateInteractionTextEmbedding(LateInteractionTextEmbeddingBase):
|
||||
EMBEDDINGS_REGISTRY: list[Type[LateInteractionTextEmbeddingBase]] = [Colbert, JinaColbert]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"model": "colbert-ir/colbertv2.0",
|
||||
"dim": 128,
|
||||
"description": "Late interaction model",
|
||||
"license": "mit",
|
||||
"size_in_GB": 0.44,
|
||||
"sources": {
|
||||
"hf": "colbert-ir/colbertv2.0",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
]
|
||||
```
|
||||
"""
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
result: list[DenseModelDescription] = []
|
||||
for embedding in cls.EMBEDDINGS_REGISTRY:
|
||||
result.extend(embedding._list_supported_models())
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
||||
supported_models = EMBEDDING_MODEL_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(
|
||||
model_name,
|
||||
cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in LateInteractionTextEmbedding."
|
||||
"Please check the supported models using `LateInteractionTextEmbedding.list_supported_models()`"
|
||||
)
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Get the embedding size of the current model"""
|
||||
if self._embedding_size is None:
|
||||
self._embedding_size = self.get_embedding_size(self.model_name)
|
||||
return self._embedding_size
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Get the embedding size of the passed model
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to get embedding size for.
|
||||
|
||||
Returns:
|
||||
int: The size of the embedding.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model name is not found in the supported models.
|
||||
"""
|
||||
descriptions = cls._list_supported_models()
|
||||
embedding_size: Optional[int] = None
|
||||
for description in descriptions:
|
||||
if description.model.lower() == model_name.lower():
|
||||
embedding_size = description.dim
|
||||
break
|
||||
if embedding_size is None:
|
||||
model_names = [description.model for description in descriptions]
|
||||
raise ValueError(
|
||||
f"Embedding size for model {model_name} was None. "
|
||||
f"Available model names: {model_names}"
|
||||
)
|
||||
return embedding_size
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
Args:
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[NdArray]: The embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.model.query_embed(query, **kwargs)
|
||||
@@ -0,0 +1,83 @@
|
||||
from dataclasses import asdict
|
||||
from typing import Union, Iterable, Optional, Any, Type
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.late_interaction.late_interaction_embedding_base import (
|
||||
LateInteractionTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding
|
||||
from fastembed.text.onnx_text_model import TextEmbeddingWorker
|
||||
|
||||
|
||||
supported_token_embeddings_models = [
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-small-en-tokens",
|
||||
dim=512,
|
||||
description="Text embeddings, Unimodal (text), English, 8192 input tokens truncation,"
|
||||
" Prefixes for queries/documents: not necessary, 2023 year.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.12,
|
||||
sources=ModelSource(hf="xenova/jina-embeddings-v2-small-en"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class TokenEmbeddingsModel(OnnxTextEmbedding, LateInteractionTextEmbeddingBase):
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_token_embeddings_models
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
"""
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[TextEmbeddingWorker[NumpyArray]]:
|
||||
return TokensEmbeddingWorker
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
# Size: (batch_size, sequence_length, hidden_size)
|
||||
embeddings = output.model_output
|
||||
# Size: (batch_size, sequence_length)
|
||||
assert output.attention_mask is not None
|
||||
masks = output.attention_mask
|
||||
|
||||
# For each document we only select those embeddings that are not masked out
|
||||
for i in range(embeddings.shape[0]):
|
||||
yield embeddings[i, masks[i] == 1]
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
yield from super().embed(documents, batch_size=batch_size, parallel=parallel, **kwargs)
|
||||
|
||||
|
||||
class TokensEmbeddingWorker(TextEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(
|
||||
self, model_name: str, cache_dir: str, **kwargs: Any
|
||||
) -> TokenEmbeddingsModel:
|
||||
return TokenEmbeddingsModel(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
from fastembed.late_interaction_multimodal.late_interaction_multimodal_embedding import (
|
||||
LateInteractionMultimodalEmbedding,
|
||||
)
|
||||
|
||||
__all__ = ["LateInteractionMultimodalEmbedding"]
|
||||
@@ -0,0 +1,305 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from tokenizers import Encoding
|
||||
|
||||
from fastembed.common import OnnxProvider, ImageInput
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.late_interaction_multimodal.late_interaction_multimodal_embedding_base import (
|
||||
LateInteractionMultimodalEmbeddingBase,
|
||||
)
|
||||
from fastembed.late_interaction_multimodal.onnx_multimodal_model import (
|
||||
OnnxMultimodalModel,
|
||||
TextEmbeddingWorker,
|
||||
ImageEmbeddingWorker,
|
||||
)
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_colpali_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="Qdrant/colpali-v1.3-fp16",
|
||||
dim=128,
|
||||
description="Text embeddings, Multimodal (text&image), English, 50 tokens query length truncation, 2024.",
|
||||
license="mit",
|
||||
size_in_GB=6.5,
|
||||
sources=ModelSource(hf="Qdrant/colpali-v1.3-fp16"),
|
||||
additional_files=["model.onnx_data"],
|
||||
model_file="model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ColPali(LateInteractionMultimodalEmbeddingBase, OnnxMultimodalModel[NumpyArray]):
|
||||
QUERY_PREFIX = "Query: "
|
||||
BOS_TOKEN = "<s>"
|
||||
PAD_TOKEN = "<pad>"
|
||||
QUERY_MARKER_TOKEN_ID = [2, 5098]
|
||||
IMAGE_PLACEHOLDER_SIZE = (3, 448, 448)
|
||||
EMPTY_TEXT_PLACEHOLDER = np.array(
|
||||
[257152] * 1024 + [2, 50721, 573, 2416, 235265, 108]
|
||||
) # This is a tokenization of '<image>' * 1024 + '<bos>Describe the image.\n' line which is used as placeholder
|
||||
# while processing an image
|
||||
EVEN_ATTENTION_MASK = np.array([1] * 1030)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
self.mask_token_id = None
|
||||
self.pad_token_id = None
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_colpali_models
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
def _post_process_onnx_image_output(
|
||||
self,
|
||||
output: OnnxOutputContext,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
|
||||
Returns:
|
||||
Iterable[NumpyArray]: Post-processed output as NumPy arrays.
|
||||
"""
|
||||
assert self.model_description.dim is not None, "Model dim is not defined"
|
||||
return output.model_output.reshape(
|
||||
output.model_output.shape[0], -1, self.model_description.dim
|
||||
)
|
||||
|
||||
def _post_process_onnx_text_output(
|
||||
self,
|
||||
output: OnnxOutputContext,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
|
||||
Returns:
|
||||
Iterable[NumpyArray]: Post-processed output as NumPy arrays.
|
||||
"""
|
||||
return output.model_output
|
||||
|
||||
def tokenize(self, documents: list[str], **kwargs: Any) -> list[Encoding]:
|
||||
texts_query: list[str] = []
|
||||
for query in documents:
|
||||
query = self.BOS_TOKEN + self.QUERY_PREFIX + query + self.PAD_TOKEN * 10
|
||||
query += "\n"
|
||||
|
||||
texts_query.append(query)
|
||||
encoded = self.tokenizer.encode_batch(texts_query) # type: ignore[union-attr]
|
||||
return encoded
|
||||
|
||||
def _preprocess_onnx_text_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
onnx_input["input_ids"] = np.array(
|
||||
[
|
||||
self.QUERY_MARKER_TOKEN_ID + input_ids[2:].tolist() # type: ignore[index]
|
||||
for input_ids in onnx_input["input_ids"]
|
||||
]
|
||||
)
|
||||
empty_image_placeholder: NumpyArray = np.zeros(
|
||||
self.IMAGE_PLACEHOLDER_SIZE, dtype=np.float32
|
||||
)
|
||||
onnx_input["pixel_values"] = np.array(
|
||||
[empty_image_placeholder for _ in onnx_input["input_ids"]],
|
||||
)
|
||||
return onnx_input
|
||||
|
||||
def _preprocess_onnx_image_input(
|
||||
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Add placeholders for text input when processing image data for ONNX.
|
||||
Args:
|
||||
onnx_input (Dict[str, NumpyArray]): Preprocessed image inputs.
|
||||
**kwargs: Additional arguments.
|
||||
Returns:
|
||||
Dict[str, NumpyArray]: ONNX input with text placeholders.
|
||||
"""
|
||||
onnx_input["input_ids"] = np.array(
|
||||
[self.EMPTY_TEXT_PLACEHOLDER for _ in onnx_input["pixel_values"]]
|
||||
)
|
||||
onnx_input["attention_mask"] = np.array(
|
||||
[self.EVEN_ATTENTION_MASK for _ in onnx_input["pixel_values"]]
|
||||
)
|
||||
return onnx_input
|
||||
|
||||
def embed_text(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def embed_image(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of images into list of embeddings.
|
||||
|
||||
Args:
|
||||
images: Iterator of image paths or single image path to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_images(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
images=images,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_text_worker_class(cls) -> Type[TextEmbeddingWorker[NumpyArray]]:
|
||||
return ColPaliTextEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def _get_image_worker_class(cls) -> Type[ImageEmbeddingWorker[NumpyArray]]:
|
||||
return ColPaliImageEmbeddingWorker
|
||||
|
||||
|
||||
class ColPaliTextEmbeddingWorker(TextEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> ColPali:
|
||||
return ColPali(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ColPaliImageEmbeddingWorker(ImageEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> ColPali:
|
||||
return ColPali(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastembed.common import OnnxProvider, ImageInput
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.late_interaction_multimodal.colpali import ColPali
|
||||
|
||||
from fastembed.late_interaction_multimodal.late_interaction_multimodal_embedding_base import (
|
||||
LateInteractionMultimodalEmbeddingBase,
|
||||
)
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
|
||||
|
||||
class LateInteractionMultimodalEmbedding(LateInteractionMultimodalEmbeddingBase):
|
||||
EMBEDDINGS_REGISTRY: list[Type[LateInteractionMultimodalEmbeddingBase]] = [ColPali]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"model": "Qdrant/colpali-v1.3-fp16",
|
||||
"dim": 128,
|
||||
"description": "Text embeddings, Unimodal (text), Aligned to image latent space, ColBERT-compatible, 512 tokens max, 2024.",
|
||||
"license": "mit",
|
||||
"size_in_GB": 6.06,
|
||||
"sources": {
|
||||
"hf": "Qdrant/colpali-v1.3-fp16",
|
||||
},
|
||||
"additional_files": [
|
||||
"model.onnx_data",
|
||||
],
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
]
|
||||
```
|
||||
"""
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
result: list[DenseModelDescription] = []
|
||||
for embedding in cls.EMBEDDINGS_REGISTRY:
|
||||
result.extend(embedding._list_supported_models())
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
||||
supported_models = EMBEDDING_MODEL_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(
|
||||
model_name,
|
||||
cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in LateInteractionMultimodalEmbedding."
|
||||
"Please check the supported models using `LateInteractionMultimodalEmbedding.list_supported_models()`"
|
||||
)
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Get the embedding size of the current model"""
|
||||
if self._embedding_size is None:
|
||||
self._embedding_size = self.get_embedding_size(self.model_name)
|
||||
return self._embedding_size
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Get the embedding size of the passed model
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to get embedding size for.
|
||||
|
||||
Returns:
|
||||
int: The size of the embedding.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model name is not found in the supported models.
|
||||
"""
|
||||
descriptions = cls._list_supported_models()
|
||||
embedding_size: Optional[int] = None
|
||||
for description in descriptions:
|
||||
if description.model.lower() == model_name.lower():
|
||||
embedding_size = description.dim
|
||||
break
|
||||
if embedding_size is None:
|
||||
model_names = [description.model for description in descriptions]
|
||||
raise ValueError(
|
||||
f"Embedding size for model {model_name} was None. "
|
||||
f"Available model names: {model_names}"
|
||||
)
|
||||
return embedding_size
|
||||
|
||||
def embed_text(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self.model.embed_text(documents, batch_size, parallel, **kwargs)
|
||||
|
||||
def embed_image(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of images into list of embeddings.
|
||||
|
||||
Args:
|
||||
images: Iterator of image paths or single image path to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per image
|
||||
"""
|
||||
yield from self.model.embed_image(images, batch_size, parallel, **kwargs)
|
||||
@@ -0,0 +1,78 @@
|
||||
from typing import Iterable, Optional, Union, Any
|
||||
|
||||
|
||||
from fastembed.common import ImageInput
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
|
||||
class LateInteractionMultimodalEmbeddingBase(ModelManagement[DenseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
self._embedding_size: Optional[int] = None
|
||||
|
||||
def embed_text(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds a list of documents into a list of embeddings.
|
||||
|
||||
Args:
|
||||
documents (Iterable[str]): The list of texts to embed.
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[NumpyArray]: The embeddings.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def embed_image(
|
||||
self,
|
||||
images: Union[ImageInput, Iterable[ImageInput]],
|
||||
batch_size: int = 16,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of images into list of embeddings.
|
||||
Args:
|
||||
images: Iterator of image paths or single image path to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per image
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Returns embedding size of the chosen model."""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Returns embedding size for the current model"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
@@ -0,0 +1,279 @@
|
||||
import contextlib
|
||||
import os
|
||||
from multiprocessing import get_all_start_methods
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from fastembed.common import OnnxProvider, ImageInput
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer, load_preprocessor
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.image.transform.operators import Compose
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
|
||||
class OnnxMultimodalModel(OnnxModel[T]):
|
||||
ONNX_OUTPUT_NAMES: Optional[list[str]] = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.tokenizer: Optional[Tokenizer] = None
|
||||
self.processor: Optional[Compose] = None
|
||||
self.special_token_to_id: dict[str, int] = {}
|
||||
|
||||
def _preprocess_onnx_text_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
def _preprocess_onnx_image_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
@classmethod
|
||||
def _get_text_worker_class(cls) -> Type["TextEmbeddingWorker[T]"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@classmethod
|
||||
def _get_image_worker_class(cls) -> Type["ImageEmbeddingWorker[T]"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _post_process_onnx_image_output(self, output: OnnxOutputContext) -> Iterable[T]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _post_process_onnx_text_output(self, output: OnnxOutputContext) -> Iterable[T]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _load_onnx_model(
|
||||
self,
|
||||
model_dir: Path,
|
||||
model_file: str,
|
||||
threads: Optional[int],
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
) -> None:
|
||||
super()._load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_file,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_id=device_id,
|
||||
)
|
||||
self.tokenizer, self.special_token_to_id = load_tokenizer(model_dir=model_dir)
|
||||
assert self.tokenizer is not None
|
||||
self.processor = load_preprocessor(model_dir=model_dir)
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def tokenize(self, documents: list[str], **kwargs: Any) -> list[Encoding]:
|
||||
return self.tokenizer.encode_batch(documents) # type: ignore[union-attr]
|
||||
|
||||
def onnx_embed_text(
|
||||
self,
|
||||
documents: list[str],
|
||||
**kwargs: Any,
|
||||
) -> OnnxOutputContext:
|
||||
encoded = self.tokenize(documents, **kwargs)
|
||||
input_ids = np.array([e.ids for e in encoded])
|
||||
attention_mask = np.array([e.attention_mask for e in encoded]) # type: ignore[union-attr]
|
||||
input_names = {node.name for node in self.model.get_inputs()} # type: ignore[union-attr]
|
||||
onnx_input: dict[str, NumpyArray] = {
|
||||
"input_ids": np.array(input_ids, dtype=np.int64),
|
||||
}
|
||||
if "attention_mask" in input_names:
|
||||
onnx_input["attention_mask"] = np.array(attention_mask, dtype=np.int64)
|
||||
if "token_type_ids" in input_names:
|
||||
onnx_input["token_type_ids"] = np.array(
|
||||
[np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
|
||||
)
|
||||
|
||||
onnx_input = self._preprocess_onnx_text_input(onnx_input, **kwargs)
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
return OnnxOutputContext(
|
||||
model_output=model_output[0],
|
||||
attention_mask=onnx_input.get("attention_mask", attention_mask),
|
||||
input_ids=onnx_input.get("input_ids", input_ids),
|
||||
)
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[T]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(documents, str):
|
||||
documents = [documents]
|
||||
is_small = True
|
||||
|
||||
if isinstance(documents, list):
|
||||
if len(documents) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_text_output(self.onnx_embed_text(batch))
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"providers": providers,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_text_worker_class(),
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
|
||||
yield from self._post_process_onnx_text_output(batch) # type: ignore
|
||||
|
||||
def onnx_embed_image(self, images: list[ImageInput], **kwargs: Any) -> OnnxOutputContext:
|
||||
with contextlib.ExitStack():
|
||||
image_files = [
|
||||
Image.open(image) if not isinstance(image, Image.Image) else image
|
||||
for image in images
|
||||
]
|
||||
assert self.processor is not None, "Processor is not initialized"
|
||||
encoded = np.array(self.processor(image_files))
|
||||
onnx_input = {"pixel_values": encoded}
|
||||
onnx_input = self._preprocess_onnx_image_input(onnx_input, **kwargs)
|
||||
model_output = self.model.run(None, onnx_input) # type: ignore[union-attr]
|
||||
embeddings = model_output[0].reshape(len(images), -1)
|
||||
return OnnxOutputContext(model_output=embeddings)
|
||||
|
||||
def _embed_images(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
images: Union[Iterable[ImageInput], ImageInput],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[T]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(images, (str, Path, Image.Image)):
|
||||
images = [images]
|
||||
is_small = True
|
||||
|
||||
if isinstance(images, list) and len(images) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
|
||||
for batch in iter_batch(images, batch_size):
|
||||
yield from self._post_process_onnx_image_output(self.onnx_embed_image(batch))
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"providers": providers,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_image_worker_class(),
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(images, batch_size), **params):
|
||||
yield from self._post_process_onnx_image_output(batch) # type: ignore
|
||||
|
||||
|
||||
class TextEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model: OnnxMultimodalModel
|
||||
super().__init__(model_name, cache_dir, **kwargs)
|
||||
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxMultimodalModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed_text(batch)
|
||||
yield idx, onnx_output
|
||||
|
||||
|
||||
class ImageEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model: OnnxMultimodalModel
|
||||
super().__init__(model_name, cache_dir, **kwargs)
|
||||
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxMultimodalModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
embeddings = self.model.onnx_embed_image(batch)
|
||||
yield idx, embeddings
|
||||
@@ -1,13 +1,15 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from multiprocessing import Queue, get_context
|
||||
from multiprocessing.context import BaseContext
|
||||
from multiprocessing.process import BaseProcess
|
||||
from multiprocessing.sharedctypes import Synchronized as BaseValue
|
||||
from queue import Empty
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
|
||||
from typing import Any, Iterable, Optional, Type
|
||||
|
||||
|
||||
# Single item should be processed in less than:
|
||||
processing_timeout = 10 * 60 # seconds
|
||||
@@ -23,10 +25,10 @@ class QueueSignals(str, Enum):
|
||||
|
||||
class Worker:
|
||||
@classmethod
|
||||
def start(cls, **kwargs: Any) -> "Worker":
|
||||
def start(cls, *args: Any, **kwargs: Any) -> "Worker":
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@@ -36,7 +38,7 @@ def _worker(
|
||||
output_queue: Queue,
|
||||
num_active_workers: BaseValue,
|
||||
worker_id: int,
|
||||
kwargs: Optional[Dict[str, Any]] = None,
|
||||
kwargs: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
A worker that pulls data pints off the input queue, and places the execution result on the output queue.
|
||||
@@ -47,7 +49,9 @@ def _worker(
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
logging.info(f"Reader worker: {worker_id} PID: {os.getpid()}")
|
||||
logging.info(
|
||||
f"Reader worker: {worker_id} PID: {os.getpid()} Device: {kwargs.get('device_id', 'CPU')}"
|
||||
)
|
||||
try:
|
||||
worker = worker_class.start(**kwargs)
|
||||
|
||||
@@ -73,7 +77,9 @@ def _worker(
|
||||
# See:
|
||||
# https://docs.python.org/3.6/library/multiprocessing.html?highlight=process#pipes-and-queues
|
||||
# https://docs.python.org/3.6/library/multiprocessing.html?highlight=process#programming-guidelines
|
||||
input_queue.close()
|
||||
output_queue.close()
|
||||
input_queue.join_thread()
|
||||
output_queue.join_thread()
|
||||
|
||||
with num_active_workers.get_lock():
|
||||
@@ -83,15 +89,24 @@ def _worker(
|
||||
|
||||
|
||||
class ParallelWorkerPool:
|
||||
def __init__(self, num_workers: int, worker: Type[Worker], start_method: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
num_workers: int,
|
||||
worker: Type[Worker],
|
||||
start_method: Optional[str] = None,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
cuda: bool = False,
|
||||
):
|
||||
self.worker_class = worker
|
||||
self.num_workers = num_workers
|
||||
self.input_queue: Optional[Queue] = None
|
||||
self.output_queue: Optional[Queue] = None
|
||||
self.ctx: BaseContext = get_context(start_method)
|
||||
self.processes: List[BaseProcess] = []
|
||||
self.processes: list[BaseProcess] = []
|
||||
self.queue_size = self.num_workers * max_internal_batch_size
|
||||
|
||||
self.emergency_shutdown = False
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
self.num_active_workers: Optional[BaseValue] = None
|
||||
|
||||
def start(self, **kwargs: Any) -> None:
|
||||
@@ -103,6 +118,12 @@ class ParallelWorkerPool:
|
||||
self.num_active_workers = ctx_value
|
||||
|
||||
for worker_id in range(0, self.num_workers):
|
||||
worker_kwargs = deepcopy(kwargs)
|
||||
if self.device_ids:
|
||||
device_id = self.device_ids[worker_id % len(self.device_ids)]
|
||||
worker_kwargs["device_id"] = device_id
|
||||
worker_kwargs["cuda"] = self.cuda
|
||||
|
||||
assert hasattr(self.ctx, "Process")
|
||||
process = self.ctx.Process(
|
||||
target=_worker,
|
||||
@@ -112,14 +133,14 @@ class ParallelWorkerPool:
|
||||
self.output_queue,
|
||||
self.num_active_workers,
|
||||
worker_id,
|
||||
kwargs.copy(),
|
||||
worker_kwargs,
|
||||
),
|
||||
)
|
||||
process.start()
|
||||
self.processes.append(process)
|
||||
|
||||
def ordered_map(self, stream: Iterable[Any], *args: Any, **kwargs: Any) -> Iterable[Any]:
|
||||
buffer = defaultdict(Any)
|
||||
buffer: defaultdict[int, Any] = defaultdict(Any) # type: ignore
|
||||
next_expected = 0
|
||||
|
||||
for idx, item in self.semi_ordered_map(stream, *args, **kwargs):
|
||||
@@ -130,7 +151,7 @@ class ParallelWorkerPool:
|
||||
|
||||
def semi_ordered_map(
|
||||
self, stream: Iterable[Any], *args: Any, **kwargs: Any
|
||||
) -> Iterable[Tuple[int, Any]]:
|
||||
) -> Iterable[tuple[int, Any]]:
|
||||
try:
|
||||
self.start(**kwargs)
|
||||
|
||||
@@ -140,6 +161,7 @@ class ParallelWorkerPool:
|
||||
pushed = 0
|
||||
read = 0
|
||||
for idx, item in enumerate(stream):
|
||||
self.check_worker_health()
|
||||
if pushed - read < self.queue_size:
|
||||
try:
|
||||
out_item = self.output_queue.get_nowait()
|
||||
@@ -166,6 +188,7 @@ class ParallelWorkerPool:
|
||||
self.input_queue.put(QueueSignals.stop)
|
||||
|
||||
while read < pushed:
|
||||
self.check_worker_health()
|
||||
out_item = self.output_queue.get(timeout=processing_timeout)
|
||||
if out_item == QueueSignals.error:
|
||||
self.join_or_terminate()
|
||||
@@ -175,8 +198,27 @@ class ParallelWorkerPool:
|
||||
finally:
|
||||
assert self.input_queue is not None, "Input queue is None"
|
||||
assert self.output_queue is not None, "Output queue is None"
|
||||
self.join()
|
||||
self.input_queue.close()
|
||||
self.output_queue.close()
|
||||
if self.emergency_shutdown:
|
||||
self.input_queue.cancel_join_thread()
|
||||
self.output_queue.cancel_join_thread()
|
||||
else:
|
||||
self.input_queue.join_thread()
|
||||
self.output_queue.join_thread()
|
||||
|
||||
def check_worker_health(self) -> None:
|
||||
"""
|
||||
Checks if any worker process has terminated unexpectedly
|
||||
"""
|
||||
for process in self.processes:
|
||||
if not process.is_alive() and process.exitcode != 0:
|
||||
self.emergency_shutdown = True
|
||||
self.join_or_terminate()
|
||||
raise RuntimeError(
|
||||
f"Worker PID: {process.pid} terminated unexpectedly with code {process.exitcode}"
|
||||
)
|
||||
|
||||
def join_or_terminate(self, timeout: Optional[int] = 1) -> None:
|
||||
"""
|
||||
@@ -206,4 +248,5 @@ class ParallelWorkerPool:
|
||||
https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python/.
|
||||
"""
|
||||
for process in self.processes:
|
||||
process.terminate()
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastembed.postprocess.muvera import Muvera
|
||||
|
||||
__all__ = ["Muvera"]
|
||||
@@ -0,0 +1,364 @@
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.late_interaction.late_interaction_embedding_base import (
|
||||
LateInteractionTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.late_interaction_multimodal.late_interaction_multimodal_embedding_base import (
|
||||
LateInteractionMultimodalEmbeddingBase,
|
||||
)
|
||||
|
||||
|
||||
MultiVectorModel = Union[LateInteractionTextEmbeddingBase, LateInteractionMultimodalEmbeddingBase]
|
||||
MAX_HAMMING_DISTANCE = 65 # 64 bits + 1
|
||||
POPCOUNT_LUT = np.array([bin(x).count("1") for x in range(256)], dtype=np.uint8)
|
||||
|
||||
|
||||
def hamming_distance_matrix(ids: np.ndarray) -> np.ndarray:
|
||||
"""Compute full Hamming distance matrix
|
||||
|
||||
Args:
|
||||
ids: shape (n,) - array of ids, only size of the array matters
|
||||
|
||||
Return:
|
||||
np.ndarray (n, n) - hamming distance matrix
|
||||
"""
|
||||
n = len(ids)
|
||||
xor_vals = np.bitwise_xor(ids[:, None], ids[None, :]) # (n, n) uint64
|
||||
bytes_view = xor_vals.view(np.uint8).reshape(n, n, 8) # (n, n, 8)
|
||||
return POPCOUNT_LUT[bytes_view].sum(axis=2)
|
||||
|
||||
|
||||
class SimHashProjection:
|
||||
"""
|
||||
SimHash projection component for MUVERA clustering.
|
||||
|
||||
This class implements locality-sensitive hashing using random hyperplanes
|
||||
to partition the vector space into 2^k_sim clusters. Each vector is assigned
|
||||
to a cluster based on which side of k_sim random hyperplanes it falls on.
|
||||
|
||||
Attributes:
|
||||
k_sim (int): Number of SimHash functions (hyperplanes)
|
||||
dim (int): Dimensionality of input vectors
|
||||
simhash_vectors (np.ndarray): Random hyperplane normal vectors of shape (dim, k_sim)
|
||||
"""
|
||||
|
||||
def __init__(self, k_sim: int, dim: int, random_generator: np.random.Generator):
|
||||
"""
|
||||
Initialize SimHash projection with random hyperplanes.
|
||||
|
||||
Args:
|
||||
k_sim (int): Number of SimHash functions, determines 2^k_sim clusters
|
||||
dim (int): Dimensionality of input vectors
|
||||
random_generator (np.random.Generator): Random number generator for reproducibility
|
||||
"""
|
||||
self.k_sim = k_sim
|
||||
self.dim = dim
|
||||
# Generate k_sim random hyperplanes (normal vectors) from standard normal distribution
|
||||
self.simhash_vectors = random_generator.normal(size=(dim, k_sim))
|
||||
|
||||
def get_cluster_ids(self, vectors: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Compute the cluster IDs for a given vector using SimHash.
|
||||
|
||||
The cluster ID is determined by computing the dot product of the vector
|
||||
with each hyperplane normal vector, taking the sign, and interpreting
|
||||
the resulting binary string as an integer.
|
||||
|
||||
Args:
|
||||
vectors (np.ndarray): Input vectors of shape (n, dim,)
|
||||
|
||||
Returns:
|
||||
np.ndarray: Cluster IDs in range [0, 2^k_sim - 1]
|
||||
|
||||
Raises:
|
||||
AssertionError: If a vector shape doesn't match expected dimensionality
|
||||
"""
|
||||
dot_product = (
|
||||
vectors @ self.simhash_vectors
|
||||
) # (token_num, dim) x (dim, k_sim) -> (token_num, k_sim)
|
||||
cluster_ids = (dot_product > 0) @ (1 << np.arange(self.k_sim))
|
||||
return cluster_ids
|
||||
|
||||
|
||||
class Muvera:
|
||||
"""
|
||||
MUVERA (Multi-Vector Retrieval Architecture) algorithm implementation.
|
||||
|
||||
This class creates Fixed Dimensional Encodings (FDEs) from variable-length
|
||||
sequences of vectors by using SimHash clustering and random projections.
|
||||
The process involves:
|
||||
1. Clustering vectors using multiple SimHash projections
|
||||
2. Computing cluster centers (with different strategies for docs vs queries)
|
||||
3. Applying random projections for dimensionality reduction
|
||||
4. Concatenating results from all projections
|
||||
|
||||
Attributes:
|
||||
k_sim (int): Number of SimHash functions per projection
|
||||
dim (int): Input vector dimensionality
|
||||
dim_proj (int): Output dimensionality after random projection
|
||||
r_reps (int): Number of random projection repetitions
|
||||
random_seed (int): Random seed for consistent random matrix generation
|
||||
simhash_projections (List[SimHashProjection]): SimHash instances for clustering
|
||||
dim_reduction_projections (np.ndarray): Random projection matrices of shape (R_reps, d, d_proj)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
k_sim: int = 5,
|
||||
dim_proj: int = 16,
|
||||
r_reps: int = 20,
|
||||
random_seed: int = 42,
|
||||
):
|
||||
"""
|
||||
Initialize MUVERA algorithm with specified parameters.
|
||||
|
||||
Args:
|
||||
dim (int): Dimensionality of individual input vectors
|
||||
k_sim (int, optional): Number of SimHash functions (creates 2^k_sim clusters).
|
||||
Defaults to 5.
|
||||
dim_proj (int, optional): Dimensionality after random projection (must be <= dim).
|
||||
Defaults to 16.
|
||||
r_reps (int, optional): Number of random projection repetitions for robustness.
|
||||
Defaults to 20.
|
||||
random_seed (int, optional): Seed for random number generator to ensure
|
||||
reproducible results. Defaults to 42.
|
||||
|
||||
Raises:
|
||||
ValueError: If dim_proj > dim (cannot project to higher dimensionality)
|
||||
"""
|
||||
if dim_proj > dim:
|
||||
raise ValueError(
|
||||
f"Cannot project to a higher dimensionality (dim_proj={dim_proj} > dim={dim})"
|
||||
)
|
||||
|
||||
self.k_sim = k_sim
|
||||
self.dim = dim
|
||||
self.dim_proj = dim_proj
|
||||
self.r_reps = r_reps
|
||||
# Create r_reps independent SimHash projections for robustness
|
||||
generator = np.random.default_rng(random_seed)
|
||||
self.simhash_projections = [
|
||||
SimHashProjection(k_sim=self.k_sim, dim=self.dim, random_generator=generator)
|
||||
for _ in range(r_reps)
|
||||
]
|
||||
# Random projection matrices with entries from {-1, +1} for each repetition
|
||||
self.dim_reduction_projections = generator.choice([-1, 1], size=(r_reps, dim, dim_proj))
|
||||
|
||||
@classmethod
|
||||
def from_multivector_model(
|
||||
cls,
|
||||
model: MultiVectorModel,
|
||||
k_sim: int = 5,
|
||||
dim_proj: int = 16,
|
||||
r_reps: int = 20, # noqa[naming]
|
||||
random_seed: int = 42,
|
||||
) -> "Muvera":
|
||||
"""
|
||||
Create a Muvera instance from a multi-vector embedding model.
|
||||
|
||||
This class method provides a convenient way to initialize a MUVERA
|
||||
that is compatible with a given multi-vector model by automatically extracting
|
||||
the embedding dimensionality from the model.
|
||||
|
||||
Args:
|
||||
model (MultiVectorModel): A late interaction text or multimodal embedding model
|
||||
that provides multi-vector embeddings. Must have an
|
||||
`embedding_size` attribute specifying the dimensionality
|
||||
of individual vectors.
|
||||
k_sim (int, optional): Number of SimHash functions (creates 2^k_sim clusters).
|
||||
Defaults to 5.
|
||||
dim_proj (int, optional): Dimensionality after random projection (must be <= model's
|
||||
embedding_size). Defaults to 16.
|
||||
r_reps (int, optional): Number of random projection repetitions for robustness.
|
||||
Defaults to 20.
|
||||
random_seed (int, optional): Seed for random number generator to ensure
|
||||
reproducible results. Defaults to 42.
|
||||
|
||||
Returns:
|
||||
Muvera: A configured MUVERA instance ready to process embeddings from the given model.
|
||||
|
||||
Raises:
|
||||
ValueError: If dim_proj > model.embedding_size (cannot project to higher dimensionality)
|
||||
|
||||
Example:
|
||||
>>> from fastembed import LateInteractionTextEmbedding
|
||||
>>> model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
|
||||
>>> muvera = Muvera.from_multivector_model(
|
||||
... model=model,
|
||||
... k_sim=6,
|
||||
... dim_proj=32
|
||||
... )
|
||||
>>> # Now use postprocessor with embeddings from the model
|
||||
>>> embeddings = np.array(list(model.embed(["sample text"])))
|
||||
>>> fde = muvera.process_document(embeddings[0])
|
||||
"""
|
||||
return cls(
|
||||
dim=model.embedding_size,
|
||||
k_sim=k_sim,
|
||||
dim_proj=dim_proj,
|
||||
r_reps=r_reps,
|
||||
random_seed=random_seed,
|
||||
)
|
||||
|
||||
def _get_output_dimension(self) -> int:
|
||||
"""
|
||||
Get the output dimension of the MUVERA algorithm.
|
||||
|
||||
Returns:
|
||||
int: Output dimension (r_reps * num_partitions * dim_proj) where b = 2^k_sim
|
||||
"""
|
||||
num_partitions = 2**self.k_sim
|
||||
return self.r_reps * num_partitions * self.dim_proj
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
return self._get_output_dimension()
|
||||
|
||||
def process_document(self, vectors: NumpyArray) -> NumpyArray:
|
||||
"""
|
||||
Encode a document's vectors into a Fixed Dimensional Encoding (FDE).
|
||||
|
||||
Uses document-specific settings: normalizes cluster centers by vector count
|
||||
and fills empty clusters using Hamming distance-based selection.
|
||||
|
||||
Args:
|
||||
vectors (NumpyArray): Document vectors of shape (n_tokens, dim)
|
||||
|
||||
Returns:
|
||||
NumpyArray: Fixed dimensional encodings of shape (r_reps * b * dim_proj,)
|
||||
"""
|
||||
return self.process(vectors, fill_empty_clusters=True, normalize_by_count=True)
|
||||
|
||||
def process_query(self, vectors: NumpyArray) -> NumpyArray:
|
||||
"""
|
||||
Encode a query's vectors into a Fixed Dimensional Encoding (FDE).
|
||||
|
||||
Uses query-specific settings: no normalization by count and no empty
|
||||
cluster filling to preserve query vector magnitudes.
|
||||
|
||||
Args:
|
||||
vectors (NumpyArray]): Query vectors of shape (n_tokens, dim)
|
||||
|
||||
Returns:
|
||||
NumpyArray: Fixed dimensional encoding of shape (r_reps * b * dim_proj,)
|
||||
"""
|
||||
return self.process(vectors, fill_empty_clusters=False, normalize_by_count=False)
|
||||
|
||||
def process(
|
||||
self,
|
||||
vectors: NumpyArray,
|
||||
fill_empty_clusters: bool = True,
|
||||
normalize_by_count: bool = True,
|
||||
) -> NumpyArray:
|
||||
"""
|
||||
Core encoding method that transforms variable-length vector sequences into FDEs.
|
||||
|
||||
The encoding process:
|
||||
1. For each of r_reps random projections:
|
||||
a. Assign vectors to clusters using SimHash
|
||||
b. Compute cluster centers (sum of vectors in each cluster)
|
||||
c. Optionally normalize by cluster size
|
||||
d. Fill empty clusters using Hamming distance if requested
|
||||
e. Apply random projection for dimensionality reduction
|
||||
f. Flatten cluster centers into a vector
|
||||
2. Concatenate all projection results
|
||||
|
||||
Args:
|
||||
vectors (np.ndarray): Input vectors of shape (n_vectors, dim)
|
||||
fill_empty_clusters (bool): Whether to fill empty clusters using nearest
|
||||
vectors based on Hamming distance of cluster IDs
|
||||
normalize_by_count (bool): Whether to normalize cluster centers by the
|
||||
number of vectors assigned to each cluster
|
||||
|
||||
Returns:
|
||||
np.ndarray: Fixed dimensional encoding of shape (r_reps * b * dim_proj)
|
||||
where B = 2^k_sim is the number of clusters
|
||||
|
||||
Raises:
|
||||
AssertionError: If input vectors don't have expected dimensionality
|
||||
"""
|
||||
assert (
|
||||
vectors.shape[1] == self.dim
|
||||
), f"Expected vectors of shape (n, {self.dim}), got {vectors.shape}"
|
||||
|
||||
# Store results from each random projection
|
||||
output_vectors = []
|
||||
|
||||
# num of space partitions in SimHash
|
||||
num_partitions = 2**self.k_sim
|
||||
cluster_center_ids = np.arange(num_partitions)
|
||||
precomputed_hamming_matrix = (
|
||||
hamming_distance_matrix(cluster_center_ids) if fill_empty_clusters else None
|
||||
)
|
||||
|
||||
for projection_index, simhash in enumerate(self.simhash_projections):
|
||||
# Initialize cluster centers and count vectors assigned to each cluster
|
||||
cluster_centers = np.zeros((num_partitions, self.dim))
|
||||
cluster_center_id_to_vectors: dict[int, list[int]] = {
|
||||
cluster_center_id: [] for cluster_center_id in cluster_center_ids
|
||||
}
|
||||
cluster_vector_counts = None
|
||||
empty_mask = None
|
||||
|
||||
# Assign each vector to its cluster and accumulate cluster centers
|
||||
vector_cluster_ids = simhash.get_cluster_ids(vectors)
|
||||
for cluster_id, (vec_idx, vec) in zip(vector_cluster_ids, enumerate(vectors)):
|
||||
cluster_centers[cluster_id] += vec
|
||||
cluster_center_id_to_vectors[cluster_id].append(vec_idx)
|
||||
|
||||
if normalize_by_count or fill_empty_clusters:
|
||||
cluster_vector_counts = np.bincount(vector_cluster_ids, minlength=num_partitions)
|
||||
empty_mask = cluster_vector_counts == 0
|
||||
|
||||
if normalize_by_count:
|
||||
assert empty_mask is not None
|
||||
assert cluster_vector_counts is not None
|
||||
non_empty_mask = ~empty_mask
|
||||
cluster_centers[non_empty_mask] /= cluster_vector_counts[non_empty_mask][:, None]
|
||||
|
||||
# Fill empty clusters using vectors with minimum Hamming distance
|
||||
if fill_empty_clusters:
|
||||
assert empty_mask is not None
|
||||
assert precomputed_hamming_matrix is not None
|
||||
masked_hamming = np.where(
|
||||
empty_mask[None, :], MAX_HAMMING_DISTANCE, precomputed_hamming_matrix
|
||||
)
|
||||
nearest_non_empty = np.argmin(masked_hamming, axis=1)
|
||||
fill_vectors = np.array(
|
||||
[
|
||||
vectors[cluster_center_id_to_vectors[cluster_id][0]]
|
||||
for cluster_id in nearest_non_empty[empty_mask]
|
||||
]
|
||||
).reshape(-1, self.dim)
|
||||
cluster_centers[empty_mask] = fill_vectors
|
||||
|
||||
# Apply random projection for dimensionality reduction if needed
|
||||
if self.dim_proj < self.dim:
|
||||
dim_reduction_projection = self.dim_reduction_projections[
|
||||
projection_index
|
||||
] # Get projection matrix for this repetition
|
||||
projected_centers = (1 / np.sqrt(self.dim_proj)) * (
|
||||
cluster_centers @ dim_reduction_projection
|
||||
)
|
||||
|
||||
# Flatten cluster centers into a single vector and add to output
|
||||
output_vectors.append(projected_centers.flatten())
|
||||
continue
|
||||
|
||||
# If no projection needed (dim_proj == dim), use original cluster centers
|
||||
output_vectors.append(cluster_centers.flatten())
|
||||
|
||||
# Concatenate results from all R_reps projections into final FDE
|
||||
return np.concatenate(output_vectors)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
v_arrs = np.random.randn(10, 100, 128)
|
||||
muvera = Muvera(128, 4, 8, 20, 42)
|
||||
|
||||
for v_arr in v_arrs:
|
||||
muvera.process(v_arr) # type: ignore
|
||||
@@ -0,0 +1 @@
|
||||
partial
|
||||
@@ -0,0 +1,3 @@
|
||||
from fastembed.rerank.cross_encoder.text_cross_encoder import TextCrossEncoder
|
||||
|
||||
__all__ = ["TextCrossEncoder"]
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Optional, Sequence, Any
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
|
||||
|
||||
|
||||
class CustomTextCrossEncoder(OnnxTextCrossEncoder):
|
||||
SUPPORTED_MODELS: list[BaseModelDescription] = []
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
device_id=device_id,
|
||||
specific_model_path=specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[BaseModelDescription]:
|
||||
return cls.SUPPORTED_MODELS
|
||||
|
||||
@classmethod
|
||||
def add_model(
|
||||
cls,
|
||||
model_description: BaseModelDescription,
|
||||
) -> None:
|
||||
cls.SUPPORTED_MODELS.append(model_description)
|
||||
@@ -0,0 +1,220 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.rerank.cross_encoder.onnx_text_model import (
|
||||
OnnxCrossEncoderModel,
|
||||
TextRerankerWorker,
|
||||
)
|
||||
from fastembed.rerank.cross_encoder.text_cross_encoder_base import TextCrossEncoderBase
|
||||
from fastembed.common.model_description import BaseModelDescription, ModelSource
|
||||
|
||||
supported_onnx_models: list[BaseModelDescription] = [
|
||||
BaseModelDescription(
|
||||
model="Xenova/ms-marco-MiniLM-L-6-v2",
|
||||
description="MiniLM-L-6-v2 model optimized for re-ranking tasks.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.08,
|
||||
sources=ModelSource(hf="Xenova/ms-marco-MiniLM-L-6-v2"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
BaseModelDescription(
|
||||
model="Xenova/ms-marco-MiniLM-L-12-v2",
|
||||
description="MiniLM-L-12-v2 model optimized for re-ranking tasks.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.12,
|
||||
sources=ModelSource(hf="Xenova/ms-marco-MiniLM-L-12-v2"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
BaseModelDescription(
|
||||
model="BAAI/bge-reranker-base",
|
||||
description="BGE reranker base model for cross-encoder re-ranking.",
|
||||
license="mit",
|
||||
size_in_GB=1.04,
|
||||
sources=ModelSource(hf="BAAI/bge-reranker-base"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
BaseModelDescription(
|
||||
model="jinaai/jina-reranker-v1-tiny-en",
|
||||
description="Designed for blazing-fast re-ranking with 8K context length and fewer parameters than jina-reranker-v1-turbo-en.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.13,
|
||||
sources=ModelSource(hf="jinaai/jina-reranker-v1-tiny-en"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
BaseModelDescription(
|
||||
model="jinaai/jina-reranker-v1-turbo-en",
|
||||
description="Designed for blazing-fast re-ranking with 8K context length.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.15,
|
||||
sources=ModelSource(hf="jinaai/jina-reranker-v1-turbo-en"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
BaseModelDescription(
|
||||
model="jinaai/jina-reranker-v2-base-multilingual",
|
||||
description="A multi-lingual reranker model for cross-encoder re-ranking with 1K context length and sliding window",
|
||||
license="cc-by-nc-4.0",
|
||||
size_in_GB=1.11,
|
||||
sources=ModelSource(hf="jinaai/jina-reranker-v2-base-multilingual"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class OnnxTextCrossEncoder(TextCrossEncoderBase, OnnxCrossEncoderModel):
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[BaseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[BaseModelDescription]: A list of BaseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_onnx_models
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. Xenova/ms-marco-MiniLM-L-6-v2.
|
||||
"""
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
if self.device_ids is not None and len(self.device_ids) > 1:
|
||||
logger.warning(
|
||||
"Parallel execution is currently not supported for cross encoders, "
|
||||
f"only the first device will be used for inference: {self.device_ids[0]}."
|
||||
)
|
||||
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
def rerank(
|
||||
self,
|
||||
query: str,
|
||||
documents: Iterable[str],
|
||||
batch_size: int = 64,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
"""Reranks documents based on their relevance to a given query.
|
||||
|
||||
Args:
|
||||
query (str): The query string to which document relevance is calculated.
|
||||
documents (Iterable[str]): Iterable of documents to be reranked.
|
||||
batch_size (int, optional): The number of documents processed in each batch. Higher batch sizes improve speed
|
||||
but require more memory. Default is 64.
|
||||
Returns:
|
||||
Iterable[float]: An iterable of relevance scores for each document.
|
||||
"""
|
||||
|
||||
yield from self._rerank_documents(
|
||||
query=query, documents=documents, batch_size=batch_size, **kwargs
|
||||
)
|
||||
|
||||
def rerank_pairs(
|
||||
self,
|
||||
pairs: Iterable[tuple[str, str]],
|
||||
batch_size: int = 64,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
yield from self._rerank_pairs(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
pairs=pairs,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[TextRerankerWorker]:
|
||||
return TextCrossEncoderWorker
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[float]:
|
||||
return (float(elem) for elem in output.model_output)
|
||||
|
||||
|
||||
class TextCrossEncoderWorker(TextRerankerWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxTextCrossEncoder:
|
||||
return OnnxTextCrossEncoder(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,184 @@
|
||||
import os
|
||||
from multiprocessing import get_all_start_methods
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type
|
||||
|
||||
import numpy as np
|
||||
from tokenizers import Encoding
|
||||
|
||||
from fastembed.common.onnx_model import (
|
||||
EmbeddingWorker,
|
||||
OnnxModel,
|
||||
OnnxOutputContext,
|
||||
OnnxProvider,
|
||||
)
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
|
||||
class OnnxCrossEncoderModel(OnnxModel[float]):
|
||||
ONNX_OUTPUT_NAMES: Optional[list[str]] = None
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["TextRerankerWorker"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _load_onnx_model(
|
||||
self,
|
||||
model_dir: Path,
|
||||
model_file: str,
|
||||
threads: Optional[int],
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
) -> None:
|
||||
super()._load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_file,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_id=device_id,
|
||||
)
|
||||
self.tokenizer, _ = load_tokenizer(model_dir=model_dir)
|
||||
assert self.tokenizer is not None
|
||||
|
||||
def tokenize(self, pairs: list[tuple[str, str]], **_: Any) -> list[Encoding]:
|
||||
return self.tokenizer.encode_batch(pairs) # type: ignore[union-attr]
|
||||
|
||||
def _build_onnx_input(self, tokenized_input: list[Encoding]) -> dict[str, NumpyArray]:
|
||||
input_names: set[str] = {node.name for node in self.model.get_inputs()} # type: ignore[union-attr]
|
||||
inputs: dict[str, NumpyArray] = {
|
||||
"input_ids": np.array([enc.ids for enc in tokenized_input], dtype=np.int64),
|
||||
}
|
||||
if "token_type_ids" in input_names:
|
||||
inputs["token_type_ids"] = np.array(
|
||||
[enc.type_ids for enc in tokenized_input], dtype=np.int64
|
||||
)
|
||||
if "attention_mask" in input_names:
|
||||
inputs["attention_mask"] = np.array(
|
||||
[enc.attention_mask for enc in tokenized_input], dtype=np.int64
|
||||
)
|
||||
return inputs
|
||||
|
||||
def onnx_embed(self, query: str, documents: list[str], **kwargs: Any) -> OnnxOutputContext:
|
||||
pairs = [(query, doc) for doc in documents]
|
||||
return self.onnx_embed_pairs(pairs, **kwargs)
|
||||
|
||||
def onnx_embed_pairs(self, pairs: list[tuple[str, str]], **kwargs: Any) -> OnnxOutputContext:
|
||||
tokenized_input = self.tokenize(pairs, **kwargs)
|
||||
inputs = self._build_onnx_input(tokenized_input)
|
||||
onnx_input = self._preprocess_onnx_input(inputs, **kwargs)
|
||||
outputs = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
relevant_output = outputs[0]
|
||||
scores: NumpyArray = relevant_output[:, 0]
|
||||
return OnnxOutputContext(model_output=scores)
|
||||
|
||||
def _rerank_documents(
|
||||
self, query: str, documents: Iterable[str], batch_size: int, **kwargs: Any
|
||||
) -> Iterable[float]:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed(query, batch, **kwargs))
|
||||
|
||||
def _rerank_pairs(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
pairs: Iterable[tuple[str, str]],
|
||||
batch_size: int,
|
||||
parallel: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(pairs, tuple):
|
||||
pairs = [pairs]
|
||||
is_small = True
|
||||
|
||||
if isinstance(pairs, list):
|
||||
if len(pairs) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(pairs, batch_size):
|
||||
yield from self._post_process_onnx_output(self.onnx_embed_pairs(batch, **kwargs))
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"providers": providers,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_worker_class(),
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(pairs, batch_size), **params):
|
||||
yield from self._post_process_onnx_output(batch) # type: ignore
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[float]:
|
||||
"""Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
**kwargs: Additional keyword arguments that may be needed by specific implementations.
|
||||
|
||||
Returns:
|
||||
Iterable[float]: Post-processed output as an iterable of float values.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
|
||||
class TextRerankerWorker(EmbeddingWorker[float]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model: OnnxCrossEncoderModel
|
||||
super().__init__(model_name, cache_dir, **kwargs)
|
||||
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxCrossEncoderModel:
|
||||
raise NotImplementedError()
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed_pairs(batch)
|
||||
yield idx, onnx_output
|
||||
@@ -0,0 +1,163 @@
|
||||
from typing import Any, Iterable, Optional, Sequence, Type
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
|
||||
from fastembed.rerank.cross_encoder.custom_text_cross_encoder import CustomTextCrossEncoder
|
||||
|
||||
from fastembed.rerank.cross_encoder.text_cross_encoder_base import TextCrossEncoderBase
|
||||
from fastembed.common.model_description import (
|
||||
ModelSource,
|
||||
BaseModelDescription,
|
||||
)
|
||||
|
||||
|
||||
class TextCrossEncoder(TextCrossEncoderBase):
|
||||
CROSS_ENCODER_REGISTRY: list[Type[TextCrossEncoderBase]] = [
|
||||
OnnxTextCrossEncoder,
|
||||
CustomTextCrossEncoder,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[BaseModelDescription]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"model": "Xenova/ms-marco-MiniLM-L-6-v2",
|
||||
"size_in_GB": 0.08,
|
||||
"sources": {
|
||||
"hf": "Xenova/ms-marco-MiniLM-L-6-v2",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
"description": "MiniLM-L-6-v2 model optimized for re-ranking tasks.",
|
||||
"license": "apache-2.0",
|
||||
}
|
||||
]
|
||||
```
|
||||
"""
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[BaseModelDescription]:
|
||||
result: list[BaseModelDescription] = []
|
||||
for encoder in cls.CROSS_ENCODER_REGISTRY:
|
||||
result.extend(encoder._list_supported_models())
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
|
||||
for CROSS_ENCODER_TYPE in self.CROSS_ENCODER_REGISTRY:
|
||||
supported_models = CROSS_ENCODER_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = CROSS_ENCODER_TYPE(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in TextCrossEncoder."
|
||||
"Please check the supported models using `TextCrossEncoder.list_supported_models()`"
|
||||
)
|
||||
|
||||
def rerank(
|
||||
self, query: str, documents: Iterable[str], batch_size: int = 64, **kwargs: Any
|
||||
) -> Iterable[float]:
|
||||
"""Rerank a list of documents based on a query.
|
||||
|
||||
Args:
|
||||
query: Query to rerank the documents against
|
||||
documents: Iterator of documents to rerank
|
||||
batch_size: Batch size for reranking
|
||||
|
||||
Returns:
|
||||
Iterable of scores for each document
|
||||
"""
|
||||
yield from self.model.rerank(query, documents, batch_size=batch_size, **kwargs)
|
||||
|
||||
def rerank_pairs(
|
||||
self,
|
||||
pairs: Iterable[tuple[str, str]],
|
||||
batch_size: int = 64,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
"""
|
||||
Rerank a list of query-document pairs.
|
||||
|
||||
Args:
|
||||
pairs (Iterable[tuple[str, str]]): An iterable of tuples, where each tuple contains a query and a document
|
||||
to be scored together.
|
||||
batch_size (int, optional): The number of query-document pairs to process in a single batch. Defaults to 64.
|
||||
parallel (Optional[int], optional): The number of parallel processes to use for reranking.
|
||||
If None, parallelization is disabled. Defaults to None.
|
||||
**kwargs (Any): Additional arguments to pass to the underlying reranking model.
|
||||
|
||||
Returns:
|
||||
Iterable[float]: An iterable of scores corresponding to each query-document pair in the input.
|
||||
Higher scores indicate a stronger match between the query and the document.
|
||||
|
||||
Example:
|
||||
>>> encoder = TextCrossEncoder("Xenova/ms-marco-MiniLM-L-6-v2")
|
||||
>>> pairs = [("What is AI?", "Artificial intelligence is ..."), ("What is ML?", "Machine learning is ...")]
|
||||
>>> scores = list(encoder.rerank_pairs(pairs))
|
||||
>>> print(list(map(lambda x: round(x, 2), scores)))
|
||||
[-1.24, -10.6]
|
||||
"""
|
||||
yield from self.model.rerank_pairs(
|
||||
pairs, batch_size=batch_size, parallel=parallel, **kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_custom_model(
|
||||
cls,
|
||||
model: str,
|
||||
sources: ModelSource,
|
||||
model_file: str = "onnx/model.onnx",
|
||||
description: str = "",
|
||||
license: str = "",
|
||||
size_in_gb: float = 0.0,
|
||||
additional_files: Optional[list[str]] = None,
|
||||
) -> None:
|
||||
registered_models = cls._list_supported_models()
|
||||
for registered_model in registered_models:
|
||||
if model == registered_model.model:
|
||||
raise ValueError(
|
||||
f"Model {model} is already registered in CrossEncoderModel, if you still want to add this model, "
|
||||
f"please use another model name"
|
||||
)
|
||||
|
||||
CustomTextCrossEncoder.add_model(
|
||||
BaseModelDescription(
|
||||
model=model,
|
||||
sources=sources,
|
||||
model_file=model_file,
|
||||
description=description,
|
||||
license=license,
|
||||
size_in_GB=size_in_gb,
|
||||
additional_files=additional_files or [],
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from fastembed.common.model_description import BaseModelDescription
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
|
||||
|
||||
class TextCrossEncoderBase(ModelManagement[BaseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
|
||||
def rerank(
|
||||
self,
|
||||
query: str,
|
||||
documents: Iterable[str],
|
||||
batch_size: int = 64,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
"""Rerank a list of documents given a query.
|
||||
|
||||
Args:
|
||||
query (str): The query to rerank the documents.
|
||||
documents (Iterable[str]): The list of texts to rerank.
|
||||
batch_size (int): The batch size to use for reranking.
|
||||
**kwargs: Additional keyword argument to pass to the rerank method.
|
||||
|
||||
Yields:
|
||||
Iterable[float]: The scores of the reranked the documents.
|
||||
"""
|
||||
raise NotImplementedError("This method should be overridden by subclasses")
|
||||
|
||||
def rerank_pairs(
|
||||
self,
|
||||
pairs: Iterable[tuple[str, str]],
|
||||
batch_size: int = 64,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[float]:
|
||||
"""Rerank query-document pairs.
|
||||
Args:
|
||||
pairs (Iterable[tuple[str, str]]): Query-document pairs to rerank
|
||||
batch_size (int): The batch size to use for reranking.
|
||||
parallel: parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
**kwargs: Additional keyword argument to pass to the rerank method.
|
||||
Yields:
|
||||
Iterable[float]: Scores for each individual pair
|
||||
"""
|
||||
raise NotImplementedError("This method should be overridden by subclasses")
|
||||
@@ -0,0 +1,352 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from multiprocessing import get_all_start_methods
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Type, Union
|
||||
|
||||
import mmh3
|
||||
import numpy as np
|
||||
from py_rust_stemmers import SnowballStemmer
|
||||
from fastembed.common.utils import (
|
||||
define_cache_dir,
|
||||
iter_batch,
|
||||
get_all_punctuation,
|
||||
remove_non_alphanumeric,
|
||||
)
|
||||
from fastembed.parallel_processor import ParallelWorkerPool, Worker
|
||||
from fastembed.sparse.sparse_embedding_base import (
|
||||
SparseEmbedding,
|
||||
SparseTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.sparse.utils.tokenizer import SimpleTokenizer
|
||||
from fastembed.common.model_description import SparseModelDescription, ModelSource
|
||||
|
||||
|
||||
supported_languages = [
|
||||
"arabic",
|
||||
"danish",
|
||||
"dutch",
|
||||
"english",
|
||||
"finnish",
|
||||
"french",
|
||||
"german",
|
||||
"greek",
|
||||
"hungarian",
|
||||
"italian",
|
||||
"norwegian",
|
||||
"portuguese",
|
||||
"romanian",
|
||||
"russian",
|
||||
"spanish",
|
||||
"swedish",
|
||||
"tamil",
|
||||
"turkish",
|
||||
]
|
||||
|
||||
supported_bm25_models: list[SparseModelDescription] = [
|
||||
SparseModelDescription(
|
||||
model="Qdrant/bm25",
|
||||
vocab_size=0,
|
||||
description="BM25 as sparse embeddings meant to be used with Qdrant",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.01,
|
||||
sources=ModelSource(hf="Qdrant/bm25"),
|
||||
additional_files=[f"{lang}.txt" for lang in supported_languages],
|
||||
requires_idf=True,
|
||||
model_file="mock.file",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Bm25(SparseTextEmbeddingBase):
|
||||
"""Implements traditional BM25 in a form of sparse embeddings.
|
||||
Uses a count of tokens in the document to evaluate the importance of the token.
|
||||
|
||||
WARNING: This model is expected to be used with `modifier="idf"` in the sparse vector index of Qdrant.
|
||||
|
||||
BM25 formula:
|
||||
|
||||
score(q, d) = SUM[ IDF(q_i) * (f(q_i, d) * (k + 1)) / (f(q_i, d) + k * (1 - b + b * (|d| / avg_len))) ],
|
||||
|
||||
where IDF is the inverse document frequency, computed on Qdrant's side
|
||||
f(q_i, d) is the term frequency of the token q_i in the document d
|
||||
k, b, avg_len are hyperparameters, described below.
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
k (float, optional): The k parameter in the BM25 formula. Defines the saturation of the term frequency.
|
||||
I.e. defines how fast the moment when additional terms stop to increase the score. Defaults to 1.2.
|
||||
b (float, optional): The b parameter in the BM25 formula. Defines the importance of the document length.
|
||||
Defaults to 0.75.
|
||||
avg_len (float, optional): The average length of the documents in the corpus. Defaults to 256.0.
|
||||
language (str): Specifies the language for the stemmer.
|
||||
disable_stemmer (bool): Disable the stemmer.
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
k: float = 1.2,
|
||||
b: float = 0.75,
|
||||
avg_len: float = 256.0,
|
||||
language: str = "english",
|
||||
token_max_length: int = 40,
|
||||
disable_stemmer: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, **kwargs)
|
||||
|
||||
if language not in supported_languages:
|
||||
raise ValueError(f"{language} language is not supported")
|
||||
else:
|
||||
self.language = language
|
||||
|
||||
self.k = k
|
||||
self.b = b
|
||||
self.avg_len = avg_len
|
||||
|
||||
model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
self.token_max_length = token_max_length
|
||||
self.punctuation = set(get_all_punctuation())
|
||||
self.disable_stemmer = disable_stemmer
|
||||
|
||||
if disable_stemmer:
|
||||
self.stopwords: set[str] = set()
|
||||
self.stemmer = None
|
||||
else:
|
||||
self.stopwords = set(self._load_stopwords(self._model_dir, self.language))
|
||||
self.stemmer = SnowballStemmer(language)
|
||||
|
||||
self.tokenizer = SimpleTokenizer
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[SparseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[SparseModelDescription]: A list of SparseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_bm25_models
|
||||
|
||||
@classmethod
|
||||
def _load_stopwords(cls, model_dir: Path, language: str) -> list[str]:
|
||||
stopwords_path = model_dir / f"{language}.txt"
|
||||
if not stopwords_path.exists():
|
||||
return []
|
||||
|
||||
with open(stopwords_path, "r") as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(documents, str):
|
||||
documents = [documents]
|
||||
is_small = True
|
||||
|
||||
if isinstance(documents, list):
|
||||
if len(documents) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self.raw_embed(batch)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"k": self.k,
|
||||
"b": self.b,
|
||||
"avg_len": self.avg_len,
|
||||
"language": self.language,
|
||||
"token_max_length": self.token_max_length,
|
||||
"disable_stemmer": self.disable_stemmer,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
}
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_worker_class(),
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
|
||||
for record in batch:
|
||||
yield record # type: ignore
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
def _stem(self, tokens: list[str]) -> list[str]:
|
||||
stemmed_tokens: list[str] = []
|
||||
for token in tokens:
|
||||
lower_token = token.lower()
|
||||
|
||||
if token in self.punctuation:
|
||||
continue
|
||||
|
||||
if lower_token in self.stopwords:
|
||||
continue
|
||||
|
||||
if len(token) > self.token_max_length:
|
||||
continue
|
||||
|
||||
stemmed_token = self.stemmer.stem_word(lower_token) if self.stemmer else lower_token
|
||||
|
||||
if stemmed_token:
|
||||
stemmed_tokens.append(stemmed_token)
|
||||
return stemmed_tokens
|
||||
|
||||
def raw_embed(
|
||||
self,
|
||||
documents: list[str],
|
||||
) -> list[SparseEmbedding]:
|
||||
embeddings: list[SparseEmbedding] = []
|
||||
for document in documents:
|
||||
document = remove_non_alphanumeric(document)
|
||||
tokens = self.tokenizer.tokenize(document)
|
||||
stemmed_tokens = self._stem(tokens)
|
||||
token_id2value = self._term_frequency(stemmed_tokens)
|
||||
embeddings.append(SparseEmbedding.from_dict(token_id2value))
|
||||
return embeddings
|
||||
|
||||
def _term_frequency(self, tokens: list[str]) -> dict[int, float]:
|
||||
"""Calculate the term frequency part of the BM25 formula.
|
||||
|
||||
(
|
||||
f(q_i, d) * (k + 1)
|
||||
) / (
|
||||
f(q_i, d) + k * (1 - b + b * (|d| / avg_len))
|
||||
)
|
||||
|
||||
Args:
|
||||
tokens (list[str]): The list of tokens in the document.
|
||||
|
||||
Returns:
|
||||
dict[int, float]: The token_id to term frequency mapping.
|
||||
"""
|
||||
tf_map: dict[int, float] = {}
|
||||
counter: defaultdict[str, int] = defaultdict(int)
|
||||
for stemmed_token in tokens:
|
||||
counter[stemmed_token] += 1
|
||||
|
||||
doc_len = len(tokens)
|
||||
for stemmed_token in counter:
|
||||
token_id = self.compute_token_id(stemmed_token)
|
||||
num_occurrences = counter[stemmed_token]
|
||||
tf_map[token_id] = num_occurrences * (self.k + 1)
|
||||
tf_map[token_id] /= num_occurrences + self.k * (
|
||||
1 - self.b + self.b * doc_len / self.avg_len
|
||||
)
|
||||
return tf_map
|
||||
|
||||
@classmethod
|
||||
def compute_token_id(cls, token: str) -> int:
|
||||
return abs(mmh3.hash(token))
|
||||
|
||||
def query_embed(
|
||||
self, query: Union[str, Iterable[str]], **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""To emulate BM25 behaviour, we don't need to use weights in the query, and
|
||||
it's enough to just hash the tokens and assign a weight of 1.0 to them.
|
||||
"""
|
||||
if isinstance(query, str):
|
||||
query = [query]
|
||||
|
||||
for text in query:
|
||||
text = remove_non_alphanumeric(text)
|
||||
tokens = self.tokenizer.tokenize(text)
|
||||
stemmed_tokens = self._stem(tokens)
|
||||
token_ids = np.array(
|
||||
list(set(self.compute_token_id(token) for token in stemmed_tokens)),
|
||||
dtype=np.int32,
|
||||
)
|
||||
values = np.ones_like(token_ids)
|
||||
yield SparseEmbedding(indices=token_ids, values=values)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["Bm25Worker"]:
|
||||
return Bm25Worker
|
||||
|
||||
|
||||
class Bm25Worker(Worker):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "Bm25Worker":
|
||||
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
|
||||
|
||||
def process(
|
||||
self, items: Iterable[tuple[int, Any]]
|
||||
) -> Iterable[tuple[int, list[SparseEmbedding]]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.raw_embed(batch)
|
||||
yield idx, onnx_output
|
||||
|
||||
@staticmethod
|
||||
def init_embedding(model_name: str, cache_dir: str, **kwargs: Any) -> Bm25:
|
||||
return Bm25(model_name=model_name, cache_dir=cache_dir, **kwargs)
|
||||
@@ -0,0 +1,351 @@
|
||||
import math
|
||||
import string
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import mmh3
|
||||
import numpy as np
|
||||
from py_rust_stemmers import SnowballStemmer
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.sparse.sparse_embedding_base import (
|
||||
SparseEmbedding,
|
||||
SparseTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
||||
from fastembed.common.model_description import SparseModelDescription, ModelSource
|
||||
|
||||
supported_bm42_models: list[SparseModelDescription] = [
|
||||
SparseModelDescription(
|
||||
model="Qdrant/bm42-all-minilm-l6-v2-attentions",
|
||||
vocab_size=30522,
|
||||
description="Light sparse embedding model, which assigns an importance score to each token in the text",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.09,
|
||||
sources=ModelSource(hf="Qdrant/all_miniLM_L6_v2_with_attentions"),
|
||||
model_file="model.onnx",
|
||||
additional_files=["stopwords.txt"],
|
||||
requires_idf=True,
|
||||
),
|
||||
]
|
||||
|
||||
MODEL_TO_LANGUAGE = {
|
||||
"Qdrant/bm42-all-minilm-l6-v2-attentions": "english",
|
||||
}
|
||||
|
||||
|
||||
class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
|
||||
"""
|
||||
Bm42 is an extension of BM25, which tries to better evaluate importance of tokens in the documents,
|
||||
by extracting attention weights from the transformer model.
|
||||
|
||||
Traditional BM25 uses a count of tokens in the document to evaluate the importance of the token,
|
||||
but this approach doesn't work well with short documents or chunks of text, as almost all tokens
|
||||
there are unique.
|
||||
|
||||
BM42 addresses this issue by replacing the token count with the attention weights from the transformer model.
|
||||
This allows sparse embeddings to work well with short documents, handle rare tokens and leverage traditional NLP
|
||||
techniques like stemming and stopwords.
|
||||
|
||||
WARNING: This model is expected to be used with `modifier="idf"` in the sparse vector index of Qdrant.
|
||||
"""
|
||||
|
||||
ONNX_OUTPUT_NAMES = ["attention_6"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
alpha: float = 0.5,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The providers to use for onnxruntime.
|
||||
alpha (float, optional): Parameter, that defines the importance of the token weight in the document
|
||||
versus the importance of the token frequency in the corpus. Defaults to 0.5, based on empirical testing.
|
||||
It is recommended to only change this parameter based on training data for a specific dataset.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
self.invert_vocab: dict[int, str] = {}
|
||||
|
||||
self.special_tokens: set[str] = set()
|
||||
self.special_tokens_ids: set[int] = set()
|
||||
self.punctuation = set(string.punctuation)
|
||||
self.stopwords = set(self._load_stopwords(self._model_dir))
|
||||
self.stemmer = SnowballStemmer(MODEL_TO_LANGUAGE[model_name])
|
||||
self.alpha = alpha
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
for token, idx in self.tokenizer.get_vocab().items(): # type: ignore[union-attr]
|
||||
self.invert_vocab[idx] = token
|
||||
self.special_tokens = set(self.special_token_to_id.keys())
|
||||
self.special_tokens_ids = set(self.special_token_to_id.values())
|
||||
self.stopwords = set(self._load_stopwords(self._model_dir))
|
||||
|
||||
def _filter_pair_tokens(self, tokens: list[tuple[str, Any]]) -> list[tuple[str, Any]]:
|
||||
result: list[tuple[str, Any]] = []
|
||||
for token, value in tokens:
|
||||
if token in self.stopwords or token in self.punctuation:
|
||||
continue
|
||||
result.append((token, value))
|
||||
return result
|
||||
|
||||
def _stem_pair_tokens(self, tokens: list[tuple[str, Any]]) -> list[tuple[str, Any]]:
|
||||
result: list[tuple[str, Any]] = []
|
||||
for token, value in tokens:
|
||||
processed_token = self.stemmer.stem_word(token)
|
||||
result.append((processed_token, value))
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _aggregate_weights(
|
||||
cls, tokens: list[tuple[str, list[int]]], weights: list[float]
|
||||
) -> list[tuple[str, float]]:
|
||||
result: list[tuple[str, float]] = []
|
||||
for token, idxs in tokens:
|
||||
sum_weight = sum(weights[idx] for idx in idxs)
|
||||
result.append((token, sum_weight))
|
||||
return result
|
||||
|
||||
def _reconstruct_bpe(
|
||||
self, bpe_tokens: Iterable[tuple[int, str]]
|
||||
) -> list[tuple[str, list[int]]]:
|
||||
result: list[tuple[str, list[int]]] = []
|
||||
acc: str = ""
|
||||
acc_idx: list[int] = []
|
||||
|
||||
continuing_subword_prefix = self.tokenizer.model.continuing_subword_prefix # type: ignore[union-attr]
|
||||
continuing_subword_prefix_len = len(continuing_subword_prefix)
|
||||
|
||||
for idx, token in bpe_tokens:
|
||||
if token in self.special_tokens:
|
||||
continue
|
||||
|
||||
if token.startswith(continuing_subword_prefix):
|
||||
acc += token[continuing_subword_prefix_len:]
|
||||
acc_idx.append(idx)
|
||||
else:
|
||||
if acc:
|
||||
result.append((acc, acc_idx))
|
||||
acc_idx = []
|
||||
acc = token
|
||||
acc_idx.append(idx)
|
||||
|
||||
if acc:
|
||||
result.append((acc, acc_idx))
|
||||
|
||||
return result
|
||||
|
||||
def _rescore_vector(self, vector: dict[str, float]) -> dict[int, float]:
|
||||
"""
|
||||
Orders all tokens in the vector by their importance and generates a new score based on the importance order.
|
||||
So that the scoring doesn't depend on absolute values assigned by the model, but on the relative importance.
|
||||
"""
|
||||
|
||||
new_vector: dict[int, float] = {}
|
||||
|
||||
for token, value in vector.items():
|
||||
token_id = abs(mmh3.hash(token))
|
||||
# Examples:
|
||||
# Num 0: Log(1/1 + 1) = 0.6931471805599453
|
||||
# Num 1: Log(1/2 + 1) = 0.4054651081081644
|
||||
# Num 2: Log(1/3 + 1) = 0.28768207245178085
|
||||
new_vector[token_id] = math.log(1.0 + value) ** self.alpha # value
|
||||
|
||||
return new_vector
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
if output.input_ids is None:
|
||||
raise ValueError("input_ids must be provided for document post-processing")
|
||||
|
||||
token_ids_batch = output.input_ids.astype(int)
|
||||
|
||||
# attention_value shape: (batch_size, num_heads, num_tokens, num_tokens)
|
||||
pooled_attention = np.mean(output.model_output[:, :, 0], axis=1) * output.attention_mask
|
||||
|
||||
for document_token_ids, attention_value in zip(token_ids_batch, pooled_attention):
|
||||
document_tokens_with_ids = (
|
||||
(idx, self.invert_vocab[token_id])
|
||||
for idx, token_id in enumerate(document_token_ids)
|
||||
)
|
||||
|
||||
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
|
||||
|
||||
filtered = self._filter_pair_tokens(reconstructed)
|
||||
|
||||
stemmed = self._stem_pair_tokens(filtered)
|
||||
|
||||
weighted = self._aggregate_weights(stemmed, attention_value)
|
||||
|
||||
max_token_weight: dict[str, float] = {}
|
||||
|
||||
for token, weight in weighted:
|
||||
max_token_weight[token] = max(max_token_weight.get(token, 0), weight)
|
||||
|
||||
rescored = self._rescore_vector(max_token_weight)
|
||||
|
||||
yield SparseEmbedding.from_dict(rescored)
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[SparseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[SparseModelDescription]: A list of SparseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_bm42_models
|
||||
|
||||
@classmethod
|
||||
def _load_stopwords(cls, model_dir: Path) -> list[str]:
|
||||
stopwords_path = model_dir / "stopwords.txt"
|
||||
if not stopwords_path.exists():
|
||||
return []
|
||||
|
||||
with open(stopwords_path, "r") as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
alpha=self.alpha,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _query_rehash(cls, tokens: Iterable[str]) -> dict[int, float]:
|
||||
result: dict[int, float] = {}
|
||||
for token in tokens:
|
||||
token_id = abs(mmh3.hash(token))
|
||||
result[token_id] = 1.0
|
||||
return result
|
||||
|
||||
def query_embed(
|
||||
self, query: Union[str, Iterable[str]], **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
To emulate BM25 behaviour, we don't need to use smart weights in the query, and
|
||||
it's enough to just hash the tokens and assign a weight of 1.0 to them.
|
||||
It is also faster, as we don't need to run the model for the query.
|
||||
"""
|
||||
if isinstance(query, str):
|
||||
query = [query]
|
||||
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
|
||||
for text in query:
|
||||
encoded = self.tokenizer.encode(text) # type: ignore[union-attr]
|
||||
document_tokens_with_ids = enumerate(encoded.tokens)
|
||||
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
|
||||
filtered = self._filter_pair_tokens(reconstructed)
|
||||
stemmed = self._stem_pair_tokens(filtered)
|
||||
|
||||
yield SparseEmbedding.from_dict(self._query_rehash(token for token, _ in stemmed))
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[TextEmbeddingWorker[SparseEmbedding]]:
|
||||
return Bm42TextEmbeddingWorker
|
||||
|
||||
|
||||
class Bm42TextEmbeddingWorker(TextEmbeddingWorker[SparseEmbedding]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> Bm42:
|
||||
return Bm42(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,356 @@
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Any, Optional, Sequence, Iterable, Union, Type
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
from py_rust_stemmers import SnowballStemmer
|
||||
from tokenizers import Tokenizer
|
||||
|
||||
from fastembed.common.model_description import SparseModelDescription, ModelSource
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.sparse.sparse_embedding_base import (
|
||||
SparseEmbedding,
|
||||
SparseTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.sparse.utils.minicoil_encoder import Encoder
|
||||
from fastembed.sparse.utils.sparse_vectors_converter import SparseVectorConverter, WordEmbedding
|
||||
from fastembed.sparse.utils.vocab_resolver import VocabResolver, VocabTokenizer
|
||||
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
||||
|
||||
|
||||
MINICOIL_MODEL_FILE = "minicoil.triplet.model.npy"
|
||||
MINICOIL_VOCAB_FILE = "minicoil.triplet.model.vocab"
|
||||
STOPWORDS_FILE = "stopwords.txt"
|
||||
|
||||
|
||||
supported_minicoil_models: list[SparseModelDescription] = [
|
||||
SparseModelDescription(
|
||||
model="Qdrant/minicoil-v1",
|
||||
vocab_size=19125,
|
||||
description="Sparse embedding model, that resolves semantic meaning of the words, "
|
||||
"while keeping exact keyword match behavior. "
|
||||
"Based on jinaai/jina-embeddings-v2-small-en-tokens",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.09,
|
||||
sources=ModelSource(hf="Qdrant/minicoil-v1"),
|
||||
model_file="onnx/model.onnx",
|
||||
additional_files=[
|
||||
STOPWORDS_FILE,
|
||||
MINICOIL_MODEL_FILE,
|
||||
MINICOIL_VOCAB_FILE,
|
||||
],
|
||||
requires_idf=True,
|
||||
),
|
||||
]
|
||||
|
||||
MODEL_TO_LANGUAGE = {
|
||||
"Qdrant/minicoil-v1": "english",
|
||||
}
|
||||
|
||||
|
||||
class MiniCOIL(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
|
||||
"""
|
||||
MiniCOIL is a sparse embedding model, that resolves semantic meaning of the words,
|
||||
while keeping exact keyword match behavior.
|
||||
|
||||
Each vocabulary token is converted into 4d component of a sparse vector, which is then weighted by the token frequency in the corpus.
|
||||
If the token is not found in the corpus, it is treated exactly like in BM25.
|
||||
`
|
||||
The model is based on `jinaai/jina-embeddings-v2-small-en-tokens`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
k: float = 1.2,
|
||||
b: float = 0.75,
|
||||
avg_len: float = 150.0,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
model_name (str): The name of the model to use.
|
||||
cache_dir (str, optional): The path to the cache directory.
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The providers to use for onnxruntime.
|
||||
k (float, optional): The k parameter in the BM25 formula. Defines the saturation of the term frequency.
|
||||
I.e. defines how fast the moment when additional terms stop to increase the score. Defaults to 1.2.
|
||||
b (float, optional): The b parameter in the BM25 formula. Defines the importance of the document length.
|
||||
Defaults to 0.75.
|
||||
avg_len (float, optional): The average length of the documents in the corpus. Defaults to 150.0.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
self.device_id = device_id
|
||||
self.k = k
|
||||
self.b = b
|
||||
self.avg_len = avg_len
|
||||
|
||||
# Initialize class attributes
|
||||
self.tokenizer: Optional[Tokenizer] = None
|
||||
self.invert_vocab: dict[int, str] = {}
|
||||
self.special_tokens: set[str] = set()
|
||||
self.special_tokens_ids: set[int] = set()
|
||||
self.stopwords: set[str] = set()
|
||||
self.vocab_resolver: Optional[VocabResolver] = None
|
||||
self.encoder: Optional[Encoder] = None
|
||||
self.output_dim: Optional[int] = None
|
||||
self.sparse_vector_converter: Optional[SparseVectorConverter] = None
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
assert self.tokenizer is not None
|
||||
|
||||
for token, idx in self.tokenizer.get_vocab().items(): # type: ignore[union-attr]
|
||||
self.invert_vocab[idx] = token
|
||||
self.special_tokens = set(self.special_token_to_id.keys())
|
||||
self.special_tokens_ids = set(self.special_token_to_id.values())
|
||||
self.stopwords = set(self._load_stopwords(self._model_dir))
|
||||
|
||||
stemmer = SnowballStemmer(MODEL_TO_LANGUAGE[self.model_name])
|
||||
|
||||
self.vocab_resolver = VocabResolver(
|
||||
tokenizer=VocabTokenizer(self.tokenizer),
|
||||
stopwords=self.stopwords,
|
||||
stemmer=stemmer,
|
||||
)
|
||||
self.vocab_resolver.load_json_vocab(str(self._model_dir / MINICOIL_VOCAB_FILE))
|
||||
|
||||
weights = np.load(str(self._model_dir / MINICOIL_MODEL_FILE), mmap_mode="r")
|
||||
self.encoder = Encoder(weights)
|
||||
self.output_dim = self.encoder.output_dim
|
||||
|
||||
self.sparse_vector_converter = SparseVectorConverter(
|
||||
stopwords=self.stopwords,
|
||||
stemmer=stemmer,
|
||||
k=self.k,
|
||||
b=self.b,
|
||||
avg_len=self.avg_len,
|
||||
)
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
|
||||
Args:
|
||||
documents: Iterator of documents or single document to embed
|
||||
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
|
||||
parallel:
|
||||
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
|
||||
If 0, use all available cores.
|
||||
If None, don't use data-parallel processing, use default onnxruntime threading instead.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
k=self.k,
|
||||
b=self.b,
|
||||
avg_len=self.avg_len,
|
||||
is_query=False,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def query_embed(
|
||||
self, query: Union[str, Iterable[str]], **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of queries into list of embeddings.
|
||||
"""
|
||||
yield from self._embed_documents(
|
||||
model_name=self.model_name,
|
||||
cache_dir=str(self.cache_dir),
|
||||
documents=query,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
k=self.k,
|
||||
b=self.b,
|
||||
avg_len=self.avg_len,
|
||||
is_query=True,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_stopwords(cls, model_dir: Path) -> list[str]:
|
||||
stopwords_path = model_dir / STOPWORDS_FILE
|
||||
if not stopwords_path.exists():
|
||||
return []
|
||||
|
||||
with open(stopwords_path, "r") as f:
|
||||
return f.read().splitlines()
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[SparseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[SparseModelDescription]: A list of SparseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_minicoil_models
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, is_query: bool = False, **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
if output.input_ids is None:
|
||||
raise ValueError("input_ids must be provided for document post-processing")
|
||||
|
||||
assert self.vocab_resolver is not None
|
||||
assert self.encoder is not None
|
||||
assert self.sparse_vector_converter is not None
|
||||
|
||||
# Size: (batch_size, sequence_length, hidden_size)
|
||||
embeddings = output.model_output
|
||||
# Size: (batch_size, sequence_length)
|
||||
assert output.attention_mask is not None
|
||||
masks = output.attention_mask
|
||||
|
||||
vocab_size = self.vocab_resolver.vocab_size()
|
||||
embedding_size = self.encoder.output_dim
|
||||
|
||||
# For each document we only select those embeddings that are not masked out
|
||||
|
||||
for i in range(embeddings.shape[0]):
|
||||
# Size: (sequence_length, hidden_size)
|
||||
token_embeddings = embeddings[i, masks[i] == 1]
|
||||
|
||||
# Size: (sequence_length)
|
||||
token_ids: NDArray[np.int64] = output.input_ids[i, masks[i] == 1]
|
||||
|
||||
word_ids_array, counts, oov, forms = self.vocab_resolver.resolve_tokens(token_ids)
|
||||
|
||||
# Size: (1, words)
|
||||
word_ids_array_expanded: NDArray[np.int64] = np.expand_dims(word_ids_array, axis=0)
|
||||
|
||||
# Size: (1, words, embedding_size)
|
||||
token_embeddings_array: NDArray[np.float32] = np.expand_dims(token_embeddings, axis=0)
|
||||
|
||||
assert word_ids_array_expanded.shape[1] == token_embeddings_array.shape[1]
|
||||
|
||||
# Size of word_ids_mapping: (unique_words, 2) - [vocab_id, batch_id]
|
||||
# Size of embeddings: (unique_words, embedding_size)
|
||||
ids_mapping, minicoil_embeddings = self.encoder.forward(
|
||||
word_ids_array_expanded, token_embeddings_array
|
||||
)
|
||||
|
||||
# Size of counts: (unique_words)
|
||||
words_ids: list[int] = ids_mapping[:, 0].tolist() # type: ignore[assignment]
|
||||
|
||||
sentence_result: dict[str, WordEmbedding] = {}
|
||||
|
||||
words = [self.vocab_resolver.lookup_word(word_id) for word_id in words_ids]
|
||||
|
||||
for word, word_id, emb in zip(words, words_ids, minicoil_embeddings.tolist()): # type: ignore[arg-type]
|
||||
if word_id == 0:
|
||||
continue
|
||||
|
||||
sentence_result[word] = WordEmbedding(
|
||||
word=word,
|
||||
forms=forms[word],
|
||||
count=int(counts[word_id]),
|
||||
word_id=int(word_id),
|
||||
embedding=emb, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
for oov_word, count in oov.items():
|
||||
# {
|
||||
# "word": oov_word,
|
||||
# "forms": [oov_word],
|
||||
# "count": int(count),
|
||||
# "word_id": -1,
|
||||
# "embedding": [1]
|
||||
# }
|
||||
sentence_result[oov_word] = WordEmbedding(
|
||||
word=oov_word, forms=[oov_word], count=int(count), word_id=-1, embedding=[1]
|
||||
)
|
||||
|
||||
if not is_query:
|
||||
yield self.sparse_vector_converter.embedding_to_vector(
|
||||
sentence_result, vocab_size=vocab_size, embedding_size=embedding_size
|
||||
)
|
||||
else:
|
||||
yield self.sparse_vector_converter.embedding_to_vector_query(
|
||||
sentence_result, vocab_size=vocab_size, embedding_size=embedding_size
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["MiniCoilTextEmbeddingWorker"]:
|
||||
return MiniCoilTextEmbeddingWorker
|
||||
|
||||
|
||||
class MiniCoilTextEmbeddingWorker(TextEmbeddingWorker[SparseEmbedding]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> MiniCOIL:
|
||||
return MiniCOIL(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,43 +1,88 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, Optional, Union
|
||||
from typing import Iterable, Optional, Union, Any
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.model_description import SparseModelDescription
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
|
||||
|
||||
@dataclass
|
||||
class SparseEmbedding:
|
||||
values: np.ndarray
|
||||
indices: np.ndarray
|
||||
values: NumpyArray
|
||||
indices: Union[NDArray[np.int64], NDArray[np.int32]]
|
||||
|
||||
def as_object(self) -> Dict[str, np.ndarray]:
|
||||
def as_object(self) -> dict[str, NumpyArray]:
|
||||
return {
|
||||
"values": self.values,
|
||||
"indices": self.indices,
|
||||
}
|
||||
|
||||
def as_dict(self) -> Dict[int, float]:
|
||||
return {i: v for i, v in zip(self.indices, self.values)}
|
||||
def as_dict(self) -> dict[int, float]:
|
||||
return {int(i): float(v) for i, v in zip(self.indices, self.values)} # type: ignore
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[int, float]) -> "SparseEmbedding":
|
||||
if len(data) == 0:
|
||||
return cls(values=np.array([]), indices=np.array([]))
|
||||
indices, values = zip(*data.items())
|
||||
return cls(values=np.array(values), indices=np.array(indices))
|
||||
|
||||
|
||||
class SparseTextEmbeddingBase(ModelManagement):
|
||||
class SparseTextEmbeddingBase(ModelManagement[SparseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Embeds a list of text passages into a list of embeddings.
|
||||
|
||||
Args:
|
||||
texts (Iterable[str]): The list of texts to embed.
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[SparseEmbedding]: The sparse embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.embed(texts, **kwargs)
|
||||
|
||||
def query_embed(
|
||||
self, query: Union[str, Iterable[str]], **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
Args:
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[SparseEmbedding]: The sparse embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
if isinstance(query, str):
|
||||
yield from self.embed([query], **kwargs)
|
||||
else:
|
||||
yield from self.embed(query, **kwargs)
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
from typing import List, Type, Dict, Any, Union, Iterable, Optional
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastembed.sparse.sparse_embedding_base import SparseTextEmbeddingBase, SparseEmbedding
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.sparse.bm25 import Bm25
|
||||
from fastembed.sparse.bm42 import Bm42
|
||||
from fastembed.sparse.minicoil import MiniCOIL
|
||||
from fastembed.sparse.sparse_embedding_base import (
|
||||
SparseEmbedding,
|
||||
SparseTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.sparse.splade_pp import SpladePP
|
||||
import warnings
|
||||
from fastembed.common.model_description import SparseModelDescription
|
||||
|
||||
|
||||
class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
EMBEDDINGS_REGISTRY: List[Type[SparseTextEmbeddingBase]] = [
|
||||
SpladePP,
|
||||
]
|
||||
EMBEDDINGS_REGISTRY: list[Type[SparseTextEmbeddingBase]] = [SpladePP, Bm42, Bm25, MiniCOIL]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
@@ -24,6 +32,7 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
"model": "prithvida/SPLADE_PP_en_v1",
|
||||
"vocab_size": 30522,
|
||||
"description": "Independent Implementation of SPLADE++ Model for English",
|
||||
"license": "apache-2.0",
|
||||
"size_in_GB": 0.532,
|
||||
"sources": {
|
||||
"hf": "qdrant/SPLADE_PP_en_v1",
|
||||
@@ -32,9 +41,13 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
]
|
||||
```
|
||||
"""
|
||||
result = []
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[SparseModelDescription]:
|
||||
result: list[SparseModelDescription] = []
|
||||
for embedding in cls.EMBEDDINGS_REGISTRY:
|
||||
result.extend(embedding.list_supported_models())
|
||||
result.extend(embedding._list_supported_models())
|
||||
return result
|
||||
|
||||
def __init__(
|
||||
@@ -42,14 +55,35 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
if model_name.lower() == "prithvida/Splade_PP_en_v1".lower():
|
||||
warnings.warn(
|
||||
"The right spelling is prithivida/Splade_PP_en_v1. "
|
||||
"Support of this name will be removed soon, please fix the model_name",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
model_name = "prithivida/Splade_PP_en_v1"
|
||||
|
||||
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
||||
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
|
||||
if any(model_name.lower() == model["model"].lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(model_name, cache_dir, threads, **kwargs)
|
||||
supported_models = EMBEDDING_MODEL_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(
|
||||
model_name,
|
||||
cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
@@ -62,7 +96,7 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
@@ -80,3 +114,17 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
|
||||
|
||||
def query_embed(
|
||||
self, query: Union[str, Iterable[str]], **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
Args:
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[SparseEmbedding]: The sparse embeddings.
|
||||
"""
|
||||
yield from self.model.query_embed(query, **kwargs)
|
||||
|
||||
+103
-51
@@ -1,44 +1,48 @@
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, Type
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.sparse.sparse_embedding_base import SparseEmbedding, SparseTextEmbeddingBase
|
||||
from fastembed.sparse.sparse_embedding_base import (
|
||||
SparseEmbedding,
|
||||
SparseTextEmbeddingBase,
|
||||
)
|
||||
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
||||
from fastembed.common.model_description import SparseModelDescription, ModelSource
|
||||
|
||||
supported_splade_models = [
|
||||
{
|
||||
"model": "prithvida/Splade_PP_en_v1",
|
||||
"vocab_size": 30522,
|
||||
"description": "Misspelled version of the model. Retained for backward compatibility. Independent Implementation of SPLADE++ Model for English",
|
||||
"size_in_GB": 0.532,
|
||||
"sources": {
|
||||
"hf": "Qdrant/SPLADE_PP_en_v1",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
{
|
||||
"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",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
supported_splade_models: list[SparseModelDescription] = [
|
||||
SparseModelDescription(
|
||||
model="prithivida/Splade_PP_en_v1",
|
||||
vocab_size=30522,
|
||||
description="Independent Implementation of SPLADE++ Model for English.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.532,
|
||||
sources=ModelSource(hf="Qdrant/Splade_PP_en_v1"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
SparseModelDescription(
|
||||
model="prithvida/Splade_PP_en_v1",
|
||||
vocab_size=30522,
|
||||
description="Independent Implementation of SPLADE++ Model for English.",
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.532,
|
||||
sources=ModelSource(hf="Qdrant/Splade_PP_en_v1"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
@classmethod
|
||||
class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
|
||||
def _post_process_onnx_output(
|
||||
cls, output: Tuple[np.ndarray, np.ndarray]
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
logits, attention_mask = output
|
||||
relu_log = np.log(1 + np.maximum(logits, 0))
|
||||
if output.attention_mask is None:
|
||||
raise ValueError("attention_mask must be provided for document post-processing")
|
||||
|
||||
weighted_log = relu_log * np.expand_dims(attention_mask, axis=-1)
|
||||
relu_log = np.log(1 + np.maximum(output.model_output, 0))
|
||||
|
||||
weighted_log = relu_log * np.expand_dims(output.attention_mask, axis=-1)
|
||||
|
||||
scores = np.max(weighted_log, axis=1)
|
||||
|
||||
@@ -50,11 +54,11 @@ class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
yield SparseEmbedding(values=scores, indices=indices)
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
def _list_supported_models(cls) -> list[SparseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
list[SparseModelDescription]: A list of SparseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_splade_models
|
||||
|
||||
@@ -63,7 +67,13 @@ class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -72,22 +82,57 @@ class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
model_description = self._get_model_description(model_name)
|
||||
cache_dir = define_cache_dir(cache_dir)
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
model_dir = self.download_model(model_description, cache_dir)
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_description["model_file"],
|
||||
threads=threads,
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
def embed(
|
||||
@@ -95,7 +140,7 @@ class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[SparseEmbedding]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
@@ -118,17 +163,24 @@ class SpladePP(SparseTextEmbeddingBase, OnnxModel[SparseEmbedding]):
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[EmbeddingWorker]:
|
||||
def _get_worker_class(cls) -> Type[TextEmbeddingWorker[SparseEmbedding]]:
|
||||
return SpladePPEmbeddingWorker
|
||||
|
||||
|
||||
class SpladePPEmbeddingWorker(EmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
) -> SpladePP:
|
||||
return SpladePP(model_name=model_name, cache_dir=cache_dir, threads=1)
|
||||
class SpladePPEmbeddingWorker(TextEmbeddingWorker[SparseEmbedding]):
|
||||
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> SpladePP:
|
||||
return SpladePP(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Pure numpy implementation of encoder model for a single word.
|
||||
|
||||
This model is not trainable, and should only be used for inference.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
|
||||
class Encoder:
|
||||
"""
|
||||
Encoder(768, 4, 10000)
|
||||
|
||||
Will look like this:
|
||||
|
||||
|
||||
Per-word
|
||||
Encoder Matrix
|
||||
┌─────────────────────┐
|
||||
│ Token Embedding(768)├──────┐ (10k, 768, 4)
|
||||
└─────────────────────┘ │ ┌─────────┐
|
||||
│ │ │
|
||||
┌─────────────────────┐ │ ┌─┴───────┐ │
|
||||
│ │ │ │ │ │
|
||||
└─────────────────────┘ │ ┌─┴───────┐ │ │ ┌─────────┐
|
||||
└────►│ │ │ ├─────►│Tanh │
|
||||
┌─────────────────────┐ │ │ │ │ └─────────┘
|
||||
│ │ │ │ ├─┘
|
||||
└─────────────────────┘ │ ├─┘
|
||||
│ │
|
||||
┌─────────────────────┐ └─────────┘
|
||||
│ │
|
||||
└─────────────────────┘
|
||||
|
||||
Final linear transformation is accompanied by a non-linear activation function: Tanh.
|
||||
|
||||
Tanh is used to ensure that the output is in the range [-1, 1].
|
||||
It would be easier to visually interpret the output of the model, assuming that each dimension
|
||||
would need to encode a type of semantic cluster.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
weights: NumpyArray,
|
||||
):
|
||||
self.weights = weights
|
||||
self.vocab_size, self.input_dim, self.output_dim = weights.shape
|
||||
|
||||
self.encoder_weights: NumpyArray = weights
|
||||
|
||||
# Activation function
|
||||
self.activation = np.tanh
|
||||
|
||||
@staticmethod
|
||||
def convert_vocab_ids(vocab_ids: NumpyArray) -> NumpyArray:
|
||||
"""
|
||||
Convert vocab_ids of shape (batch_size, seq_len) into (batch_size, seq_len, 2)
|
||||
by appending batch_id alongside each vocab_id.
|
||||
"""
|
||||
batch_size, seq_len = vocab_ids.shape
|
||||
batch_ids = np.arange(batch_size, dtype=vocab_ids.dtype).reshape(batch_size, 1)
|
||||
batch_ids = np.repeat(batch_ids, seq_len, axis=1)
|
||||
# Stack vocab_ids and batch_ids along the last dimension
|
||||
combined: NumpyArray = np.stack((vocab_ids, batch_ids), axis=2).astype(np.int32)
|
||||
return combined
|
||||
|
||||
@classmethod
|
||||
def avg_by_vocab_ids(
|
||||
cls, vocab_ids: NumpyArray, embeddings: NumpyArray
|
||||
) -> tuple[NumpyArray, NumpyArray]:
|
||||
"""
|
||||
Takes:
|
||||
vocab_ids: (batch_size, seq_len) int array
|
||||
embeddings: (batch_size, seq_len, input_dim) float array
|
||||
|
||||
Returns:
|
||||
unique_flattened_vocab_ids: (total_unique, 2) array of [vocab_id, batch_id]
|
||||
unique_flattened_embeddings: (total_unique, input_dim) averaged embeddings
|
||||
"""
|
||||
input_dim = embeddings.shape[2]
|
||||
|
||||
# Flatten vocab_ids and embeddings
|
||||
# flattened_vocab_ids: (batch_size*seq_len, 2)
|
||||
flattened_vocab_ids = cls.convert_vocab_ids(vocab_ids).reshape(-1, 2)
|
||||
|
||||
# flattened_embeddings: (batch_size*seq_len, input_dim)
|
||||
flattened_embeddings = embeddings.reshape(-1, input_dim)
|
||||
|
||||
# Find unique (vocab_id, batch_id) pairs
|
||||
unique_flattened_vocab_ids, inverse_indices = np.unique(
|
||||
flattened_vocab_ids, axis=0, return_inverse=True
|
||||
)
|
||||
|
||||
# Prepare arrays to accumulate sums
|
||||
unique_count = unique_flattened_vocab_ids.shape[0]
|
||||
unique_flattened_embeddings = np.zeros((unique_count, input_dim), dtype=np.float32)
|
||||
unique_flattened_count = np.zeros(unique_count, dtype=np.int32)
|
||||
|
||||
# Use np.add.at to accumulate sums based on inverse indices
|
||||
np.add.at(unique_flattened_embeddings, inverse_indices, flattened_embeddings)
|
||||
np.add.at(unique_flattened_count, inverse_indices, 1)
|
||||
|
||||
# Compute averages
|
||||
unique_flattened_embeddings /= unique_flattened_count[:, None]
|
||||
|
||||
return unique_flattened_vocab_ids.astype(np.int32), unique_flattened_embeddings.astype(
|
||||
np.float32
|
||||
)
|
||||
|
||||
def forward(
|
||||
self, vocab_ids: NumpyArray, embeddings: NumpyArray
|
||||
) -> tuple[NumpyArray, NumpyArray]:
|
||||
"""
|
||||
Args:
|
||||
vocab_ids: (batch_size, seq_len) int array
|
||||
embeddings: (batch_size, seq_len, input_dim) float array
|
||||
|
||||
Returns:
|
||||
unique_flattened_vocab_ids_and_batch_ids: (total_unique, 2)
|
||||
unique_flattened_encoded: (total_unique, output_dim)
|
||||
"""
|
||||
# Average embeddings for duplicate vocab_ids
|
||||
unique_flattened_vocab_ids_and_batch_ids, unique_flattened_embeddings = (
|
||||
self.avg_by_vocab_ids(vocab_ids, embeddings)
|
||||
)
|
||||
|
||||
# Select the encoder weights for each unique vocab_id
|
||||
unique_flattened_vocab_ids = unique_flattened_vocab_ids_and_batch_ids[:, 0].astype(
|
||||
np.int32
|
||||
)
|
||||
|
||||
# unique_encoder_weights: (total_unique, input_dim, output_dim)
|
||||
unique_encoder_weights = self.encoder_weights[unique_flattened_vocab_ids]
|
||||
|
||||
# Compute linear transform: (total_unique, output_dim)
|
||||
# Using Einstein summation for matrix multiplication:
|
||||
# 'bi,bio->bo' means: for each "b" (batch element), multiply embeddings (b,i) by weights (b,i,o) -> (b,o)
|
||||
unique_flattened_encoded = np.einsum(
|
||||
"bi,bio->bo", unique_flattened_embeddings, unique_encoder_weights
|
||||
)
|
||||
|
||||
# Apply Tanh activation and ensure float32 type
|
||||
unique_flattened_encoded = self.activation(unique_flattened_encoded).astype(np.float32)
|
||||
|
||||
return unique_flattened_vocab_ids_and_batch_ids.astype(np.int32), unique_flattened_encoded
|
||||
@@ -0,0 +1,247 @@
|
||||
from typing import Dict, List, Set
|
||||
from py_rust_stemmers import SnowballStemmer
|
||||
from fastembed.common.utils import get_all_punctuation, remove_non_alphanumeric
|
||||
import mmh3
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.sparse.sparse_embedding_base import SparseEmbedding
|
||||
|
||||
GAP = 32000
|
||||
INT32_MAX = 2**31 - 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class WordEmbedding:
|
||||
word: str
|
||||
forms: List[str]
|
||||
count: int
|
||||
word_id: int
|
||||
embedding: List[float]
|
||||
|
||||
|
||||
class SparseVectorConverter:
|
||||
def __init__(
|
||||
self,
|
||||
stopwords: Set[str],
|
||||
stemmer: SnowballStemmer,
|
||||
k: float = 1.2,
|
||||
b: float = 0.75,
|
||||
avg_len: float = 150.0,
|
||||
):
|
||||
punctuation = set(get_all_punctuation())
|
||||
special_tokens = {"[CLS]", "[SEP]", "[PAD]", "[UNK]", "[MASK]"}
|
||||
|
||||
self.stemmer = stemmer
|
||||
self.unwanted_tokens = punctuation | special_tokens | stopwords
|
||||
|
||||
self.k = k
|
||||
self.b = b
|
||||
self.avg_len = avg_len
|
||||
|
||||
@classmethod
|
||||
def unkn_word_token_id(
|
||||
cls, word: str, shift: int
|
||||
) -> int: # 2-3 words can collide in 1 index with this mapping, not considering mm3 collisions
|
||||
token_hash = abs(mmh3.hash(word))
|
||||
|
||||
range_size = INT32_MAX - shift
|
||||
remapped_hash = shift + (token_hash % range_size)
|
||||
|
||||
return remapped_hash
|
||||
|
||||
def bm25_tf(self, num_occurrences: int, sentence_len: int) -> float:
|
||||
res = num_occurrences * (self.k + 1)
|
||||
res /= num_occurrences + self.k * (1 - self.b + self.b * sentence_len / self.avg_len)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def normalize_vector(cls, vector: List[float]) -> List[float]:
|
||||
norm = sum([x**2 for x in vector]) ** 0.5
|
||||
if norm < 1e-8:
|
||||
return vector
|
||||
return [x / norm for x in vector]
|
||||
|
||||
def clean_words(
|
||||
self, sentence_embedding: Dict[str, WordEmbedding], token_max_length: int = 40
|
||||
) -> Dict[str, WordEmbedding]:
|
||||
"""
|
||||
Clean miniCOIL-produced sentence_embedding, as unknown to the miniCOIL's stemmer tokens should fully resemble
|
||||
our BM25 token representation.
|
||||
|
||||
sentence_embedding = {"9°": {"word": "9°", "word_id": -1, "count": 2, "embedding": [1], "forms": ["9°"]},
|
||||
"9": {"word": "9", "word_id": -1, "count": 2, "embedding": [1], "forms": ["9"]},
|
||||
"bat": {"word": "bat", "word_id": 2, "count": 3, "embedding": [0.2, 0.1, -0.2, -0.2], "forms": ["bats", "bat"]},
|
||||
"9°9": {"word": "9°9", "word_id": -1, "count": 1, "embedding": [1], "forms": ["9°9"]},
|
||||
"screech": {"word": "screech", "word_id": -1, "count": 1, "embedding": [1], "forms": ["screech"]},
|
||||
"screeched": {"word": "screeched", "word_id": -1, "count": 1, "embedding": [1], "forms": ["screeched"]}
|
||||
}
|
||||
cleaned_embedding_ground_truth = {
|
||||
"9": {"word": "9", "word_id": -1, "count": 6, "embedding": [1], "forms": ["9°", "9", "9°9", "9°9"]},
|
||||
"bat": {"word": "bat", "word_id": 2, "count": 3, "embedding": [0.2, 0.1, -0.2, -0.2], "forms": ["bats", "bat"]},
|
||||
"screech": {"word": "screech", "word_id": -1, "count": 2, "embedding": [1], "forms": ["screech", "screeched"]}
|
||||
}
|
||||
"""
|
||||
|
||||
new_sentence_embedding: Dict[str, WordEmbedding] = {}
|
||||
|
||||
for word, embedding in sentence_embedding.items():
|
||||
# embedding = {
|
||||
# "word": "vector",
|
||||
# "forms": ["vector", "vectors"],
|
||||
# "count": 2,
|
||||
# "word_id": 1231,
|
||||
# "embedding": [0.1, 0.2, 0.3, 0.4]
|
||||
# }
|
||||
if embedding.word_id > 0:
|
||||
# Known word, no need to clean
|
||||
new_sentence_embedding[word] = embedding
|
||||
else:
|
||||
# Unknown word
|
||||
if word in self.unwanted_tokens:
|
||||
continue
|
||||
|
||||
# Example complex word split:
|
||||
# word = `word^vec`
|
||||
word_cleaned = remove_non_alphanumeric(word).strip()
|
||||
# word_cleaned = `word vec`
|
||||
|
||||
if len(word_cleaned) > 0:
|
||||
# Subwords: ['word', 'vec']
|
||||
for subword in word_cleaned.split():
|
||||
stemmed_subword: str = self.stemmer.stem_word(subword)
|
||||
if (
|
||||
len(stemmed_subword) <= token_max_length
|
||||
and stemmed_subword not in self.unwanted_tokens
|
||||
):
|
||||
if stemmed_subword not in new_sentence_embedding:
|
||||
new_sentence_embedding[stemmed_subword] = copy.deepcopy(embedding)
|
||||
new_sentence_embedding[stemmed_subword].word = stemmed_subword
|
||||
else:
|
||||
new_sentence_embedding[stemmed_subword].count += embedding.count
|
||||
new_sentence_embedding[stemmed_subword].forms += embedding.forms
|
||||
|
||||
return new_sentence_embedding
|
||||
|
||||
def embedding_to_vector(
|
||||
self,
|
||||
sentence_embedding: Dict[str, WordEmbedding],
|
||||
embedding_size: int,
|
||||
vocab_size: int,
|
||||
) -> SparseEmbedding:
|
||||
"""
|
||||
Convert miniCOIL sentence embedding to Qdrant sparse vector
|
||||
|
||||
Example input:
|
||||
|
||||
```
|
||||
{
|
||||
"vector": WordEmbedding({ // Vocabulary word, encoded with miniCOIL normally
|
||||
"word": "vector",
|
||||
"forms": ["vector", "vectors"],
|
||||
"count": 2,
|
||||
"word_id": 1231,
|
||||
"embedding": [0.1, 0.2, 0.3, 0.4]
|
||||
}),
|
||||
"axiotic": WordEmbedding({ // Out-of-vocabulary word, fallback to BM25
|
||||
"word": "axiotic",
|
||||
"forms": ["axiotics"],
|
||||
"count": 1,
|
||||
"word_id": -1,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
indices: List[int] = []
|
||||
values: List[float] = []
|
||||
|
||||
# Example:
|
||||
# vocab_size = 10000
|
||||
# embedding_size = 4
|
||||
# GAP = 32000
|
||||
#
|
||||
# We want to start random words section from the bucket, that is guaranteed to not
|
||||
# include any vocab words.
|
||||
# We need (vocab_size * embedding_size) slots for vocab words.
|
||||
# Therefore we need (vocab_size * embedding_size) // GAP + 1 buckets for vocab words.
|
||||
# Therefore, we can start random words from bucket (vocab_size * embedding_size) // GAP + 1 + 1
|
||||
|
||||
# ID at which the scope of OOV words starts
|
||||
unknown_words_shift = (
|
||||
(vocab_size * embedding_size) // GAP + 2
|
||||
) * GAP
|
||||
sentence_embedding_cleaned = self.clean_words(sentence_embedding)
|
||||
|
||||
# Calculate sentence length after cleaning
|
||||
sentence_len = 0
|
||||
for embedding in sentence_embedding_cleaned.values():
|
||||
sentence_len += embedding.count
|
||||
|
||||
for embedding in sentence_embedding_cleaned.values():
|
||||
word_id = embedding.word_id
|
||||
num_occurrences = embedding.count
|
||||
tf = self.bm25_tf(num_occurrences, sentence_len)
|
||||
if (
|
||||
word_id > 0
|
||||
): # miniCOIL starts with ID 1, we generally won't have word_id == 0 (UNK), as we don't add
|
||||
# these words to sentence_embedding
|
||||
embedding_values = embedding.embedding
|
||||
normalized_embedding = self.normalize_vector(embedding_values)
|
||||
|
||||
for val_id, value in enumerate(normalized_embedding):
|
||||
indices.append(
|
||||
word_id * embedding_size + val_id
|
||||
) # since miniCOIL IDs start with 1
|
||||
values.append(value * tf)
|
||||
else:
|
||||
indices.append(self.unkn_word_token_id(embedding.word, unknown_words_shift))
|
||||
values.append(tf)
|
||||
|
||||
return SparseEmbedding(
|
||||
indices=np.array(indices, dtype=np.int32),
|
||||
values=np.array(values, dtype=np.float32),
|
||||
)
|
||||
|
||||
def embedding_to_vector_query(
|
||||
self,
|
||||
sentence_embedding: Dict[str, WordEmbedding],
|
||||
embedding_size: int,
|
||||
vocab_size: int,
|
||||
) -> SparseEmbedding:
|
||||
"""
|
||||
Same as `embedding_to_vector`, but no TF
|
||||
"""
|
||||
|
||||
indices: List[int] = []
|
||||
values: List[float] = []
|
||||
|
||||
# ID at which the scope of OOV words starts
|
||||
unknown_words_shift = ((vocab_size * embedding_size) // GAP + 2) * GAP
|
||||
|
||||
sentence_embedding_cleaned = self.clean_words(sentence_embedding)
|
||||
|
||||
for embedding in sentence_embedding_cleaned.values():
|
||||
word_id = embedding.word_id
|
||||
tf = 1.0
|
||||
|
||||
if word_id >= 0: # miniCOIL starts with ID 1
|
||||
embedding_values = embedding.embedding
|
||||
normalized_embedding = self.normalize_vector(embedding_values)
|
||||
|
||||
for val_id, value in enumerate(normalized_embedding):
|
||||
indices.append(
|
||||
word_id * embedding_size + val_id
|
||||
) # since miniCOIL IDs start with 1
|
||||
values.append(value * tf)
|
||||
else:
|
||||
indices.append(self.unkn_word_token_id(embedding.word, unknown_words_shift))
|
||||
values.append(tf)
|
||||
|
||||
return SparseEmbedding(
|
||||
indices=np.array(indices, dtype=np.int32),
|
||||
values=np.array(values, dtype=np.float32),
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
# This code is a modified copy of the `NLTKWordTokenizer` class from `NLTK` library.
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class SimpleTokenizer:
|
||||
@staticmethod
|
||||
def tokenize(text: str) -> list[str]:
|
||||
text = re.sub(r"[^\w]", " ", text.lower())
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
|
||||
return text.strip().split()
|
||||
|
||||
|
||||
class WordTokenizer:
|
||||
"""The tokenizer is "destructive" such that the regexes applied will munge the
|
||||
input string to a state beyond re-construction.
|
||||
"""
|
||||
|
||||
# Starting quotes.
|
||||
STARTING_QUOTES = [
|
||||
(re.compile("([«“‘„]|[`]+)", re.U), r" \1 "),
|
||||
(re.compile(r"^\""), r"``"),
|
||||
(re.compile(r"(``)"), r" \1 "),
|
||||
(re.compile(r"([ \(\[{<])(\"|\'{2})"), r"\1 `` "),
|
||||
(re.compile(r"(?i)(\')(?!re|ve|ll|m|t|s|d|n)(\w)\b", re.U), r"\1 \2"),
|
||||
]
|
||||
|
||||
# Ending quotes.
|
||||
ENDING_QUOTES = [
|
||||
(re.compile("([»”’])", re.U), r" \1 "),
|
||||
(re.compile(r"''"), " '' "),
|
||||
(re.compile(r'"'), " '' "),
|
||||
(re.compile(r"([^' ])('[sS]|'[mM]|'[dD]|') "), r"\1 \2 "),
|
||||
(re.compile(r"([^' ])('ll|'LL|'re|'RE|'ve|'VE|n't|N'T) "), r"\1 \2 "),
|
||||
]
|
||||
|
||||
# Punctuation.
|
||||
PUNCTUATION = [
|
||||
(re.compile(r'([^\.])(\.)([\]\)}>"\'' "»”’ " r"]*)\s*$", re.U), r"\1 \2 \3 "),
|
||||
(re.compile(r"([:,])([^\d])"), r" \1 \2"),
|
||||
(re.compile(r"([:,])$"), r" \1 "),
|
||||
(
|
||||
re.compile(r"\.{2,}", re.U),
|
||||
r" \g<0> ",
|
||||
),
|
||||
(re.compile(r"[;@#$%&]"), r" \g<0> "),
|
||||
(
|
||||
re.compile(r'([^\.])(\.)([\]\)}>"\']*)\s*$'),
|
||||
r"\1 \2\3 ",
|
||||
), # Handles the final period.
|
||||
(re.compile(r"[?!]"), r" \g<0> "),
|
||||
(re.compile(r"([^'])' "), r"\1 ' "),
|
||||
(
|
||||
re.compile(r"[*]", re.U),
|
||||
r" \g<0> ",
|
||||
),
|
||||
]
|
||||
|
||||
# Pads parentheses
|
||||
PARENS_BRACKETS = (re.compile(r"[\]\[\(\)\{\}\<\>]"), r" \g<0> ")
|
||||
DOUBLE_DASHES = (re.compile(r"--"), r" -- ")
|
||||
|
||||
# List of contractions adapted from Robert MacIntyre's tokenizer.
|
||||
CONTRACTIONS2 = [
|
||||
re.compile(pattern)
|
||||
for pattern in (
|
||||
r"(?i)\b(can)(?#X)(not)\b",
|
||||
r"(?i)\b(d)(?#X)('ye)\b",
|
||||
r"(?i)\b(gim)(?#X)(me)\b",
|
||||
r"(?i)\b(gon)(?#X)(na)\b",
|
||||
r"(?i)\b(got)(?#X)(ta)\b",
|
||||
r"(?i)\b(lem)(?#X)(me)\b",
|
||||
r"(?i)\b(more)(?#X)('n)\b",
|
||||
r"(?i)\b(wan)(?#X)(na)(?=\s)",
|
||||
)
|
||||
]
|
||||
CONTRACTIONS3 = [
|
||||
re.compile(pattern) for pattern in (r"(?i) ('t)(?#X)(is)\b", r"(?i) ('t)(?#X)(was)\b")
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def tokenize(cls, text: str) -> list[str]:
|
||||
"""Return a tokenized copy of `text`.
|
||||
|
||||
>>> s = '''Good muffins cost $3.88 (roughly 3,36 euros)\nin New York.'''
|
||||
>>> WordTokenizer().tokenize(s)
|
||||
['Good', 'muffins', 'cost', '$', '3.88', '(', 'roughly', '3,36', 'euros', ')', 'in', 'New', 'York', '.']
|
||||
|
||||
Args:
|
||||
text: The text to be tokenized.
|
||||
|
||||
Returns:
|
||||
A list of tokens.
|
||||
"""
|
||||
for regexp, substitution in cls.STARTING_QUOTES:
|
||||
text = regexp.sub(substitution, text)
|
||||
|
||||
for regexp, substitution in cls.PUNCTUATION:
|
||||
text = regexp.sub(substitution, text)
|
||||
|
||||
# Handles parentheses.
|
||||
regexp, substitution = cls.PARENS_BRACKETS
|
||||
text = regexp.sub(substitution, text)
|
||||
|
||||
# Handles double dash.
|
||||
regexp, substitution = cls.DOUBLE_DASHES
|
||||
text = regexp.sub(substitution, text)
|
||||
|
||||
# add extra space to make things easier
|
||||
text = " " + text + " "
|
||||
|
||||
for regexp, substitution in cls.ENDING_QUOTES:
|
||||
text = regexp.sub(substitution, text)
|
||||
|
||||
for regexp in cls.CONTRACTIONS2:
|
||||
text = regexp.sub(r" \1 \2 ", text)
|
||||
for regexp in cls.CONTRACTIONS3:
|
||||
text = regexp.sub(r" \1 \2 ", text)
|
||||
return text.split()
|
||||
@@ -0,0 +1,202 @@
|
||||
from collections import defaultdict
|
||||
from typing import Iterable
|
||||
|
||||
from py_rust_stemmers import SnowballStemmer
|
||||
import numpy as np
|
||||
from tokenizers import Tokenizer
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
|
||||
|
||||
class VocabTokenizerBase:
|
||||
def tokenize(self, sentence: str) -> NumpyArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def convert_ids_to_tokens(self, token_ids: NumpyArray) -> list[str]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class VocabTokenizer(VocabTokenizerBase):
|
||||
def __init__(self, tokenizer: Tokenizer):
|
||||
self.tokenizer = tokenizer
|
||||
|
||||
def tokenize(self, sentence: str) -> NumpyArray:
|
||||
return np.array(self.tokenizer.encode(sentence).ids)
|
||||
|
||||
def convert_ids_to_tokens(self, token_ids: NumpyArray) -> list[str]:
|
||||
return [self.tokenizer.id_to_token(token_id) for token_id in token_ids]
|
||||
|
||||
|
||||
class VocabResolver:
|
||||
def __init__(self, tokenizer: VocabTokenizerBase, stopwords: set[str], stemmer: SnowballStemmer):
|
||||
# Word to id mapping
|
||||
self.vocab: dict[str, int] = {}
|
||||
# Id to word mapping
|
||||
self.words: list[str] = []
|
||||
# Lemma to word mapping
|
||||
self.stem_mapping: dict[str, str] = {}
|
||||
self.tokenizer: VocabTokenizerBase = tokenizer
|
||||
self.stemmer = stemmer
|
||||
self.stopwords: set[str] = stopwords
|
||||
|
||||
def tokenize(self, sentence: str) -> NumpyArray:
|
||||
return self.tokenizer.tokenize(sentence)
|
||||
|
||||
def lookup_word(self, word_id: int) -> str:
|
||||
if word_id == 0:
|
||||
return "UNK"
|
||||
return self.words[word_id - 1]
|
||||
|
||||
def convert_ids_to_tokens(self, token_ids: NumpyArray) -> list[str]:
|
||||
return self.tokenizer.convert_ids_to_tokens(token_ids)
|
||||
|
||||
def vocab_size(self) -> int:
|
||||
# We need +1 for UNK token
|
||||
return len(self.vocab) + 1
|
||||
|
||||
def save_vocab(self, path: str) -> None:
|
||||
with open(path, "w") as f:
|
||||
for word in self.words:
|
||||
f.write(word + "\n")
|
||||
|
||||
def save_json_vocab(self, path: str) -> None:
|
||||
import json
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump({"vocab": self.words, "stem_mapping": self.stem_mapping}, f, indent=2)
|
||||
|
||||
def load_json_vocab(self, path: str) -> None:
|
||||
import json
|
||||
|
||||
with open(path, "r") as f:
|
||||
data = json.load(f)
|
||||
self.words = data["vocab"]
|
||||
self.vocab = {word: idx + 1 for idx, word in enumerate(self.words)}
|
||||
self.stem_mapping = data["stem_mapping"]
|
||||
|
||||
def add_word(self, word: str) -> None:
|
||||
if word not in self.vocab:
|
||||
self.vocab[word] = len(self.vocab) + 1
|
||||
self.words.append(word)
|
||||
stem = self.stemmer.stem_word(word)
|
||||
if stem not in self.stem_mapping:
|
||||
self.stem_mapping[stem] = word
|
||||
else:
|
||||
existing_word = self.stem_mapping[stem]
|
||||
if len(existing_word) > len(word):
|
||||
# Prefer shorter words for the same stem
|
||||
# Example: "swim" is preferred over "swimming"
|
||||
self.stem_mapping[stem] = word
|
||||
|
||||
def load_vocab(self, path: str) -> None:
|
||||
with open(path, "r") as f:
|
||||
for line in f:
|
||||
self.add_word(line.strip())
|
||||
|
||||
@classmethod
|
||||
def _reconstruct_bpe(
|
||||
cls, bpe_tokens: Iterable[tuple[int, str]]
|
||||
) -> list[tuple[str, list[int]]]:
|
||||
result: list[tuple[str, list[int]]] = []
|
||||
acc: str = ""
|
||||
acc_idx: list[int] = []
|
||||
|
||||
continuing_subword_prefix = "##"
|
||||
continuing_subword_prefix_len = len(continuing_subword_prefix)
|
||||
|
||||
for idx, token in bpe_tokens:
|
||||
if token.startswith(continuing_subword_prefix):
|
||||
acc += token[continuing_subword_prefix_len:]
|
||||
acc_idx.append(idx)
|
||||
else:
|
||||
if acc:
|
||||
result.append((acc, acc_idx))
|
||||
acc_idx = []
|
||||
acc = token
|
||||
acc_idx.append(idx)
|
||||
|
||||
if acc:
|
||||
result.append((acc, acc_idx))
|
||||
return result
|
||||
|
||||
def resolve_tokens(
|
||||
self, token_ids: NDArray[np.int64]
|
||||
) -> tuple[NDArray[np.int64], dict[int, int], dict[str, int], dict[str, list[str]]]:
|
||||
"""
|
||||
Mark known tokens (including composed tokens) with vocab ids.
|
||||
|
||||
Args:
|
||||
token_ids: (seq_len) - list of ids of tokens
|
||||
Example:
|
||||
[
|
||||
101, 3897, 19332, 12718, 23348,
|
||||
1010, 1996, 7151, 2296, 4845,
|
||||
2359, 2005, 4234, 1010, 4332,
|
||||
2871, 3191, 2062, 102
|
||||
]
|
||||
|
||||
returns:
|
||||
- token_ids with vocab ids
|
||||
[
|
||||
0, 151, 151, 0, 0,
|
||||
912, 0, 0, 0, 332,
|
||||
332, 332, 0, 7121, 191,
|
||||
0, 0, 332, 0
|
||||
]
|
||||
- counts of each token
|
||||
{
|
||||
151: 1,
|
||||
332: 3,
|
||||
7121: 1,
|
||||
191: 1,
|
||||
912: 1
|
||||
}
|
||||
- oov counts of each token
|
||||
{
|
||||
"the": 1,
|
||||
"a": 1,
|
||||
"[CLS]": 1,
|
||||
"[SEP]": 1,
|
||||
...
|
||||
}
|
||||
- forms of each token
|
||||
{
|
||||
"hello": ["hello"],
|
||||
"world": ["worlds", "world", "worlding"],
|
||||
}
|
||||
|
||||
"""
|
||||
tokens = self.convert_ids_to_tokens(token_ids)
|
||||
tokens_mapping = self._reconstruct_bpe(enumerate(tokens))
|
||||
|
||||
counts: dict[int, int] = defaultdict(int)
|
||||
oov_count: dict[str, int] = defaultdict(int)
|
||||
|
||||
forms: dict[str, list[str]] = defaultdict(list)
|
||||
|
||||
for token, mapped_token_ids in tokens_mapping:
|
||||
vocab_id = 0
|
||||
if token in self.stopwords:
|
||||
vocab_id = 0
|
||||
elif token in self.vocab:
|
||||
vocab_id = self.vocab[token]
|
||||
forms[token].append(token)
|
||||
elif token in self.stem_mapping:
|
||||
vocab_id = self.vocab[self.stem_mapping[token]]
|
||||
forms[self.stem_mapping[token]].append(token)
|
||||
else:
|
||||
stem = self.stemmer.stem_word(token)
|
||||
if stem in self.stem_mapping:
|
||||
vocab_id = self.vocab[self.stem_mapping[stem]]
|
||||
forms[self.stem_mapping[stem]].append(token)
|
||||
|
||||
for token_id in mapped_token_ids:
|
||||
token_ids[token_id] = vocab_id
|
||||
|
||||
if vocab_id == 0:
|
||||
oov_count[token] += 1
|
||||
else:
|
||||
counts[vocab_id] += 1
|
||||
return token_ids, counts, oov_count, forms
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
from typing import Any, Iterable, Type
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_clip_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="Qdrant/clip-ViT-B-32-text",
|
||||
dim=512,
|
||||
description=(
|
||||
"Text embeddings, Multimodal (text&image), English, 77 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2021 year"
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.25,
|
||||
sources=ModelSource(hf="Qdrant/clip-ViT-B-32-text"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class CLIPOnnxEmbedding(OnnxTextEmbedding):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[OnnxTextEmbeddingWorker]:
|
||||
return CLIPEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_clip_models
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
return output.model_output
|
||||
|
||||
|
||||
class CLIPEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxTextEmbedding:
|
||||
return CLIPOnnxEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,98 @@
|
||||
from typing import Optional, Sequence, Any, Iterable
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common import OnnxProvider
|
||||
from fastembed.common.model_description import (
|
||||
PoolingType,
|
||||
DenseModelDescription,
|
||||
)
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.utils import normalize, mean_pooling
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PostprocessingConfig:
|
||||
pooling: PoolingType
|
||||
normalization: bool
|
||||
|
||||
|
||||
class CustomTextEmbedding(OnnxTextEmbedding):
|
||||
SUPPORTED_MODELS: list[DenseModelDescription] = []
|
||||
POSTPROCESSING_MAPPING: dict[str, PostprocessingConfig] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
device_id=device_id,
|
||||
specific_model_path=specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
self._pooling = self.POSTPROCESSING_MAPPING[model_name].pooling
|
||||
self._normalization = self.POSTPROCESSING_MAPPING[model_name].normalization
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
return cls.SUPPORTED_MODELS
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
return self._normalize(self._pool(output.model_output, output.attention_mask))
|
||||
|
||||
def _pool(
|
||||
self, embeddings: NumpyArray, attention_mask: Optional[NDArray[np.int64]] = None
|
||||
) -> NumpyArray:
|
||||
if self._pooling == PoolingType.CLS:
|
||||
return embeddings[:, 0]
|
||||
|
||||
if self._pooling == PoolingType.MEAN:
|
||||
if attention_mask is None:
|
||||
raise ValueError("attention_mask must be provided for mean pooling")
|
||||
return mean_pooling(embeddings, attention_mask)
|
||||
|
||||
if self._pooling == PoolingType.DISABLED:
|
||||
return embeddings
|
||||
|
||||
raise ValueError(
|
||||
f"Unsupported pooling type {self._pooling}. "
|
||||
f"Supported types are: {PoolingType.CLS}, {PoolingType.MEAN}, {PoolingType.DISABLED}."
|
||||
)
|
||||
|
||||
def _normalize(self, embeddings: NumpyArray) -> NumpyArray:
|
||||
return normalize(embeddings) if self._normalization else embeddings
|
||||
|
||||
@classmethod
|
||||
def add_model(
|
||||
cls,
|
||||
model_description: DenseModelDescription,
|
||||
pooling: PoolingType,
|
||||
normalization: bool,
|
||||
) -> None:
|
||||
cls.SUPPORTED_MODELS.append(model_description)
|
||||
cls.POSTPROCESSING_MAPPING[model_description.model] = PostprocessingConfig(
|
||||
pooling=pooling, normalization=normalization
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
from typing import Type, List, Dict, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.onnx_model import EmbeddingWorker
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
|
||||
|
||||
supported_multilingual_e5_models = [
|
||||
{
|
||||
"model": "intfloat/multilingual-e5-large",
|
||||
"dim": 1024,
|
||||
"description": "Multilingual model, e5-large. Recommend using this model for non-English languages",
|
||||
"size_in_GB": 2.24,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
|
||||
"hf": "qdrant/multilingual-e5-large-onnx",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
"additional_files": ["model.onnx_data"],
|
||||
},
|
||||
{
|
||||
"model": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
||||
"dim": 768,
|
||||
"description": "Sentence-transformers model for tasks like clustering or semantic search",
|
||||
"size_in_GB": 1.00,
|
||||
"sources": {
|
||||
"hf": "xenova/paraphrase-multilingual-mpnet-base-v2",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class E5OnnxEmbedding(OnnxTextEmbedding):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["EmbeddingWorker"]:
|
||||
return E5OnnxEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
"""
|
||||
return supported_multilingual_e5_models
|
||||
|
||||
def _preprocess_onnx_input(self, onnx_input: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
onnx_input.pop("token_type_ids", None)
|
||||
return onnx_input
|
||||
|
||||
|
||||
class E5OnnxEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
) -> E5OnnxEmbedding:
|
||||
return E5OnnxEmbedding(model_name=model_name, cache_dir=cache_dir, threads=1)
|
||||
@@ -1,67 +0,0 @@
|
||||
from typing import Type, List, Dict, Any, Tuple, Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.models import normalize
|
||||
from fastembed.common.onnx_model import EmbeddingWorker
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
|
||||
|
||||
supported_jina_models = [
|
||||
{
|
||||
"model": "jinaai/jina-embeddings-v2-base-en",
|
||||
"dim": 768,
|
||||
"description": "English embedding model supporting 8192 sequence length",
|
||||
"size_in_GB": 0.52,
|
||||
"sources": {"hf": "xenova/jina-embeddings-v2-base-en"},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "jinaai/jina-embeddings-v2-small-en",
|
||||
"dim": 512,
|
||||
"description": "English embedding model supporting 8192 sequence length",
|
||||
"size_in_GB": 0.12,
|
||||
"sources": {"hf": "xenova/jina-embeddings-v2-small-en"},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class JinaOnnxEmbedding(OnnxTextEmbedding):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[EmbeddingWorker]:
|
||||
return JinaEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def mean_pooling(cls, model_output, attention_mask) -> np.ndarray:
|
||||
token_embeddings = model_output
|
||||
input_mask_expanded = (np.expand_dims(attention_mask, axis=-1)).astype(float)
|
||||
|
||||
sum_embeddings = np.sum(token_embeddings * input_mask_expanded, axis=1)
|
||||
mask_sum = np.clip(np.sum(input_mask_expanded, axis=1), a_min=1e-9, a_max=None)
|
||||
|
||||
return sum_embeddings / mask_sum
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
"""
|
||||
return supported_jina_models
|
||||
|
||||
@classmethod
|
||||
def _post_process_onnx_output(
|
||||
cls, output: Tuple[np.ndarray, np.ndarray]
|
||||
) -> Iterable[np.ndarray]:
|
||||
embeddings, attn_mask = output
|
||||
return normalize(cls.mean_pooling(embeddings, attn_mask)).astype(np.float32)
|
||||
|
||||
|
||||
class JinaEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
) -> OnnxTextEmbedding:
|
||||
return JinaOnnxEmbedding(model_name=model_name, cache_dir=cache_dir, threads=1)
|
||||
@@ -0,0 +1,111 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Type, Iterable, Union, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.text.pooled_normalized_embedding import PooledNormalizedEmbedding
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbeddingWorker
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_multitask_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v3",
|
||||
dim=1024,
|
||||
tasks={
|
||||
"retrieval.query": 0,
|
||||
"retrieval.passage": 1,
|
||||
"separation": 2,
|
||||
"classification": 3,
|
||||
"text-matching": 4,
|
||||
},
|
||||
description=(
|
||||
"Multi-task unimodal (text) embedding model, multi-lingual (~100), "
|
||||
"1024 tokens truncation, and 8192 sequence length. Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="cc-by-nc-4.0",
|
||||
size_in_GB=2.29,
|
||||
sources=ModelSource(hf="jinaai/jina-embeddings-v3"),
|
||||
model_file="onnx/model.onnx",
|
||||
additional_files=["onnx/model.onnx_data"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Task(int, Enum):
|
||||
RETRIEVAL_QUERY = 0
|
||||
RETRIEVAL_PASSAGE = 1
|
||||
SEPARATION = 2
|
||||
CLASSIFICATION = 3
|
||||
TEXT_MATCHING = 4
|
||||
|
||||
|
||||
class JinaEmbeddingV3(PooledNormalizedEmbedding):
|
||||
PASSAGE_TASK = Task.RETRIEVAL_PASSAGE
|
||||
QUERY_TASK = Task.RETRIEVAL_QUERY
|
||||
|
||||
def __init__(self, *args: Any, task_id: Optional[int] = None, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.default_task_id: Union[Task, int] = (
|
||||
task_id if task_id is not None else self.PASSAGE_TASK
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[OnnxTextEmbeddingWorker]:
|
||||
return JinaEmbeddingV3Worker
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
return supported_multitask_models
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self,
|
||||
onnx_input: dict[str, NumpyArray],
|
||||
task_id: Optional[Union[int, Task]] = None,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, NumpyArray]:
|
||||
if task_id is None:
|
||||
raise ValueError(f"task_id must be provided for JinaEmbeddingV3, got <{task_id}>")
|
||||
onnx_input["task_id"] = np.array(task_id, dtype=np.int64)
|
||||
return onnx_input
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
task_id: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
task_id = (
|
||||
task_id if task_id is not None else self.default_task_id
|
||||
) # required for multiprocessing
|
||||
yield from super().embed(documents, batch_size, parallel, task_id=task_id, **kwargs)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
yield from super().embed(query, task_id=self.QUERY_TASK, **kwargs)
|
||||
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
yield from super().embed(texts, task_id=self.PASSAGE_TASK, **kwargs)
|
||||
|
||||
|
||||
class JinaEmbeddingV3Worker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> JinaEmbeddingV3:
|
||||
return JinaEmbeddingV3(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, OnnxOutputContext]]:
|
||||
self.model: JinaEmbeddingV3 # mypy complaints `self.model` does not have `default_task_id`
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed(batch, task_id=self.model.default_task_id)
|
||||
yield idx, onnx_output
|
||||
+263
-212
@@ -1,208 +1,198 @@
|
||||
from typing import Dict, Optional, Tuple, Union, Iterable, Type, List, Any
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.common.onnx_model import OnnxModel, EmbeddingWorker
|
||||
from fastembed.common.models import normalize
|
||||
from fastembed.common.utils import define_cache_dir
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import define_cache_dir, normalize
|
||||
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
|
||||
from fastembed.text.text_embedding_base import TextEmbeddingBase
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_onnx_models = [
|
||||
{
|
||||
"model": "BAAI/bge-base-en",
|
||||
"dim": 768,
|
||||
"description": "Base English model",
|
||||
"size_in_GB": 0.42,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en.tar.gz",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "BAAI/bge-base-en-v1.5",
|
||||
"dim": 768,
|
||||
"description": "Base English model, v1.5",
|
||||
"size_in_GB": 0.21,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
|
||||
"hf": "qdrant/bge-base-en-v1.5-onnx-q",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "BAAI/bge-large-en-v1.5",
|
||||
"dim": 1024,
|
||||
"description": "Large English model, v1.5",
|
||||
"size_in_GB": 1.20,
|
||||
"sources": {
|
||||
"hf": "qdrant/bge-large-en-v1.5-onnx",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "BAAI/bge-small-en",
|
||||
"dim": 384,
|
||||
"description": "Fast English model",
|
||||
"size_in_GB": 0.13,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/BAAI-bge-small-en.tar.gz",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "BAAI/bge-small-en-v1.5",
|
||||
"dim": 384,
|
||||
"description": "Fast and Default English model",
|
||||
"size_in_GB": 0.067,
|
||||
"sources": {
|
||||
"hf": "qdrant/bge-small-en-v1.5-onnx-q",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "BAAI/bge-small-zh-v1.5",
|
||||
"dim": 512,
|
||||
"description": "Fast and recommended Chinese model",
|
||||
"size_in_GB": 0.09,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-small-zh-v1.5.tar.gz",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "sentence-transformers/all-MiniLM-L6-v2",
|
||||
"dim": 384,
|
||||
"description": "Sentence Transformer model, MiniLM-L6-v2",
|
||||
"size_in_GB": 0.09,
|
||||
"sources": {
|
||||
"url": "https://storage.googleapis.com/qdrant-fastembed/sentence-transformers-all-MiniLM-L6-v2.tar.gz",
|
||||
"hf": "qdrant/all-MiniLM-L6-v2-onnx",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"dim": 384,
|
||||
"description": "Sentence Transformer model, paraphrase-multilingual-MiniLM-L12-v2",
|
||||
"size_in_GB": 0.22,
|
||||
"sources": {
|
||||
"hf": "qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q",
|
||||
},
|
||||
"model_file": "model_optimized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "nomic-ai/nomic-embed-text-v1",
|
||||
"dim": 768,
|
||||
"description": "8192 context length english model",
|
||||
"size_in_GB": 0.52,
|
||||
"sources": {
|
||||
"hf": "nomic-ai/nomic-embed-text-v1",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "nomic-ai/nomic-embed-text-v1.5",
|
||||
"dim": 768,
|
||||
"description": "8192 context length english model",
|
||||
"size_in_GB": 0.52,
|
||||
"sources": {
|
||||
"hf": "nomic-ai/nomic-embed-text-v1.5",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "nomic-ai/nomic-embed-text-v1.5-Q",
|
||||
"dim": 768,
|
||||
"description": "Quantized 8192 context length english model",
|
||||
"size_in_GB": 0.13,
|
||||
"sources": {
|
||||
"hf": "nomic-ai/nomic-embed-text-v1.5",
|
||||
},
|
||||
"model_file": "onnx/model_quantized.onnx",
|
||||
},
|
||||
{
|
||||
"model": "thenlper/gte-large",
|
||||
"dim": 1024,
|
||||
"description": "Large general text embeddings model",
|
||||
"size_in_GB": 1.20,
|
||||
"sources": {
|
||||
"hf": "qdrant/gte-large-onnx",
|
||||
},
|
||||
"model_file": "model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "mixedbread-ai/mxbai-embed-large-v1",
|
||||
"dim": 1024,
|
||||
"description": "MixedBread Base sentence embedding model, does well on MTEB",
|
||||
"size_in_GB": 0.64,
|
||||
"sources": {
|
||||
"hf": "mixedbread-ai/mxbai-embed-large-v1",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "snowflake/snowflake-arctic-embed-xs",
|
||||
"dim": 384,
|
||||
"description": "Based on all-MiniLM-L6-v2 model with only 22m parameters, ideal for latency/TCO budgets.",
|
||||
"size_in_GB": 0.09,
|
||||
"sources": {
|
||||
"hf": "snowflake/snowflake-arctic-embed-xs",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "snowflake/snowflake-arctic-embed-s",
|
||||
"dim": 384,
|
||||
"description": "Based on infloat/e5-small-unsupervised, does not trade off retrieval accuracy for its small size.",
|
||||
"size_in_GB": 0.13,
|
||||
"sources": {
|
||||
"hf": "snowflake/snowflake-arctic-embed-s",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "snowflake/snowflake-arctic-embed-m",
|
||||
"dim": 768,
|
||||
"description": "Based on intfloat/e5-base-unsupervised model, provides the best retrieval without slowing down inference.",
|
||||
"size_in_GB": 0.43,
|
||||
"sources": {
|
||||
"hf": "Snowflake/snowflake-arctic-embed-m",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "snowflake/snowflake-arctic-embed-m-long",
|
||||
"dim": 768,
|
||||
"description": "Based on nomic-ai/nomic-embed-text-v1-unsupervised model, 8192 context-length model",
|
||||
"size_in_GB": 0.54,
|
||||
"sources": {
|
||||
"hf": "snowflake/snowflake-arctic-embed-m-long",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
{
|
||||
"model": "snowflake/snowflake-arctic-embed-l",
|
||||
"dim": 1024,
|
||||
"description": "Based on intfloat/e5-large-unsupervised, large model for most accurate retrieval.",
|
||||
"size_in_GB": 1.02,
|
||||
"sources": {
|
||||
"hf": "snowflake/snowflake-arctic-embed-l",
|
||||
},
|
||||
"model_file": "onnx/model.onnx",
|
||||
},
|
||||
supported_onnx_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-base-en",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.42,
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/fast-bge-base-en",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-base-en-v1.5",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not so necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.21,
|
||||
sources=ModelSource(
|
||||
hf="qdrant/bge-base-en-v1.5-onnx-q",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-large-en-v1.5",
|
||||
dim=1024,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not so necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=1.20,
|
||||
sources=ModelSource(hf="qdrant/bge-large-en-v1.5-onnx"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-small-en",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.13,
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/bge-small-en",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/BAAI-bge-small-en.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-small-en-v1.5",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not so necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.067,
|
||||
sources=ModelSource(hf="qdrant/bge-small-en-v1.5-onnx-q"),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="BAAI/bge-small-zh-v1.5",
|
||||
dim=512,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Chinese, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not so necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.09,
|
||||
sources=ModelSource(
|
||||
hf="Qdrant/bge-small-zh-v1.5",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-bge-small-zh-v1.5.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="mixedbread-ai/mxbai-embed-large-v1",
|
||||
dim=1024,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.64,
|
||||
sources=ModelSource(hf="mixedbread-ai/mxbai-embed-large-v1"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="snowflake/snowflake-arctic-embed-xs",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.09,
|
||||
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-xs"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="snowflake/snowflake-arctic-embed-s",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.13,
|
||||
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-s"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="snowflake/snowflake-arctic-embed-m",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.43,
|
||||
sources=ModelSource(hf="Snowflake/snowflake-arctic-embed-m"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="snowflake/snowflake-arctic-embed-m-long",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 2048 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.54,
|
||||
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-m-long"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="snowflake/snowflake-arctic-embed-l",
|
||||
dim=1024,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=1.02,
|
||||
sources=ModelSource(hf="snowflake/snowflake-arctic-embed-l"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-clip-v1",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Multimodal (text&image), English, Prefixes for queries/documents: "
|
||||
"not necessary, 2024 year"
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.55,
|
||||
sources=ModelSource(hf="jinaai/jina-clip-v1"),
|
||||
model_file="onnx/text_model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class OnnxTextEmbedding(TextEmbeddingBase, OnnxModel[np.ndarray]):
|
||||
class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[NumpyArray]):
|
||||
"""Implementation of the Flag Embedding model."""
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_onnx_models
|
||||
|
||||
@@ -211,7 +201,13 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxModel[np.ndarray]):
|
||||
model_name: str = "BAAI/bge-small-en-v1.5",
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -220,30 +216,55 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxModel[np.ndarray]):
|
||||
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
|
||||
Defaults to `fastembed_cache` in the system's temp directory.
|
||||
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
|
||||
providers (Optional[Sequence[OnnxProvider]], optional): The list of onnxruntime providers to use.
|
||||
Mutually exclusive with the `cuda` and `device_ids` arguments. Defaults to None.
|
||||
cuda (bool, optional): Whether to use cuda for inference. Mutually exclusive with `providers`
|
||||
Defaults to False.
|
||||
device_ids (Optional[list[int]], optional): The list of device ids to use for data parallel processing in
|
||||
workers. Should be used with `cuda=True`, mutually exclusive with `providers`. Defaults to None.
|
||||
lazy_load (bool, optional): Whether to load the model during class initialization or on demand.
|
||||
Should be set to True when using multiple-gpu and parallel encoding. Defaults to False.
|
||||
device_id (Optional[int], optional): The device id to use for loading the model in the worker process.
|
||||
specific_model_path (Optional[str], optional): The specific path to the onnx model dir if it should be imported from somewhere else
|
||||
|
||||
Raises:
|
||||
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
|
||||
"""
|
||||
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
self.providers = providers
|
||||
self.lazy_load = lazy_load
|
||||
|
||||
model_description = self._get_model_description(model_name)
|
||||
cache_dir = define_cache_dir(cache_dir)
|
||||
model_dir = self.download_model(model_description, cache_dir)
|
||||
# List of device ids, that can be used for data parallel processing in workers
|
||||
self.device_ids = device_ids
|
||||
self.cuda = cuda
|
||||
|
||||
self.load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_description["model_file"],
|
||||
threads=threads,
|
||||
# This device_id will be used if we need to load model in current process
|
||||
self.device_id: Optional[int] = None
|
||||
if device_id is not None:
|
||||
self.device_id = device_id
|
||||
elif self.device_ids is not None:
|
||||
self.device_id = self.device_ids[0]
|
||||
|
||||
self.model_description = self._get_model_description(model_name)
|
||||
self.cache_dir = str(define_cache_dir(cache_dir))
|
||||
self._specific_model_path = specific_model_path
|
||||
self._model_dir = self.download_model(
|
||||
self.model_description,
|
||||
self.cache_dir,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
)
|
||||
|
||||
if not self.lazy_load:
|
||||
self.load_onnx_model()
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Iterable[np.ndarray]:
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
@@ -265,30 +286,60 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxModel[np.ndarray]):
|
||||
documents=documents,
|
||||
batch_size=batch_size,
|
||||
parallel=parallel,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_ids=self.device_ids,
|
||||
local_files_only=self._local_files_only,
|
||||
specific_model_path=self._specific_model_path,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["EmbeddingWorker"]:
|
||||
def _get_worker_class(cls) -> Type["TextEmbeddingWorker[NumpyArray]"]:
|
||||
return OnnxTextEmbeddingWorker
|
||||
|
||||
def _preprocess_onnx_input(self, onnx_input: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, NumpyArray]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
@classmethod
|
||||
def _post_process_onnx_output(
|
||||
cls, output: Tuple[np.ndarray, np.ndarray]
|
||||
) -> Iterable[np.ndarray]:
|
||||
embeddings, _ = output
|
||||
return normalize(embeddings[:, 0]).astype(np.float32)
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
embeddings = output.model_output
|
||||
|
||||
if embeddings.ndim == 3: # (batch_size, seq_len, embedding_dim)
|
||||
processed_embeddings = embeddings[:, 0]
|
||||
elif embeddings.ndim == 2: # (batch_size, embedding_dim)
|
||||
processed_embeddings = embeddings
|
||||
else:
|
||||
raise ValueError(f"Unsupported embedding shape: {embeddings.shape}")
|
||||
return normalize(processed_embeddings)
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
self._load_onnx_model(
|
||||
model_dir=self._model_dir,
|
||||
model_file=self.model_description.model_file,
|
||||
threads=self.threads,
|
||||
providers=self.providers,
|
||||
cuda=self.cuda,
|
||||
device_id=self.device_id,
|
||||
)
|
||||
|
||||
|
||||
class OnnxTextEmbeddingWorker(EmbeddingWorker):
|
||||
class OnnxTextEmbeddingWorker(TextEmbeddingWorker[NumpyArray]):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxTextEmbedding:
|
||||
return OnnxTextEmbedding(model_name=model_name, cache_dir=cache_dir, threads=1)
|
||||
return OnnxTextEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import os
|
||||
from multiprocessing import get_all_start_methods
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
from tokenizers import Encoding, Tokenizer
|
||||
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
|
||||
from fastembed.common.preprocessor_utils import load_tokenizer
|
||||
from fastembed.common.utils import iter_batch
|
||||
from fastembed.parallel_processor import ParallelWorkerPool
|
||||
|
||||
|
||||
class OnnxTextModel(OnnxModel[T]):
|
||||
ONNX_OUTPUT_NAMES: Optional[list[str]] = None
|
||||
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type["TextEmbeddingWorker[T]"]:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def _post_process_onnx_output(self, output: OnnxOutputContext, **kwargs: Any) -> Iterable[T]:
|
||||
"""Post-process the ONNX model output to convert it into a usable format.
|
||||
|
||||
Args:
|
||||
output (OnnxOutputContext): The raw output from the ONNX model.
|
||||
**kwargs: Additional keyword arguments that may be needed by specific implementations.
|
||||
|
||||
Returns:
|
||||
Iterable[T]: Post-processed output as an iterable of type T.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.tokenizer: Optional[Tokenizer] = None
|
||||
self.special_token_to_id: dict[str, int] = {}
|
||||
|
||||
def _preprocess_onnx_input(
|
||||
self, onnx_input: dict[str, NumpyArray], **kwargs: Any
|
||||
) -> dict[str, Union[NumpyArray, NDArray[np.int64]]]:
|
||||
"""
|
||||
Preprocess the onnx input.
|
||||
"""
|
||||
return onnx_input
|
||||
|
||||
def _load_onnx_model(
|
||||
self,
|
||||
model_dir: Path,
|
||||
model_file: str,
|
||||
threads: Optional[int],
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_id: Optional[int] = None,
|
||||
) -> None:
|
||||
super()._load_onnx_model(
|
||||
model_dir=model_dir,
|
||||
model_file=model_file,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_id=device_id,
|
||||
)
|
||||
self.tokenizer, self.special_token_to_id = load_tokenizer(model_dir=model_dir)
|
||||
|
||||
def load_onnx_model(self) -> None:
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def tokenize(self, documents: list[str], **kwargs: Any) -> list[Encoding]:
|
||||
return self.tokenizer.encode_batch(documents) # type: ignore[union-attr]
|
||||
|
||||
def onnx_embed(
|
||||
self,
|
||||
documents: list[str],
|
||||
**kwargs: Any,
|
||||
) -> OnnxOutputContext:
|
||||
encoded = self.tokenize(documents, **kwargs)
|
||||
input_ids = np.array([e.ids for e in encoded])
|
||||
attention_mask = np.array([e.attention_mask for e in encoded])
|
||||
input_names = {node.name for node in self.model.get_inputs()} # type: ignore[union-attr]
|
||||
onnx_input: dict[str, NumpyArray] = {
|
||||
"input_ids": np.array(input_ids, dtype=np.int64),
|
||||
}
|
||||
if "attention_mask" in input_names:
|
||||
onnx_input["attention_mask"] = np.array(attention_mask, dtype=np.int64)
|
||||
if "token_type_ids" in input_names:
|
||||
onnx_input["token_type_ids"] = np.array(
|
||||
[np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
|
||||
)
|
||||
onnx_input = self._preprocess_onnx_input(onnx_input, **kwargs)
|
||||
|
||||
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input) # type: ignore[union-attr]
|
||||
return OnnxOutputContext(
|
||||
model_output=model_output[0],
|
||||
attention_mask=onnx_input.get("attention_mask", attention_mask),
|
||||
input_ids=onnx_input.get("input_ids", input_ids),
|
||||
)
|
||||
|
||||
def _embed_documents(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
local_files_only: bool = False,
|
||||
specific_model_path: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterable[T]:
|
||||
is_small = False
|
||||
|
||||
if isinstance(documents, str):
|
||||
documents = [documents]
|
||||
is_small = True
|
||||
|
||||
if isinstance(documents, list):
|
||||
if len(documents) < batch_size:
|
||||
is_small = True
|
||||
|
||||
if parallel is None or is_small:
|
||||
if not hasattr(self, "model") or self.model is None:
|
||||
self.load_onnx_model()
|
||||
for batch in iter_batch(documents, batch_size):
|
||||
yield from self._post_process_onnx_output(
|
||||
self.onnx_embed(batch, **kwargs), **kwargs
|
||||
)
|
||||
else:
|
||||
if parallel == 0:
|
||||
parallel = os.cpu_count()
|
||||
|
||||
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
|
||||
params = {
|
||||
"model_name": model_name,
|
||||
"cache_dir": cache_dir,
|
||||
"providers": providers,
|
||||
"local_files_only": local_files_only,
|
||||
"specific_model_path": specific_model_path,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
pool = ParallelWorkerPool(
|
||||
num_workers=parallel or 1,
|
||||
worker=self._get_worker_class(),
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
start_method=start_method,
|
||||
)
|
||||
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
|
||||
yield from self._post_process_onnx_output(batch, **kwargs) # type: ignore
|
||||
|
||||
|
||||
class TextEmbeddingWorker(EmbeddingWorker[T]):
|
||||
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, OnnxOutputContext]]:
|
||||
for idx, batch in items:
|
||||
onnx_output = self.model.onnx_embed(batch)
|
||||
yield idx, onnx_output
|
||||
@@ -0,0 +1,136 @@
|
||||
from typing import Any, Iterable, Type
|
||||
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import mean_pooling
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_pooled_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="nomic-ai/nomic-embed-text-v1.5",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Multimodal (text, image), English, 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.52,
|
||||
sources=ModelSource(hf="nomic-ai/nomic-embed-text-v1.5"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="nomic-ai/nomic-embed-text-v1.5-Q",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Multimodal (text, image), English, 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.13,
|
||||
sources=ModelSource(hf="nomic-ai/nomic-embed-text-v1.5"),
|
||||
model_file="onnx/model_quantized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="nomic-ai/nomic-embed-text-v1",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Multimodal (text, image), English, 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.52,
|
||||
sources=ModelSource(hf="nomic-ai/nomic-embed-text-v1"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Multilingual (~50 languages), 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2019 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.22,
|
||||
sources=ModelSource(hf="qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q"),
|
||||
model_file="model_optimized.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Multilingual (~50 languages), 384 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2021 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=1.00,
|
||||
sources=ModelSource(hf="xenova/paraphrase-multilingual-mpnet-base-v2"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="intfloat/multilingual-e5-large",
|
||||
dim=1024,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Multilingual (~100 languages), 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: necessary, 2024 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=2.24,
|
||||
sources=ModelSource(
|
||||
hf="qdrant/multilingual-e5-large-onnx",
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model.onnx",
|
||||
additional_files=["model.onnx_data"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PooledEmbedding(OnnxTextEmbedding):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[OnnxTextEmbeddingWorker]:
|
||||
return PooledEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def mean_pooling(
|
||||
cls, model_output: NumpyArray, attention_mask: NDArray[np.int64]
|
||||
) -> NumpyArray:
|
||||
return mean_pooling(model_output, attention_mask)
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_pooled_models
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
if output.attention_mask is None:
|
||||
raise ValueError("attention_mask must be provided for document post-processing")
|
||||
|
||||
embeddings = output.model_output
|
||||
attn_mask = output.attention_mask
|
||||
return self.mean_pooling(embeddings, attn_mask)
|
||||
|
||||
|
||||
class PooledEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxTextEmbedding:
|
||||
return PooledEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
from typing import Any, Iterable, Type
|
||||
|
||||
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.onnx_model import OnnxOutputContext
|
||||
from fastembed.common.utils import normalize
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
|
||||
from fastembed.text.pooled_embedding import PooledEmbedding
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource
|
||||
|
||||
supported_pooled_normalized_models: list[DenseModelDescription] = [
|
||||
DenseModelDescription(
|
||||
model="sentence-transformers/all-MiniLM-L6-v2",
|
||||
dim=384,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 256 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2021 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.09,
|
||||
sources=ModelSource(
|
||||
url="https://storage.googleapis.com/qdrant-fastembed/sentence-transformers-all-MiniLM-L6-v2.tar.gz",
|
||||
hf="qdrant/all-MiniLM-L6-v2-onnx",
|
||||
_deprecated_tar_struct=True,
|
||||
),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-base-en",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2023 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.52,
|
||||
sources=ModelSource(hf="xenova/jina-embeddings-v2-base-en"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-small-en",
|
||||
dim=512,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2023 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.12,
|
||||
sources=ModelSource(hf="xenova/jina-embeddings-v2-small-en"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-base-de",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Multilingual (German, English), 8192 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.32,
|
||||
sources=ModelSource(hf="jinaai/jina-embeddings-v2-base-de"),
|
||||
model_file="onnx/model_fp16.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-base-code",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), Multilingual (English, 30 programming languages), "
|
||||
"8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.64,
|
||||
sources=ModelSource(hf="jinaai/jina-embeddings-v2-base-code"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-base-zh",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), supports mixed Chinese-English input text, "
|
||||
"8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.64,
|
||||
sources=ModelSource(hf="jinaai/jina-embeddings-v2-base-zh"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="jinaai/jina-embeddings-v2-base-es",
|
||||
dim=768,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), supports mixed Spanish-English input text, "
|
||||
"8192 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="apache-2.0",
|
||||
size_in_GB=0.64,
|
||||
sources=ModelSource(hf="jinaai/jina-embeddings-v2-base-es"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="thenlper/gte-base",
|
||||
dim=768,
|
||||
description=(
|
||||
"General text embeddings, Unimodal (text), supports English only input text, "
|
||||
"512 input tokens truncation, Prefixes for queries/documents: not necessary, 2024 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=0.44,
|
||||
sources=ModelSource(hf="thenlper/gte-base"),
|
||||
model_file="onnx/model.onnx",
|
||||
),
|
||||
DenseModelDescription(
|
||||
model="thenlper/gte-large",
|
||||
dim=1024,
|
||||
description=(
|
||||
"Text embeddings, Unimodal (text), English, 512 input tokens truncation, "
|
||||
"Prefixes for queries/documents: not necessary, 2023 year."
|
||||
),
|
||||
license="mit",
|
||||
size_in_GB=1.20,
|
||||
sources=ModelSource(hf="qdrant/gte-large-onnx"),
|
||||
model_file="model.onnx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class PooledNormalizedEmbedding(PooledEmbedding):
|
||||
@classmethod
|
||||
def _get_worker_class(cls) -> Type[OnnxTextEmbeddingWorker]:
|
||||
return PooledNormalizedEmbeddingWorker
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
list[DenseModelDescription]: A list of DenseModelDescription objects containing the model information.
|
||||
"""
|
||||
return supported_pooled_normalized_models
|
||||
|
||||
def _post_process_onnx_output(
|
||||
self, output: OnnxOutputContext, **kwargs: Any
|
||||
) -> Iterable[NumpyArray]:
|
||||
if output.attention_mask is None:
|
||||
raise ValueError("attention_mask must be provided for document post-processing")
|
||||
|
||||
embeddings = output.model_output
|
||||
attn_mask = output.attention_mask
|
||||
return normalize(self.mean_pooling(embeddings, attn_mask))
|
||||
|
||||
|
||||
class PooledNormalizedEmbeddingWorker(OnnxTextEmbeddingWorker):
|
||||
def init_embedding(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str,
|
||||
**kwargs: Any,
|
||||
) -> OnnxTextEmbedding:
|
||||
return PooledNormalizedEmbedding(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=1,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -1,76 +1,174 @@
|
||||
from typing import Any, Dict, Iterable, List, Optional, Type, Union
|
||||
import warnings
|
||||
from typing import Any, Iterable, Optional, Sequence, Type, Union
|
||||
from dataclasses import asdict
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fastembed.text.e5_onnx_embedding import E5OnnxEmbedding
|
||||
from fastembed.text.jina_onnx_embedding import JinaOnnxEmbedding
|
||||
from fastembed.common.types import NumpyArray, OnnxProvider
|
||||
from fastembed.text.clip_embedding import CLIPOnnxEmbedding
|
||||
from fastembed.text.custom_text_embedding import CustomTextEmbedding
|
||||
from fastembed.text.pooled_normalized_embedding import PooledNormalizedEmbedding
|
||||
from fastembed.text.pooled_embedding import PooledEmbedding
|
||||
from fastembed.text.multitask_embedding import JinaEmbeddingV3
|
||||
from fastembed.text.onnx_embedding import OnnxTextEmbedding
|
||||
from fastembed.text.text_embedding_base import TextEmbeddingBase
|
||||
from fastembed.common.model_description import DenseModelDescription, ModelSource, PoolingType
|
||||
|
||||
|
||||
class TextEmbedding(TextEmbeddingBase):
|
||||
EMBEDDINGS_REGISTRY: List[Type[TextEmbeddingBase]] = [
|
||||
EMBEDDINGS_REGISTRY: list[Type[TextEmbeddingBase]] = [
|
||||
OnnxTextEmbedding,
|
||||
E5OnnxEmbedding,
|
||||
JinaOnnxEmbedding,
|
||||
CLIPOnnxEmbedding,
|
||||
PooledNormalizedEmbedding,
|
||||
PooledEmbedding,
|
||||
JinaEmbeddingV3,
|
||||
CustomTextEmbedding,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def list_supported_models(cls) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Lists the supported models.
|
||||
def list_supported_models(cls) -> list[dict[str, Any]]:
|
||||
"""Lists the supported models.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
|
||||
Example:
|
||||
```
|
||||
[
|
||||
{
|
||||
"model": "intfloat/multilingual-e5-large",
|
||||
"dim": 1024,
|
||||
"description": "Multilingual model, e5-large. Recommend using this model for non-English languages",
|
||||
"size_in_GB": 2.24,
|
||||
"sources": {
|
||||
"gcp": "https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
|
||||
"hf": "qdrant/multilingual-e5-large-onnx",
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
list[dict[str, Any]]: A list of dictionaries containing the model information.
|
||||
"""
|
||||
result = []
|
||||
return [asdict(model) for model in cls._list_supported_models()]
|
||||
|
||||
@classmethod
|
||||
def _list_supported_models(cls) -> list[DenseModelDescription]:
|
||||
result: list[DenseModelDescription] = []
|
||||
for embedding in cls.EMBEDDINGS_REGISTRY:
|
||||
result.extend(embedding.list_supported_models())
|
||||
result.extend(embedding._list_supported_models())
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def add_custom_model(
|
||||
cls,
|
||||
model: str,
|
||||
pooling: PoolingType,
|
||||
normalization: bool,
|
||||
sources: ModelSource,
|
||||
dim: int,
|
||||
model_file: str = "onnx/model.onnx",
|
||||
description: str = "",
|
||||
license: str = "",
|
||||
size_in_gb: float = 0.0,
|
||||
additional_files: Optional[list[str]] = None,
|
||||
) -> None:
|
||||
registered_models = cls._list_supported_models()
|
||||
for registered_model in registered_models:
|
||||
if model.lower() == registered_model.model.lower():
|
||||
raise ValueError(
|
||||
f"Model {model} is already registered in TextEmbedding, if you still want to add this model, "
|
||||
f"please use another model name"
|
||||
)
|
||||
|
||||
CustomTextEmbedding.add_model(
|
||||
DenseModelDescription(
|
||||
model=model,
|
||||
sources=sources,
|
||||
dim=dim,
|
||||
model_file=model_file,
|
||||
description=description,
|
||||
license=license,
|
||||
size_in_GB=size_in_gb,
|
||||
additional_files=additional_files or [],
|
||||
),
|
||||
pooling=pooling,
|
||||
normalization=normalization,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str = "BAAI/bge-small-en-v1.5",
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
providers: Optional[Sequence[OnnxProvider]] = None,
|
||||
cuda: bool = False,
|
||||
device_ids: Optional[list[int]] = None,
|
||||
lazy_load: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(model_name, cache_dir, threads, **kwargs)
|
||||
|
||||
if model_name.lower() == "nomic-ai/nomic-embed-text-v1.5-Q".lower():
|
||||
warnings.warn(
|
||||
"The model 'nomic-ai/nomic-embed-text-v1.5-Q' has been updated on HuggingFace. Please review "
|
||||
"the latest documentation on HF and release notes to ensure compatibility with your workflow. ",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if model_name.lower() in {
|
||||
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2".lower(),
|
||||
"thenlper/gte-large".lower(),
|
||||
"intfloat/multilingual-e5-large".lower(),
|
||||
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2".lower(),
|
||||
}:
|
||||
warnings.warn(
|
||||
f"The model {model_name} now uses mean pooling instead of CLS embedding. "
|
||||
f"In order to preserve the previous behaviour, consider either pinning fastembed version to 0.5.1 or "
|
||||
"using `add_custom_model` functionality.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
|
||||
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
|
||||
if any(model_name.lower() == model["model"].lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(model_name, cache_dir, threads, **kwargs)
|
||||
supported_models = EMBEDDING_MODEL_TYPE._list_supported_models()
|
||||
if any(model_name.lower() == model.model.lower() for model in supported_models):
|
||||
self.model = EMBEDDING_MODEL_TYPE(
|
||||
model_name=model_name,
|
||||
cache_dir=cache_dir,
|
||||
threads=threads,
|
||||
providers=providers,
|
||||
cuda=cuda,
|
||||
device_ids=device_ids,
|
||||
lazy_load=lazy_load,
|
||||
**kwargs,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Model {model_name} is not supported in TextEmbedding."
|
||||
f"Model {model_name} is not supported in TextEmbedding. "
|
||||
"Please check the supported models using `TextEmbedding.list_supported_models()`"
|
||||
)
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Get the embedding size of the current model"""
|
||||
if self._embedding_size is None:
|
||||
self._embedding_size = self.get_embedding_size(self.model_name)
|
||||
return self._embedding_size
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Get the embedding size of the passed model
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model to get embedding size for.
|
||||
|
||||
Returns:
|
||||
int: The size of the embedding.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model name is not found in the supported models.
|
||||
"""
|
||||
descriptions = cls._list_supported_models()
|
||||
embedding_size: Optional[int] = None
|
||||
for description in descriptions:
|
||||
if description.model.lower() == model_name.lower():
|
||||
embedding_size = description.dim
|
||||
break
|
||||
if embedding_size is None:
|
||||
model_names = [description.model for description in descriptions]
|
||||
raise ValueError(
|
||||
f"Embedding size for model {model_name} was None. "
|
||||
f"Available model names: {model_names}"
|
||||
)
|
||||
return embedding_size
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Iterable[np.ndarray]:
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Encode a list of documents into list of embeddings.
|
||||
We use mean pooling with attention so that the model can handle variable-length inputs.
|
||||
@@ -87,3 +185,30 @@ class TextEmbedding(TextEmbeddingBase):
|
||||
List of embeddings, one per document
|
||||
"""
|
||||
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
Args:
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[NumpyArray]: The embeddings.
|
||||
"""
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.model.query_embed(query, **kwargs)
|
||||
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds a list of text passages into a list of embeddings.
|
||||
|
||||
Args:
|
||||
texts (Iterable[str]): The list of texts to embed.
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[SparseEmbedding]: The sparse embeddings.
|
||||
"""
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.model.passage_embed(texts, **kwargs)
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
from typing import Iterable, Optional, Union, Any
|
||||
|
||||
from fastembed.common.model_description import DenseModelDescription
|
||||
from fastembed.common.types import NumpyArray
|
||||
from fastembed.common.model_management import ModelManagement
|
||||
|
||||
|
||||
class TextEmbeddingBase(ModelManagement):
|
||||
class TextEmbeddingBase(ModelManagement[DenseModelDescription]):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: Optional[str] = None,
|
||||
threads: Optional[int] = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.model_name = model_name
|
||||
self.cache_dir = cache_dir
|
||||
self.threads = threads
|
||||
self._local_files_only = kwargs.pop("local_files_only", False)
|
||||
self._embedding_size: Optional[int] = None
|
||||
|
||||
def embed(
|
||||
self,
|
||||
documents: Union[str, Iterable[str]],
|
||||
batch_size: int = 256,
|
||||
parallel: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Iterable[np.ndarray]:
|
||||
**kwargs: Any,
|
||||
) -> Iterable[NumpyArray]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs) -> Iterable[np.ndarray]:
|
||||
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds a list of text passages into a list of embeddings.
|
||||
|
||||
@@ -35,13 +37,13 @@ class TextEmbeddingBase(ModelManagement):
|
||||
**kwargs: Additional keyword argument to pass to the embed method.
|
||||
|
||||
Yields:
|
||||
Iterable[np.ndarray]: The embeddings.
|
||||
Iterable[NumpyArray]: The embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
yield from self.embed(texts, **kwargs)
|
||||
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[np.ndarray]:
|
||||
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[NumpyArray]:
|
||||
"""
|
||||
Embeds queries
|
||||
|
||||
@@ -49,11 +51,21 @@ class TextEmbeddingBase(ModelManagement):
|
||||
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
|
||||
|
||||
Returns:
|
||||
Iterable[np.ndarray]: The embeddings.
|
||||
Iterable[NumpyArray]: The embeddings.
|
||||
"""
|
||||
|
||||
# This is model-specific, so that different models can have specialized implementations
|
||||
if isinstance(query, str):
|
||||
yield from self.embed([query], **kwargs)
|
||||
if isinstance(query, Iterable):
|
||||
else:
|
||||
yield from self.embed(query, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_embedding_size(cls, model_name: str) -> int:
|
||||
"""Returns embedding size of the passed model."""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
@property
|
||||
def embedding_size(self) -> int:
|
||||
"""Returns embedding size for the current model"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
Generated
+1999
-1514
File diff suppressed because it is too large
Load Diff
+32
-15
@@ -1,8 +1,8 @@
|
||||
[tool.poetry]
|
||||
name = "fastembed"
|
||||
version = "0.2.6"
|
||||
version = "0.7.3"
|
||||
description = "Fast, light, accurate library built for retrieval embedding generation"
|
||||
authors = ["NirantK <nirant.bits@gmail.com>"]
|
||||
authors = ["Qdrant Team <info@qdrant.tech>", "NirantK <nirant.bits@gmail.com>"]
|
||||
license = "Apache License"
|
||||
readme = "README.md"
|
||||
packages = [{include = "fastembed"}]
|
||||
@@ -11,36 +11,53 @@ repository = "https://github.com/qdrant/fastembed"
|
||||
keywords = ["vector", "embedding", "neural", "search", "qdrant", "sentence-transformers"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8.0,<3.13"
|
||||
onnx = "^1.15.0"
|
||||
onnxruntime = "^1.17.0"
|
||||
python = ">=3.9.0"
|
||||
numpy = [
|
||||
{ version = ">=1.21", python = ">=3.10,<3.12" },
|
||||
{ version = ">=1.26", python = ">=3.12,<3.13" },
|
||||
{ version = ">=2.1.0", python = ">=3.13" },
|
||||
{ version = ">=1.21,<2.1.0", python = "<3.10" },
|
||||
]
|
||||
onnxruntime = [
|
||||
{ version = ">=1.17.0,<1.20.0", python = "<3.10" },
|
||||
{ version = ">1.20.0", python = ">=3.13" },
|
||||
{ version = ">=1.17.0,!=1.20.0", python = ">=3.10,<3.13" },
|
||||
]
|
||||
tqdm = "^4.66"
|
||||
requests = "^2.31"
|
||||
tokenizers = "^0.15.1"
|
||||
huggingface-hub = "^0.20"
|
||||
tokenizers = ">=0.15,<1.0"
|
||||
huggingface-hub = ">=0.20,<1.0"
|
||||
loguru = "^0.7.2"
|
||||
numpy = [
|
||||
{ version = ">=1.21", python = "<3.12" },
|
||||
{ version = ">=1.26", python = ">=3.12" }
|
||||
]
|
||||
pillow = ">=10.3.0,<12.0.0"
|
||||
mmh3 = ">=4.1.0,<6.0.0"
|
||||
py-rust-stemmers = "^0.1.0"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "^7.4.2"
|
||||
ruff = ">=0.3.1,<1.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.2"
|
||||
ruff = "^0.3.1"
|
||||
notebook = ">=7.0.2"
|
||||
pre-commit = {version = "^3.6.2", python = ">=3.9,<3.12" }
|
||||
pre-commit = "^3.6.2"
|
||||
onnx = ">=1.15.0"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.10"
|
||||
mkdocstrings = "^0.24.0"
|
||||
pillow = "^10.2.0"
|
||||
pillow = ">=10.3.0,<12.0.0"
|
||||
cairosvg = "^2.7.1"
|
||||
mknotebooks = "^0.8.0"
|
||||
|
||||
[tool.poetry.group.types.dependencies]
|
||||
pyright = ">=1.1.293"
|
||||
mypy = "^1.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "strict"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 99
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import os
|
||||
|
||||
# disable DeprecationWarning https://github.com/jupyter/jupyter_core/issues/398
|
||||
os.environ["JUPYTER_PLATFORM_DIRS"] = "1"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from pathlib import Path
|
||||
|
||||
TEST_DIR = Path(__file__).parent
|
||||
TEST_MISC_DIR = TEST_DIR / "misc"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+12
-6
@@ -9,7 +9,7 @@
|
||||
|
||||
# %%
|
||||
import time
|
||||
from typing import Callable, List, Tuple
|
||||
from typing import Callable
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import torch.nn.functional as F
|
||||
@@ -23,7 +23,7 @@ from fastembed.embedding import DefaultEmbedding
|
||||
# data is a list of strings, each string is a document.
|
||||
|
||||
# %%
|
||||
documents: List[str] = [
|
||||
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",
|
||||
@@ -56,8 +56,10 @@ class HF:
|
||||
self.model = AutoModel.from_pretrained(model_id)
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
|
||||
|
||||
def embed(self, texts: List[str]):
|
||||
encoded_input = self.tokenizer(texts, max_length=512, padding=True, truncation=True, return_tensors="pt")
|
||||
def embed(self, texts: list[str]):
|
||||
encoded_input = self.tokenizer(
|
||||
texts, max_length=512, padding=True, truncation=True, return_tensors="pt"
|
||||
)
|
||||
model_output = self.model(**encoded_input)
|
||||
sentence_embeddings = model_output[0][:, 0]
|
||||
sentence_embeddings = F.normalize(sentence_embeddings)
|
||||
@@ -84,7 +86,9 @@ embedding_model = DefaultEmbedding()
|
||||
|
||||
|
||||
# %%
|
||||
def calculate_time_stats(embed_func: Callable, documents: list, k: int) -> Tuple[float, float, float]:
|
||||
def calculate_time_stats(
|
||||
embed_func: Callable, documents: list, k: int
|
||||
) -> tuple[float, float, float]:
|
||||
times = []
|
||||
for _ in range(k):
|
||||
# Timing the embed_func call
|
||||
@@ -107,7 +111,9 @@ print(f"FastEmbed (Average, Max, Min): {fst_stats}")
|
||||
|
||||
# %%
|
||||
def plot_character_per_second_comparison(
|
||||
hf_stats: Tuple[float, float, float], fst_stats: Tuple[float, float, float], documents: list
|
||||
hf_stats: tuple[float, float, float],
|
||||
fst_stats: tuple[float, float, float],
|
||||
documents: list,
|
||||
):
|
||||
# Calculating total characters in documents
|
||||
total_characters = sum(len(doc) for doc in documents)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from fastembed import SparseTextEmbedding
|
||||
from tests.utils import delete_model_cache
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/bm42-all-minilm-l6-v2-attentions", "Qdrant/bm25"])
|
||||
def test_attention_embeddings(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
|
||||
output = list(
|
||||
model.query_embed(
|
||||
[
|
||||
"I must not fear. Fear is the mind-killer.",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
assert len(output) == 1
|
||||
|
||||
for result in output:
|
||||
assert len(result.indices) == len(result.values)
|
||||
assert np.allclose(result.values, np.ones(len(result.values)))
|
||||
|
||||
quotes = [
|
||||
"I must not fear. Fear is the mind-killer.",
|
||||
"All animals are equal, but some animals are more equal than others.",
|
||||
"It was a pleasure to burn.",
|
||||
"The sky above the port was the color of television, tuned to a dead channel.",
|
||||
"In the beginning, the universe was created."
|
||||
" This has made a lot of people very angry and been widely regarded as a bad move.",
|
||||
"It's a truth universally acknowledged that a zombie in possession of brains must be in want of more brains.",
|
||||
"War is peace. Freedom is slavery. Ignorance is strength.",
|
||||
"We're not in Infinity; we're in the suburbs.",
|
||||
"I was a thousand times more evil than thou!",
|
||||
"History is merely a list of surprises... It can only prepare us to be surprised yet again.",
|
||||
".", # Empty string
|
||||
]
|
||||
|
||||
output = list(model.embed(quotes))
|
||||
|
||||
assert len(output) == len(quotes)
|
||||
|
||||
for result in output[:-1]:
|
||||
assert len(result.indices) == len(result.values)
|
||||
assert len(result.indices) > 0
|
||||
|
||||
assert len(output[-1].indices) == 0
|
||||
|
||||
# Test support for unknown languages
|
||||
output = list(
|
||||
model.query_embed(
|
||||
[
|
||||
"привет мир!",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
assert len(output) == 1
|
||||
|
||||
for result in output:
|
||||
assert len(result.indices) == len(result.values)
|
||||
assert len(result.indices) == 2
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/bm42-all-minilm-l6-v2-attentions", "Qdrant/bm25"])
|
||||
def test_parallel_processing(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name)
|
||||
|
||||
docs = ["hello world", "attention embedding", "Mangez-vous vraiment des grenouilles?"] * 100
|
||||
embeddings = list(model.embed(docs, batch_size=10, parallel=2))
|
||||
|
||||
embeddings_2 = list(model.embed(docs, batch_size=10, parallel=None))
|
||||
|
||||
embeddings_3 = list(model.embed(docs, batch_size=10, parallel=0))
|
||||
|
||||
assert len(embeddings) == len(docs)
|
||||
|
||||
for emb_1, emb_2, emb_3 in zip(embeddings, embeddings_2, embeddings_3):
|
||||
assert np.allclose(emb_1.indices, emb_2.indices)
|
||||
assert np.allclose(emb_1.indices, emb_3.indices)
|
||||
assert np.allclose(emb_1.values, emb_2.values)
|
||||
assert np.allclose(emb_1.values, emb_3.values)
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/bm25"])
|
||||
def test_multilanguage(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
docs = ["Mangez-vous vraiment des grenouilles?", "Je suis au lit"]
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name, language="french")
|
||||
embeddings = list(model.embed(docs))[:2]
|
||||
assert embeddings[0].values.shape == (3,)
|
||||
assert embeddings[0].indices.shape == (3,)
|
||||
|
||||
assert embeddings[1].values.shape == (1,)
|
||||
assert embeddings[1].indices.shape == (1,)
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name, language="english")
|
||||
embeddings = list(model.embed(docs))[:2]
|
||||
assert embeddings[0].values.shape == (5,)
|
||||
assert embeddings[0].indices.shape == (5,)
|
||||
|
||||
assert embeddings[1].values.shape == (4,)
|
||||
assert embeddings[1].indices.shape == (4,)
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/bm25"])
|
||||
def test_special_characters(model_name: str) -> None:
|
||||
is_ci = os.getenv("CI")
|
||||
|
||||
docs = [
|
||||
"Über den größten Flüssen Österreichs äußern sich Experten häufig: Öko-Systeme müssen geschützt werden!",
|
||||
"L'élève français s'écrie : « Où est mon crayon ? J'ai besoin de finir cet exercice avant la récréation!",
|
||||
"Într-o zi însorită, Ștefan și Ioana au mâncat mămăligă cu brânză și au băut țuică la cabană.",
|
||||
"Üzgün öğretmen öğrencilere seslendi: Lütfen gürültü yapmayın, sınavınızı bitirmeye çalışıyorum!",
|
||||
"Ο Ξενοφών είπε: «Ψάχνω για ένα ωραίο δώρο για τη γιαγιά μου. Ίσως ένα φυτό ή ένα βιβλίο;»",
|
||||
"Hola! ¿Cómo estás? Estoy muy emocionado por el cumpleaños de mi hermano, ¡va a ser increíble! También quiero comprar un pastel de chocolate con fresas y un regalo especial: un libro titulado «Cien años de soledad",
|
||||
]
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name, language="english")
|
||||
embeddings = list(model.embed(docs))
|
||||
for idx, shape in enumerate([14, 18, 15, 10, 15]):
|
||||
assert embeddings[idx].values.shape == (shape,)
|
||||
assert embeddings[idx].indices.shape == (shape,)
|
||||
|
||||
if is_ci:
|
||||
delete_model_cache(model.model._model_dir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_name", ["Qdrant/bm42-all-minilm-l6-v2-attentions"])
|
||||
def test_lazy_load(model_name: str) -> None:
|
||||
model = SparseTextEmbedding(model_name=model_name, lazy_load=True)
|
||||
assert not hasattr(model.model, "model")
|
||||
docs = ["hello world", "flag embedding"]
|
||||
list(model.embed(docs))
|
||||
assert hasattr(model.model, "model")
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name, lazy_load=True)
|
||||
list(model.query_embed(docs))
|
||||
|
||||
model = SparseTextEmbedding(model_name=model_name, lazy_load=True)
|
||||
list(model.passage_embed(docs))
|
||||
@@ -0,0 +1,30 @@
|
||||
from fastembed import (
|
||||
TextEmbedding,
|
||||
SparseTextEmbedding,
|
||||
ImageEmbedding,
|
||||
LateInteractionMultimodalEmbedding,
|
||||
LateInteractionTextEmbedding,
|
||||
)
|
||||
|
||||
|
||||
def test_text_list_supported_models():
|
||||
for model_type in [
|
||||
TextEmbedding,
|
||||
SparseTextEmbedding,
|
||||
ImageEmbedding,
|
||||
LateInteractionMultimodalEmbedding,
|
||||
LateInteractionTextEmbedding,
|
||||
]:
|
||||
supported_models = model_type.list_supported_models()
|
||||
assert isinstance(supported_models, list)
|
||||
description = supported_models[0]
|
||||
assert isinstance(description, dict)
|
||||
|
||||
assert "model" in description and description["model"]
|
||||
if model_type != SparseTextEmbedding:
|
||||
assert "dim" in description and description["dim"]
|
||||
assert "license" in description and description["license"]
|
||||
assert "size_in_GB" in description and description["size_in_GB"]
|
||||
assert "model_file" in description and description["model_file"]
|
||||
assert "sources" in description and description["sources"]
|
||||
assert "hf" in description["sources"] or "url" in description["sources"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user