Compare commits

...
57 Commits
Author SHA1 Message Date
Dmitrii Ogn a32500fa6d Merge branch 'main' into mean_pooling 2025-01-30 15:59:50 +03:00
Hossam HagagandGeorge Panchuk 993dcd5f68 chore: Add missing type hints in functions (#453)
* chore: Add missing type hints in functions

* add missing import, small type refactor

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2025-01-29 12:08:20 +01:00
Hossam HagagandGeorge Panchuk 73e1e5ecb9 chore: Add missing returns in defs (#451)
* chore: Add missing returns in defs

* remove return type from init

* remove incorrect ndarray specifier

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2025-01-29 08:33:49 +01:00
Dmitrii OgnandGeorge Panchuk 105d6cfb97 E5 pooling fix (#445)
* HF sources for all models

* Proper normalization for e5 models

* Rollback to origin/master

* Warning

* Tests fix

* Logging + model refactoring

* fix: refactor warnings, make e5-large non-normalized

* remove redundant code, update canonical values for e5

* align warning style

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2025-01-28 23:14:43 +01:00
Hossam HagagandGeorge Panchuk bb815405aa chore: Add any to kwargs (#450)
* new: Add mypy and pyright deps

* chore: Add any to kwargs

* chore: Add any to args

* add missing kwargs

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2025-01-28 22:49:37 +01:00
Dmitrii OgnandGeorge Panchuk 314842121d Load from local dir (#443)
* HF sources for all models

* Specific_model_path model path support

* Fix hf download

* fix: rollback incorrect model replacement

* refactor: remove redundant type imports

* refactor: replace List with list

* fix: remove redundant param in late interaction text embedding

* Update fastembed/common/model_management.py

* fix: rollback post process onnx output

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2025-01-28 12:46:16 +01:00
Hossam Hagag c2f6fd1c90 Fix paraphrase minilm (#436)
* fix: Fix minilm paraphrase by adding it to pool models

* tests: Updated minilm paraphrase canonical vector

* chore: Added a warning message for updating the model

* chore: Added version where model will be removed
2025-01-27 23:27:47 +01:00
Hossam HagagandGeorge b05877de93 new: Added jina embedding v3 (#428)
* new: Added jina embedding v3

* refactor: Changed dim to int value

* new: Updated notice

* new: Extended text embedding with query embed and passage embed

* fix: Fix lazy load in query and passage embed

* tests: Added test for multitask embeddings

* nit: Remove cache dir from tests

* tests: Updated tests

* improve: Improve task selection

* fix: Fix ci

* fix: Update fastembed/text/multitask_embedding.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* Update fastembed/text/multitask_embedding.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* fix: Pass task id using kwargs to parallel processor

* tests: Added test for task assignment

* prefer enums over ints

* tests: Added test for parallel

* improve: Updated model description

* fix: Fix ci

* fix: Fix ci

* refactor: Refactor query_embed and passage_embed

* tests: Added task propagation to parallel

* refactor: Set default task as retrieval passage

* chore: Update default task in tests

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2025-01-27 23:27:23 +01:00
54f6cd9cbc Improve progress bar new (#440)
* improve: Improve progress bar

* fix: Fix error downloading when internet connection down

* new: Added file hash computation to track new versions

* refactor: Removed redundant hash check
fix: Fix ci

* new: Verify using hf_api

* new: Improve progress bar

* refactor new progress bar (#446)

* refactor

* chore: Remove redundant enable progress bar

---------

Co-authored-by: hh-space-invader <h.hagag.ali@gmail.com>

* refactor comments

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2025-01-27 22:34:39 +01:00
Hossam Hagag ae37da3bd4 fix: Update nomic ai model (#441)
* fix: Updated nomic ai with mean pooling

* chore: Updated warning message

* nit

* fix: Fix ci
2025-01-27 11:25:04 +01:00
George Panchuk fa11d0f0c7 bump version to 0.5.1 2025-01-16 11:11:43 +01:00
George 50289c62ed new: move onnx dependency to dev (#439)
* new: move onnx dependency to dev

* update poetry install, add more groups in pyproject
2025-01-15 17:39:06 +01:00
d.rudenko e41197dc0b Fix 2025-01-15 12:43:37 +01:00
d.rudenko c6facad6e8 Fix 2025-01-15 12:24:16 +01:00
d.rudenko 66839cf36a Fix 2025-01-15 12:23:24 +01:00
d.rudenko 0c0581d04a Fix 2025-01-15 12:22:36 +01:00
d.rudenko 276de5a49e Added pooling + tests 2025-01-15 12:18:40 +01:00
d.rudenko 16c41dff18 HF sources for all models 2024-12-27 12:39:51 +01:00
Dmitrii Ogn 3c10b6625b V0.5.0 (#430)
* Bump version
2024-12-24 16:32:53 +00:00
Dmitrii Ogn e89654d435 Hf sources (#429)
* HF sources for all models
2024-12-24 14:10:16 +00:00
Hossam Hagag cec8d54502 new: Provide userwarning when specifying providers and cuda (#425)
* new: Provide userwarning when specifying providers and cuda

* Updated warning message
2024-12-24 12:37:58 +02:00
Hossam Hagag 55b985c1ad new: Added multi-gpu example (#422)
* new: Added multi-gpu example

* improve: Updated multi-gpu example

* improve: Updated fastembed multi gpu docs example
2024-12-17 13:09:34 +01:00
Dmitrii OgnandGeorge c8b1a18cfc Cross encoders parallelism (#419)
* Merge master

* rerank_pairs interface + parallelism support

* remove test notebook

* Removed unused code

* New tests for cross encoders and new interface

* Importing Self fix. We will need it for mypy support in newer versions

* Removed Self typing

* Removed non-needed changes from text

* Isort + black

* wip: start reviewing (#420)

Co-authored-by: Dmitrii Ogn <dimitriy_rudenko@mail.ru>

* Test fix

* Update fastembed/rerank/cross_encoder/text_cross_encoder.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* Update fastembed/rerank/cross_encoder/text_cross_encoder.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* Update fastembed/rerank/cross_encoder/text_cross_encoder.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* Update fastembed/rerank/cross_encoder/text_cross_encoder_base.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* Test for parallel processing + bugfix of PosixPath passing

* Removed non-needed import and added docstring

* Typing fix + argument passing

* Test parametrization
Moved to selected models set to test

* Run base test on all models

* Typing fix + improvement of input_names check

* nit: fix post process, update docstring, update tokenize, remove redundant imports

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-12-16 21:59:34 +03:00
Hossam HagagandGeorge 3b5e4c8722 new: Added jina clip v1 (#408)
* WIP: Added jina clip text embedding

* WIP: Added preprocess for jina clip

* WIP: Added jina clip vision (not sure if it works yet)

* improve: Improved mean pooling if the output doesnt have seq length

* fix: Fixed jina clip text

* nit

* fix: Fixed jina clip image preprocessor

* fix: Fix type hints
new: added resize2square

* tests: Add jina clip vision test case

* nit

* refactor: Update fastembed/image/transform/operators.py

Co-authored-by: George <george.panchuk@qdrant.tech>

* fix: Fix indentation

* refactor: Refactored how we call padding for image

* fix: Fix pad to image when resized size larger than new square canvas

* refactor: minor refactor

* refactor: Refactor some functions in preprocess image

* fix: Fix to pad image with specified fill color

* refactor: Change resize to classmethod

* fix: Fix jina clip text v1

* fix: fix pad to square for some rectangular images (#421)

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-12-16 12:45:09 +02:00
George 516170cbaf new: add python 3.13 support (#404) 2024-12-11 19:18:54 +00:00
Hossam HagagandGeorge Panchuk 0f79d3f9d8 feat: Added a toggle to disable stemmer in bm25 (#416)
* feat: Added a toggle to disable stemmer in bm25

* refactor: Refactored how to disable stemming in bm25

* refactor: Refactored the way of disabling stemmer in bm25

* new: Added english fallback if language = None

* tests: Added test case for disable stemmer

* fix: Fix language to be only string

* tests: Updated bm25 toggle stemmer tests

* refactor: fix stopwords type

* fix: fix param propagation in parallel embed in bm25

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-12-10 21:12:12 +01:00
Hossam Hagag 2ef9c38b8b Tsk 409 support gte models (#415)
* new: Added support for gte base model

* tests: Added test cannonical vectors for gte model
2024-12-04 09:25:35 +02:00
Hossam Hagag da30f934d8 fix: Fix colbert model shape mismatch (#413)
* fix: Fix colbert model shape mismatch

* refactor: Added the truncation after tokenizer init
2024-11-28 13:59:52 +02:00
Hossam Hagag adfc03ed0d Improve models cache progressbar (#406)
* chore: Remove typing hints of Python less than 3.9

* chore: Removed optional from cache as it cannot be undefined

* improve: Turned off progress bar of huggingface models if cached
2024-11-21 12:47:46 +02:00
Hossam Hagag e9dc3b1060 Support jina embeddings v2 models (#405)
* new: Added support for jinaai/jina-embeddings-v2-base-zh

* new: Added support for jinaai/jina-embeddings-v2-base-es
2024-11-19 14:54:51 +01:00
George 1343e55076 new: drop python 3.8 support, update type hints, ci (#403) 2024-11-15 15:54:19 +01:00
amietn 9841666bd5 Remove numpy<2 dependency (#362)
This causes an incompatibility with spaCy because no matching
versions of numpy can be found when using both libraries at once.

It looks like the tests pass again without the numpy<2 requirement
so there is no reason to keep this requirement anymore.
2024-11-13 11:43:02 +01:00
Hossam HagagandGeorge Panchuk 860e2ad691 Tsk 375 add jina rerankers (#379)
* feat: Added jina reranker models

* chore: Added jina reranker canonical score values

* chore: added rounding of the output for easier reproducability

* chore: Added jina reranker models in batch test

* chore: remove redundant np.round

* chore: test only <1gb files in local

* chore: Updated docs to add rerankers

* fix: recompute canonical values with fp16

* new: extend NOTICE with jina reranker v2

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-11-13 10:48:45 +01:00
Hossam HagagandGeorge d141e29784 Fix type hint (#392)
* fix: Fix return of onnx_embed

* fix: Fix type hint of start method in worker class

* fix: Fix not passing kwargs in _preprocess_onnx_input and tokenize as base class

* fix: Fix not passing kwargs in _preprocess_onnx_input as base class

* fix: change tokenize in simpleTokenizer to classmethod

* chore: Changed query argument to Iterable to match base class

* chore: changed mask token id and pad token id to be int

* review suggestions (#398)

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-11-13 10:32:46 +01:00
Hossam HagagandGeorge 7c935717d2 improve: Changed the way we are adding the query and document markers in colbert (#391)
* improve: Changed the way we are adding the query and document markers in colbert

* fix: Truncate the inout_ids and attention_mask when adding query and document markers to original input length

* fix: Fix broadcast issue

* chore: Remove redundant if condition

* nit

* refactor (#397)

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-11-13 10:23:26 +01:00
paulmartrencharproandGeorge Panchuk 8413066f7b Add rerankers to the list of supported models (Supported_Models.ipynb) (#393)
* Add rerankers to the list of supported models (Supported_Models.ipynb)

* fix: update imports

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-11-13 09:59:28 +01:00
George faed7f1320 fix: fix model cleanup in tests (#400)
* fix: fix model cleanup in tests
2024-11-12 19:08:39 +01:00
George 5b895420b1 Update GitHub templates (#389)
* new: add PR template, decouple bug template with new model request

* new: add feature request, model request
2024-11-08 11:45:21 +01:00
GeorgeandAnush e868bbaebc new: add reranker example to readme (#390)
* new: add reranker example to readme

* Update README.md

Co-authored-by: Anush  <anushshetty90@gmail.com>

* fix: compute the scores

---------

Co-authored-by: Anush <anushshetty90@gmail.com>
2024-11-07 17:31:13 +01:00
George Panchuk a5f3f11829 bump version to v0.4.2 2024-11-07 12:03:34 +01:00
Hossam HagagandGeorge 3fc0e2b382 chore: Add notice file for jina ai models (#380)
* chore: Add notice file for jina ai models

* chore: Update notice

* Update NOTICE

Co-authored-by: George <george.panchuk@qdrant.tech>

* chore: added jina embeddings v3

* chore: removed unsupported models

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-11-05 14:22:03 +02:00
Hossam Hagag 12b06ece51 chore: Lock onnxruntime version to be < 1.20.0 (#386) 2024-11-04 22:53:50 +02:00
Hossam Hagag 1deb830328 Tsk 374 add jina colbert v2 (#378)
* feat: Added support for jina-colbert-v2

* chore: Generalized query marker and document marker

* nit: remove github action on dispatch

* chore: updated license

* fix: Fix attention mask to be all 1 in xlmrobertatokenizer

* feat: Added class for JinaColbertV2

* feat: Added jina colbert

* chore: Change tolerance of the test

* chore: Changed encoding of attention mask to 1 to be only in queries

* chore: Changed the replacable token to be ' @' as its considered as one token

* chore: Removed redundant functions

* chore: Updated supported models docs

* nit: Remove print statement

* nit: visual stuff

* fix: Fix dimention of jina colbert in description

* fix: canonical query and document values for jina colbert
2024-11-03 11:02:41 +02:00
George Panchuk aba8fb43cf bump version to v0.4.1 2024-10-21 23:56:19 +04:00
Dmitrii OgnandGeorge Panchuk 69ffaa0f48 Pystemmer -> py-rust_stemmers (#366)
* bump version to 0.4.0

* py-rust-stemmers support instead of snowball stemmer and pystemmer

* py-rust-stemmers support instead of snowball stemmer and pystemmer

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-10-21 23:46:20 +04:00
Hossam Hagag cf67a80ff7 Type fix (#371)
* fix: Fix type when calling parallelworkerpool
parallelworkerpool accepts num_workers as int only

* fix: Fix type OnnxOutputContext.
OnnxOutputContext takes attention_mask and input_ids as optional while we cannot assign none to ndarray

* fix: Fix progress might not be bool and might be literal 0
2024-10-21 21:31:17 +04:00
Hossam Hagag a21e925000 chore: Updated colab gpu instructions (#367)
* chore: Updated colab gpu instructions

* chore: Update gpu docs

* chore: update gpu docs

* chore: Update gpu docs, add cuda 11 doc
2024-10-21 14:06:07 +04:00
Hossam Hagag 0638c011dc chore: Added license for all models (#364)
* chore: Added license for all models

* chore: added license as key value pair

* chore: Updated clip models text/image license
2024-10-17 08:57:33 +02:00
eaecf7d471 Multi gpu support (#358)
* feat: Added multi gpu support for text embedding

* feat: Add support for multi-gpu for special text models

* fix: Fix lazy_load to load the model to child processes when parallel is not none

* feat: Added lazy_load and multi-gpu to colbert

* feat: Add lazy_load and multi gpu to image models

* feat: Support lazy_load and multi-gpu to sparse models (except BM25)

* fix: Fixed BM25 not working

* refactor: Remove redundant GPUParallelProcessor

* refactor: Refactor _embed_*_parallel

* feat: Add cuda argument
refactor: Refactor how worker assign device

* fix: Fix if providers and cuda are None

* fix: Fix providers and cuda are none

* WIP: Multi gpu support review (#361)

* WIP: review

* wip: review

* refactor: refactor images

* refactor: refactor sparse

* refactor: refactor late interaction

* add model loading

* add tests

* fix: uncomment models in tests

* fix: fix variable declaration order

* fix: fix device id assignment

* tests: add multi gpu tests

* fix: fix device id assignment for sparse embeddings

* tests: update multi gpu tests

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>

* refactor: remove redundant declarations

* fix: rollback redundant changes

* fix: remove num workers device ids dep, fix type hint

* fix: fix post process for sparse models

* fix: remove redundant model loading

* new: add lazy load and new gpu support to cross encoders

* fix: add rerankers to multi gpu tests

* fix: unlock multilingual test

* fix: fix gpu test with cross encoder

---------

Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-10-16 23:42:38 +02:00
Hossam Hagag 58b5a8ed9a fix: Fix error message when late interaction model name not found (#365)
chore: Updated type hint
2024-10-14 23:38:52 +03:00
519b310f22 Api cross encoder (#355)
* create cross encoder api

* create cross encoder api

* create cross encoder api

* Create cross encoder api

* fix cross encoder

* update api cross encoder

* update cross encoder

* update cross encoder

* update cross encoder

* update cross encoder

* Fixes over comments

* Add CI space management and mere refactoring

* fix: update interface, update tests, add docstrings

* fix: fix input dtype

---------

Co-authored-by: quynhhuong <quynhhuong@ortho.fashion>
Co-authored-by: quynhhuong <hh3009@nyu.edu>
Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-10-09 22:14:57 +02:00
Dmitrii Ogn e2e1f93685 Support gcs offline mode (#352)
* Added support of gcs offline init

* Proper error messages

* Moved local_files-only to class field

* Type hint for retries + retries fix
2024-09-30 22:08:52 +02:00
Hossam HagagandGeorge Panchuk dab4dcc99a chore: Updating docs of installing fastembed-gpu (#344)
* chore: Updating docs of installing fastembed-gpu

* chore: updated common issues

* chore: Update gcp setup

* chore: Updated an example of installing cuda12 and cudnn9 on ubuntu 22.04

* rephrasing

* update readme

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-09-24 17:09:35 +02:00
Hossam Hagag 40a03740ff fix: Fix deadlock when child gets kill -9 sig (#340)
* fix: Fix deadlock when child gets kill -9 sig

* chore: Better cleanup for resources

* chore: changed place of processes.clear

* fix: Added cancle_join_thread for emergency shutdown
2024-09-24 15:55:24 +02:00
Dmitrii Ogn 65c2efd6f1 Tf-idf fix: punctuation removal + lowercasing (#339)
* Tf-idf fix:
Remove punctuation properly + lowercase

* Test fix

* Test fix

* Test fix

* Redudant accum removal

* Redudant accum removal

* 0.878890950070688

* Latest changes with
Average recall: 0.8915516690721613

* Support of special characters

* Removal of debug print

* Type annotations

* Type annotation support for python 3.8

* Type annotation support for python 3.8
2024-09-24 13:21:07 +02:00
George 97f2fb278e wip: remove model dir (#350)
* wip: remove model dir

* fix: update pytest run

* wip: disable some tests not used atm

* wip: disable some tests in ci

* wip: add debug print

* fix: fix ci, remove models after usage

* fix: fix bm25 deletion

* fix: remove redundant ci commands
2024-09-24 12:18:44 +02:00
n0x29aandH4-8ZSI fa2205115d Fix: Normalize tokens to lowercase before checking stopwords in BM25 (#337)
* Fix: Normalize tokens to lowercase before checking stopwords in BM25

* Test: Normalize tokens to lowercase before checking stopwords in BM25

* Test Fix test_multilanguage: in "Je suis au lit", the "Je" should be skipped because it in the stopwords.

* chore: apply ruff

---------

Co-authored-by: H4-8ZSI <H4-8ZSI@EXAMPLE.COM>
2024-09-06 11:42:00 +02:00
72 changed files with 4088 additions and 1059 deletions
+20 -18
View File
@@ -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,11 +10,22 @@ 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: 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:
@@ -23,21 +34,12 @@ body:
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.7 (Latest)
- 0.2.6
- 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 -1
View File
@@ -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>
+22
View File
@@ -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>
+19
View File
@@ -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?
+4 -13
View File
@@ -3,8 +3,6 @@ name: Tests
on:
push:
branches: [ master, main, gpu ]
schedule:
- cron: 0 0 * * *
pull_request:
env:
@@ -16,11 +14,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 +38,8 @@ 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'
run: |
poetry run pytest
+16
View File
@@ -0,0 +1,16 @@
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.
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.
+17 -3
View File
@@ -28,10 +28,10 @@ pip install fastembed-gpu
```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.",
]
@@ -137,6 +137,20 @@ embeddings = list(model.embed(images))
# ]
```
### 🔄 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]
```
## ⚡️ FastEmbed on a GPU
@@ -147,7 +161,7 @@ It requires installation of the `fastembed-gpu` package.
pip install fastembed-gpu
```
Check our [example](https://qdrant.github.io/fastembed/examples/FastEmbed_GPU/) for detailed instructions and CUDA 12.x support.
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
+1 -3
View File
@@ -65,15 +65,13 @@
}
],
"source": [
"from typing import List\n",
"\n",
"import numpy as np\n",
"\n",
"from fastembed import TextEmbedding\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",
+14 -3
View File
@@ -54,7 +54,14 @@
},
{
"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'}]"
"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": {},
@@ -212,7 +219,9 @@
"outputs": [
{
"data": {
"text/plain": "((26, 128), (32, 128))"
"text/plain": [
"((26, 128), (32, 128))"
]
},
"execution_count": 18,
"metadata": {},
@@ -271,7 +280,9 @@
"import numpy as np\n",
"\n",
"\n",
"def compute_relevance_scores(query_embedding: np.array, document_embeddings: np.array, k: int):\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",
+133 -29
View File
@@ -37,28 +37,7 @@
"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`.\n",
"\n",
"### CUDA 12.x support\n",
"\n",
"By default `onnxruntime-gpu` is shipped with CUDA 11.8 support.\n",
"CUDA 12.x support requires installation of `onnxruntime-gpu` with providing of a direct url:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "OoSfWFFZeJ5t",
"outputId": "417b9332-6a7b-4000-c74b-4ed2b5b76590"
},
"outputs": [],
"source": [
"!pip install onnxruntime-gpu -i https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/ -qq\n",
"!pip install fastembed-gpu -qqq"
"**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`."
]
},
{
@@ -67,9 +46,65 @@
"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",
"Google Colab notebooks have CUDA 12.x."
"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."
]
},
{
@@ -82,7 +117,80 @@
"\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",
"Dependencies required for the chosen onnxruntime version can be found [here](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements)"
"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",
"```"
]
},
{
@@ -280,8 +388,6 @@
}
],
"source": [
"from typing import List\n",
"\n",
"import numpy as np\n",
"\n",
"from fastembed import TextEmbedding\n",
@@ -299,9 +405,7 @@
"id": "iPtoHf7GeV-i"
},
"outputs": [],
"source": [
"documents: List[str] = list(np.repeat(\"Demonstrating GPU acceleration in fastembed\", 500))"
]
"source": "documents: list[str] = list(np.repeat(\"Demonstrating GPU acceleration in fastembed\", 500))"
},
{
"cell_type": "code",
+88
View File
@@ -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"
]
+11 -12
View File
@@ -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",
@@ -489,11 +488,11 @@
}
],
"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(\n",
"sparse_embedding: list[SparseEmbedding] = make_sparse_embedding(\n",
" [\"Fastembed is a great library for text embeddings!\"]\n",
")\n",
"sparse_embedding"
@@ -616,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",
@@ -627,7 +626,7 @@
" 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",
@@ -662,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",
@@ -872,7 +871,7 @@
},
"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",
@@ -899,7 +898,7 @@
" return points\n",
"\n",
"\n",
"points: List[PointStruct] = make_points(df)"
"points: list[PointStruct] = make_points(df)"
]
},
{
@@ -942,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",
@@ -1075,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",
@@ -1149,7 +1148,7 @@
],
"source": [
"def find_point_by_id(\n",
" client: QdrantClient, collection_name: str, rrf_rank_list: List[Tuple[int, float]]\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",
+14 -9
View File
@@ -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",
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -26,14 +26,14 @@ pip install fastembed
```python
from fastembed import TextEmbedding
documents: List[str] = [
documents: list[str] = [
"passage: Hello, World!",
"query: Hello, World!",
"passage: This is an example passage.",
"fastembed is supported by and maintained by Qdrant."
]
embedding_model = TextEmbedding()
embeddings: List[np.ndarray] = embedding_model.embed(documents)
embeddings: list[np.ndarray] = embedding_model.embed(documents)
```
## Usage with Qdrant
+2 -3
View File
@@ -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",
+4 -3
View File
@@ -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)"
]
},
+11 -5
View File
@@ -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",
File diff suppressed because one or more lines are too long
+200 -43
View File
@@ -1,29 +1,37 @@
import os
import time
import json
import shutil
import tarfile
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Optional
import requests
from huggingface_hub import snapshot_download
from huggingface_hub.utils import RepositoryNotFoundError
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
class ModelManagement:
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[dict[str, Any]]: A list of dictionaries containing the model information.
"""
raise NotImplementedError()
@classmethod
def _get_model_description(cls, model_name: str) -> Dict[str, Any]:
def _get_model_description(cls, model_name: str) -> dict[str, Any]:
"""
Gets the model description from the model_name.
@@ -34,7 +42,7 @@ class ModelManagement:
ValueError: If the model_name is not supported.
Returns:
Dict[str, Any]: The model description.
dict[str, Any]: The model description.
"""
for model in cls.list_supported_models():
if model_name.lower() == model["model"].lower():
@@ -74,7 +82,7 @@ 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,
@@ -93,20 +101,74 @@ class ModelManagement:
def download_files_from_huggingface(
cls,
hf_source_repo: str,
cache_dir: Optional[str] = None,
extra_patterns: Optional[List[str]] = None,
**kwargs,
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.
specific_model_path (Optional[str], optional): The path to the model dir already pooled from external source
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, int]]:
meta = {}
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, int]]) -> 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",
@@ -114,18 +176,86 @@ class ModelManagement:
"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=kwargs.get("local_files_only", False),
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.
@@ -162,9 +292,10 @@ class ModelManagement:
return cache_dir
@classmethod
def retrieve_model_gcs(cls, model_name: str, source_url: str, cache_dir: str) -> Path:
def retrieve_model_gcs(
cls, model_name: str, source_url: str, cache_dir: str, local_files_only: bool = False
) -> Path:
fast_model_name = f"fast-{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
@@ -183,27 +314,37 @@ 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, retries=3, **kwargs) -> Path:
def download_model(
cls, model: dict[str, Any], cache_dir: Path, 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 (dict[str, Any]): The model description.
Example:
```
{
@@ -223,7 +364,11 @@ class ModelManagement:
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.get("sources", {}).get("hf")
url_source = model.get("sources", {}).get("url")
@@ -241,24 +386,36 @@ class ModelManagement:
hf_source,
cache_dir=str(cache_dir),
extra_patterns=extra_patterns,
local_files_only=kwargs.get("local_files_only", False),
**kwargs,
)
)
except (EnvironmentError, RepositoryNotFoundError, ValueError) as e:
logger.error(
f"Could not download model from HuggingFace: {e} "
"Falling back to other sources."
)
if url_source:
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"], url_source, str(cache_dir))
return cls.retrieve_model_gcs(
model["model"],
url_source,
str(cache_dir),
local_files_only=local_files_only,
)
except Exception:
logger.error(f"Could not download model from url: {url_source}")
if not local_files_only:
logger.error(f"Could not download model from url: {url_source}")
logger.error(
f"Could not download model from either source, sleeping for {sleep} seconds, {retries} retries left."
)
if local_files_only:
logger.error("Could not find model in cache_dir")
else:
logger.error(
f"Could not download model from either source, sleeping for {sleep} seconds, {retries} retries left."
)
time.sleep(sleep)
sleep *= 3
raise ValueError(f"Could not download model {model['model']} from any source.")
raise ValueError(f"Could not load model {model['model']} from any source.")
+30 -21
View File
@@ -1,17 +1,7 @@
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import (
Any,
Dict,
Generic,
Iterable,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
)
from typing import Any, Generic, Iterable, Optional, Sequence, Type, TypeVar
import numpy as np
import onnxruntime as ort
@@ -43,26 +33,42 @@ class OnnxModel(Generic[T]):
self.tokenizer = None
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
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 providers is None else list(providers)
)
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 = []
for provider in onnx_providers:
@@ -94,7 +100,10 @@ class OnnxModel(Generic[T]):
RuntimeWarning,
)
def onnx_embed(self, *args, **kwargs) -> OnnxOutputContext:
def load_onnx_model(self) -> None:
raise NotImplementedError("Subclasses must implement this method")
def onnx_embed(self, *args: Any, **kwargs: Any) -> OnnxOutputContext:
raise NotImplementedError("Subclasses must implement this method")
@@ -103,7 +112,7 @@ class EmbeddingWorker(Worker):
self,
model_name: str,
cache_dir: str,
**kwargs,
**kwargs: Any,
) -> OnnxModel:
raise NotImplementedError()
@@ -111,7 +120,7 @@ class EmbeddingWorker(Worker):
self,
model_name: str,
cache_dir: str,
**kwargs,
**kwargs: Any,
):
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
@@ -119,5 +128,5 @@ class EmbeddingWorker(Worker):
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker":
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
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("Subclasses must implement this method")
+38
View File
@@ -0,0 +1,38 @@
import numpy as np
from numpy import ufunc
class LateInteractionPooler(object):
def __init__(self, agg_type="row", agg="mean"):
self.agg = agg
self.type = agg_type
def _pick_operation(self) -> ufunc:
if self.agg == "mean":
return np.mean
elif self.agg == "max":
return np.max
elif self.agg == "min":
return np.min
else:
raise NotImplementedError(
f"LateInteractionPooler only supports agg=mean,min,max, provided {self.agg}"
)
def pool(self, embeddings_batch) -> np.array:
if isinstance(embeddings_batch, np.ndarray) and len(embeddings_batch.shape) == 2:
embeddings_batch = [embeddings_batch]
if self.type == "row":
pooled_embedding = self.pool_row(embeddings_batch)
elif self.type == "col":
pooled_embedding = self.pool_col(embeddings_batch)
else:
raise ValueError("type must be 'row' or 'col'")
return pooled_embedding
def pool_row(self, embeddings_batch) -> np.array:
return self._pick_operation()(embeddings_batch, axis=-1)
def pool_col(self, embeddings_batch) -> np.array:
return self._pick_operation()(embeddings_batch, axis=-2)
+2 -2
View File
@@ -1,6 +1,6 @@
import json
from pathlib import Path
from typing import Tuple
from tokenizers import AddedToken, Tokenizer
from fastembed.image.transform.operators import Compose
@@ -17,7 +17,7 @@ def load_special_tokens(model_dir: Path) -> dict:
return tokens_map
def load_tokenizer(model_dir: Path) -> Tuple[Tokenizer, dict]:
def load_tokenizer(model_dir: Path) -> tuple[Tokenizer, dict]:
config_path = model_dir / "config.json"
if not config_path.exists():
raise ValueError(f"Could not find config.json in {model_dir}")
+4 -4
View File
@@ -1,7 +1,7 @@
import os
from pathlib import Path
import sys
from PIL import Image
from typing import Any, Dict, Iterable, Tuple, Union
from typing import Any, Iterable, Union
if sys.version_info >= (3, 10):
from typing import TypeAlias
@@ -9,8 +9,8 @@ else:
from typing_extensions import TypeAlias
PathInput: TypeAlias = Union[str, os.PathLike]
PathInput: TypeAlias = Union[str, Path]
PilInput: TypeAlias = Union[Image.Image, Iterable[Image.Image]]
ImageInput: TypeAlias = Union[PathInput, Iterable[PathInput], PilInput]
OnnxProvider: TypeAlias = Union[str, Tuple[str, Dict[Any, Any]]]
OnnxProvider: TypeAlias = Union[str, tuple[str, dict[Any, Any]]]
+19 -5
View File
@@ -1,13 +1,18 @@
import os
import sys
import re
import tempfile
from itertools import islice
import unicodedata
from pathlib import Path
from typing import Generator, Iterable, Optional, Union
from itertools import islice
from typing import Iterable, Optional, TypeVar
import numpy as np
T = TypeVar("T")
def normalize(input_array, p=2, dim=1, eps=1e-12) -> np.ndarray:
def normalize(input_array: np.ndarray, p: int = 2, dim: int = 1, eps: float = 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
@@ -15,7 +20,7 @@ def normalize(input_array, p=2, dim=1, eps=1e-12) -> np.ndarray:
return normalized_array
def iter_batch(iterable: Union[Iterable, Generator], size: int) -> Iterable:
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]]
@@ -37,7 +42,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)
+2 -2
View File
@@ -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)
+14 -11
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type
from typing import Any, Iterable, Optional, Sequence, Type
import numpy as np
@@ -8,15 +8,15 @@ from fastembed.image.onnx_embedding import OnnxImageEmbedding
class ImageEmbedding(ImageEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[ImageEmbeddingBase]] = [OnnxImageEmbedding]
EMBEDDINGS_REGISTRY: list[Type[ImageEmbeddingBase]] = [OnnxImageEmbedding]
@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:
```
@@ -25,6 +25,7 @@ class ImageEmbedding(ImageEmbeddingBase):
"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",
@@ -45,21 +46,23 @@ class ImageEmbedding(ImageEmbeddingBase):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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
):
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
@@ -74,7 +77,7 @@ class ImageEmbedding(ImageEmbeddingBase):
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
+4 -4
View File
@@ -1,4 +1,4 @@
from typing import Iterable, Optional
from typing import Iterable, Optional, Any
import numpy as np
@@ -12,7 +12,7 @@ class ImageEmbeddingBase(ModelManagement):
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
@@ -24,13 +24,13 @@ class ImageEmbeddingBase(ModelManagement):
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Embeds a list of images into a list of embeddings.
Args:
images - The list of image paths to preprocess and embed.
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.
+82 -18
View File
@@ -1,7 +1,6 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type
from typing import Any, Iterable, Optional, Sequence, Type
import numpy as np
from fastembed.common import ImageInput, OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import define_cache_dir, normalize
@@ -13,6 +12,7 @@ supported_onnx_models = [
"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": {
"hf": "Qdrant/clip-ViT-B-32-vision",
@@ -23,6 +23,7 @@ supported_onnx_models = [
"model": "Qdrant/resnet50-onnx",
"dim": 2048,
"description": "Image embeddings, Unimodal (image), 2016 year",
"license": "apache-2.0",
"size_in_GB": 0.1,
"sources": {
"hf": "Qdrant/resnet50-onnx",
@@ -33,6 +34,7 @@ supported_onnx_models = [
"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": {
"hf": "Qdrant/Unicom-ViT-B-16",
@@ -43,12 +45,24 @@ supported_onnx_models = [
"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": {
"hf": "Qdrant/Unicom-ViT-B-32",
},
"model_file": "model.onnx",
},
{
"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": {
"hf": "jinaai/jina-clip-v1",
},
"model_file": "onnx/vision_model.onnx",
},
]
@@ -59,7 +73,12 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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:
@@ -68,33 +87,69 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[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)
# 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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.load_onnx_model(
model_dir=model_dir,
model_file=model_description["model_file"],
threads=threads,
providers=providers,
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[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.
"""
return supported_onnx_models
@@ -103,7 +158,7 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of images into list of embeddings.
@@ -120,12 +175,16 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
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,
**kwargs,
)
@@ -134,8 +193,8 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
return OnnxImageEmbeddingWorker
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
@@ -147,5 +206,10 @@ class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
class OnnxImageEmbeddingWorker(ImageEmbeddingWorker):
def init_embedding(self, model_name: str, cache_dir: str, **kwargs) -> OnnxImageEmbedding:
return OnnxImageEmbedding(model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs)
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,
)
+41 -23
View File
@@ -2,7 +2,7 @@ import contextlib
import os
from multiprocessing import get_all_start_methods
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type
from typing import Any, Iterable, Optional, Sequence, Type
import numpy as np
from PIL import Image
@@ -24,37 +24,44 @@ class OnnxImageModel(OnnxModel[T]):
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[T]:
raise NotImplementedError("Subclasses must implement this method")
def __init__(self) -> None:
def __init__(self):
super().__init__()
self.processor = None
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
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:
super().load_onnx_model(
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 _build_onnx_input(self, encoded: np.ndarray) -> Dict[str, np.ndarray]:
def load_onnx_model(self) -> None:
raise NotImplementedError("Subclasses must implement this method")
def _build_onnx_input(self, encoded: np.ndarray) -> dict[str, np.ndarray]:
return {node.name: encoded for node in self.model.get_inputs()}
def onnx_embed(self, images: List[ImageInput], **kwargs) -> OnnxOutputContext:
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
@@ -74,40 +81,51 @@ class OnnxImageModel(OnnxModel[T]):
images: ImageInput,
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
providers: Optional[Sequence[OnnxProvider]] = None,
cuda: bool = False,
device_ids: Optional[list[int]] = None,
**kwargs: Any,
) -> Iterable[T]:
is_small = False
if (
isinstance(images, str)
or isinstance(images, Path)
or (isinstance(images, Image.Image))
):
if isinstance(images, (str, Path, Image.Image)):
images = [images]
is_small = True
if isinstance(images, list):
if len(images) < batch_size:
is_small = True
if parallel == 0:
parallel = os.cpu_count()
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))
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, **kwargs}
params = {
"model_name": model_name,
"cache_dir": cache_dir,
"providers": providers,
**kwargs,
}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), start_method=start_method
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)
class ImageEmbeddingWorker(EmbeddingWorker):
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
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
+36 -10
View File
@@ -1,4 +1,4 @@
from typing import Sized, Tuple, Union
from typing import Sized, Union
import numpy as np
from PIL import Image
@@ -14,7 +14,7 @@ def convert_to_rgb(image: Image.Image) -> Image.Image:
def center_crop(
image: Union[Image.Image, np.ndarray],
size: Tuple[int, int],
size: tuple[int, int],
) -> np.ndarray:
if isinstance(image, np.ndarray):
_, orig_height, orig_width = image.shape
@@ -62,8 +62,8 @@ def center_crop(
def normalize(
image: np.ndarray,
mean=Union[float, np.ndarray],
std=Union[float, np.ndarray],
mean: Union[float, np.ndarray],
std: Union[float, np.ndarray],
) -> np.ndarray:
if not isinstance(image, np.ndarray):
raise ValueError("image must be a numpy array")
@@ -96,10 +96,10 @@ def normalize(
def resize(
image: Image,
size: Union[int, Tuple[int, int]],
resample: Image.Resampling = Image.Resampling.BILINEAR,
) -> Image:
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)
@@ -114,11 +114,37 @@ def resize(
return image.resize(new_size, resample)
def rescale(image: np.ndarray, scale: float, dtype=np.float32) -> np.ndarray:
def rescale(image: np.ndarray, scale: float, dtype: type = np.float32) -> np.ndarray:
return (image * scale).astype(dtype)
def pil2ndarray(image: Union[Image.Image, np.ndarray]):
def pil2ndarray(image: Union[Image.Image, np.ndarray]) -> np.ndarray:
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
+99 -29
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Tuple, Union
from typing import Any, Union, Optional
import numpy as np
from PIL import Image
@@ -10,93 +10,111 @@ from fastembed.image.transform.functional import (
pil2ndarray,
rescale,
resize,
pad2square,
)
class Transform:
def __call__(self, images: List) -> Union[List[Image.Image], List[np.ndarray]]:
def __call__(self, images: list) -> Union[list[Image.Image], list[np.ndarray]]:
raise NotImplementedError("Subclasses must implement this method")
class ConvertToRGB(Transform):
def __call__(self, images: List[Image.Image]) -> List[Image.Image]:
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]):
def __init__(self, size: tuple[int, int]):
self.size = size
def __call__(self, images: List[Image.Image]) -> List[np.ndarray]:
def __call__(self, images: list[Image.Image]) -> list[np.ndarray]:
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]]):
def __init__(self, mean: Union[float, list[float]], std: Union[float, list[float]]):
self.mean = mean
self.std = std
def __call__(self, images: List[np.ndarray]) -> List[np.ndarray]:
def __call__(self, images: list[np.ndarray]) -> list[np.ndarray]:
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]],
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
]
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[np.ndarray]) -> List[np.ndarray]:
def __call__(self, images: list[np.ndarray]) -> list[np.ndarray]:
return [rescale(image, scale=self.scale) for image in images]
class PILtoNDarray(Transform):
def __call__(
self, images: List[Union[Image.Image, np.ndarray]]
) -> List[np.ndarray]:
def __call__(self, images: list[Union[Image.Image, np.ndarray]]) -> list[np.ndarray]:
return [pil2ndarray(image) for image in images]
class PadtoSquare(Transform):
def __init__(
self,
size: int,
fill_color: Optional[Union[str, int, tuple[int, ...]]] = None,
):
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]):
def __init__(self, transforms: list[Transform]):
self.transforms = transforms
def __call__(
self, images: Union[List[Image.Image], List[np.ndarray]]
) -> Union[List[np.ndarray], List[Image.Image]]:
self, images: Union[list[Image.Image], list[np.ndarray]]
) -> Union[list[np.ndarray], list[Image.Image]]:
for transform in self.transforms:
images = transform(images)
return images
@classmethod
def from_config(cls, config: Dict[str, Any]) -> "Compose":
def from_config(cls, config: dict[str, Any]) -> "Compose":
"""Creates processor from a config dict.
Args:
config (Dict[str, Any]): Configuration dictionary.
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"}
@@ -107,6 +125,7 @@ class Compose:
transforms = []
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)
@@ -114,11 +133,11 @@ class Compose:
return cls(transforms=transforms)
@staticmethod
def _get_convert_to_rgb(transforms: List[Transform], config: Dict[str, Any]):
def _get_convert_to_rgb(transforms: list[Transform], config: dict[str, Any]) -> None:
transforms.append(ConvertToRGB())
@staticmethod
def _get_resize(transforms: List[Transform], config: Dict[str, Any]):
@classmethod
def _get_resize(cls, transforms: list[Transform], config: dict[str, Any]) -> None:
mode = config.get("image_processor_type", "CLIPImageProcessor")
if mode == "CLIPImageProcessor":
if config.get("do_resize", False):
@@ -161,9 +180,27 @@ class Compose:
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]):
def _get_center_crop(transforms: list[Transform], config: dict[str, Any]) -> None:
mode = config.get("image_processor_type", "CLIPImageProcessor")
if mode == "CLIPImageProcessor":
if config.get("do_center_crop", False):
@@ -177,22 +214,55 @@ class Compose:
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]):
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]):
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]):
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(
Normalize(mean=config["image_mean"], std=config["image_std"])
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}")
+91 -34
View File
@@ -1,5 +1,5 @@
import string
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
from tokenizers import Encoding
@@ -12,11 +12,13 @@ from fastembed.late_interaction.late_interaction_embedding_base import (
)
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
supported_colbert_models = [
{
"model": "colbert-ir/colbertv2.0",
"dim": 128,
"description": "Late interaction model",
"license": "mit",
"size_in_GB": 0.44,
"sources": {
"hf": "colbert-ir/colbertv2.0",
@@ -27,6 +29,7 @@ supported_colbert_models = [
"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": {
"hf": "answerdotai/answerai-colbert-small-v1",
@@ -39,7 +42,7 @@ supported_colbert_models = [
class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
QUERY_MARKER_TOKEN_ID = 1
DOCUMENT_MARKER_TOKEN_ID = 2
MIN_QUERY_LENGTH = 32
MIN_QUERY_LENGTH = 31 # it's 32, we add one additional special token in the beginning
MASK_TOKEN = "[MASK]"
def _post_process_onnx_output(
@@ -48,6 +51,11 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
if not is_doc:
return output.model_output.astype(np.float32)
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):
if token_id in self.skip_list or token_id == self.pad_token_id:
@@ -60,25 +68,22 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
return output.model_output.astype(np.float32)
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], is_doc: bool = True
) -> Dict[str, np.ndarray]:
if is_doc:
onnx_input["input_ids"][:, 1] = self.DOCUMENT_MARKER_TOKEN_ID
else:
onnx_input["input_ids"][:, 1] = self.QUERY_MARKER_TOKEN_ID
self, onnx_input: dict[str, np.ndarray], is_doc: bool = True, **kwargs: Any
) -> dict[str, np.ndarray]:
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"], 1, marker_token, axis=1)
onnx_input["attention_mask"] = np.insert(onnx_input["attention_mask"], 1, 1, axis=1)
return onnx_input
def tokenize(self, documents: List[str], is_doc: bool = True) -> List[Encoding]:
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]:
# ". " is added to a query to be replaced with a special query token
query = [f". {query}"]
encoded = self.tokenizer.encode_batch(query)
def _tokenize_query(self, query: str) -> list[Encoding]:
encoded = self.tokenizer.encode_batch([query])
# colbert authors recommend to pad queries with [MASK] tokens for query augmentation to improve performance
if len(encoded[0].ids) < self.MIN_QUERY_LENGTH:
prev_padding = None
@@ -89,25 +94,23 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
pad_id=self.mask_token_id,
length=self.MIN_QUERY_LENGTH,
)
encoded = self.tokenizer.encode_batch(query)
encoded = self.tokenizer.encode_batch([query])
if prev_padding is None:
self.tokenizer.no_padding()
else:
self.tokenizer.enable_padding(**prev_padding)
return encoded
def _tokenize_documents(self, documents: List[str]) -> List[Encoding]:
# ". " is added to a document to be replaced with a special document token
documents = [". " + doc for doc in documents]
def _tokenize_documents(self, documents: list[str]) -> list[Encoding]:
encoded = self.tokenizer.encode_batch(documents)
return encoded
@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.
"""
return supported_colbert_models
@@ -117,7 +120,12 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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:
@@ -126,40 +134,78 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[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)
# 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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.mask_token_id = None
self.pad_token_id = None
self.skip_list = set()
self.load_onnx_model(
model_dir=model_dir,
model_file=model_description["model_file"],
threads=threads,
providers=providers,
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.mask_token_id = self.special_token_to_id["[MASK]"]
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)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
@@ -182,13 +228,19 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
documents=documents,
batch_size=batch_size,
parallel=parallel,
providers=self.providers,
cuda=self.cuda,
device_ids=self.device_ids,
**kwargs,
)
def query_embed(self, query: Union[str, List[str]], **kwargs) -> np.ndarray:
def query_embed(self, query: Union[str, Iterable[str]], **kwargs: Any) -> Iterable[np.ndarray]:
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
@@ -200,5 +252,10 @@ class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
class ColbertEmbeddingWorker(TextEmbeddingWorker):
def init_embedding(self, model_name: str, cache_dir: str, **kwargs) -> Colbert:
return Colbert(model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs)
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,62 @@
from typing import Any, Type
import numpy as np
from fastembed.late_interaction.colbert import Colbert
from fastembed.text.onnx_text_model import TextEmbeddingWorker
supported_jina_colbert_models = [
{
"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": {
"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[TextEmbeddingWorker]:
return JinaColbertEmbeddingWorker
@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_colbert_models
def _preprocess_onnx_input(
self, onnx_input: dict[str, np.ndarray], is_doc: bool = True, **kwargs: Any
) -> dict[str, np.ndarray]:
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(TextEmbeddingWorker):
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,
)
@@ -1,4 +1,4 @@
from typing import Iterable, Optional, Union
from typing import Iterable, Optional, Union, Any
import numpy as np
@@ -11,7 +11,7 @@ class LateInteractionTextEmbeddingBase(ModelManagement):
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
@@ -23,11 +23,11 @@ class LateInteractionTextEmbeddingBase(ModelManagement):
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
raise NotImplementedError()
def passage_embed(self, texts: Iterable[str], **kwargs) -> Iterable[np.ndarray]:
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[np.ndarray]:
"""
Embeds a list of text passages into a list of embeddings.
@@ -42,9 +42,7 @@ class LateInteractionTextEmbeddingBase(ModelManagement):
# 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[np.ndarray]:
"""
Embeds queries
@@ -1,39 +1,40 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
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,
]
EMBEDDINGS_REGISTRY: list[Type[LateInteractionTextEmbeddingBase]] = [Colbert, JinaColbert]
@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:
```
[
{
"model": "prithvida/SPLADE_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"size_in_GB": 0.532,
"model": "colbert-ir/colbertv2.0",
"dim": 128,
"description": "Late interaction model",
"license": "mit",
"size_in_GB": 0.44,
"sources": {
"hf": "qdrant/SPLADE_PP_en_v1",
"hf": "colbert-ir/colbertv2.0",
},
}
"model_file": "model.onnx",
},
]
```
"""
@@ -48,24 +49,30 @@ class LateInteractionTextEmbedding(LateInteractionTextEmbeddingBase):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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
):
if any(model_name.lower() == model["model"].lower() for model in supported_models):
self.model = EMBEDDING_MODEL_TYPE(
model_name, cache_dir, threads, providers=providers, **kwargs
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 SparseTextEmbedding."
"Please check the supported models using `SparseTextEmbedding.list_supported_models()`"
f"Model {model_name} is not supported in LateInteractionTextEmbedding."
"Please check the supported models using `LateInteractionTextEmbedding.list_supported_models()`"
)
def embed(
@@ -73,7 +80,7 @@ class LateInteractionTextEmbedding(LateInteractionTextEmbeddingBase):
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
@@ -92,9 +99,7 @@ class LateInteractionTextEmbedding(LateInteractionTextEmbeddingBase):
"""
yield from self.model.embed(documents, batch_size, parallel, **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[np.ndarray]:
"""
Embeds queries
+53 -14
View File
@@ -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():
@@ -84,16 +90,23 @@ def _worker(
class ParallelWorkerPool:
def __init__(
self, num_workers: int, worker: Type[Worker], start_method: Optional[str] = None
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:
@@ -105,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,
@@ -114,15 +133,13 @@ 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]:
def ordered_map(self, stream: Iterable[Any], *args: Any, **kwargs: Any) -> Iterable[Any]:
buffer = defaultdict(Any)
next_expected = 0
@@ -134,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)
@@ -144,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()
@@ -170,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()
@@ -179,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:
"""
@@ -210,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.rerank.cross_encoder.text_cross_encoder import TextCrossEncoder
__all__ = ["TextCrossEncoder"]
@@ -0,0 +1,227 @@
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
supported_onnx_models = [
{
"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",
},
{
"model": "Xenova/ms-marco-MiniLM-L-12-v2",
"size_in_GB": 0.12,
"sources": {
"hf": "Xenova/ms-marco-MiniLM-L-12-v2",
},
"model_file": "onnx/model.onnx",
"description": "MiniLM-L-12-v2 model optimized for re-ranking tasks.",
"license": "apache-2.0",
},
{
"model": "BAAI/bge-reranker-base",
"size_in_GB": 1.04,
"sources": {
"hf": "BAAI/bge-reranker-base",
},
"model_file": "onnx/model.onnx",
"description": "BGE reranker base model for cross-encoder re-ranking.",
"license": "mit",
},
{
"model": "jinaai/jina-reranker-v1-tiny-en",
"size_in_GB": 0.13,
"sources": {
"hf": "jinaai/jina-reranker-v1-tiny-en",
},
"model_file": "onnx/model.onnx",
"description": "Designed for blazing-fast re-ranking with 8K context length and fewer parameters than jina-reranker-v1-turbo-en.",
"license": "apache-2.0",
},
{
"model": "jinaai/jina-reranker-v1-turbo-en",
"size_in_GB": 0.15,
"sources": {
"hf": "jinaai/jina-reranker-v1-turbo-en",
},
"model_file": "onnx/model.onnx",
"description": "Designed for blazing-fast re-ranking with 8K context length.",
"license": "apache-2.0",
},
{
"model": "jinaai/jina-reranker-v2-base-multilingual",
"size_in_GB": 1.11,
"sources": {
"hf": "jinaai/jina-reranker-v2-base-multilingual",
},
"model_file": "onnx/model.onnx",
"description": "A multi-lingual reranker model for cross-encoder re-ranking with 1K context length and sliding window",
"license": "cc-by-nc-4.0",
},
]
class OnnxTextCrossEncoder(TextCrossEncoderBase, OnnxCrossEncoderModel):
@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_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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=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,
**kwargs,
)
@classmethod
def _get_worker_class(cls) -> Type[TextRerankerWorker]:
return TextCrossEncoderWorker
def _post_process_onnx_output(self, output: OnnxOutputContext) -> 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,153 @@
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
from fastembed.common.onnx_model import (
EmbeddingWorker,
OnnxModel,
OnnxOutputContext,
OnnxProvider,
)
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)
def tokenize(self, pairs: list[tuple[str, str]], **_: Any) -> list[Encoding]:
return self.tokenizer.encode_batch(pairs)
def _build_onnx_input(
self, tokenized_input
) -> dict[str, NDArray[Union[np.float32, np.int64]]]:
input_names = {node.name for node in self.model.get_inputs()}
inputs = {
"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)
relevant_output = outputs[0]
scores = 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,
**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,
**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)
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[float]:
raise NotImplementedError("Subclasses must implement this method")
def _preprocess_onnx_input(
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
return onnx_input
class TextRerankerWorker(EmbeddingWorker):
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,120 @@
from typing import Any, Iterable, Optional, Sequence, Type
from fastembed.common import OnnxProvider
from fastembed.rerank.cross_encoder.onnx_text_cross_encoder import OnnxTextCrossEncoder
from fastembed.rerank.cross_encoder.text_cross_encoder_base import TextCrossEncoderBase
class TextCrossEncoder(TextCrossEncoderBase):
CROSS_ENCODER_REGISTRY: list[Type[TextCrossEncoderBase]] = [
OnnxTextCrossEncoder,
]
@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": "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",
}
]
```
"""
result = []
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
)
@@ -0,0 +1,58 @@
from typing import Any, Iterable, Optional
from fastembed.common.model_management import ModelManagement
class TextCrossEncoderBase(ModelManagement):
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")
+67 -33
View File
@@ -1,21 +1,24 @@
import os
import string
from collections import defaultdict
from multiprocessing import get_all_start_methods
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
from typing import Any, Iterable, Optional, Type, Union
import mmh3
import numpy as np
from snowballstemmer import stemmer as get_stemmer
from fastembed.common.utils import define_cache_dir, iter_batch
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 WordTokenizer
from fastembed.sparse.utils.tokenizer import SimpleTokenizer
supported_languages = [
"arabic",
@@ -53,6 +56,7 @@ supported_bm25_models = [
{
"model": "Qdrant/bm25",
"description": "BM25 as sparse embeddings meant to be used with Qdrant",
"license": "apache-2.0",
"size_in_GB": 0.01,
"sources": {
"hf": "Qdrant/bm25",
@@ -88,6 +92,8 @@ class Bm25(SparseTextEmbeddingBase):
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.
"""
@@ -100,7 +106,10 @@ class Bm25(SparseTextEmbeddingBase):
b: float = 0.75,
avg_len: float = 256.0,
language: str = "english",
**kwargs,
token_max_length: int = 40,
disable_stemmer: bool = False,
specific_model_path: Optional[str] = None,
**kwargs: Any,
):
super().__init__(model_name, cache_dir, **kwargs)
@@ -116,26 +125,37 @@ class Bm25(SparseTextEmbeddingBase):
model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
self._model_dir = self.download_model(
model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.punctuation = set(string.punctuation)
self.stopwords = set(self._load_stopwords(model_dir, self.language))
self.stemmer = get_stemmer(language)
self.tokenizer = WordTokenizer
self.token_max_length = token_max_length
self.punctuation = set(get_all_punctuation())
self.disable_stemmer = disable_stemmer
if disable_stemmer:
self.stopwords = 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[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.
"""
return supported_bm25_models
@classmethod
def _load_stopwords(cls, model_dir: Path, language: str) -> List[str]:
def _load_stopwords(cls, model_dir: Path, language: str) -> list[str]:
stopwords_path = model_dir / f"{language}.txt"
if not stopwords_path.exists():
return []
@@ -161,13 +181,13 @@ class Bm25(SparseTextEmbeddingBase):
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.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,
@@ -175,9 +195,14 @@ class Bm25(SparseTextEmbeddingBase):
"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,
}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), start_method=start_method
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:
@@ -188,7 +213,7 @@ class Bm25(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.
@@ -213,16 +238,21 @@ class Bm25(SparseTextEmbeddingBase):
parallel=parallel,
)
def _stem(self, tokens: List[str]) -> List[str]:
def _stem(self, tokens: list[str]) -> list[str]:
stemmed_tokens = []
for token in tokens:
lower_token = token.lower()
if token in self.punctuation:
continue
if token in self.stopwords:
if lower_token in self.stopwords:
continue
stemmed_token = self.stemmer.stemWord(token)
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)
@@ -230,17 +260,18 @@ class Bm25(SparseTextEmbeddingBase):
def raw_embed(
self,
documents: List[str],
) -> List[SparseEmbedding]:
documents: list[str],
) -> list[SparseEmbedding]:
embeddings = []
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]:
def _term_frequency(self, tokens: list[str]) -> dict[int, float]:
"""Calculate the term frequency part of the BM25 formula.
(
@@ -250,10 +281,10 @@ class Bm25(SparseTextEmbeddingBase):
)
Args:
tokens (List[str]): The list of tokens in the document.
tokens (list[str]): The list of tokens in the document.
Returns:
Dict[int, float]: The token_id to term frequency mapping.
dict[int, float]: The token_id to term frequency mapping.
"""
tf_map = {}
counter = defaultdict(int)
@@ -274,7 +305,9 @@ class Bm25(SparseTextEmbeddingBase):
def compute_token_id(cls, token: str) -> int:
return abs(mmh3.hash(token))
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
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.
"""
@@ -282,6 +315,7 @@ class Bm25(SparseTextEmbeddingBase):
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(
@@ -301,7 +335,7 @@ class Bm25Worker(Worker):
self,
model_name: str,
cache_dir: str,
**kwargs,
**kwargs: Any,
):
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
@@ -309,11 +343,11 @@ class Bm25Worker(Worker):
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, Any]]:
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
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) -> Bm25:
def init_embedding(model_name: str, cache_dir: str, **kwargs: Any) -> Bm25:
return Bm25(model_name=model_name, cache_dir=cache_dir, **kwargs)
+90 -36
View File
@@ -1,11 +1,11 @@
import math
import string
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import mmh3
import numpy as np
from snowballstemmer import stemmer as get_stemmer
from py_rust_stemmers import SnowballStemmer
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
@@ -21,6 +21,7 @@ supported_bm42_models = [
"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": {
"hf": "Qdrant/all_miniLM_L6_v2_with_attentions",
@@ -61,7 +62,12 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
alpha: float = 0.5,
**kwargs,
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:
@@ -74,40 +80,73 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
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
model_description = self._get_model_description(model_name)
# 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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
)
self.load_onnx_model(
model_dir=model_dir,
model_file=model_description["model_file"],
threads=threads,
providers=providers,
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.invert_vocab = {}
for token, idx in self.tokenizer.get_vocab().items():
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.special_tokens = set()
self.special_tokens_ids = set()
self.punctuation = set(string.punctuation)
self.stopwords = set(self._load_stopwords(model_dir))
self.stemmer = get_stemmer(MODEL_TO_LANGUAGE[model_name])
self.stopwords = set(self._load_stopwords(self._model_dir))
self.stemmer = SnowballStemmer(MODEL_TO_LANGUAGE[model_name])
self.alpha = alpha
def _filter_pair_tokens(self, tokens: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]:
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():
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 = []
for token, value in tokens:
if token in self.stopwords or token in self.punctuation:
@@ -115,17 +154,17 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
result.append((token, value))
return result
def _stem_pair_tokens(self, tokens: List[Tuple[str, Any]]) -> List[Tuple[str, Any]]:
def _stem_pair_tokens(self, tokens: list[tuple[str, Any]]) -> list[tuple[str, Any]]:
result = []
for token, value in tokens:
processed_token = self.stemmer.stemWord(token)
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]]:
cls, tokens: list[tuple[str, list[int]]], weights: list[float]
) -> list[tuple[str, float]]:
result = []
for token, idxs in tokens:
sum_weight = sum(weights[idx] for idx in idxs)
@@ -133,8 +172,8 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
return result
def _reconstruct_bpe(
self, bpe_tokens: Iterable[Tuple[int, str]]
) -> List[Tuple[str, List[int]]]:
self, bpe_tokens: Iterable[tuple[int, str]]
) -> list[tuple[str, list[int]]]:
result = []
acc = ""
acc_idx = []
@@ -161,7 +200,7 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
return result
def _rescore_vector(self, vector: Dict[str, float]) -> Dict[int, float]:
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.
@@ -180,6 +219,9 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
return new_vector
def _post_process_onnx_output(self, output: OnnxOutputContext) -> 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
# attention_value shape: (batch_size, num_heads, num_tokens, num_tokens)
@@ -209,16 +251,16 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
yield SparseEmbedding.from_dict(rescored)
@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.
"""
return supported_bm42_models
@classmethod
def _load_stopwords(cls, model_dir: Path) -> List[str]:
def _load_stopwords(cls, model_dir: Path) -> list[str]:
stopwords_path = model_dir / "stopwords.txt"
if not stopwords_path.exists():
return []
@@ -231,7 +273,7 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[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.
@@ -254,18 +296,23 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
documents=documents,
batch_size=batch_size,
parallel=parallel,
providers=self.providers,
cuda=self.cuda,
device_ids=self.device_ids,
alpha=self.alpha,
)
@classmethod
def _query_rehash(cls, tokens: Iterable[str]) -> Dict[int, float]:
def _query_rehash(cls, tokens: Iterable[str]) -> dict[int, float]:
result = {}
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) -> Iterable[SparseEmbedding]:
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.
@@ -274,6 +321,9 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
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)
document_tokens_with_ids = enumerate(encoded.tokens)
@@ -289,5 +339,9 @@ class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
class Bm42TextEmbeddingWorker(TextEmbeddingWorker):
def init_embedding(self, model_name: str, cache_dir: str, **kwargs) -> Bm42:
return Bm42(model_name=model_name, cache_dir=cache_dir, **kwargs)
def init_embedding(self, model_name: str, cache_dir: str, **kwargs: Any) -> Bm42:
return Bm42(
model_name=model_name,
cache_dir=cache_dir,
**kwargs,
)
+8 -10
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Dict, Iterable, Optional, Union
from typing import Iterable, Optional, Union, Any
import numpy as np
@@ -11,17 +11,17 @@ class SparseEmbedding:
values: np.ndarray
indices: np.ndarray
def as_object(self) -> Dict[str, np.ndarray]:
def as_object(self) -> dict[str, np.ndarray]:
return {
"values": self.values,
"indices": self.indices,
}
def as_dict(self) -> Dict[int, float]:
def as_dict(self) -> dict[int, float]:
return {i: v for i, v in zip(self.indices, self.values)}
@classmethod
def from_dict(cls, data: Dict[int, float]) -> "SparseEmbedding":
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())
@@ -34,7 +34,7 @@ class SparseTextEmbeddingBase(ModelManagement):
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
@@ -46,13 +46,11 @@ class SparseTextEmbeddingBase(ModelManagement):
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
) -> Iterable[SparseEmbedding]:
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[SparseEmbedding]:
"""
Embeds a list of text passages into a list of embeddings.
@@ -68,7 +66,7 @@ class SparseTextEmbeddingBase(ModelManagement):
yield from self.embed(texts, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
self, query: Union[str, Iterable[str]], **kwargs: Any
) -> Iterable[SparseEmbedding]:
"""
Embeds queries
+16 -7
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
from fastembed.common import OnnxProvider
from fastembed.sparse.bm25 import Bm25
@@ -12,15 +12,15 @@ import warnings
class SparseTextEmbedding(SparseTextEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[SparseTextEmbeddingBase]] = [SpladePP, Bm42, Bm25]
EMBEDDINGS_REGISTRY: list[Type[SparseTextEmbeddingBase]] = [SpladePP, Bm42, Bm25]
@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:
```
@@ -29,6 +29,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",
@@ -48,7 +49,10 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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 == "prithvida/Splade_PP_en_v1":
@@ -68,6 +72,9 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
cache_dir,
threads=threads,
providers=providers,
cuda=cuda,
device_ids=device_ids,
lazy_load=lazy_load,
**kwargs,
)
return
@@ -82,7 +89,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.
@@ -101,7 +108,9 @@ class SparseTextEmbedding(SparseTextEmbeddingBase):
"""
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs: Any
) -> Iterable[SparseEmbedding]:
"""
Embeds queries
+69 -17
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
from fastembed.common import OnnxProvider
@@ -14,7 +14,8 @@ supported_splade_models = [
{
"model": "prithivida/Splade_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"description": "Independent Implementation of SPLADE++ Model for English.",
"license": "apache-2.0",
"size_in_GB": 0.532,
"sources": {
"hf": "Qdrant/SPLADE_PP_en_v1",
@@ -24,7 +25,8 @@ supported_splade_models = [
{
"model": "prithvida/Splade_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"description": "Independent Implementation of SPLADE++ Model for English.",
"license": "apache-2.0",
"size_in_GB": 0.532,
"sources": {
"hf": "Qdrant/SPLADE_PP_en_v1",
@@ -36,6 +38,9 @@ supported_splade_models = [
class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[SparseEmbedding]:
if output.attention_mask is None:
raise ValueError("attention_mask must be provided for document post-processing")
relu_log = np.log(1 + np.maximum(output.model_output, 0))
weighted_log = relu_log * np.expand_dims(output.attention_mask, axis=-1)
@@ -50,11 +55,11 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
yield SparseEmbedding(values=scores, indices=indices)
@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.
"""
return supported_splade_models
@@ -64,7 +69,12 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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:
@@ -73,24 +83,57 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[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)
# 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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.load_onnx_model(
model_dir=model_dir,
model_file=model_description["model_file"],
threads=threads,
providers=providers,
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(
@@ -98,7 +141,7 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[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.
@@ -121,6 +164,10 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
documents=documents,
batch_size=batch_size,
parallel=parallel,
providers=self.providers,
cuda=self.cuda,
device_ids=self.device_ids,
**kwargs,
)
@classmethod
@@ -129,5 +176,10 @@ class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
class SpladePPEmbeddingWorker(TextEmbeddingWorker):
def init_embedding(self, model_name: str, cache_dir: str, **kwargs) -> SpladePP:
return SpladePP(model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs)
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,
)
+11 -4
View File
@@ -1,7 +1,15 @@
# This code is a modified copy of the `NLTKWordTokenizer` class from `NLTK` library.
import re
from typing import List
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:
@@ -68,12 +76,11 @@ class WordTokenizer:
)
]
CONTRACTIONS3 = [
re.compile(pattern)
for pattern in (r"(?i) ('t)(?#X)(is)\b", r"(?i) ('t)(?#X)(was)\b")
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]:
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.'''
+13 -8
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Type
from typing import Any, Iterable, Type
import numpy as np
@@ -11,6 +11,7 @@ supported_clip_models = [
"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": {
"hf": "Qdrant/clip-ViT-B-32-text",
@@ -26,24 +27,28 @@ class CLIPOnnxEmbedding(OnnxTextEmbedding):
return CLIPEmbeddingWorker
@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.
"""
return supported_clip_models
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[np.ndarray]:
return output.model_output
class CLIPEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self, model_name: str, cache_dir: str, **kwargs
self,
model_name: str,
cache_dir: str,
**kwargs: Any,
) -> OnnxTextEmbedding:
return CLIPOnnxEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
-64
View File
@@ -1,64 +0,0 @@
from typing import Any, Dict, List, Type
import numpy as np
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
from fastembed.text.onnx_text_model import TextEmbeddingWorker
supported_multilingual_e5_models = [
{
"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",
"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": "Text embeddings, Unimodal (text), Multilingual (~50 languages), 384 input tokens truncation, Prefixes for queries/documents: not necessary, 2021 year",
"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["TextEmbeddingWorker"]:
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], **kwargs
) -> 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, **kwargs
) -> E5OnnxEmbedding:
return E5OnnxEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
)
+98
View File
@@ -0,0 +1,98 @@
from enum import Enum
from typing import Any, Type, Iterable, Union, Optional
import numpy as np
from fastembed.text.pooled_normalized_embedding import PooledNormalizedEmbedding
from fastembed.text.onnx_embedding import OnnxTextEmbeddingWorker
from fastembed.text.onnx_text_model import TextEmbeddingWorker
supported_multitask_models = [
{
"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": {
"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, **kwargs: Any):
super().__init__(*args, **kwargs)
self._current_task_id = self.PASSAGE_TASK
@classmethod
def _get_worker_class(cls) -> Type["TextEmbeddingWorker"]:
return JinaEmbeddingV3Worker
@classmethod
def list_supported_models(cls) -> list[dict[str, Any]]:
return supported_multitask_models
def _preprocess_onnx_input(
self, onnx_input: dict[str, np.ndarray], **kwargs
) -> dict[str, np.ndarray]:
onnx_input["task_id"] = np.array(self._current_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: int = PASSAGE_TASK,
**kwargs,
) -> Iterable[np.ndarray]:
self._current_task_id = task_id
kwargs["task_id"] = task_id
yield from super().embed(documents, batch_size, parallel, **kwargs)
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[np.ndarray]:
self._current_task_id = self.QUERY_TASK
yield from super().embed(query, **kwargs)
def passage_embed(self, texts: Iterable[str], **kwargs) -> Iterable[np.ndarray]:
self._current_task_id = self.PASSAGE_TASK
yield from super().embed(texts, **kwargs)
class JinaEmbeddingV3Worker(OnnxTextEmbeddingWorker):
def init_embedding(
self,
model_name: str,
cache_dir: str,
**kwargs,
) -> JinaEmbeddingV3:
model = JinaEmbeddingV3(
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
model._current_task_id = kwargs["task_id"]
return model
+111 -43
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
@@ -12,9 +12,11 @@ supported_onnx_models = [
{
"model": "BAAI/bge-base-en",
"dim": 768,
"description": "Text embeddings, Unimodal (text), English, 512 input tokens truncation, Prefixes for queries/documents: necessary, 2023 year",
"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": {
"hf": "Qdrant/fast-bge-base-en",
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en.tar.gz",
},
"model_file": "model_optimized.onnx",
@@ -22,7 +24,8 @@ supported_onnx_models = [
{
"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",
"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": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
@@ -33,7 +36,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "qdrant/bge-large-en-v1.5-onnx",
@@ -43,9 +47,11 @@ supported_onnx_models = [
{
"model": "BAAI/bge-small-en",
"dim": 384,
"description": "Text embeddings, Unimodal (text), English, 512 input tokens truncation, Prefixes for queries/documents: necessary, 2023 year",
"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": {
"hf": "Qdrant/bge-small-en",
"url": "https://storage.googleapis.com/qdrant-fastembed/BAAI-bge-small-en.tar.gz",
},
"model_file": "model_optimized.onnx",
@@ -53,7 +59,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "qdrant/bge-small-en-v1.5-onnx-q",
@@ -63,27 +70,20 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "Qdrant/bge-small-zh-v1.5",
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-small-zh-v1.5.tar.gz",
},
"model_file": "model_optimized.onnx",
},
{
"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",
"size_in_GB": 0.22,
"sources": {
"hf": "qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q",
},
"model_file": "model_optimized.onnx",
},
{
"model": "thenlper/gte-large",
"dim": 1024,
"description": "Text embeddings, Unimodal (text), English, 512 input tokens truncation, Prefixes for queries/documents: not necessary, 2023 year",
"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": {
"hf": "qdrant/gte-large-onnx",
@@ -93,7 +93,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "mixedbread-ai/mxbai-embed-large-v1",
@@ -103,7 +104,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "snowflake/snowflake-arctic-embed-xs",
@@ -113,7 +115,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "snowflake/snowflake-arctic-embed-s",
@@ -123,7 +126,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "Snowflake/snowflake-arctic-embed-m",
@@ -133,7 +137,8 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "snowflake/snowflake-arctic-embed-m-long",
@@ -143,13 +148,25 @@ supported_onnx_models = [
{
"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",
"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": {
"hf": "snowflake/snowflake-arctic-embed-l",
},
"model_file": "onnx/model.onnx",
},
{
"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": {
"hf": "jinaai/jina-clip-v1",
},
"model_file": "onnx/text_model.onnx",
},
]
@@ -157,12 +174,12 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
"""Implementation of the Flag Embedding model."""
@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.
"""
return supported_onnx_models
@@ -172,7 +189,12 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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:
@@ -181,32 +203,54 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[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)
# 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
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]
else:
self.device_id = None
self.model_description = self._get_model_description(model_name)
self.cache_dir = define_cache_dir(cache_dir)
model_dir = self.download_model(
model_description, self.cache_dir, local_files_only=self._local_files_only
self._model_dir = self.download_model(
self.model_description,
self.cache_dir,
local_files_only=self._local_files_only,
specific_model_path=specific_model_path,
)
self.load_onnx_model(
model_dir=model_dir,
model_file=model_description["model_file"],
threads=threads,
providers=providers,
)
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,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
@@ -229,6 +273,9 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
documents=documents,
batch_size=batch_size,
parallel=parallel,
providers=self.providers,
cuda=self.cuda,
device_ids=self.device_ids,
**kwargs,
)
@@ -237,8 +284,8 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
return OnnxTextEmbeddingWorker
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
@@ -246,7 +293,23 @@ class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[np.ndarray]:
embeddings = output.model_output
return normalize(embeddings[:, 0]).astype(np.float32)
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).astype(np.float32)
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(TextEmbeddingWorker):
@@ -254,6 +317,11 @@ class OnnxTextEmbeddingWorker(TextEmbeddingWorker):
self,
model_name: str,
cache_dir: str,
**kwargs,
**kwargs: Any,
) -> OnnxTextEmbedding:
return OnnxTextEmbedding(model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs)
return OnnxTextEmbedding(
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
+41 -21
View File
@@ -1,7 +1,7 @@
import os
from multiprocessing import get_all_start_methods
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
from tokenizers import Encoding
@@ -14,7 +14,7 @@ from fastembed.parallel_processor import ParallelWorkerPool
class OnnxTextModel(OnnxModel[T]):
ONNX_OUTPUT_NAMES: Optional[List[str]] = None
ONNX_OUTPUT_NAMES: Optional[list[str]] = None
@classmethod
def _get_worker_class(cls) -> Type["TextEmbeddingWorker"]:
@@ -23,41 +23,49 @@ class OnnxTextModel(OnnxModel[T]):
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[T]:
raise NotImplementedError("Subclasses must implement this method")
def __init__(self) -> None:
def __init__(self):
super().__init__()
self.tokenizer = None
self.special_token_to_id = {}
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
self, onnx_input: dict[str, np.ndarray], **kwargs: Any
) -> dict[str, np.ndarray]:
"""
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,
specific_model_path: Optional[str] = None,
) -> None:
super().load_onnx_model(
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 tokenize(self, documents: List[str], **kwargs) -> List[Encoding]:
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)
def onnx_embed(
self,
documents: List[str],
**kwargs,
documents: list[str],
**kwargs: Any,
) -> OnnxOutputContext:
encoded = self.tokenize(documents, **kwargs)
input_ids = np.array([e.ids for e in encoded])
@@ -72,7 +80,6 @@ class OnnxTextModel(OnnxModel[T]):
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)
@@ -89,7 +96,10 @@ class OnnxTextModel(OnnxModel[T]):
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
providers: Optional[Sequence[OnnxProvider]] = None,
cuda: bool = False,
device_ids: Optional[list[int]] = None,
**kwargs: Any,
) -> Iterable[T]:
is_small = False
@@ -101,26 +111,36 @@ class OnnxTextModel(OnnxModel[T]):
if len(documents) < batch_size:
is_small = True
if parallel == 0:
parallel = os.cpu_count()
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))
else:
start_method = (
"forkserver" if "forkserver" in get_all_start_methods() else "spawn"
)
params = {"model_name": model_name, "cache_dir": cache_dir, **kwargs}
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,
**kwargs,
}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), start_method=start_method
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)
class TextEmbeddingWorker(EmbeddingWorker):
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
def process(self, items: Iterable[tuple[int, Any]]) -> Iterable[tuple[int, Any]]:
for idx, batch in items:
onnx_output = self.model.onnx_embed(batch)
yield idx, onnx_output
+58 -18
View File
@@ -1,9 +1,8 @@
from typing import Any, Dict, Iterable, List, Type
from typing import Any, Iterable, Type
import numpy as np
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import normalize
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
from fastembed.text.onnx_text_model import TextEmbeddingWorker
@@ -11,7 +10,8 @@ supported_pooled_models = [
{
"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",
"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": {
"hf": "nomic-ai/nomic-embed-text-v1.5",
@@ -21,7 +21,8 @@ supported_pooled_models = [
{
"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",
"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": {
"hf": "nomic-ai/nomic-embed-text-v1.5",
@@ -31,13 +32,49 @@ supported_pooled_models = [
{
"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",
"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": {
"hf": "nomic-ai/nomic-embed-text-v1",
},
"model_file": "onnx/model.onnx",
},
{
"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": {
"hf": "qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q",
},
"model_file": "model_optimized.onnx",
},
{
"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": {
"hf": "xenova/paraphrase-multilingual-mpnet-base-v2",
},
"model_file": "onnx/model.onnx",
},
{
"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": {
"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"],
},
]
@@ -47,14 +84,10 @@ class PooledEmbedding(OnnxTextEmbedding):
return PooledEmbeddingWorker
@classmethod
def mean_pooling(
cls, model_output: np.ndarray, attention_mask: np.ndarray
) -> np.ndarray:
def mean_pooling(cls, model_output: np.ndarray, attention_mask: np.ndarray) -> np.ndarray:
token_embeddings = model_output
input_mask_expanded = np.expand_dims(attention_mask, axis=-1)
input_mask_expanded = np.tile(
input_mask_expanded, (1, 1, token_embeddings.shape[-1])
)
input_mask_expanded = np.tile(input_mask_expanded, (1, 1, token_embeddings.shape[-1]))
input_mask_expanded = input_mask_expanded.astype(float)
sum_embeddings = np.sum(token_embeddings * input_mask_expanded, axis=1)
sum_mask = np.sum(input_mask_expanded, axis=1)
@@ -62,17 +95,18 @@ class PooledEmbedding(OnnxTextEmbedding):
return pooled_embeddings
@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.
"""
return supported_pooled_models
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[np.ndarray]:
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).astype(np.float32)
@@ -80,8 +114,14 @@ class PooledEmbedding(OnnxTextEmbedding):
class PooledEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self, model_name: str, cache_dir: str, **kwargs
self,
model_name: str,
cache_dir: str,
**kwargs: Any,
) -> OnnxTextEmbedding:
return PooledEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
+52 -13
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, Iterable, List, Type
from typing import Any, Iterable, Type
import numpy as np
@@ -12,7 +12,8 @@ supported_pooled_normalized_models = [
{
"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",
"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": {
"url": "https://storage.googleapis.com/qdrant-fastembed/sentence-transformers-all-MiniLM-L6-v2.tar.gz",
@@ -23,7 +24,8 @@ supported_pooled_normalized_models = [
{
"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",
"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": {"hf": "xenova/jina-embeddings-v2-base-en"},
"model_file": "onnx/model.onnx",
@@ -31,7 +33,8 @@ supported_pooled_normalized_models = [
{
"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",
"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": {"hf": "xenova/jina-embeddings-v2-small-en"},
"model_file": "onnx/model.onnx",
@@ -39,7 +42,8 @@ supported_pooled_normalized_models = [
{
"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",
"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": {"hf": "jinaai/jina-embeddings-v2-base-de"},
"model_file": "onnx/model_fp16.onnx",
@@ -47,11 +51,39 @@ supported_pooled_normalized_models = [
{
"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",
"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": {"hf": "jinaai/jina-embeddings-v2-base-code"},
"model_file": "onnx/model.onnx",
},
{
"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": {"hf": "jinaai/jina-embeddings-v2-base-zh"},
"model_file": "onnx/model.onnx",
},
{
"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": {"hf": "jinaai/jina-embeddings-v2-base-es"},
"model_file": "onnx/model.onnx",
},
{
"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": {"hf": "thenlper/gte-base"},
"model_file": "onnx/model.onnx",
},
]
@@ -61,17 +93,18 @@ class PooledNormalizedEmbedding(PooledEmbedding):
return PooledNormalizedEmbeddingWorker
@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.
"""
return supported_pooled_normalized_models
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[np.ndarray]:
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)).astype(np.float32)
@@ -79,8 +112,14 @@ class PooledNormalizedEmbedding(PooledEmbedding):
class PooledNormalizedEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self, model_name: str, cache_dir: str, **kwargs
self,
model_name: str,
cache_dir: str,
**kwargs: Any,
) -> OnnxTextEmbedding:
return PooledNormalizedEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
model_name=model_name,
cache_dir=cache_dir,
threads=1,
**kwargs,
)
+71 -16
View File
@@ -1,32 +1,32 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
import warnings
from typing import Any, Iterable, Optional, Sequence, Type, Union
import numpy as np
from fastembed.common import OnnxProvider
from fastembed.text.clip_embedding import CLIPOnnxEmbedding
from fastembed.text.e5_onnx_embedding import E5OnnxEmbedding
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
class TextEmbedding(TextEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[TextEmbeddingBase]] = [
EMBEDDINGS_REGISTRY: list[Type[TextEmbeddingBase]] = [
OnnxTextEmbedding,
E5OnnxEmbedding,
CLIPOnnxEmbedding,
PooledNormalizedEmbedding,
PooledEmbedding,
JinaEmbeddingV3,
]
@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:
```
@@ -35,6 +35,7 @@ class TextEmbedding(TextEmbeddingBase):
"model": "intfloat/multilingual-e5-large",
"dim": 1024,
"description": "Multilingual model, e5-large. Recommend using this model for non-English languages",
"license": "mit",
"size_in_GB": 2.24,
"sources": {
"gcp": "https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
@@ -55,27 +56,54 @@ class TextEmbedding(TextEmbeddingBase):
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
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 == "nomic-ai/nomic-embed-text-v1.5-Q":
warnings.warn(
"The model 'nomic-ai/nomic-embed-text-v1.5-Q' has been updated on HuggingFace. "
"Please review the latest documentation and release notes to ensure compatibility with your workflow. ",
UserWarning,
stacklevel=2,
)
if model_name == "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2":
warnings.warn(
"The model 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' has been updated to "
"include a mean pooling layer. Please ensure your usage aligns with the new functionality. "
"Support for the previous version without mean pooling will be removed as of version 0.5.2.",
UserWarning,
stacklevel=2,
)
if model_name in {
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
"intfloat/multilingual-e5-large",
}:
warnings.warn(
f"{model_name} has been updated as of fastembed 0.5.2, outputs are now average pooled.",
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
):
if any(model_name.lower() == model["model"].lower() for model in supported_models):
self.model = EMBEDDING_MODEL_TYPE(
model_name,
cache_dir,
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()`"
)
@@ -84,7 +112,7 @@ class TextEmbedding(TextEmbeddingBase):
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
@@ -102,3 +130,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[np.ndarray]:
"""
Embeds queries
Args:
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
Returns:
Iterable[np.ndarray]: 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[np.ndarray]:
"""
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)
+6 -7
View File
@@ -1,4 +1,4 @@
from typing import Iterable, Optional, Union
from typing import Iterable, Optional, Union, Any
import numpy as np
@@ -11,7 +11,7 @@ class TextEmbeddingBase(ModelManagement):
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
@@ -23,11 +23,11 @@ class TextEmbeddingBase(ModelManagement):
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
**kwargs: Any,
) -> Iterable[np.ndarray]:
raise NotImplementedError()
def passage_embed(self, texts: Iterable[str], **kwargs) -> Iterable[np.ndarray]:
def passage_embed(self, texts: Iterable[str], **kwargs: Any) -> Iterable[np.ndarray]:
"""
Embeds a list of text passages into a list of embeddings.
@@ -42,9 +42,8 @@ class TextEmbeddingBase(ModelManagement):
# 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[np.ndarray]:
"""
Embeds queries
+26 -13
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastembed"
version = "0.3.6"
version = "0.5.1"
description = "Fast, light, accurate library built for retrieval embedding generation"
authors = ["Qdrant Team <info@qdrant.tech>", "NirantK <nirant.bits@gmail.com>"]
license = "Apache License"
@@ -11,28 +11,35 @@ 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,<2.1.0", python = "<3.10" },
{ version = ">=1.21", python = ">=3.10,<3.12" },
{ version = ">=1.26", python = ">=3.12,<3.13" },
{ version = ">=2.1.0", python = ">=3.13" }
]
onnxruntime = [
{ version = ">=1.17.0,<1.20.0", python = "<3.10" },
{ version = ">=1.17.0,!=1.20.0", python = ">=3.10,<3.13" },
{ version = ">1.20.0", python = ">=3.13" }
]
tqdm = "^4.66"
requests = "^2.31"
tokenizers = ">=0.15,<1.0"
huggingface-hub = ">=0.20,<1.0"
loguru = "^0.7.2"
numpy = [
{ version = ">=1.21, <2", python = "<3.12" },
{ version = ">=1.26, <2", python = ">=3.12" }
]
pillow = "^10.3.0"
snowballstemmer = "^2.2.0"
PyStemmer = "^2.2.0"
mmh3 = "^4.0"
mmh3 = "^4.1.0"
py-rust-stemmers = "^0.1.0"
[tool.poetry.group.dev.dependencies]
[tool.poetry.group.test.dependencies]
pytest = "^7.4.2"
ruff = ">=0.3.1,<1.0"
[tool.poetry.group.dev.dependencies]
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"
@@ -41,10 +48,16 @@ pillow = "^10.2.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
+7 -9
View File
@@ -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,7 +56,7 @@ class HF:
self.model = AutoModel.from_pretrained(model_id)
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
def embed(self, texts: List[str]):
def embed(self, texts: list[str]):
encoded_input = self.tokenizer(
texts, max_length=512, padding=True, truncation=True, return_tensors="pt"
)
@@ -88,7 +88,7 @@ embedding_model = DefaultEmbedding()
# %%
def calculate_time_stats(
embed_func: Callable, documents: list, k: int
) -> Tuple[float, float, float]:
) -> tuple[float, float, float]:
times = []
for _ in range(k):
# Timing the embed_func call
@@ -105,16 +105,14 @@ def calculate_time_stats(
# %%
hf_stats = calculate_time_stats(hf.embed, documents, k=2)
print(f"Huggingface Transformers (Average, Max, Min): {hf_stats}")
fst_stats = calculate_time_stats(
lambda x: list(embedding_model.embed(x)), documents, k=2
)
fst_stats = calculate_time_stats(lambda x: list(embedding_model.embed(x)), documents, k=2)
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],
hf_stats: tuple[float, float, float],
fst_stats: tuple[float, float, float],
documents: list,
):
# Calculating total characters in documents
+62 -7
View File
@@ -1,11 +1,15 @@
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):
def test_attention_embeddings(model_name: str) -> None:
is_ci = os.getenv("CI")
model = SparseTextEmbedding(model_name=model_name)
output = list(
@@ -62,9 +66,14 @@ def test_attention_embeddings(model_name):
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):
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
@@ -82,9 +91,14 @@ def test_parallel_processing(model_name):
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):
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")
@@ -92,13 +106,54 @@ def test_multilanguage(model_name):
assert embeddings[0].values.shape == (3,)
assert embeddings[0].indices.shape == (3,)
assert embeddings[1].values.shape == (2,)
assert embeddings[1].indices.shape == (2,)
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 == (4,)
assert embeddings[0].indices.shape == (4,)
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))
+31 -3
View File
@@ -8,6 +8,7 @@ from PIL import Image
from fastembed import ImageEmbedding
from tests.config import TEST_MISC_DIR
from tests.utils import delete_model_cache
CANONICAL_VECTOR_VALUES = {
"Qdrant/clip-ViT-B-32-vision": np.array([-0.0098, 0.0128, -0.0274, 0.002, -0.0059]),
@@ -20,10 +21,13 @@ CANONICAL_VECTOR_VALUES = {
"Qdrant/Unicom-ViT-B-32": np.array(
[0.0418, 0.0550, 0.0003, 0.0253, -0.0185, 0.0016, -0.0368, -0.0402, -0.0891, -0.0186]
),
"jinaai/jina-clip-v1": np.array(
[-0.029, 0.0216, 0.0396, 0.0283, -0.0023, 0.0151, 0.011, -0.0235, 0.0251, -0.0343]
),
}
def test_embedding():
def test_embedding() -> None:
is_ci = os.getenv("CI")
for model_desc in ImageEmbedding.list_supported_models():
@@ -52,9 +56,13 @@ def test_embedding():
assert np.allclose(embeddings[1], embeddings[2]), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize("n_dims,model_name", [(512, "Qdrant/clip-ViT-B-32-vision")])
def test_batch_embedding(n_dims, model_name):
def test_batch_embedding(n_dims: int, model_name: str) -> None:
is_ci = os.getenv("CI")
model = ImageEmbedding(model_name=model_name)
n_images = 32
test_images = [
@@ -68,10 +76,13 @@ def test_batch_embedding(n_dims, model_name):
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(test_images) * n_images, n_dims)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize("n_dims,model_name", [(512, "Qdrant/clip-ViT-B-32-vision")])
def test_parallel_processing(n_dims, model_name):
def test_parallel_processing(n_dims: int, model_name: str) -> None:
is_ci = os.getenv("CI")
model = ImageEmbedding(model_name=model_name)
n_images = 32
@@ -93,3 +104,20 @@ def test_parallel_processing(n_dims, model_name):
assert embeddings.shape == (n_images * len(test_images), n_dims)
assert np.allclose(embeddings, embeddings_2, atol=1e-3)
assert np.allclose(embeddings, embeddings_3, atol=1e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize("model_name", ["Qdrant/clip-ViT-B-32-vision"])
def test_lazy_load(model_name: str) -> None:
is_ci = os.getenv("CI")
model = ImageEmbedding(model_name=model_name, lazy_load=True)
assert not hasattr(model.model, "model")
images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
]
list(model.embed(images))
assert hasattr(model.model, "model")
if is_ci:
delete_model_cache(model.model._model_dir)
+92 -3
View File
@@ -1,8 +1,12 @@
import os
import pytest
import numpy as np
from fastembed.late_interaction.late_interaction_text_embedding import (
LateInteractionTextEmbedding,
)
from tests.utils import delete_model_cache
# vectors are abridged and rounded for brevity
CANONICAL_COLUMN_VALUES = {
@@ -24,6 +28,15 @@ CANONICAL_COLUMN_VALUES = {
[-0.07281, 0.04633, -0.04711, 0.00762, -0.07374],
]
),
"jinaai/jina-colbert-v2": np.array(
[
[0.0742, 0.0591, -0.2403, -0.1774, 0.02],
[0.1318, 0.0882, -0.1138, -0.2066, 0.146],
[-0.0183, -0.1354, -0.0139, -0.1079, -0.051],
[0.0003, -0.1184, -0.07, -0.0479, -0.0649],
[0.0766, 0.0452, -0.2343, -0.183, 0.0058],
]
),
}
CANONICAL_QUERY_VALUES = {
@@ -99,12 +112,49 @@ CANONICAL_QUERY_VALUES = {
[-0.03473, 0.04792, -0.07033, 0.02196, -0.05314],
]
),
"jinaai/jina-colbert-v2": np.array(
[
[0.0477, 0.0255, -0.2224, -0.1085, -0.03],
[0.0206, -0.0845, -0.0075, -0.1712, 0.0156],
[-0.0056, -0.0957, -0.0147, -0.1277, -0.0225],
[0.0486, -0.0499, -0.1609, 0.0194, 0.0274],
[0.0481, 0.0253, -0.2278, -0.1126, -0.0294],
[0.0599, -0.0678, -0.0956, -0.0757, 0.0236],
[0.0592, -0.0862, -0.0621, -0.1084, 0.0155],
[0.0874, -0.0714, -0.0772, -0.1414, 0.037],
[0.1009, -0.0552, -0.0669, -0.163, 0.0493],
[0.1135, -0.047, -0.0576, -0.1699, 0.0538],
[0.1228, -0.0428, -0.0507, -0.1725, 0.0562],
[0.1291, -0.0388, -0.042, -0.1753, 0.0569],
[0.1365, -0.0337, -0.0326, -0.1786, 0.0574],
[0.1439, -0.026, -0.024, -0.1831, 0.0574],
[0.1527, -0.0099, -0.0179, -0.1874, 0.057],
[0.1555, 0.0186, -0.023, -0.1801, 0.0539],
[0.1389, 0.054, -0.0345, -0.1636, 0.0429],
[0.1058, 0.0862, -0.0418, -0.1455, 0.0222],
[0.0713, 0.1061, -0.0438, -0.1288, 0.0002],
[0.0453, 0.1143, -0.0457, -0.1119, -0.019],
[0.0346, 0.1131, -0.0487, -0.0952, -0.0338],
[0.0355, 0.1073, -0.0493, -0.0823, -0.0438],
[0.0424, 0.1041, -0.0459, -0.0761, -0.048],
[0.048, 0.102, -0.0421, -0.0718, -0.0477],
[0.0474, 0.0989, -0.0413, -0.0654, -0.0431],
[0.0434, 0.095, -0.0415, -0.0589, -0.0345],
[0.0408, 0.0897, -0.0405, -0.0554, -0.0197],
[0.0433, 0.0811, -0.0407, -0.0545, 0.0055],
[0.0514, 0.0629, -0.0446, -0.0549, 0.0368],
[0.058, 0.048, -0.0527, -0.0607, 0.0568],
[0.0561, 0.0447, -0.0661, -0.0702, 0.0764],
[0.0204, -0.0856, -0.0386, -0.1232, -0.0332],
]
),
}
docs = ["Hello World"]
def test_batch_embedding():
is_ci = os.getenv("CI")
docs_to_embed = docs * 10
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
@@ -114,10 +164,14 @@ def test_batch_embedding():
for value in result:
token_num, abridged_dim = expected_result.shape
assert np.allclose(value[:, :abridged_dim], expected_result, atol=10e-4)
assert np.allclose(value[:, :abridged_dim], expected_result, atol=2e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding():
is_ci = os.getenv("CI")
docs_to_embed = docs
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
@@ -125,10 +179,14 @@ def test_single_embedding():
model = LateInteractionTextEmbedding(model_name=model_name)
result = next(iter(model.embed(docs_to_embed, batch_size=6)))
token_num, abridged_dim = expected_result.shape
assert np.allclose(result[:, :abridged_dim], expected_result, atol=10e-4)
assert np.allclose(result[:, :abridged_dim], expected_result, atol=2e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding_query():
is_ci = os.getenv("CI")
queries_to_embed = docs
for model_name, expected_result in CANONICAL_QUERY_VALUES.items():
@@ -136,10 +194,14 @@ def test_single_embedding_query():
model = LateInteractionTextEmbedding(model_name=model_name)
result = next(iter(model.query_embed(queries_to_embed)))
token_num, abridged_dim = expected_result.shape
assert np.allclose(result[:, :abridged_dim], expected_result, atol=10e-4)
assert np.allclose(result[:, :abridged_dim], expected_result, atol=2e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
def test_parallel_processing():
is_ci = os.getenv("CI")
model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
token_dim = 128
docs = ["hello world", "flag embedding"] * 100
@@ -155,3 +217,30 @@ def test_parallel_processing():
assert embeddings.shape[0] == len(docs) and embeddings.shape[-1] == token_dim
assert np.allclose(embeddings, embeddings_2, atol=1e-3)
assert np.allclose(embeddings, embeddings_3, atol=1e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
["colbert-ir/colbertv2.0"],
)
def test_lazy_load(model_name: str):
is_ci = os.getenv("CI")
model = LateInteractionTextEmbedding(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 = LateInteractionTextEmbedding(model_name=model_name, lazy_load=True)
list(model.query_embed(docs))
model = LateInteractionTextEmbedding(model_name=model_name, lazy_load=True)
list(model.passage_embed(docs))
if is_ci:
delete_model_cache(model.model._model_dir)
+221
View File
@@ -0,0 +1,221 @@
import pytest
from typing import Optional
from fastembed import (
TextEmbedding,
SparseTextEmbedding,
LateInteractionTextEmbedding,
ImageEmbedding,
)
from fastembed.rerank.cross_encoder import TextCrossEncoder
from tests.config import TEST_MISC_DIR
CACHE_DIR = "../model_cache"
@pytest.mark.skip(reason="Requires a multi-gpu server")
@pytest.mark.parametrize("device_id", [None, 0, 1])
def test_gpu_via_providers(device_id: Optional[int]) -> None:
docs = ["hello world", "flag embedding"]
device_id = device_id if device_id is not None else 0
providers = (
["CUDAExecutionProvider"]
if device_id is None
else [("CUDAExecutionProvider", {"device_id": device_id})]
)
embedding_model = TextEmbedding(
"sentence-transformers/all-MiniLM-L6-v2",
providers=providers,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
embedding_model = SparseTextEmbedding(
"prithvida/Splade_PP_en_v1",
providers=providers,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
embedding_model = SparseTextEmbedding(
"Qdrant/bm42-all-minilm-l6-v2-attentions",
providers=providers,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
embedding_model = LateInteractionTextEmbedding(
"colbert-ir/colbertv2.0",
providers=providers,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
embedding_model = ImageEmbedding(
model_name="Qdrant/clip-ViT-B-32-vision",
providers=providers,
cache_dir=CACHE_DIR,
)
images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
]
list(embedding_model.embed(images))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
model = TextCrossEncoder(
model_name="Xenova/ms-marco-MiniLM-L-6-v2",
providers=providers,
cache_dir=CACHE_DIR,
)
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
list(model.rerank(query, documents))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id)
@pytest.mark.skip(reason="Requires a multi-gpu server")
@pytest.mark.parametrize("device_ids", [None, [0], [1], [0, 1]])
def test_gpu_cuda_device_ids(device_ids: Optional[list[int]]) -> None:
docs = ["hello world", "flag embedding"]
device_id = device_ids[0] if device_ids else 0
embedding_model = TextEmbedding(
"sentence-transformers/all-MiniLM-L6-v2",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(
device_id
), f"Text embedding: {options}"
embedding_model = SparseTextEmbedding(
"prithvida/Splade_PP_en_v1",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(
device_id
), f"Sparse text embedding: {options}"
embedding_model = SparseTextEmbedding(
"Qdrant/bm42-all-minilm-l6-v2-attentions",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(device_id), f"Bm42: {options}"
embedding_model = LateInteractionTextEmbedding(
"colbert-ir/colbertv2.0",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(
device_id
), f"Late interaction text embedding: {options}"
embedding_model = ImageEmbedding(
model_name="Qdrant/clip-ViT-B-32-vision",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
]
list(embedding_model.embed(images))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(
device_id
), f"Image embedding: {options}"
if device_ids is None or len(device_ids) == 1:
model = TextCrossEncoder(
model_name="Xenova/ms-marco-MiniLM-L-6-v2",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
list(model.rerank(query, documents))
options = embedding_model.model.model.get_provider_options()
assert options["CUDAExecutionProvider"]["device_id"] == str(
device_id
), f"Text cross encoder: {options}"
@pytest.mark.skip(reason="Requires a multi-gpu server")
@pytest.mark.parametrize(
"device_ids,parallel", [(None, None), (None, 2), ([1], None), ([1], 1), ([1], 2), ([0, 1], 2)]
)
def test_multi_gpu_parallel_inference(device_ids: Optional[list[int]], parallel: int) -> None:
docs = ["hello world", "flag embedding"] * 100
batch_size = 5
embedding_model = TextEmbedding(
"sentence-transformers/all-MiniLM-L6-v2",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
lazy_load=True,
)
list(embedding_model.embed(docs, batch_size=batch_size, parallel=parallel))
embedding_model = SparseTextEmbedding(
"prithvida/Splade_PP_en_v1",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs, batch_size=batch_size, parallel=parallel))
embedding_model = SparseTextEmbedding(
"Qdrant/bm42-all-minilm-l6-v2-attentions",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs, batch_size=batch_size, parallel=parallel))
embedding_model = LateInteractionTextEmbedding(
"colbert-ir/colbertv2.0",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
list(embedding_model.embed(docs, batch_size=batch_size, parallel=parallel))
embedding_model = ImageEmbedding(
model_name="Qdrant/clip-ViT-B-32-vision",
cuda=True,
device_ids=device_ids,
cache_dir=CACHE_DIR,
)
images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
] * 100
list(embedding_model.embed(images, batch_size=batch_size, parallel=parallel))
+34
View File
@@ -0,0 +1,34 @@
import os
import numpy as np
from fastembed.late_interaction.late_interaction_text_embedding import (
LateInteractionTextEmbedding,
)
from fastembed.common.pooling import LateInteractionPooler
from tests.utils import delete_model_cache
CANONICAL_COLUMN_VALUES = {
"colbert-ir/colbertv2.0": np.array(
[4.0727495e-03, -2.4026826e-03, -6.8204990e-04, -7.1383954e-05, 4.4963313e-03]
),
}
docs = ["Hello World"]
def test_batch_embedding():
is_ci = os.getenv("CI")
docs_to_embed = docs * 10
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
print("evaluating", model_name)
model = LateInteractionTextEmbedding(model_name=model_name)
pooler = LateInteractionPooler()
result = list(model.embed(docs_to_embed, batch_size=6))
pooled_result = pooler.pool(result)
assert np.allclose(pooled_result[0], expected_result, atol=2e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
+106 -12
View File
@@ -1,6 +1,11 @@
import pytest
import os
import pytest
import numpy as np
from fastembed.sparse.bm25 import Bm25
from fastembed.sparse.sparse_text_embedding import SparseTextEmbedding
from tests.utils import delete_model_cache
CANONICAL_COLUMN_VALUES = {
"prithvida/Splade_PP_en_v1": {
@@ -44,7 +49,8 @@ CANONICAL_COLUMN_VALUES = {
docs = ["Hello World"]
def test_batch_embedding():
def test_batch_embedding() -> None:
is_ci = os.getenv("CI")
docs_to_embed = docs * 10
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
@@ -54,9 +60,12 @@ def test_batch_embedding():
for i, value in enumerate(result.values):
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding():
def test_single_embedding() -> None:
is_ci = os.getenv("CI")
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
model = SparseTextEmbedding(model_name=model_name)
@@ -67,11 +76,12 @@ def test_single_embedding():
for i, value in enumerate(result.values):
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_parallel_processing():
import numpy as np
def test_parallel_processing() -> None:
is_ci = os.getenv("CI")
model = SparseTextEmbedding(model_name="prithivida/Splade_PP_en_v1")
docs = ["hello world", "flag embedding"] * 30
sparse_embeddings_duo = list(model.embed(docs, batch_size=10, parallel=2))
@@ -93,9 +103,93 @@ def test_parallel_processing():
== sparse_embedding_duo.indices.tolist()
== sparse_embedding_all.indices.tolist()
)
assert np.allclose(
sparse_embedding.values, sparse_embedding_duo.values, atol=1e-3
)
assert np.allclose(
sparse_embedding.values, sparse_embedding_all.values, atol=1e-3
)
assert np.allclose(sparse_embedding.values, sparse_embedding_duo.values, atol=1e-3)
assert np.allclose(sparse_embedding.values, sparse_embedding_all.values, atol=1e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.fixture
def bm25_instance() -> None:
ci = os.getenv("CI", True)
model = Bm25("Qdrant/bm25", language="english")
yield model
if ci:
delete_model_cache(model._model_dir)
def test_stem_with_stopwords_and_punctuation(bm25_instance: Bm25) -> None:
# Setup
bm25_instance.stopwords = {"the", "is", "a"}
bm25_instance.punctuation = {".", ",", "!"}
# Test data
tokens = ["The", "quick", "brown", "fox", "is", "a", "test", "sentence", ".", "!"]
# Execute
result = bm25_instance._stem(tokens)
# Assert
expected = ["quick", "brown", "fox", "test", "sentenc"]
assert result == expected, f"Expected {expected}, but got {result}"
def test_stem_case_insensitive_stopwords(bm25_instance: Bm25) -> None:
# Setup
bm25_instance.stopwords = {"the", "is", "a"}
bm25_instance.punctuation = {".", ",", "!"}
# Test data
tokens = ["THE", "Quick", "Brown", "Fox", "IS", "A", "Test", "Sentence", ".", "!"]
# Execute
result = bm25_instance._stem(tokens)
# Assert
expected = ["quick", "brown", "fox", "test", "sentenc"]
assert result == expected, f"Expected {expected}, but got {result}"
@pytest.mark.parametrize("disable_stemmer", [True, False])
def test_disable_stemmer_behavior(disable_stemmer: bool) -> None:
# Setup
model = Bm25("Qdrant/bm25", language="english", disable_stemmer=disable_stemmer)
model.stopwords = {"the", "is", "a"}
model.punctuation = {".", ",", "!"}
# Test data
tokens = ["The", "quick", "brown", "fox", "is", "a", "test", "sentence", ".", "!"]
# Execute
result = model._stem(tokens)
# Assert
if disable_stemmer:
expected = ["quick", "brown", "fox", "test", "sentence"] # no stemming, lower case only
else:
expected = ["quick", "brown", "fox", "test", "sentenc"]
assert result == expected, f"Expected {expected}, but got {result}"
@pytest.mark.parametrize(
"model_name",
["prithivida/Splade_PP_en_v1"],
)
def test_lazy_load(model_name: str) -> None:
is_ci = os.getenv("CI")
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))
if is_ci:
delete_model_cache(model.model._model_dir)
+119
View File
@@ -0,0 +1,119 @@
import os
import numpy as np
import pytest
from fastembed.rerank.cross_encoder import TextCrossEncoder
from tests.utils import delete_model_cache
CANONICAL_SCORE_VALUES = {
"Xenova/ms-marco-MiniLM-L-6-v2": np.array([8.500708, -2.541011]),
"Xenova/ms-marco-MiniLM-L-12-v2": np.array([9.330912, -2.0380247]),
"BAAI/bge-reranker-base": np.array([6.15733337, -3.65939403]),
"jinaai/jina-reranker-v1-tiny-en": np.array([2.5911, 0.1122]),
"jinaai/jina-reranker-v1-turbo-en": np.array([1.8295, -2.8908]),
"jinaai/jina-reranker-v2-base-multilingual": np.array([1.6533, -1.6455]),
}
SELECTED_MODELS = {
"Xenova": "Xenova/ms-marco-MiniLM-L-6-v2",
"BAAI": "BAAI/bge-reranker-base",
"jinaai": "jinaai/jina-reranker-v1-tiny-en",
}
@pytest.mark.parametrize(
"model_name",
[model_name for model_name in CANONICAL_SCORE_VALUES],
)
def test_rerank(model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextCrossEncoder(model_name=model_name)
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
scores = np.array(list(model.rerank(query, documents)))
pairs = [(query, doc) for doc in documents]
scores2 = np.array(list(model.rerank_pairs(pairs)))
assert np.allclose(
scores, scores2, atol=1e-5
), f"Model: {model_name}, Scores: {scores}, Scores2: {scores2}"
canonical_scores = CANONICAL_SCORE_VALUES[model_name]
assert np.allclose(
scores, canonical_scores, atol=1e-3
), f"Model: {model_name}, Scores: {scores}, Expected: {canonical_scores}"
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
[model_name for model_name in SELECTED_MODELS.values()],
)
def test_batch_rerank(model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextCrossEncoder(model_name=model_name)
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."] * 50
scores = np.array(list(model.rerank(query, documents, batch_size=10)))
pairs = [(query, doc) for doc in documents]
scores2 = np.array(list(model.rerank_pairs(pairs)))
assert np.allclose(
scores, scores2, atol=1e-5
), f"Model: {model_name}, Scores: {scores}, Scores2: {scores2}"
canonical_scores = np.tile(CANONICAL_SCORE_VALUES[model_name], 50)
assert scores.shape == canonical_scores.shape, f"Unexpected shape for model {model_name}"
assert np.allclose(
scores, canonical_scores, atol=1e-3
), f"Model: {model_name}, Scores: {scores}, Expected: {canonical_scores}"
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
["Xenova/ms-marco-MiniLM-L-6-v2"],
)
def test_lazy_load(model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextCrossEncoder(model_name=model_name, lazy_load=True)
assert not hasattr(model.model, "model")
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."]
list(model.rerank(query, documents))
assert hasattr(model.model, "model")
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
[model_name for model_name in SELECTED_MODELS.values()],
)
def test_rerank_pairs_parallel(model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextCrossEncoder(model_name=model_name)
query = "What is the capital of France?"
documents = ["Paris is the capital of France.", "Berlin is the capital of Germany."] * 10
pairs = [(query, doc) for doc in documents]
scores_parallel = np.array(list(model.rerank_pairs(pairs, parallel=2, batch_size=10)))
scores_sequential = np.array(list(model.rerank_pairs(pairs, batch_size=10)))
assert np.allclose(
scores_parallel, scores_sequential, atol=1e-5
), f"Model: {model_name}, Scores (Parallel): {scores_parallel}, Scores (Sequential): {scores_sequential}"
canonical_scores = CANONICAL_SCORE_VALUES[model_name]
assert np.allclose(
scores_parallel[: len(canonical_scores)], canonical_scores, atol=1e-3
), f"Model: {model_name}, Scores (Parallel): {scores_parallel}, Expected: {canonical_scores}"
if is_ci:
delete_model_cache(model.model._model_dir)
+253
View File
@@ -0,0 +1,253 @@
import os
import numpy as np
import pytest
from fastembed import TextEmbedding
from fastembed.text.multitask_embedding import Task
from tests.utils import delete_model_cache
CANONICAL_VECTOR_VALUES = {
"jinaai/jina-embeddings-v3": [
{
"task_id": Task.RETRIEVAL_QUERY,
"vectors": np.array(
[
[0.0623, -0.0402, 0.1706, -0.0143, 0.0617],
[-0.1064, -0.0733, 0.0353, 0.0096, 0.0667],
]
),
},
{
"task_id": Task.RETRIEVAL_PASSAGE,
"vectors": np.array(
[
[0.0513, -0.0247, 0.1751, -0.0075, 0.0679],
[-0.0987, -0.0786, 0.09, 0.0087, 0.0577],
]
),
},
{
"task_id": Task.SEPARATION,
"vectors": np.array(
[
[0.094, -0.1065, 0.1305, 0.0547, 0.0556],
[0.0315, -0.1468, 0.065, 0.0568, 0.0546],
]
),
},
{
"task_id": Task.CLASSIFICATION,
"vectors": np.array(
[
[0.0606, -0.0877, 0.1384, 0.0065, 0.0722],
[-0.0502, -0.119, 0.032, 0.0514, 0.0689],
]
),
},
{
"task_id": Task.TEXT_MATCHING,
"vectors": np.array(
[
[0.0911, -0.0341, 0.1305, -0.026, 0.0576],
[-0.1432, -0.05, 0.0133, 0.0464, 0.0789],
]
),
},
]
}
docs = ["Hello World", "Follow the white rabbit."]
def test_batch_embedding():
is_ci = os.getenv("CI")
docs_to_embed = docs * 10
default_task = Task.RETRIEVAL_PASSAGE
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
model_name = model_desc["model"]
dim = model_desc["dim"]
if model_name not in CANONICAL_VECTOR_VALUES.keys():
continue
model = TextEmbedding(model_name=model_name)
print(f"evaluating {model_name} default task")
embeddings = list(model.embed(documents=docs_to_embed, batch_size=6))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(docs_to_embed), dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][default_task]["vectors"]
assert np.allclose(
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding():
is_ci = os.getenv("CI")
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
model_name = model_desc["model"]
dim = model_desc["dim"]
if model_name not in CANONICAL_VECTOR_VALUES.keys():
continue
model = TextEmbedding(model_name=model_name)
for task in CANONICAL_VECTOR_VALUES[model_name]:
print(f"evaluating {model_name} task_id: {task['task_id']}")
embeddings = list(model.embed(documents=docs, task_id=task["task_id"]))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(docs), dim)
canonical_vector = task["vectors"]
assert np.allclose(
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding_query():
is_ci = os.getenv("CI")
task_id = Task.RETRIEVAL_QUERY
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
model_name = model_desc["model"]
dim = model_desc["dim"]
if model_name not in CANONICAL_VECTOR_VALUES.keys():
continue
model = TextEmbedding(model_name=model_name)
print(f"evaluating {model_name} query_embed task_id: {task_id}")
embeddings = list(model.query_embed(query=docs))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(docs), dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][task_id]["vectors"]
assert np.allclose(
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_single_embedding_passage():
is_ci = os.getenv("CI")
task_id = Task.RETRIEVAL_PASSAGE
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
model_name = model_desc["model"]
dim = model_desc["dim"]
if model_name not in CANONICAL_VECTOR_VALUES.keys():
continue
model = TextEmbedding(model_name=model_name)
print(f"evaluating {model_name} passage_embed task_id: {task_id}")
embeddings = list(model.passage_embed(texts=docs))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(docs), dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][task_id]["vectors"]
assert np.allclose(
embeddings[: len(docs), : canonical_vector.shape[1]], canonical_vector, atol=1e-4
), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
def test_parallel_processing():
is_ci = os.getenv("CI")
docs = ["Hello World", "Follow the white rabbit."] * 10
model_name = "jinaai/jina-embeddings-v3"
dim = 1024
model = TextEmbedding(model_name=model_name)
task_id = Task.SEPARATION
embeddings_1 = list(model.embed(docs, batch_size=10, parallel=None, task_id=task_id))
embeddings_1 = np.stack(embeddings_1, axis=0)
embeddings_2 = list(model.embed(docs, batch_size=10, parallel=1, task_id=task_id))
embeddings_2 = np.stack(embeddings_2, axis=0)
assert embeddings_1.shape[0] == len(docs) and embeddings_1.shape[-1] == dim
assert np.allclose(embeddings_1, embeddings_2, atol=1e-4)
canonical_vector = CANONICAL_VECTOR_VALUES[model_name][task_id]["vectors"]
assert np.allclose(embeddings_2[:2, : canonical_vector.shape[1]], canonical_vector, atol=1e-4)
if is_ci:
delete_model_cache(model.model._model_dir)
def test_task_assignment():
is_ci = os.getenv("CI")
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
model_name = model_desc["model"]
if model_name not in CANONICAL_VECTOR_VALUES.keys():
continue
model = TextEmbedding(model_name=model_name)
for i, task_id in enumerate(Task):
_ = list(model.embed(documents=docs, batch_size=1, task_id=i))
assert model.model._current_task_id == task_id
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
["jinaai/jina-embeddings-v3"],
)
def test_lazy_load(model_name: str):
is_ci = os.getenv("CI")
model = TextEmbedding(model_name=model_name, lazy_load=True)
assert not hasattr(model.model, "model")
list(model.embed(docs))
assert hasattr(model.model, "model")
if is_ci:
delete_model_cache(model.model._model_dir)
+52 -8
View File
@@ -1,9 +1,11 @@
import os
import platform
import numpy as np
import pytest
from fastembed.text.text_embedding import TextEmbedding
from tests.utils import delete_model_cache
CANONICAL_VECTOR_VALUES = {
"BAAI/bge-small-en": np.array([-0.0232, -0.0255, 0.0174, -0.0639, -0.0006]),
@@ -30,22 +32,24 @@ CANONICAL_VECTOR_VALUES = {
[-0.034478, 0.03102, 0.00673, 0.02611, -0.039362]
),
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2": np.array(
[0.0094, 0.0184, 0.0328, 0.0072, -0.0351]
[0.0361, 0.1862, 0.2776, 0.2461, -0.1904]
),
"intfloat/multilingual-e5-large": np.array([0.0098, 0.0045, 0.0066, -0.0354, 0.0070]),
"intfloat/multilingual-e5-large": np.array([0.4544, -0.0968, 0.1054, -1.3753, 0.1500]),
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2": np.array(
[-0.01341097, 0.0416553, -0.00480805, 0.02844842, 0.0505299]
[0.0047, 0.1334, -0.0102, 0.0714, 0.1930]
),
"jinaai/jina-embeddings-v2-small-en": np.array([-0.0455, -0.0428, -0.0122, 0.0613, 0.0015]),
"jinaai/jina-embeddings-v2-base-en": np.array([-0.0332, -0.0509, 0.0287, -0.0043, -0.0077]),
"jinaai/jina-embeddings-v2-base-de": np.array([-0.0085, 0.0417, 0.0342, 0.0309, -0.0149]),
"jinaai/jina-embeddings-v2-base-code": np.array([0.0145, -0.0164, 0.0136, -0.0170, 0.0734]),
"jinaai/jina-embeddings-v2-base-zh": np.array([0.0381, 0.0286, -0.0231, 0.0052, -0.0151]),
"jinaai/jina-embeddings-v2-base-es": np.array([-0.0108, -0.0092, -0.0373, 0.0171, -0.0301]),
"nomic-ai/nomic-embed-text-v1": np.array([0.3708, 0.2031, -0.3406, -0.2114, -0.3230]),
"nomic-ai/nomic-embed-text-v1.5": np.array(
[-0.15407836, -0.03053198, -3.9138033, 0.1910364, 0.13224715]
),
"nomic-ai/nomic-embed-text-v1.5-Q": np.array(
[-0.12525563, 0.38030425, -3.961622, 0.04176439, -0.0758301]
[0.0802303, 0.3700881, -4.3053818, 0.4431803, -0.271572]
),
"thenlper/gte-large": np.array(
[-0.01920587, 0.00113156, -0.00708992, -0.00632304, -0.04025577]
@@ -61,14 +65,23 @@ CANONICAL_VECTOR_VALUES = {
),
"snowflake/snowflake-arctic-embed-l": np.array([0.0189, -0.0673, 0.0183, 0.0124, 0.0146]),
"Qdrant/clip-ViT-B-32-text": np.array([0.0083, 0.0103, -0.0138, 0.0199, -0.0069]),
"thenlper/gte-base": np.array([0.0038, 0.0355, 0.0181, 0.0092, 0.0654]),
"jinaai/jina-clip-v1": np.array([-0.0862, -0.0101, -0.0056, 0.0375, -0.0472]),
}
MULTI_TASK_MODELS = ["jinaai/jina-embeddings-v3"]
def test_embedding():
def test_embedding() -> None:
is_ci = os.getenv("CI")
is_mac = platform.system() == "Darwin"
for model_desc in TextEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
if (
(not is_ci and model_desc["size_in_GB"] > 1)
or model_desc["model"] in MULTI_TASK_MODELS
or (is_mac and model_desc["model"] == "nomic-ai/nomic-embed-text-v1.5-Q")
):
continue
dim = model_desc["dim"]
@@ -83,13 +96,16 @@ def test_embedding():
assert np.allclose(
embeddings[0, : canonical_vector.shape[0]], canonical_vector, atol=1e-3
), model_desc["model"]
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"n_dims,model_name",
[(384, "BAAI/bge-small-en-v1.5"), (768, "jinaai/jina-embeddings-v2-base-en")],
)
def test_batch_embedding(n_dims, model_name):
def test_batch_embedding(n_dims: int, model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextEmbedding(model_name=model_name)
docs = ["hello world", "flag embedding"] * 100
@@ -97,13 +113,16 @@ def test_batch_embedding(n_dims, model_name):
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (200, n_dims)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"n_dims,model_name",
[(384, "BAAI/bge-small-en-v1.5"), (768, "jinaai/jina-embeddings-v2-base-en")],
)
def test_parallel_processing(n_dims, model_name):
def test_parallel_processing(n_dims: int, model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextEmbedding(model_name=model_name)
docs = ["hello world", "flag embedding"] * 100
@@ -119,3 +138,28 @@ def test_parallel_processing(n_dims, model_name):
assert embeddings.shape == (200, n_dims)
assert np.allclose(embeddings, embeddings_2, atol=1e-3)
assert np.allclose(embeddings, embeddings_3, atol=1e-3)
if is_ci:
delete_model_cache(model.model._model_dir)
@pytest.mark.parametrize(
"model_name",
["BAAI/bge-small-en-v1.5"],
)
def test_lazy_load(model_name: str) -> None:
is_ci = os.getenv("CI")
model = TextEmbedding(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 = TextEmbedding(model_name=model_name, lazy_load=True)
list(model.query_embed(docs))
model = TextEmbedding(model_name=model_name, lazy_load=True)
list(model.passage_embed(docs))
if is_ci:
delete_model_cache(model.model._model_dir)
+37
View File
@@ -0,0 +1,37 @@
import shutil
import traceback
from pathlib import Path
from types import TracebackType
from typing import Union, Callable, Any, Type
def delete_model_cache(model_dir: Union[str, Path]) -> None:
"""Delete the model cache directory.
If a model was downloaded from the HuggingFace model hub, then _model_dir is the dir to snapshots, removing
it won't help to release the memory, because data is in blobs directory.
If a model was downloaded from GCS, then we can just remove model_dir
Args:
model_dir (Union[str, Path]): The path to the model cache directory.
"""
def on_error(
func: Callable[..., Any],
path: str,
exc_info: tuple[Type[BaseException], BaseException, TracebackType],
) -> None:
print("Failed to remove: ", path)
print("Exception: ", exc_info)
traceback.print_exception(*exc_info)
if isinstance(model_dir, str):
model_dir = Path(model_dir)
if model_dir.parent.parent.name.startswith("models--"):
model_dir = model_dir.parent.parent
if model_dir.exists():
# todo: PermissionDenied is raised on blobs removal in Windows, with blobs > 2GB
shutil.rmtree(model_dir, onerror=on_error)