Compare commits

...
191 Commits
Author SHA1 Message Date
Anush008 6fe808628a docs: Updated README.md 2024-08-07 14:02:03 +05:30
Anush 9a828da000 Merge branch 'main' into remove-pystemmer 2024-08-07 13:54:52 +05:30
Anush008 63b2dad4d7 chore: Make Pystemmer optional 2024-08-07 13:54:03 +05:30
Dmitrii OgnandGeorge Panchuk 9c72d2f59f Opened images support (#315)
* Opened image support

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-07-31 13:23:17 +03:00
generall 9d2175e97b remove PyStemmer and see what happens 2024-07-23 22:38:55 +02:00
Anush 0e258ab875 feat: Added jina-embeddings-v2-base-code (#301)
* feat: Added jina-embeddings-v2-base-code

* fix: test embeddings for "hello world" not "Hello"

* docs: Updated supported models
2024-07-18 18:17:46 +05:30
George e49789c129 fix: update push gpu command (#300) 2024-07-18 12:22:12 +03:00
Anush 1bf72922ce docs: fixed README.md examples (#298) 2024-07-17 17:49:29 +05:30
Anush 70566dff99 docs: Updated supported models (#302) 2024-07-17 16:44:50 +05:30
George Panchuk fd116dd507 bump version to 0.3.4 2024-07-15 15:59:52 +03:00
George 0315c3b8c6 new: add modifier flag into bm models config (#299)
* new: add modifier flag into bm models config

* refactoring: rename modifier field
2024-07-15 15:57:21 +03:00
Dmitrii Ogn 54e0f38914 Update README.md (#295) 2024-07-11 13:39:19 +03:00
George 3a8985b35c new: add retry logic for model downloading (#293)
* new: add retry logic for model downloading

* fix: add sleep
2024-07-10 20:09:11 +03:00
Dmitrii Ogn f0ff09c546 Oml zoo (#291)
* Support of Qdrant/Unicom-ViT-B-16 and Qdrant/Unicom-ViT-B-32
2024-07-10 16:43:54 +03:00
Dmitrii Ognandd.rudenko d09af55edd Nomic-embeddings-support (#280)
* Nomic-embeddings-support

* Jina models moved to pooled-normalized embeddings

* Canonical vector for nomic-ai/nomic-embed-text-v1.5-Q

* Moved all nomics to pooled_embeddings

---------

Co-authored-by: d.rudenko <dimitriyrudenk@gmail.com>
2024-07-10 11:45:30 +03:00
generall 9387ca3205 bump version to v0.3.3 2024-07-06 00:53:45 +02:00
Andrey Vasnetsov 9c74fb3cfb unique tokens in query (#287) 2024-07-06 00:52:38 +02:00
generall 1fe42d8d18 bump version to v0.3.2 2024-07-05 13:31:04 +02:00
Andrey Vasnetsov f820c36656 fix + test for empty from_dict (#285) 2024-07-05 13:30:14 +02:00
Anush 35c535aae3 chore: Pin numpy <2 (#278) 2024-06-17 23:33:09 +05:30
George e071c84f22 fix: fix hybrid search example for pydantic v1 (#263) 2024-06-14 17:25:15 +02:00
George e1ecfe9c2f fix: fix None cache dir in parallel mode (#277) 2024-06-14 17:19:53 +02:00
Dmitrii Ognandd.rudenko 331207976e MiniLM fix (#275)
* MiniLM fix

* Added MiniLM to text embedding
Fixed MiniLM source destination
Black + isort for repo

* Fixed model all-MiniLM-L6-v2 description
Recomputed canonical vector for all-MiniLM-L6-v2 in test

---------

Co-authored-by: d.rudenko <dimitriyrudenk@gmail.com>
2024-06-14 16:42:31 +03:00
Klaus HueckandGeorge fd0b26f009 Add support for jinaai/jina-embeddings-v2-base-de (#270)
* feat: add support for SOTA german embedding model with long context length jinaai/jina-embeddings-v2-base-de

* Fix jina de model weight

---------

Co-authored-by: George <panchuk.george@outlook.com>
2024-06-14 13:39:13 +02:00
George 5461012ab1 new: add bm25, fix param propagation in parallel mode, fix bm42 parallel (#274)
* new: add bm25, fix param propagation in parallel mode, fix bm42 parallel

* refactoring: remove redundant example

* fix: fix mp start method in bm25

* refactoring: refactor token id generation

* new: replace model repository
2024-06-13 19:52:48 +02:00
Andrey VasnetsovandGeorge 29cfcda056 add examples with supported typed of models into readme (#271)
* add examples with supported typed of models into readme

* fix link

* Update README.md

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

* Update README.md

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

* Update README.md

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

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-06-13 10:58:48 +02:00
NirantandGeorge Panchuk 615d6ee2b6 Replace Data Source (#206)
* Re-run of identical hardware and generate graphs

* Re-run of identical hardware and generate graphs
Fixes https://github.com/qdrant/fastembed/issues/174

* Change dataset source

* Refactor code for better readability and maintainability

* Inline outputs

* Replace hard coded constants with dataset specific n_dim

* fix: fix binary quant from scratch notebook

* fix: fix result table, explain corner case

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-06-06 20:59:27 +02:00
George Panchuk bf4ef9d513 bump version to v0.3.0 2024-06-05 18:03:25 +02:00
George 48c59dc7b3 new: update supported models (#253) 2024-06-05 17:50:07 +02:00
George 7099dec962 fix: remove outdated widgets state (#262) 2024-06-05 17:44:57 +02:00
George a2660f8a3a new: add image embedding example notebook (#258) 2024-06-04 20:30:54 +02:00
George e7d9abaee1 new: add a brief colbert example (#260) 2024-06-04 20:30:35 +02:00
George 01097708fe new: add gpu example, update readme (#256) 2024-06-04 20:30:23 +02:00
George d725974fc4 new: update docs (#257) 2024-06-04 20:30:11 +02:00
George 14c067e15e new: unlock huggingface hub and ruff (#250) 2024-05-31 17:41:44 +02:00
George c8fff66b18 Colbert (#248)
* new: add late interaction embedding, colbert

* new: update imports

* new: add comments

* fix: rollback mp methods

* fix: restore existing padding after embed query

* fix: fix OnnxOutputContext in onnx embed, fix preprocessing for colbert
2024-05-31 17:06:43 +02:00
85aaae4c08 Add resnet (#246)
* Resnet support added

* Tests fixed
Shapes matching for Resnet50-onnx
Example of Resnet50 to onnx conversion (basic)

* Removed optional conversion from PIL to np.ndarray and now it it's made default
Fixed test accordingly

* Refactoring of pil2ndarray

* Partial support of convnext preprocessing
Resize logic

* normalize canonical value

* Style changes for review

* new: update resnet repo

---------

Co-authored-by: d.rudenko <dimitriyrudenk@gmail.com>
Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-05-31 16:56:13 +02:00
Andrey VasnetsovandGeorge dfd25d41c9 Attention sparse embeddings (#235)
* WIP: sparse embeddings using attention

* support for stopwords

* apply stopwords

* proceed implementation of sparse attention embeddings (#234)

* complete inference

* query embed + comment

* use simpler weights formula instead of sorting of words

* update tests

* fix: fix bm42 usage, add query_embed to SparseTextEmbedding, update tests

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-05-24 15:25:34 +02:00
George 316c33634b new: add docstring with preprocessor keys (#245) 2024-05-23 13:10:23 +02:00
George 490c340a76 new: update tokenizers dep (#244) 2024-05-22 13:27:30 +02:00
George ec8f06978f new: update readme for CUDA 12.x, add warning for version conflicts (#239)
* new: update readme for CUDA 12.x, add warning about onnxruntime-gpu and cuda compatibility

* fix: change warning type

* new: update readme
2024-05-14 21:43:03 +02:00
GeorgeandNirant cbe00107ec chore: update bug-report (#232)
Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-05-14 07:35:31 +05:30
George 99164c9050 Clip (#219)
* wip: init image embeddings

* new: add clip

* new: fix clip text embedding

* fix: fix image parallel

* fix: add test images

* fix: add PIL

* fix: fix generics

* fix: fix sparse worker

* fix: fix image test path

* fix: replace models repo

* new: follow-up for onnx providers and local_files_only option

* fix: add types, refactor a bit

* refactoring: move onnxprovider type alias to types

* fix: fix type alias import
2024-05-09 11:12:25 +02:00
Andrey Vasnetsov 6ecab6d40d bump v0.2.7 2024-05-03 16:33:06 +00:00
d8c592032b new: allow users to override providers (#214)
* new: add gpu support, allow users to override providers

* fix: update poetry.lock

* fix: fix type hint for 3.8

* [readme] Remove similar work

* [README] Add GPU support for FastEmbed library

* [README]  Add device check

* fix: revert changes to pyproject and lock, update readme

* Update poetry.lock

* new: add type alias for providers, add explicit providers to embeddings

---------

Co-authored-by: Nirant Kasliwal <nirant.bits@gmail.com>
Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-05-03 18:31:22 +02:00
GeorgeandAndrey Vasnetsov da603b8b7d new: add release instructions (#231)
* new: add release instructions

* review fixes

---------

Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com>
2024-05-03 18:30:54 +02:00
Andrey Vasnetsov 562d604375 Merge pull request #223 from Waffleboy/main
[Bugfix] Allow user to pick local mode only so huggingface does not do a network call and timeout
2024-05-03 18:29:54 +02:00
Andrey Vasnetsov 8b1a98a6a3 Merge pull request #230 from qdrant/update-tokenizers
new: update tokenizers
2024-05-03 18:26:09 +02:00
Andrey Vasnetsov 3c9b147e0a version 2024-05-03 16:21:33 +00:00
George Panchuk 6abd415f4a fix: add local_files_only to sparse, formatting, refactor 2024-05-03 17:49:10 +02:00
George Panchuk 8184acbb39 new: update tokenizers 2024-05-03 16:57:45 +02:00
George 47cf7f9f92 new: add gpu package into workflow (#228)
* new: add gpu package into workflow

* remove gpu tag
2024-05-03 16:30:23 +02:00
generall f7896c81f3 do not ship poetry.lock with the repo, as package users wont have it anyway 2024-05-03 13:00:53 +02:00
Thiru 4a59d09248 Allow user to pick local mode only so huggingface does not do a network call and timeout 2024-05-02 00:25:24 +08:00
Arun 432da42c11 fix links (#215) 2024-04-27 22:24:29 +05:30
George 5cde2898bc new: remove slurm environment variables (#213) 2024-04-26 21:45:02 +02:00
AnushandGeorge ab7a99a748 feat: Quantized models (#201)
* feat: Quantized models

* refactor: use model_file for GCS

* refactoring: refactor model downloading (#209)

* refactoring: refactor model downloading

* refactor: update docstring

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

* Update fastembed/common/model_management.py

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

* fix: model_file for Snowflake models

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-04-26 17:27:16 +02:00
Anush 466886a317 ci: Schedule python-tests.yml (#211)
* ci: Schedule python-tests.yml

* ci: use emojis

* ci: Bump action versions python-tests.yml

* ci: python-tests.yml
2024-04-26 10:28:37 +05:30
Anush cc4112d859 feat: Snowflake models (#207)
* feat: Snowflake models

* Added snowflake/snowflake-arctic-embed-m

* docs: snowflake/snowflake-arctic-embed-m
2024-04-19 19:00:10 +05:30
Nirant 864217a7d9 Re-run of identical hardware and generate graphs (#205) 2024-04-18 11:13:18 +05:30
Andrew Green 9bad44368e Workaround for running on SLURM (#198)
* Workaround for running on SLURM

onnxruntime would usually get the number of threads from OMP_NUM_THREADS, but that isn't set on SLURM which handles the number of threads differently.

This addition tries to figure out if we're running under SLURM, and if so sets the session options accordingly using SLURM environment variables instead.

Tested working with latest versions of onnxruntime and fastembed on slurm 23.02.07

* onnxruntime requires number of threads be an integer

Caused by me having mis-matched version from my machine and the slurm cluster :(

* `os.getenv` returns None for unset environment variables, fix logic

Instead of empty string, which I thought it did
2024-04-15 19:14:11 +05:30
dependabot[bot]andNirant a5cab7a31a build(deps): bump idna from 3.6 to 3.7 (#195)
Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-04-15 06:04:54 +05:30
Anush e55c145924 chore: Exclude unused model repo files (#196)
* chore: Exclude unused model files

* fix: blob pattern
2024-04-12 20:15:02 +05:30
dependabot[bot]andNirant ad02f60eea build(deps-dev): bump pillow from 10.2.0 to 10.3.0 (#186)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-04-08 16:21:32 +05:30
NirantandGeorge 2fcec07f1b Add Python version to the ISSUE_TEMPLATE (#188)
* Add Python version

* Update .github/ISSUE_TEMPLATE/bug-report.yml

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

---------

Co-authored-by: George <george.panchuk@qdrant.tech>
2024-04-08 16:05:29 +05:30
George f340a73a3e refactoring: update binary quantization notebook (#180) 2024-04-02 14:45:27 +05:30
Nirant 335f673f3a Update bug-report.yml 2024-04-02 13:51:51 +05:30
Nirant ee7ba0a536 Update bug-report.yml 2024-04-02 13:51:27 +05:30
George 912e95e5c8 fix: remove model archive if model extraction was not finished correctly (#179) 2024-04-01 20:11:32 +02:00
Nirant c09909773e Add Execution Counts (#177)
* Update notebooks

* Update FastEmbed usage across docs

* Refactor code for better readability and maintainability

* Clean outputs

* Change dataset

* Add numbers inline in output

* Remove inline outputs since I used :memory:

* Fix syntax error in Hindi_Tamil_RAG_with_Navarasa7B.ipynb
2024-04-01 20:48:26 +05:30
Nirant 3d2254d215 Bump version to 0.2.6 in pyproject.toml (#175) 2024-04-01 17:16:07 +05:30
GeorgeandNirant Kasliwal e11f66bb55 refactoring: update imports in notebooks (#173)
* new: simplify imports

* refactoring: update import

* refactoring: update imports in notebooks

* fix: fix notebook output

* Re-run notebook with revised imports

---------

Co-authored-by: Nirant Kasliwal <nirant.bits@gmail.com>
2024-04-01 17:02:13 +05:30
Nirant d6f9ace425 Update size_in_GB for BAAI/bge-small-en-v1.5 model (#176) 2024-04-01 16:40:45 +05:30
8b7b8476a5 fix: fix model sizes in supported models lists (#167)
* fix: fix model sizes in supported models lists

* fix: remove redundant comment

* fix: fix test

* fix: update supported models notebook

* Consistentcy around quantization in supported_onnx_models

---------

Co-authored-by: Nirant <NirantK@users.noreply.github.com>
Co-authored-by: Nirant Kasliwal <nirant.bits@gmail.com>
2024-04-01 16:18:49 +05:30
George ae35a96bcb new: simplify imports (#171)
* new: simplify imports

* refactoring: update import
2024-04-01 12:58:26 +05:30
GeorgeandNirant 25671ec349 Update ruff (#172)
* refactoring: reduce max line-length

* new: update ruff

---------

Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-04-01 12:55:15 +05:30
George ce98631b9a Fix spladepp parallelism (#169)
* fix: add get_worker_class implementation to spladepp

* fix: add tests for parallel embed for spladepp
2024-04-01 10:22:24 +05:30
George 0a4ed42b58 fix: unify existing patterns, remove redundant (#168) 2024-03-30 14:40:19 +05:30
NirantandAnush e3d2e1dc44 Hybrid Search Tutorial (#165)
* Re-organize docs

* Rename notebooks

* Move nbs

* Working Sparse and Dense Search

* Add RRF

* Refactor code to improve performance and readability

* Add ESCI label for the RRF results

* Update docs/examples/Hybrid_Search.ipynb

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

* Update docs/examples/Hybrid_Search.ipynb

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

* Remove unnecessary code and update vector format

---------

Co-authored-by: Anush <anushshetty90@gmail.com>
2024-03-29 21:10:14 +05:30
Nirant 62c21b0237 Add misspelled version of SPLADE++ model for English (#161) 2024-03-22 21:06:03 +05:30
Anush d791f38704 chore: case-insensitive model_management.py (#160) 2024-03-22 21:02:31 +05:30
Nirant Kasliwal c651b2b539 Update SPLADE notebook with new sections 2024-03-22 15:18:34 +05:30
Yuvraj WaleandNirant bc1e23849b feat: support mixedbread-ai/mxbai-embed-large-v1 (#158)
* add: support mixedbread-ai/mxbai-embed-large-v1

* refactor: canonical vector

* Update fastembed/text/onnx_embedding.py

---------

Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-03-22 06:23:54 +05:30
Nirant 4db4839995 [PyPi Publish] Bump version to 0.2.5 in pyproject.toml (#156)
* Bump version to 0.2.5 in pyproject.toml

* chore: case insensitive check (#157)
2024-03-20 19:11:44 +05:30
NirantandAnush 96a2a9097a Fix model name typo + Add SPLADE notebook (#155)
* Rename model + Add SPLADE notebook

* Update docs/examples/SPLADE_with_FastEmbed.ipynb

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

* Update docs/examples/SPLADE_with_FastEmbed.ipynb

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

* Update docs/examples/SPLADE_with_FastEmbed.ipynb

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

* Update CANONICAL_COLUMN_VALUES in test_sparse_embeddings.py

---------

Co-authored-by: Anush <anushshetty90@gmail.com>
2024-03-20 18:15:53 +05:30
NirantandKumar Shivendu 256b2265d5 Move CONTRIBUTING.md + Add Test for Adding New Models (#154)
* Move CONTRIBUTING.md + Ad Test for Adding New Models

* Update CONTRIBUTING.md

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>

---------

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>
2024-03-18 19:58:02 +05:30
NirantandKumar Shivendu 1e91c8d165 Fix Issue Template forms (#152)
* Add CONTRIBUTING.md file with guidelines for contributing to FastEmbed

* Add code linting and pre-commit info to CONTRIBUTING

* Update CONTRIBUTING.md

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>

* Add bug/new model issue template and move CONTRIBUTING.md

* Re-organize issue templates

---------

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>
2024-03-14 15:11:23 +05:30
Nirant a761f456b2 Add import statement for version debugging (#151) 2024-03-14 15:02:15 +05:30
NirantandKumar Shivendu 287e19c494 Add CONTRIBUTING.md file with guidelines for contributing to FastEmbed (#150)
* Add CONTRIBUTING.md file with guidelines for contributing to FastEmbed

* Add code linting and pre-commit info to CONTRIBUTING

* Update CONTRIBUTING.md

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>

* Add bug/new model issue template and move CONTRIBUTING.md

---------

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>
2024-03-14 14:50:48 +05:30
Anush 6a94994038 Release v0.2.4 (#149) 2024-03-13 23:56:01 +05:30
Andrey Vasnetsov 361f674e47 avoid changing output dimentionality for a single input (#148) 2024-03-13 23:38:51 +05:30
Nirant 041a606285 Merge pull request #146 from qdrant:v0.2.3
Publish to PyPi with SPLADE models
2024-03-13 18:17:41 +05:30
Nirant Kasliwal 5b937c29f6 Update poetry install command to exclude docs 2024-03-13 18:15:51 +05:30
Nirant Kasliwal 8d368889c0 Update version and add pre-commit dependency 2024-03-13 18:07:31 +05:30
d817da2e01 Add Splade v1 (#144)
* Add SPLADE v1

* WIP SPLADE Export errors

* add ONNX model to HF hub and use that

* Update sentences in Converting_SPLADE_to_ONNX.ipynb

* Remove unnecessary files and directories

* Rename var in TextEmbedding class to use EMBEDDING_MODEL_TYPE

* Add SPLADE to list of text embeddings

* Add SPLADE model support for text embedding

* Fix deprecation warning in embedding.py

* Add test for batch embedding with sparse embeddings

* Refactor import statement in test_sparse_embeddings.py

* Rename nbs

* Update vocab size in SPLADE model

* Fix canonical vector lookup in test_text_onnx_embeddings.py

* review refactoring

* restore list_supported_models in OnnxTextEmbedding

* Remove unused method _preprocess_onnx_input() in SpladePP class

* Update SPLADE_PP_en_v1 source in splade_pp.py

* Refactor onnx_model.py to change base model behavior

* extend tests to sparse values as well as indicies

* chore: pre-commit hooks

---------

Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Anush008 <anushshetty90@gmail.com>
2024-03-13 18:04:44 +05:30
Artem Daineko 68635efa3f Fix link to Optimus in docs (#143) 2024-03-10 11:45:44 +05:30
Nirant 9ffde58df6 Getting Started Improvements (#138)
* Update FastEmbed README.md

* Rewrite GettingStarted to use TextEmbedding instead of DefaultEmbedding

* Improve grammar

* Update Getting Started.ipynb with model information and document format
2024-03-07 17:01:11 +05:30
Nirant Kasliwal 5603fbe1fb Fix naming typo 2024-03-06 14:30:54 +05:30
Nirant Kasliwal 9ed4486d9c Rename typo in notebook 2024-03-06 14:24:48 +05:30
Nirant Kasliwal e36a39c388 Update author information in notebook 2024-03-06 14:18:48 +05:30
Nirant Kasliwal 97c359c1b9 Add Navrasa LLM download and explanation sections 2024-03-06 14:17:41 +05:30
Nirant Kasliwal 9fd51425fe Add author information and Colab link to notebook 2024-03-06 14:12:04 +05:30
Nirant Kasliwal 0dec22d02d Remove inline outputs 2024-03-06 14:06:46 +05:30
Nirant Kasliwal 8a3b746b71 Rename notebook 2024-03-06 14:05:13 +05:30
Nirant 337ad9c93f Hindi RAG with Qdrant and FastEmbed (#135)
* Add workingnb

* Add A100 Colab

* Remove old checkpoint

* Refactor code to separate HF Token
2024-03-06 14:02:32 +05:30
NirantandAnush 74062e8607 Add attention export functionality to experiments (#134)
* Add attention export functionality

* Update experiments/attention_export.py

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

---------

Co-authored-by: Anush <anushshetty90@gmail.com>
2024-03-04 16:30:17 +05:30
Anush 1e298a00b3 feat: Added gte-large, nomic-text 1.5, cleanup (#130) 2024-02-21 17:54:32 +05:30
Nathan LeRoyandNirant 38c4eb1cc5 Check for existing files in cache dir before instantiating a model (#128)
* check for existing files

* (chore: model_management.py):  Add comment

---------

Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-02-21 13:56:14 +05:30
Armaghan 406f432edc feat: Support sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (#129)
* feat: Support sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

* test: Include sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2

* docs: supported models update
2024-02-21 13:35:49 +05:30
Nirant defb6183c1 * feat(pyproject): updated version to '0.2.2' (#124)
* chore(pyproject): updated dev dependencies versions
2024-02-19 14:46:01 +05:30
AnushandNirant 98141cc8d3 feat: Added nomic-embed-text-v1 support + formatting changes + import fixes (#118)
* feat: Added nomic-embed-text-v1 support

* chore: xenova/nomic-embed-text-v1 -> nomic-ai/nomic-embed-text-v1

---------

Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-02-19 14:17:33 +05:30
Nirant b1f5e7a989 Add import statement to the warning message (#119) 2024-02-15 12:40:11 +05:30
Kumar Shivendu 558a837531 Merge pull request #112 from qdrant/KShivendu-patch-1
docs: Describe how to change the model and how to just create embeddings
2024-02-13 10:33:41 +05:30
Nirant b81e40c95d Merge branch 'main' into KShivendu-patch-1 2024-02-13 08:25:51 +05:30
Nirant 81bab0cd1d Make 0.2.1 Release + Update docs (#116)
* Update version from 0.2.0 (yanked) to 0.2.1

* Update text embedding to include prefix for passages and queries

* Update supported models to use the latest API

* * fix(text_embedding_base.py): remove unnecessary prefix from texts in embed method
* feat(text_embedding_base.py): update query_embed method to updated instruction for the v1.5 model

* Remove comparison, since the ranking is identical even with varying embedding

* Refactor text embedding query handling
2024-02-08 09:06:00 +05:30
Nirant 973da354ae Fix query to align with Qdrant mixin usage (#115)
* fix: query in text_embedding_base to work with both Iterable and str as users might supply both

* Fix Qdrant query to align with future usage

* * refactor(text_embedding_base.py): change query parameter type from str to Union[str, Iterable[str]] in query_embed method

* Update return type of query_embed method

* Update return type in TextEmbeddingBase
2024-02-07 22:01:31 +05:30
46968181ad Simplify imports: #110 (#113)
* Simplify imports: #110

* Update fastembed/__init__.py

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>

* Remove outdated import

* Remove outdated import

---------

Co-authored-by: Kumar Shivendu <kshivendu1@gmail.com>
Co-authored-by: Nirant <NirantK@users.noreply.github.com>
2024-02-07 21:12:46 +05:30
Nirant Kasliwal 3948f0db2e Update fastembed v0.2.0 2024-02-07 20:36:46 +05:30
Nirant cf66d0e5e1 Update Python and dependency versions (#111) 2024-02-05 12:37:52 +01:00
Kumar Shivendu c11ba70fbc Update README.md 2024-02-05 07:17:10 +01:00
Kumar Shivendu ea3ef26fa2 Improve README 2024-02-05 07:07:41 +01:00
Kumar Shivendu 4b1ffb47f0 docs: Describe how to change the model and how to just create embeddings 2024-02-05 11:24:47 +05:30
Kumar Shivendu d3f5f29ee0 docs: Improve README (#109) 2024-02-05 10:49:45 +05:30
Kumar Shivendu 05885a36dd refactor: Introduce experiments dir (#108) 2024-02-05 10:49:12 +05:30
Andrey Vasnetsov a3bc73c556 Merge pull request #105 from qdrant/refactoring-off-everything
Refactoring of internal structure
2024-02-02 16:48:04 +01:00
generall fcdc5690b9 rename models 2024-02-02 15:51:19 +01:00
generall 7883fa3c41 new multilingual models 2024-02-02 15:32:42 +01:00
generall 5dbd0073e2 rename flag -> onnx 2024-02-02 15:12:57 +01:00
generall 8b800da7bc ruff 2024-02-02 15:12:57 +01:00
generall 3e6f69e2eb review fixes 2024-02-02 15:12:57 +01:00
generallandGeorge Panchuk 4813b18854 refactoring
Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
2024-02-02 15:12:57 +01:00
Anush 96f7d83d33 feat: Support xenova/multilingual-e5-large, xenova/paraphrase-multili… (#103)
* feat: Support xenova/multilingual-e5-large, xenova/paraphrase-multilingual-mpnet-base-v2

* chore: updated exclude_token_type_ids check

* docs: supported models update
2024-02-02 15:16:35 +05:30
Nirant 2e3e5508c0 Update poetry lock to latest versions (#98)
* Update poetry lock to latest versions

* Update poetry lock to latest versions
2024-01-30 21:39:52 +05:30
Anush 87decb0d53 chore: port to Xenova Jina source (#102)
* chore: xenova jina

* chore: try recusive model location

* chore: updated doc string, blob pattern
2024-01-30 21:27:16 +05:30
Anush ede507e2cf feat: HuggingFace download support for FlagEmbedding (#94)
* feat: HF support for FlagEmbedding

* chore: update docstring embedding.py

* refactor: GCS URLs models.json

* chore: toLower() models.json

* chore: update tqdm declarative

* chore: exclude keys list_supported_models

* chore: review changes
2024-01-23 12:47:55 +05:30
David Janes f87330fcd1 use "with" to open JSON files (#96) 2024-01-22 12:20:06 +05:30
Nirant 3b32619a4c Update Python version and add pre-commit dependency (#93)
* Update Python version and add pre-commit dependency

* Remove Python 3.8.x from matrix

* Update Python version and pre-commit configuration
2024-01-16 19:30:47 +05:30
AnushandNirant Kasliwal 9b63427118 chore: pre-commit formatting (#91)
* chore: formatting

* chore: formatting

* chore: remove other hooks

* Update poetry lock

---------

Co-authored-by: Nirant Kasliwal <nirant.bits@gmail.com>
2024-01-16 15:06:54 +05:30
Nirant b01f882df7 Revert "feat: embedding progress bar (#71)" (#77)
This reverts commit 2c7fee3b95.
2023-12-13 15:13:00 +05:30
Nirant 55379539ef * chore(docs): update Getting Started.ipynb with progressbar + New Models
* * feat(Supported_Models.ipynb): add support for BAAI/bge-small-zh-v1.5 model
* feat(Supported_Models.ipynb): add support for jinaai/jina-embeddings-v2-base-en model
* feat(Supported_Models.ip

* * chore(docs): update Getting Started.ipynb with progressbar
2023-12-13 14:17:25 +05:30
NirantK ea85e7430f Bump version 2023-12-13 13:27:30 +05:30
Anush 2c7fee3b95 feat: embedding progress bar (#71)
* feat: embedding progress

* refactor: with auto __close__

* refactor: with __exit__ tqdm
2023-12-12 19:33:12 +05:30
Anush e274dd0fc2 chore: bump tokenizers (#75) 2023-12-12 18:50:47 +05:30
Anush 0a94425735 feat: Added support for FASTEMBED_CACHE_PATH env var (#68)
* chore: FASTEMBED_CACHE_PATH env

* chore: temp directory fallback

* chore: tempdir fallback JinaEmbedding
2023-11-22 10:39:18 +05:30
Joan FontanalsandJoan Fontanals Martinez f222d7cd87 add JinaEmbeddings class (#67)
* add JinaEmbeddings class

* fix tests dimensions

---------

Co-authored-by: Joan Fontanals Martinez <joan.fontanals.martinez@jina.ai>
2023-11-20 16:37:23 +05:30
Nirant d64b8f42f0 * chore(pyproject.toml): add huggingface-hub dependency (#66)
* chore(pyproject.toml): update pytest version to 7.4.2
2023-11-20 15:10:56 +05:30
dependabot[bot] 2f95205b23 build(deps): bump urllib3 from 2.0.6 to 2.0.7 (#65)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-16 11:37:48 +05:30
Nirant a39ee46c0b Update EmbeddingModel class to remove ABC (#57)
inheritance
2023-11-02 15:37:43 +05:30
Dominik Weckmüller bb86b30707 Add typing and numpy import
typing and numpy import were missing
2023-11-01 20:20:47 +05:30
Andrey Vasnetsov 8c20c7c172 Merge pull request #55 from qdrant/tokenizers-upgrade
Update tokenizers dependency version to >=0.14
2023-11-01 15:50:27 +01:00
NirantK 5f40fc2f14 * chore(pyproject.toml): update tokenizers dependency version to be at least 0.14 2023-11-01 20:14:01 +05:30
NirantK ab2f41ef8b * chore(pyproject.toml): update tokenizers dependency version to ^0.14.1 2023-11-01 20:12:18 +05:30
Nirant 78416dd728 Merge pull request #48 from qdrant/remove-docs-clutter
Docs: Move cluttered notebook + Fix typos
2023-10-30 23:07:09 +05:30
NirantK 9c85a899c9 * docs(experimental): update dataset size in Binary Quantization with Qdrant.ipynb from 10K to 100K 2023-10-30 23:02:18 +05:30
NirantK 299042d592 * docs(examples): add explanation of Qdrant Client usage with FastEmbed library and Qdrant API 2023-10-30 23:01:26 +05:30
NirantK d35ff16994 * chore(docs): rename Throughput_Across_Models.ipynb to fooling_around/Throughput_Across_Models.ipynb 2023-10-30 23:01:19 +05:30
Nirant f8f8316fea Merge pull request #38 from qdrant/explain_cossim
* docs(examples): update FastEmbed_vs_HF_Comparison.ipynb
2023-10-19 22:56:04 +05:30
NirantK 4999fa17b5 * docs(examples): update FastEmbed_vs_HF_Comparison.ipynb
with cosine similarity values for BAAI/bge-small-en and BAAI/bge-small-en-v1.5 embeddings
2023-10-19 22:49:25 +05:30
Nirant 7535d0e49f Merge pull request #34 from qdrant/fix-broken-link-for-docs
Fix broken link in README
2023-10-19 20:21:33 +05:30
Nirant eaa8c534f3 Fix broken link in README 2023-10-19 15:54:04 +05:30
Nirant e04f0b161b Merge pull request #32 from qdrant/supported-models-doc-update
Documentation Improvements
2023-10-19 14:06:37 +05:30
NirantK ad297c4f13 * chore(Usage_With_Qdrant.ipynb): remove unnecessary outputs in code cells 2023-10-18 23:03:52 +05:30
NirantK c1fdaf3303 * chore(Supported_Models.ipynb): update supported models table
* feat(Supported_Models.ipynb): add size_in_GB column to supported models table
2023-10-18 23:02:50 +05:30
Nirant 1608599bcb Merge pull request #31 from qdrant/fix-defaults
Consistent Default to v1.5
2023-10-18 20:56:28 +05:30
NirantK b61f8a48cc Update to v1.5 model 2023-10-18 20:49:31 +05:30
NirantK fd55b46f4b * fix(embedding.py): update default model_name to "BAAI/bge-small-en-v1.5" 2023-10-18 20:49:01 +05:30
NirantK a14aab8ef4 * refactor(Getting Started.ipynb): simplify code for initializing DefaultEmbedding class 2023-10-18 20:48:51 +05:30
Nirant 72591fe5d2 Merge pull request #27 from qdrant/add-bge-small-zh
* feat(embedding.py): add "BAAI/bge-small-zh-v1.5" model
2023-10-18 19:32:45 +05:30
Nirant 911b51d01c Merge pull request #28 from qdrant/fix-parallel-in-embed-passage
pass embed arguments in `passage_embed` method
2023-10-16 19:00:52 +05:30
generall 219185e677 pass embed arguments in passage_embed method 2023-10-16 14:57:51 +02:00
NirantK f28087c71a * fix(embedding.py): change dim value from 384 to 512 for the "BAAI/bge-small-zh-v1.5" model
* fix(test_onnx_embeddings.py): add canonical vector values for the "BAAI/bge-small-zh-v1.5"
2023-10-16 18:23:02 +05:30
NirantK 0203b0ae9e * feat(embedding.py): add support for BAAI/bge-small-zh-v1.5 Chinese model 2023-10-16 18:20:56 +05:30
Andrey Vasnetsov f02d713e93 Merge pull request #24 from qdrant/streaming-inference
implement data-parallel inference and up version
2023-10-16 14:31:40 +02:00
Andrey Vasnetsov c901da0820 Merge pull request #26 from qdrant/add_model_size
* feat(embedding.py): add size_in_GB information for each model
2023-10-16 14:26:13 +02:00
Nirant f79de09ff7 Merge branch 'streaming-inference' into add_model_size 2023-10-16 17:54:33 +05:30
NirantK 7799180b18 * fix(embedding.py): update return type of list_supported_models method to include Union[int, float] for values in the dictionary 2023-10-16 17:52:43 +05:30
NirantK e24ea64e21 * test(test_onnx_embeddings.py): skip specific model if size_in_GB is greater than 1 2023-10-16 17:52:35 +05:30
NirantK 299c76c099 * feat(embedding.py): add size_in_GB information for each model 2023-10-16 17:50:28 +05:30
generall 35ec40b3a3 review fixes 2023-10-16 14:19:58 +02:00
NirantK b35cb28eeb * refactor(embedding.py): reorder import statements in alphabetical order
* feat(embedding.py): add optional 'threads' parameter to DefaultEmbedding constructor
2023-10-16 17:39:21 +05:30
Nirant c953a083cc Merge branch 'main' into streaming-inference 2023-10-16 17:33:36 +05:30
Nirant 66c5cf76c6 Merge pull request #25 from qdrant/support-v1.5-models
* feat(embedding.py): add support for v1.5 models
2023-10-16 17:29:33 +05:30
NirantK 9e5d37846c * feat(embedding.py): add support for BAAI/bge-small-en-v1.5 and BAAI/bge-base-en-v1.5 models
* feat(embedding.py): change default model to v1.5
2023-10-16 17:08:58 +05:30
generall c719fc696d disable large models on non-ubuntu CI 2023-10-16 13:27:28 +02:00
generall 24dc24b02d implement data-parallel inference and up version 2023-10-16 13:07:07 +02:00
NirantK c408b7e13e * chore(main.html): add utm parameters to Qdrant Cloud link 2023-10-10 19:01:49 +05:30
NirantK d2bdfee4e0 * docs(examples): update comparison notebook with more accurate description of embeddings similarity 2023-10-10 17:56:25 +05:30
NirantK ac4375516f Rename nbs; add Cosine similarity check 2023-10-10 17:55:37 +05:30
NirantK ca6f9d629a Add skeleton 2023-10-05 19:20:01 +05:30
NirantK e0e7e5721e Add generator note to the comment in Python block 2023-10-05 19:17:32 +05:30
NirantK bc402694bd Update code to handle Generator 2023-10-05 19:17:18 +05:30
83 changed files with 11208 additions and 5978 deletions
View File
+57
View File
@@ -0,0 +1,57 @@
name: Bug/New Model Request
description: File a bug report/Request a new Model
title: "[Bug/Model Request]: "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
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!"
validations:
required: true
- type: textarea
id: python-version
attributes:
label: What Python version are you on? e.g. python --version
description: Also tell us, what package manager are you using e.g. conda, pip, poetry?
placeholder: Python3.10
validations:
required: true
- type: dropdown
id: version
attributes:
label: 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
validations:
required: true
- type: dropdown
id: os
attributes:
label: What os are you seeing the problem on?
multiple: true
options:
- Linux
- MacOS
- Windows
- type: textarea
id: logs
attributes:
label: Relevant stack traces and/or logs
description: Please copy and paste any relevant raised exceptions. This will be automatically formatted into code, so no need for backticks.
render: shell
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Community Support
url: https://github.com/qdrant/fastembed/discussions
about: Please ask and answer questions here.
+3 -3
View File
@@ -1,8 +1,8 @@
name: ci
name: ci
on:
push:
branches:
- master
- master
- main
permissions:
contents: write
@@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
key: mkdocs-material-${{ env.cache_id }}
-1
View File
@@ -15,7 +15,6 @@ on:
tags:
- 'v*' # Push events to every version tag
jobs:
deploy:
+18 -7
View File
@@ -2,7 +2,9 @@ name: Tests
on:
push:
branches: [ master, main ]
branches: [ master, main, gpu ]
schedule:
- cron: 0 0 * * *
pull_request:
env:
@@ -18,6 +20,7 @@ jobs:
- '3.9.x'
- '3.10.x'
- '3.11.x'
- '3.12.x'
os:
- ubuntu-latest
- macos-latest
@@ -28,16 +31,24 @@ jobs:
name: Python ${{ matrix.python-version }} on ${{ matrix.os }} test
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install poetry
poetry config virtualenvs.create false
poetry install --no-interaction --no-ansi
- name: Run tests
run: pytest
shell: bash
poetry install --no-interaction --no-ansi --without docs
- name: Install Test Dependencies
run: pip install pytest pytest-md pytest-emoji
- name: Run pytest
uses: pavelzw/pytest-action@v2
with:
verbose: true
emoji: true
job-summary: true
report-title: 'FastEmbed Test Report'
+4 -40
View File
@@ -85,28 +85,8 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
@@ -152,27 +132,11 @@ dmypy.json
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
.DS_Store
nbs/*.tar.gz
*.tar.gz
nbs/fast-*/*
local_cache/*/*
*/local_cache/*/*
*/*/local_cache/*/*
**/local_cache/
docs/experimental/*.parquet
docs/experimental/*.bin
qdrant_storage/*
fooling_around/fast-multilingual-e5-large/config.json
fooling_around/fast-multilingual-e5-large/model_optimized.onnx
fooling_around/fast-multilingual-e5-large/model_optimized.onnx.data
fooling_around/fast-multilingual-e5-large/ort_config.json
fooling_around/fast-multilingual-e5-large/sentencepiece.bpe.model
fooling_around/fast-multilingual-e5-large/special_tokens_map.json
fooling_around/fast-multilingual-e5-large/tokenizer_config.json
fooling_around/fast-multilingual-e5-large/tokenizer.json
experiments/models/*
+8 -11
View File
@@ -1,12 +1,9 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4
hooks:
- id: ruff
types_or: [ python, pyi, jupyter ]
args: [ --fix ]
- id: ruff-format
types_or: [ python, pyi, jupyter ]
+78
View File
@@ -0,0 +1,78 @@
# Contributing to FastEmbed!
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
The following is a set of guidelines for contributing to FastEmbed. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
## Table Of Contents
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
[How Can I Contribute?](#how-can-i-contribute)
* [Your First Code Contribution](#your-first-code-contribution)
* [Adding New Models](#adding-new-models)
[Styleguides](#styleguides)
* [Code Lint](#code-lint)
* [Pre-Commit Hooks](#pre-commit-hooks)
## I don't want to read this whole thing I just have a question!!!
> **Note:** Please don't file an issue to ask a question. You'll get faster results by using the resources below:
* [FastEmbed Docs](https://qdrant.github.io/fastembed/)
* [Qdrant Discord](https://discord.gg/Qy6HCJK9Dc)
## How Can I Contribute?
## How Do I Submit A (Good) Bug Report?
Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/).
Explain the problem and include additional details to help maintainers reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you are using FastEmbed, e.g. with Langchain, Qdrant Client, Llama Index and which command exactly you used. When listing steps, **don't just say what you did, but explain how you did it**.
* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
* **If the problem is related to performance or memory**, include a [call stack profile capture](https://github.com/joerick/pyinstrument) and your observations.
Include details about your configuration and environment:
* **Which version of FastEmbed are you using?** You can get the exact version by running `python -c "import fastembed; print(fastembed.__version__)"`.
* **What's the name and version of the OS you're using**?
* **Which packages do you have installed?** You can get that list by running `pip freeze`
### Your First Code Contribution
Unsure where to begin contributing to FastEmbed? You can start by looking through these `good-first-issue`issues:
* [Good First Issue](https://github.com/qdrant/fastembed/labels/good%20first%20issue) - issues which should only require a few lines of code, and a test or two. These are a great way to get started with FastEmbed. This includes adding new models which are already tested and ready on Huggingface Hub.
## Pull Requests
The best way to learn about the mechanics of FastEmbed is to start working on it.
### Your First Code Contribution
Your first code contribution can be small bug fixes:
1. This PR adds a small bug fix for a single input: https://github.com/qdrant/fastembed/pull/148
2. This PR adds a check for the right file location and extension, specific to an OS: https://github.com/qdrant/fastembed/pull/128
Even documentation improvements and tests are most welcome:
1. This PR fixes a README link: https://github.com/qdrant/fastembed/pull/143
### Adding New Models
1. Open Requests for New Models are [here](https://github.com/qdrant/fastembed/labels/model%20request).
2. There are quite a few pull requests that were merged for this purpose and you can use them as a reference. Here is an example: https://github.com/qdrant/fastembed/pull/129
3. Make sure to add tests for the new model
- The CANONICAL_VECTOR values must come from a reference implementation usually from Huggingface Transformers or Sentence Transformers
- Here is a reference [Colab Notebook](https://colab.research.google.com/drive/1tNdV3DsiwsJzu2AXnUnoeF5av1Hp8HF1?usp=sharing) for how we will evaluate whether your VECTOR values in the test are correct or not.
## Styleguides
### Code Lint
We use ruff for code linting. It should be installed with poetry since it's a dev dependency.
### Pre-Commit Hooks
We use pre-commit hooks to ensure that the code is linted before it's committed. You can install pre-commit hooks by running `pre-commit install` in the root directory of the project.
+173 -27
View File
@@ -1,40 +1,178 @@
# ⚡️ What is FastEmbed?
FastEmbed is a lightweight, fast, Python library built for embedding generation. We [support popular text models](https://qdrant.github.io/fastembed/examples/Supported_Models/). Please [open a Github issue](https://github.com/qdrant/fastembed/issues/new) if you want us to add a new model.
FastEmbed is a lightweight, fast, Python library built for embedding generation. We [support popular text models](https://qdrant.github.io/fastembed/examples/Supported_Models/). Please [open a GitHub issue](https://github.com/qdrant/fastembed/issues/new) if you want us to add a new model.
The default embedding supports "query" and "passage" prefixes for the input text. The default model is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard. Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/examples/Retrieval%20with%20FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/).
The default text embedding (`TextEmbedding`) model is Flag Embedding, presented in the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard. It supports "query" and "passage" prefixes for the input text. Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/qdrant/Retrieval_with_FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/qdrant/Usage_With_Qdrant/).
1. Light & Fast
- Quantized model weights
- ONNX Runtime, no PyTorch dependency
- CPU-first design
## 📈 Why FastEmbed?
2. Accuracy/Recall
- Better than OpenAI Ada-002
- Default is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard
- List of [supported models](https://qdrant.github.io/fastembed/examples/Supported_Models/) - including multilingual models
1. Light: FastEmbed is a lightweight library with few external dependencies. We don't require a GPU and don't download GBs of PyTorch dependencies, and instead use the ONNX Runtime. This makes it a great candidate for serverless runtimes like AWS Lambda.
2. Fast: FastEmbed is designed for speed. We use the ONNX Runtime, which is faster than PyTorch. We also use data parallelism for encoding large datasets.
3. Accurate: FastEmbed is better than OpenAI Ada-002. We also [support](https://qdrant.github.io/fastembed/examples/Supported_Models/) an ever-expanding set of models, including a few multilingual models.
## 🚀 Installation
To install the FastEmbed library, pip works:
To install the FastEmbed library, pip works best. You can install it with or without GPU support:
```bash
pip install fastembed
# or with GPU support
pip install fastembed-gpu
```
## 📖 Usage
## 📖 Quickstart
```python
from fastembed.embedding import FlagEmbedding as Embedding
from fastembed import TextEmbedding
from typing import List
# Example list of documents
documents: List[str] = [
"passage: Hello, World!",
"query: Hello, World!", # these are two different embedding
"passage: This is an example passage.",
"fastembed is supported by and maintained by Qdrant." # You can leave out the prefix but it's recommended
"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.",
]
embedding_model = Embedding(model_name="BAAI/bge-base-en", max_length=512)
embeddings: List[np.ndarray] = list(embedding_model.embed(documents)) # If you use
# This will trigger the model download and initialization
embedding_model = TextEmbedding()
print("The model BAAI/bge-small-en-v1.5 is ready to use.")
embeddings_generator = embedding_model.embed(documents) # reminder this is a generator
embeddings_list = list(embedding_model.embed(documents))
# you can also convert the generator to a list, and that to a numpy array
len(embeddings_list[0]) # Vector of 384 dimensions
```
Fastembed supports a variety of models for different tasks and modalities.
The list of all the available models can be found [here](https://qdrant.github.io/fastembed/examples/Supported_Models/)
### 🎒 Dense text embeddings
```python
from fastembed import TextEmbedding
model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
embeddings = list(model.embed(documents))
# [
# array([-0.1115, 0.0097, 0.0052, 0.0195, ...], dtype=float32),
# array([-0.1019, 0.0635, -0.0332, 0.0522, ...], dtype=float32)
# ]
```
### 🔱 Sparse text embeddings
* SPLADE++
```python
from fastembed import SparseTextEmbedding
model = SparseTextEmbedding(model_name="prithivida/Splade_PP_en_v1")
embeddings = list(model.embed(documents))
# [
# SparseEmbedding(indices=[ 17, 123, 919, ... ], values=[0.71, 0.22, 0.39, ...]),
# SparseEmbedding(indices=[ 38, 12, 91, ... ], values=[0.11, 0.22, 0.39, ...])
# ]
```
* BM25
```python
from fastembed import SparseTextEmbedding
model = SparseTextEmbedding(model_name="Qdrant/bm25")
embeddings = list(model.embed(documents))
# [
# SparseEmbedding(indices=[ 129793020, 1999429279, 819028769, ... ], values=[1.6477, 1.6327, 1.2377, ...]),
# SparseEmbedding(indices=[ 682147660, 1100855371, 339478471, ... ], values=[1.6741, 1.5432, 1.6741, ...])
# ]
```
* [BM42](https://qdrant.tech/articles/bm42/)
```python
from fastembed import SparseTextEmbedding
model = SparseTextEmbedding(model_name="Qdrant/bm42-all-minilm-l6-v2-attentions")
embeddings = list(model.embed(documents))
# [
# SparseEmbedding(indices=[ 17, 123, 919, ... ], values=[0.71, 0.22, 0.39, ...]),
# SparseEmbedding(indices=[ 38, 12, 91, ... ], values=[0.11, 0.22, 0.39, ...])
# ]
```
You can install [PyStemmer](https://pypi.org/project/PyStemmer/) to improve the stemming performance when using BM25, BM42.
```shell
pip install fastembed[pystemmer]
```
### 🦥 Late interaction models (aka ColBERT)
```python
from fastembed import LateInteractionTextEmbedding
model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
embeddings = list(model.embed(documents))
# [
# array([
# [-0.1115, 0.0097, 0.0052, 0.0195, ...],
# [-0.1019, 0.0635, -0.0332, 0.0522, ...],
# ]),
# array([
# [-0.9019, 0.0335, -0.0032, 0.0991, ...],
# [-0.2115, 0.8097, 0.1052, 0.0195, ...],
# ]),
# ]
```
### 🖼️ Image embeddings
```python
from fastembed import ImageEmbedding
images = [
"./path/to/image1.jpg",
"./path/to/image2.jpg",
]
model = ImageEmbedding(model_name="Qdrant/clip-ViT-B-32-vision")
embeddings = list(model.embed(images))
# [
# array([-0.1115, 0.0097, 0.0052, 0.0195, ...], dtype=float32),
# array([-0.1019, 0.0635, -0.0332, 0.0522, ...], dtype=float32)
# ]
```
## ⚡️ FastEmbed on a GPU
FastEmbed supports running on GPU devices.
It requires installation of the `fastembed-gpu` package.
```bash
pip install fastembed-gpu
```
Check our [example](https://qdrant.github.io/fastembed/examples/FastEmbed_GPU/) for detailed instructions and CUDA 12.x support.
```python
from fastembed import TextEmbedding
embedding_model = TextEmbedding(
model_name="BAAI/bge-small-en-v1.5",
providers=["CUDAExecutionProvider"]
)
print("The model BAAI/bge-small-en-v1.5 is ready to use on a GPU.")
```
## Usage with Qdrant
@@ -45,23 +183,35 @@ Installation with Qdrant Client in Python:
pip install qdrant-client[fastembed]
```
Might have to use ```pip install 'qdrant-client[fastembed]'``` on zsh.
or
```bash
pip install qdrant-client[fastembed-gpu]
```
You might have to use quotes ```pip install 'qdrant-client[fastembed]'``` on zsh.
```python
from qdrant_client import QdrantClient
# Initialize the client
client = QdrantClient(":memory:") # or QdrantClient(path="path/to/db")
client = QdrantClient("localhost", port=6333) # For production
# client = QdrantClient(":memory:") # For small experiments
# Prepare your documents, metadata, and IDs
docs = ["Qdrant has Langchain integrations", "Qdrant also has Llama Index integrations"]
metadata = [
{"source": "Langchain-docs"},
{"source": "Linkedin-docs"},
{"source": "Llama-index-docs"},
]
ids = [42, 2]
# Use the new add method
# If you want to change the model:
# client.set_model("sentence-transformers/all-MiniLM-L6-v2")
# List of supported models: https://qdrant.github.io/fastembed/examples/Supported_Models
# Use the new add() instead of upsert()
# This internally calls embed() of the configured embedding model
client.add(
collection_name="demo_collection",
documents=docs,
@@ -75,7 +225,3 @@ search_result = client.query(
)
print(search_result)
```
#### Similar Work
Ilyas M. wrote about using [FlagEmbeddings with Optimum](https://twitter.com/IlysMoutawwakil/status/1705215192425288017) over CUDA.
+41
View File
@@ -0,0 +1,41 @@
# Releasing FastEmbed
This is a guide how to release `fastembed` and `fastembed-gpu` packages.
## How to
1. Accumulate changes in the `main` branch.
2. Bump the version in `pyproject.toml`
3. Rebase the `gpu` branch on `main` and resolve conflicts if occurred:
```bash
git checkout gpu
git rebase main
git push -f origin gpu
```
4. Draft release notes
5. Checkout to `main` and create a tag, e.g.:
```bash
git checkout main
git tag -a v0.1.0 -m "Release v0.1.0"
```
6. Checkout `gpu` and create a tag, e.g.:
```bash
git checkout gpu
git tag -a v0.1.0-gpu -m "Release v0.1.0"
```
7. Push tags:
```bash
git push --tags
```
8. Verify that both packages have been published successfully on PyPI. Try installing them and verify imports.
9. Create a release on GitHub with the written release notes.
+136 -130
View File
@@ -11,7 +11,9 @@
"\n",
"## Quick Start\n",
"\n",
"The fastembed package is designed to be easy to use. The main class is the `Embedding` class. It takes a list of strings as input and returns a list of vectors as output. The `Embedding` class is initialized with a model file."
"The fastembed package is designed to be easy to use. We'll be using `TextEmbedding` class. It takes a list of strings as input and returns a generator of vectors.\n",
"\n",
"> 💡 You can learn more about generators from [Python Wiki](https://wiki.python.org/moin/Generators)"
]
},
{
@@ -21,15 +23,7 @@
"metadata": {},
"outputs": [],
"source": [
"!pip install fastembed --upgrade --quiet # Install fastembed "
]
},
{
"cell_type": "markdown",
"id": "ed81d725",
"metadata": {},
"source": [
"Make the necessary imports, initialize the `Embedding` class, and embed your data into vectors:"
"!pip install -Uqq fastembed"
]
},
{
@@ -39,35 +33,115 @@
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.\n"
]
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "890cc3b969354eec8d149d143e301a7a",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Fetching 9 files: 0%| | 0/9 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"torch.Size([384])\n"
"The model BAAI/bge-small-en-v1.5 is ready to use.\n"
]
},
{
"data": {
"text/plain": [
"384"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from typing import List\n",
"\n",
"import numpy as np\n",
"from fastembed.embedding import DefaultEmbedding\n",
"\n",
"from fastembed import TextEmbedding\n",
"\n",
"\n",
"# Example list of documents\n",
"documents: List[str] = [\n",
" \"Hello, World!\",\n",
" \"This is an example document.\",\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",
"# Initialize the DefaultEmbedding class with the desired parameters\n",
"embedding_model = DefaultEmbedding(model_name=\"BAAI/bge-small-en\", max_length=512)\n",
"embeddings: List[np.ndarray] = embedding_model.embed(documents)\n",
"print(embeddings[0].shape)"
"\n",
"# This will trigger the model download and initialization\n",
"embedding_model = TextEmbedding()\n",
"print(\"The model BAAI/bge-small-en-v1.5 is ready to use.\")\n",
"\n",
"embeddings_generator = embedding_model.embed(documents)\n",
"embeddings_list = list(embeddings_generator)\n",
"len(embeddings_list[0]) # Vector of 384 dimensions"
]
},
{
"cell_type": "markdown",
"id": "d772190b",
"metadata": {},
"source": [
"> 💡 **Why do we use generators?**\n",
"> \n",
"> We use them to save memory mostly. Instead of loading all the vectors into memory, we can load them one by one. This is useful when you have a large dataset and you don't want to load all the vectors at once."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "8a225cb8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Document: This is built to be faster and lighter than other embedding libraries e.g. Transformers, Sentence-Transformers, etc.\n",
"Vector of type: <class 'numpy.ndarray'> with shape: (384,)\n",
"Document: fastembed is supported by and maintained by Qdrant.\n",
"Vector of type: <class 'numpy.ndarray'> with shape: (384,)\n"
]
}
],
"source": [
"embeddings_generator = embedding_model.embed(documents)\n",
"\n",
"for doc, vector in zip(documents, embeddings_generator):\n",
" print(\"Document:\", doc)\n",
" print(f\"Vector of type: {type(vector)} with shape: {vector.shape}\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "769a1be9",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(2, 384)"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"embeddings_list = np.array(list(embedding_model.embed(documents)))\n",
"embeddings_list.shape"
]
},
{
@@ -75,142 +149,74 @@
"id": "8c49ae50",
"metadata": {},
"source": [
"## Let's think step by step"
]
},
{
"cell_type": "markdown",
"id": "92cf4b76",
"metadata": {},
"source": [
"### Setup\n",
"\n",
"Importing the required classes and modules:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "c0a6f634",
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"import numpy as np\n",
"from fastembed.embedding import DefaultEmbedding as Embedding"
]
},
{
"cell_type": "markdown",
"id": "3fd03a71",
"metadata": {},
"source": [
"Notice that we are using the DefaultEmbedding -- which is a quantized, state of the Art Flag Embedding model which beats OpenAI's Embedding by a large margin. \n",
"\n",
"### Prepare your Documents\n",
"You can define a list of documents that you'd like to embed. These can be sentences, paragraphs, or even entire documents. \n",
"We're using [BAAI/bge-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5) a state of the art Flag Embedding model. The model does better than OpenAI text-embedding-ada-002. We've made it even faster by converting it to ONNX format and quantizing the model for you.\n",
"\n",
"#### Format of the Document List\n",
"\n",
"1. List of Strings: Your documents must be in a list, and each document must be a string\n",
"2. For Retrieval Tasks: If you're working with queries and passages, you can add special labels to them:\n",
"2. For Retrieval Tasks with our default: If you're working with queries and passages, you can add special labels to them:\n",
"- **Queries**: Add \"query:\" at the beginning of each query string\n",
"- **Passages**: Add \"passage:\" at the beginning of each passage string"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "145a56ce",
"metadata": {},
"outputs": [],
"source": [
"# Example list of documents\n",
"documents: List[str] = [\n",
" \"passage: Hello, World!\",\n",
" \"query: Hello, World!\", # these are two different embedding\n",
" \"passage: This is an example passage.\",\n",
" # You can leave out the prefix but it's recommended\n",
" \"fastembed is supported by and maintained by Qdrant.\",\n",
"]"
]
},
{
"cell_type": "markdown",
"id": "1cb3cc87",
"metadata": {},
"source": [
"### Load the Embedding Model Weights\n",
"Next, initialize the Embedding class with the desired parameters. Here, \"BAAI/bge-small-en\" is the pre-trained model name, and max_length=512 is the maximum token length for each document.\n",
"- **Passages**: Add \"passage:\" at the beginning of each passage string\n",
"\n",
"This will download the model weights, decompress to directory `local_cache` and load them into the Embedding class.\n",
"## Beyond the default model\n",
"\n",
"#### Initialize DefaultEmbedding\n",
"\n",
"We will initialize Flag Embeddings with the model name and the maximum token length. That is the DefaultEmbedding class with the model name \"BAAI/bge-small-en\" and max_length=512."
"The default model is built for speed and efficiency. If you need a more accurate model, you can use the `TextEmbedding` class to load any model from our list of available models. You can find the list of available models using `TextEmbedding.list_supported_models()`."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "272c8915",
"id": "2e9c8766",
"metadata": {},
"outputs": [],
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "9470ec542f3c4400a42452c2489a1abc",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Fetching 8 files: 0%| | 0/8 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"embedding_model = DefaultEmbedding()"
]
},
{
"cell_type": "markdown",
"id": "5549d501",
"metadata": {},
"source": [
"### Embed your Documents\n",
"\n",
"Use the embed method of the embedding model to transform the documents into a List of np.array. The method returns a generator, so we cast it to a list to get the embeddings."
"multilingual_large_model = TextEmbedding(\"intfloat/multilingual-e5-large\")"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "8013eee9",
"id": "a9e70f0e",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.\n"
]
"data": {
"text/plain": [
"(4, 1024)"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"embeddings: List[np.ndarray] = embedding_model.embed(documents)"
"np.array(\n",
" list(multilingual_large_model.embed([\"Hello, world!\", \"你好世界\", \"¡Hola Mundo!\", \"नमस्ते!\"]))\n",
").shape # Vector of 1024 dimensions"
]
},
{
"cell_type": "markdown",
"id": "e5b5a6ad",
"id": "64fe20ed",
"metadata": {},
"source": [
"You can print the shape of the embeddings to understand their dimensions. Typically, the shape will indicate the number of dimensions in the vector."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "0d8c8e08",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"torch.Size([384])\n"
]
}
],
"source": [
"print(embeddings[0].shape) # (384,) or similar output"
"Next: Checkout how to use FastEmbed with Qdrant for similarity search: [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/)"
]
}
],
@@ -230,7 +236,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
"version": "3.10.13"
}
},
"nbformat": 4,
+410
View File
@@ -0,0 +1,410 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d14d29ebd3592ecb",
"metadata": {
"collapsed": false
},
"source": [
"# Late Interaction Text Embedding Models\n",
"\n",
"As of version 0.3.0 FastEmbed supports Late Interaction Text Embedding Models and currently available with one of the most popular embedding model of the family - ColBERT.\n",
"\n",
"## What is a Late Interaction Text Embedding Model?\n",
"\n",
"Late Interaction Text Embedding Model is a kind of information retrieval model which performs query and documents interactions at the scoring stage.\n",
"In order to better understand it, we can compare it to the models without interaction. \n",
"For instance, if you take a sentence-transformer model, compute embeddings for your documents, compute embeddings for your queries, and just compare them by cosine similarity, then you're retrieving points without interaction.\n",
"\n",
"It is a pretty much easy and straightforward approach, however we might be sacrificing some precision due to its simplicity. It is caused by several facts: \n",
"- there is no interaction between queries and documents at the early stage (embedding generation) nor at the late stage (during scoring). \n",
"- we are trying to encapsulate all the document information in only one pooled embedding, and obviously, some information might be lost.\n",
"\n",
"Late Interaction Text Embedding models are trying to address it by computing embeddings for each token in queries and documents, and then finding the most similar ones via model specific operation, e.g. ColBERT (Contextual Late Interaction over BERT) uses MaxSim operation.\n",
"With this approach we can have not only a better representation of the documents, but also make queries and documents more aware one of another.\n",
"\n",
"For more information on ColBERT and MaxSim operation, you can check out [this blogpost](https://jina.ai/news/what-is-colbert-and-late-interaction-and-why-they-matter-in-search/) by Jina AI.\n",
"\n",
"## ColBERT in FastEmbed\n",
"\n",
"FastEmbed provides a simple way to use ColBERT model, similar to the ones it has with `TextEmbedding`.\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "7f1053b17c810be5",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:20:26.927643Z",
"start_time": "2024-06-03T17:20:25.128994Z"
},
"collapsed": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/joein/work/qdrant/fastembed/venv/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
" from .autonotebook import tqdm as notebook_tqdm\n"
]
},
{
"data": {
"text/plain": "[{'model': 'colbert-ir/colbertv2.0',\n 'dim': 128,\n 'description': 'Late interaction model',\n 'size_in_GB': 0.44,\n 'sources': {'hf': 'colbert-ir/colbertv2.0'},\n 'model_file': 'model.onnx'}]"
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from fastembed import LateInteractionTextEmbedding\n",
"\n",
"LateInteractionTextEmbedding.list_supported_models()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c2c15893df422631",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:23:35.764183Z",
"start_time": "2024-06-03T17:23:21.630277Z"
},
"collapsed": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]\n",
"config.json: 100%|██████████| 743/743 [00:00<00:00, 4.56MB/s]\n",
"\n",
"tokenizer_config.json: 100%|██████████| 405/405 [00:00<00:00, 3.34MB/s]\n",
"Fetching 5 files: 20%|██ | 1/5 [00:00<00:01, 3.64it/s]\n",
"tokenizer.json: 0%| | 0.00/466k [00:00<?, ?B/s]\u001b[A\n",
"\n",
"special_tokens_map.json: 100%|██████████| 112/112 [00:00<00:00, 727kB/s]\n",
"\n",
"tokenizer.json: 100%|██████████| 466k/466k [00:00<00:00, 1.48MB/s]\u001b[A\n",
"\n",
"model.onnx: 0%| | 0.00/436M [00:00<?, ?B/s]\u001b[A\n",
"model.onnx: 2%|▏ | 10.5M/436M [00:00<00:34, 12.2MB/s]\u001b[A\n",
"model.onnx: 5%|▍ | 21.0M/436M [00:01<00:20, 20.3MB/s]\u001b[A\n",
"model.onnx: 7%|▋ | 31.5M/436M [00:01<00:15, 25.7MB/s]\u001b[A\n",
"model.onnx: 10%|▉ | 41.9M/436M [00:01<00:13, 29.4MB/s]\u001b[A\n",
"model.onnx: 12%|█▏ | 52.4M/436M [00:01<00:12, 31.9MB/s]\u001b[A\n",
"model.onnx: 14%|█▍ | 62.9M/436M [00:02<00:11, 33.7MB/s]\u001b[A\n",
"model.onnx: 17%|█▋ | 73.4M/436M [00:02<00:10, 34.9MB/s]\u001b[A\n",
"model.onnx: 19%|█▉ | 83.9M/436M [00:02<00:09, 35.5MB/s]\u001b[A\n",
"model.onnx: 22%|██▏ | 94.4M/436M [00:03<00:09, 36.1MB/s]\u001b[A\n",
"model.onnx: 24%|██▍ | 105M/436M [00:03<00:09, 36.6MB/s] \u001b[A\n",
"model.onnx: 26%|██▋ | 115M/436M [00:03<00:08, 36.9MB/s]\u001b[A\n",
"model.onnx: 29%|██▉ | 126M/436M [00:03<00:08, 37.1MB/s]\u001b[A\n",
"model.onnx: 31%|███▏ | 136M/436M [00:04<00:08, 37.3MB/s]\u001b[A\n",
"model.onnx: 34%|███▎ | 147M/436M [00:04<00:07, 37.4MB/s]\u001b[A\n",
"model.onnx: 36%|███▌ | 157M/436M [00:04<00:07, 37.4MB/s]\u001b[A\n",
"model.onnx: 38%|███▊ | 168M/436M [00:05<00:07, 37.5MB/s]\u001b[A\n",
"model.onnx: 41%|████ | 178M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
"model.onnx: 43%|████▎ | 189M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
"model.onnx: 46%|████▌ | 199M/436M [00:05<00:06, 37.6MB/s]\u001b[A\n",
"model.onnx: 48%|████▊ | 210M/436M [00:06<00:06, 37.5MB/s]\u001b[A\n",
"model.onnx: 50%|█████ | 220M/436M [00:06<00:05, 37.5MB/s]\u001b[A\n",
"model.onnx: 53%|█████▎ | 231M/436M [00:06<00:05, 37.6MB/s]\u001b[A\n",
"model.onnx: 55%|█████▌ | 241M/436M [00:06<00:05, 37.6MB/s]\u001b[A\n",
"model.onnx: 58%|█████▊ | 252M/436M [00:07<00:04, 37.6MB/s]\u001b[A\n",
"model.onnx: 60%|██████ | 262M/436M [00:07<00:04, 37.7MB/s]\u001b[A\n",
"model.onnx: 63%|██████▎ | 273M/436M [00:07<00:04, 37.7MB/s]\u001b[A\n",
"model.onnx: 65%|██████▍ | 283M/436M [00:08<00:04, 36.0MB/s]\u001b[A\n",
"model.onnx: 67%|██████▋ | 294M/436M [00:08<00:03, 36.4MB/s]\u001b[A\n",
"model.onnx: 70%|██████▉ | 304M/436M [00:08<00:03, 36.8MB/s]\u001b[A\n",
"model.onnx: 72%|███████▏ | 315M/436M [00:08<00:03, 37.0MB/s]\u001b[A\n",
"model.onnx: 75%|███████▍ | 325M/436M [00:09<00:02, 37.3MB/s]\u001b[A\n",
"model.onnx: 77%|███████▋ | 336M/436M [00:09<00:03, 30.8MB/s]\u001b[A\n",
"model.onnx: 79%|███████▉ | 346M/436M [00:10<00:02, 32.6MB/s]\u001b[A\n",
"model.onnx: 82%|████████▏ | 357M/436M [00:10<00:02, 33.9MB/s]\u001b[A\n",
"model.onnx: 84%|████████▍ | 367M/436M [00:10<00:01, 34.8MB/s]\u001b[A\n",
"model.onnx: 87%|████████▋ | 377M/436M [00:10<00:01, 35.7MB/s]\u001b[A\n",
"model.onnx: 89%|████████▉ | 388M/436M [00:11<00:01, 36.2MB/s]\u001b[A\n",
"model.onnx: 91%|█████████▏| 398M/436M [00:11<00:01, 36.6MB/s]\u001b[A\n",
"model.onnx: 94%|█████████▍| 409M/436M [00:11<00:00, 36.9MB/s]\u001b[A\n",
"model.onnx: 96%|█████████▌| 419M/436M [00:11<00:00, 37.1MB/s]\u001b[A\n",
"model.onnx: 99%|█████████▊| 430M/436M [00:12<00:00, 37.3MB/s]\u001b[A\n",
"model.onnx: 100%|██████████| 436M/436M [00:12<00:00, 35.1MB/s]\u001b[A\n",
"Fetching 5 files: 100%|██████████| 5/5 [00:13<00:00, 2.68s/it]\n"
]
}
],
"source": [
"embedding_model = LateInteractionTextEmbedding(\"colbert-ir/colbertv2.0\")"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "e560b5fa7d63bea3",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:39:33.400876Z",
"start_time": "2024-06-03T17:39:33.397431Z"
},
"collapsed": false
},
"outputs": [],
"source": [
"documents = [\n",
" \"ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.\",\n",
" \"On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process\",\n",
"]\n",
"queries = [\n",
" \"Are there any other late interaction text embedding models except ColBERT?\",\n",
" \"What is the difference between late interaction and early interaction text embedding models?\",\n",
"]"
]
},
{
"cell_type": "markdown",
"id": "347ad924a3449743",
"metadata": {
"collapsed": false
},
"source": [
"*NOTE*: ColBERT computes query and documents embeddings differently, make sure to use the corresponding methods."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "496fbf51e4eaaae",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:39:34.379885Z",
"start_time": "2024-06-03T17:39:34.316257Z"
},
"collapsed": false
},
"outputs": [],
"source": [
"document_embeddings = list(\n",
" embedding_model.embed(documents)\n",
") # embed and qury_embed return generators,\n",
"# which we need to evaluate by writing them to a list\n",
"query_embeddings = list(embedding_model.query_embed(queries))"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "50595bb0498f0c7c",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:39:34.793528Z",
"start_time": "2024-06-03T17:39:34.788545Z"
},
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": "((26, 128), (32, 128))"
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"document_embeddings[0].shape, query_embeddings[0].shape"
]
},
{
"cell_type": "markdown",
"id": "13e43f2c24a7d5fc",
"metadata": {
"collapsed": false
},
"source": [
"Don't worry about query embeddings having the bigger shape in this case. \n",
"ColBERT authors recommend to pad queries with [MASK] tokens to 32 tokens.\n",
"They also recommends to truncate queries to 32 tokens, however we don't do that in FastEmbed, so you can put some straight into the queries."
]
},
{
"cell_type": "markdown",
"id": "bb1a4011effd3699",
"metadata": {
"collapsed": false
},
"source": [
"## MaxSim operator"
]
},
{
"cell_type": "markdown",
"id": "e9ea4cf82521f2de",
"metadata": {
"collapsed": false
},
"source": [
"Qdrant will support ColBERT as of the next version (v1.10), however, at the moment, you can compute embedding similarities manually. "
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "f84392f63d2c6076",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:39:36.431622Z",
"start_time": "2024-06-03T17:39:36.427363Z"
},
"collapsed": false
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"\n",
"def compute_relevance_scores(query_embedding: np.array, document_embeddings: np.array, k: int):\n",
" \"\"\"\n",
" Compute relevance scores for top-k documents given a query.\n",
"\n",
" :param query_embedding: Numpy array representing the query embedding, shape: [num_query_terms, embedding_dim]\n",
" :param document_embeddings: Numpy array representing embeddings for documents, shape: [num_documents, max_doc_length, embedding_dim]\n",
" :param k: Number of top documents to return\n",
" :return: Indices of the top-k documents based on their relevance scores\n",
" \"\"\"\n",
" # Compute batch dot-product of query_embedding and document_embeddings\n",
" # Resulting shape: [num_documents, num_query_terms, max_doc_length]\n",
" scores = np.matmul(query_embedding, document_embeddings.transpose(0, 2, 1))\n",
"\n",
" # Apply max-pooling across document terms (axis=2) to find the max similarity per query term\n",
" # Shape after max-pool: [num_documents, num_query_terms]\n",
" max_scores_per_query_term = np.max(scores, axis=2)\n",
"\n",
" # Sum the scores across query terms to get the total score for each document\n",
" # Shape after sum: [num_documents]\n",
" total_scores = np.sum(max_scores_per_query_term, axis=1)\n",
"\n",
" # Sort the documents based on their total scores and get the indices of the top-k documents\n",
" sorted_indices = np.argsort(total_scores)[::-1][:k]\n",
"\n",
" return sorted_indices"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "c61d07bed7b60e35",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:39:37.053383Z",
"start_time": "2024-06-03T17:39:37.050926Z"
},
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Sorted document indices: [0 1]\n"
]
}
],
"source": [
"sorted_indices = compute_relevance_scores(\n",
" np.array(query_embeddings[0]), np.array(document_embeddings), k=3\n",
")\n",
"print(\"Sorted document indices:\", sorted_indices)"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "b24df2569970d9e8",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-03T17:40:52.276846Z",
"start_time": "2024-06-03T17:40:52.273789Z"
},
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Query: Are there any other late interaction text embedding models except ColBERT?\n",
"Document: ColBERT is a late interaction text embedding model, however, there are also other models such as TwinBERT.\n",
"Document: On the contrary to the late interaction models, the early interaction models contains interaction steps at embedding generation process\n"
]
}
],
"source": [
"print(f\"Query: {queries[0]}\")\n",
"for index in sorted_indices:\n",
" print(f\"Document: {documents[index]}\")"
]
},
{
"cell_type": "markdown",
"id": "6de537c37aff3927",
"metadata": {
"collapsed": false
},
"source": [
"## Use-case recommendation"
]
},
{
"cell_type": "markdown",
"id": "37e3525d3259cd2b",
"metadata": {
"collapsed": false
},
"source": [
"Despite ColBERT allows to compute embeddings independently and spare some workload offline, it still computes more resources than no interaction models. Due to this, it might be more reasonable to use ColBERT not as a first-stage retriever, but as a re-ranker.\n",
"\n",
"The first-stage retriever would then be a no-interaction model, which e.g. retrieves first 100 or 500 examples, and leave the final ranking to the ColBERT model."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cfa922793454b4ad",
"metadata": {
"collapsed": false
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+436
View File
@@ -0,0 +1,436 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "ntGNDuSCeAR2"
},
"source": [
"# FastEmbed on GPU\n",
"\n",
"As of version 0.2.7 FastEmbed supports GPU acceleration.\n",
"\n",
"This notebook covers the installation process and usage of fastembed on GPU.\n",
"\n",
"## Installation\n",
"\n",
"Fastembed depends on `onnxruntime` and inherits its scheme of GPU support.\n",
"\n",
"In order to use GPU with onnx models, you would need to have `onnxruntime-gpu` package, which substitutes all the `onnxruntime` functionality.\n",
"Fastembed mimics this behavior and requires `fastembed-gpu` package to be installed."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "GK2XADwUeEK7"
},
"outputs": [],
"source": [
"!pip install fastembed-gpu"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3aiGPqjCeGzo"
},
"source": [
"**NOTE**: `onnxruntime-gpu` and `onnxruntime` can't be installed in the same environment. If you have `onnxruntime` installed, you would need to uninstall it before installing `onnxruntime-gpu`. Same is true for `fastembed` and `fastembed-gpu`.\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"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3xx3r-9jgAMi"
},
"source": [
"You can check your CUDA version using such commands as `nvidia-smi` or `nvcc --version`\n",
"\n",
"Google Colab notebooks have CUDA 12.x."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Igv5RXhSeO68"
},
"source": [
"### CUDA drivers\n",
"\n",
"FastEmbed does not include CUDA drivers and CuDNN libraries.\n",
"You would need to take care of the environment setup on your own.\n",
"Dependencies required for the chosen onnxruntime version can be found [here](https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Usage"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 334,
"referenced_widgets": [
"aacf08a7aa444b64a2efad1967d28a53",
"5606aa785de74d65a9928b31c0be8a53",
"d4ec9d3b74ec4412894da2161ed2bddf",
"8edd544c3e074ec1813e5b9d1aef43d9",
"9898890f8a75468ea20e3ce319d0b6e2",
"da3b18abb16241a0a7191ee9afcb0510",
"258a619168824253a6a329efdc51ebe6",
"53c7cdc967d24faba0b5c659c94c50b8",
"e9348d8be28d408e8e760c71b21ab294",
"0ba06e0816714f2fbdec8260f160abc0",
"0b96563334964d449dd34f35b6b3e715",
"11c2eec490e8479b944eec7f30cb1ca2",
"91463da0d1c5466795e06ab586002259",
"30f4f7833406474f89ef0700b00a33aa",
"4302c304ec6a4b5985797e300bd7e353",
"2605640c7b824ed7aa137d404e14b774",
"b02efe3a33d04f06aa8938719ab35671",
"50408e5d052343b1a1b44a0fae0f801d",
"1be01c95d9e84f8ea88367c987a72fdc",
"a109c13bc93a449186424542dc330be8",
"4adce304ce1947b5a01dde10bbb3bb8c",
"a761366a37e44837a25e0f25b18efed2",
"94512b9055e546389471197b76ad5449",
"072dca00bd7b4918a178f90ccabf698a",
"48a856c59ef74cc3834521b1bf616541",
"c020c503aeaa464cad643ade5ee3ae24",
"a4e7e40c0bbd4f878c20a9f65fe3a048",
"e8c0a1c339fd47668d944a9defad79d4",
"cd782d35c6bd40c0a60d57b1828a7251",
"04f638ab08da4d20928644c4ba03f8ef",
"17f20477fc79475f97adf1c1f64a4192",
"96f7b5a2e224462e9fcffd03f906a593",
"755cd32d9fc9407c80a160f45c802d1e",
"a886258e7cd14c048b58391d7b772901",
"bc3e48f826a74840867a6209e622b75e",
"125b2ac0f78043bba7eca53474ca44c4",
"82f186d1ffb4435d94a6c7e9025242ef",
"77000333e5ca4094be291ad82d4a627a",
"7fe64fb53055431488d002c76c8e331e",
"7de59ae9919f4a5bb2b6e601a3c02412",
"97a69423a6644eab87fc636e182f23a4",
"4df936d1065b41f4bf02ed394fdf7b7e",
"3918bd1affa3454e8e9044a418a056ea",
"163b27ae0bce41e5b48efcb4b3fd780d",
"94631fd6e0744085bc79c3121de4a9f7",
"31cd98d66bc54418b35e70fbbc0fa3c0",
"6d21627a638b4ddca6fe7bfb80a621b5",
"b37bed9dc4fe45c08b8397288fe5b1a9",
"164fef95d1414177a40d563f5682f6a3",
"1a9a0ea53448413a8e4b360b7bb69e26",
"dd1a4483b4b045c6929e3d2cf1338f63",
"496ddd8e05f949cd8cbba8e677f476ac",
"2813be951d7f48b2aad1dd4a444ce3eb",
"8e9a2c2dd21942edbdfecb3b7dffc70b",
"08a10fe247f1425db044cfc13f2fb384",
"b8786aded92d421592bc7623c5c7899e",
"c91a20a9433d4016ba2db69fa50e0b4d",
"e997820738594c6dadb061908d7afdc1",
"a5fc751f81ae498f9aa55ece0e6853b2",
"2aee4fc8cda64c5eb8722be81e48e0ca",
"3a53e8624dff48b3959875ef58ee99ce",
"50a70044f77542108fe188598e70797e",
"13cf998b35ae4507a63e797f6fa3eada",
"6209eb6a68cf4a378767ef34d0d9216d",
"7395db766b944af9b41d6b56c9ada0b1",
"42122c317ec648688f0164a1adb5df28"
]
},
"id": "Ttf4YggPeQQK",
"outputId": "aa75129d-9e2d-4c88-cf03-251dd43a11b1"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.10/dist-packages/huggingface_hub/utils/_token.py:88: UserWarning: \n",
"The secret `HF_TOKEN` does not exist in your Colab secrets.\n",
"To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n",
"You will be able to reuse this secret in all of your notebooks.\n",
"Please note that authentication is recommended but still optional to access public models or datasets.\n",
" warnings.warn(\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "aacf08a7aa444b64a2efad1967d28a53",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "11c2eec490e8479b944eec7f30cb1ca2",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"tokenizer_config.json: 0%| | 0.00/1.24k [00:00<?, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "94512b9055e546389471197b76ad5449",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"config.json: 0%| | 0.00/706 [00:00<?, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "a886258e7cd14c048b58391d7b772901",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"special_tokens_map.json: 0%| | 0.00/695 [00:00<?, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "94631fd6e0744085bc79c3121de4a9f7",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"tokenizer.json: 0%| | 0.00/711k [00:00<?, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "b8786aded92d421592bc7623c5c7899e",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"model_optimized.onnx: 0%| | 0.00/66.5M [00:00<?, ?B/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"['CUDAExecutionProvider', 'CPUExecutionProvider']"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from typing import List\n",
"\n",
"import numpy as np\n",
"\n",
"from fastembed import TextEmbedding\n",
"\n",
"embedding_model_gpu = TextEmbedding(\n",
" model_name=\"BAAI/bge-small-en-v1.5\", providers=[\"CUDAExecutionProvider\"]\n",
")\n",
"embedding_model_gpu.model.model.get_providers()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"id": "iPtoHf7GeV-i"
},
"outputs": [],
"source": [
"documents: List[str] = list(np.repeat(\"Demonstrating GPU acceleration in fastembed\", 500))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "islhyLf4ed-H",
"outputId": "8c8ed09b-9eac-438f-97bc-578751975148"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"43.4 ms ± 2.06 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
]
}
],
"source": [
"%%timeit\n",
"list(embedding_model_gpu.embed(documents))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 67,
"referenced_widgets": [
"9c306ce5188c45feb8dfb9089592591c",
"296ff54c6e61441f978084df59626598",
"d6d42b4f245a49b7ba7769e23a3202fc",
"39ce7754480147759c16a3089d8105af",
"8253960a069d4106863a75faae54b90d",
"7ccf959452af4c0b873c7567747f0816",
"ac9d0b5a5b1f401e90a1cc9ffe6d4b4c",
"0aada067dec3472f9aba1772d6b775a5",
"07597b1287e04653b80c47a771549376",
"054be1dd9f084cae911745b692ccd929",
"ab19e8e831694e308a4b79f05aff728e"
]
},
"id": "bOKVUvWJegYJ",
"outputId": "dde74917-08b0-4ce2-9a2b-cc31e02cafb2"
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "9c306ce5188c45feb8dfb9089592591c",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Fetching 5 files: 0%| | 0/5 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"['CPUExecutionProvider']"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"embedding_model_cpu = TextEmbedding(model_name=\"BAAI/bge-small-en-v1.5\")\n",
"embedding_model_cpu.model.model.get_providers()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "0NJj9RvSfASP",
"outputId": "526f5280-99bd-454e-8af8-6a860ad96e54"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"4.33 s ± 591 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
]
}
],
"source": [
"%%timeit\n",
"list(embedding_model_cpu.embed(documents))"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"gpuType": "T4",
"provenance": []
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+128
View File
@@ -0,0 +1,128 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "aa0a86859809102",
"metadata": {
"collapsed": false
},
"source": [
"# Image Embedding\n",
"As of version 0.3.0 fastembed supports computation of image embeddings.\n",
"\n",
"The process is as easy and straightforward as with text embeddings. Let's see how it works."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "cea8fd5c019571fe",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-02T11:35:40.126023Z",
"start_time": "2024-06-02T11:35:39.864701Z"
},
"collapsed": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Fetching 3 files: 100%|██████████| 3/3 [00:00<00:00, 47482.69it/s]\n"
]
},
{
"data": {
"text/plain": "[array([0. , 0. , 0. , ..., 0. , 0.01139933,\n 0. ], dtype=float32),\n array([0.02169187, 0. , 0. , ..., 0. , 0.00848291,\n 0. ], dtype=float32)]"
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from fastembed import ImageEmbedding\n",
"\n",
"model = ImageEmbedding(\"Qdrant/resnet50-onnx\")\n",
"\n",
"embeddings_generator = model.embed(\n",
" [\"../../tests/misc/image.jpeg\", \"../../tests/misc/small_image.jpeg\"]\n",
")\n",
"embeddings_list = list(embeddings_generator)\n",
"embeddings_list"
]
},
{
"cell_type": "markdown",
"id": "3f838f18523ad1e0",
"metadata": {
"collapsed": false
},
"source": [
"## Preprocessing\n",
"\n",
"Preprocessing is encapsulated in the ImageEmbedding class, applied operations are identical to the ones provided by [Hugging Face Transformers](https://huggingface.co/docs/transformers/en/index).\n",
"You don't need to think about batching, opening/closing files, resizing images, etc., Fastembed will take care of it."
]
},
{
"cell_type": "markdown",
"id": "894b33ff9b385d72",
"metadata": {
"collapsed": false
},
"source": [
"## Supported models\n",
"\n",
"List of supported image embedding models can either be found [here](https://qdrant.github.io/fastembed/examples/Supported_Models/#supported-image-embedding-models) or by calling the `ImageEmbedding.list_supported_models()` method."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "6d6a4cbbd2200d14",
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-02T11:40:19.313226Z",
"start_time": "2024-06-02T11:40:19.309845Z"
},
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": "[{'model': 'Qdrant/clip-ViT-B-32-vision',\n 'dim': 512,\n 'description': 'CLIP vision encoder based on ViT-B/32',\n 'size_in_GB': 0.34,\n 'sources': {'hf': 'Qdrant/clip-ViT-B-32-vision'},\n 'model_file': 'model.onnx'},\n {'model': 'Qdrant/resnet50-onnx',\n 'dim': 2048,\n 'description': 'ResNet-50 from `Deep Residual Learning for Image Recognition <https://arxiv.org/abs/1512.03385>`__.',\n 'size_in_GB': 0.1,\n 'sources': {'hf': 'Qdrant/resnet50-onnx'},\n 'model_file': 'model.onnx'}]"
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ImageEmbedding.list_supported_models()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+384
View File
@@ -0,0 +1,384 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Introduction to SPLADE with FastEmbed\n",
"\n",
"In this notebook, we will explore how to generate Sparse Vectors -- in particular a variant of the [SPLADE](https://arxiv.org/abs/2107.05720).\n",
"\n",
"> 💡 The original [naver/SPLADE](https://github.com/naver/splade) models were licensed CC BY-NC-SA 4.0 -- Not for Commercial Use. This [SPLADE++](https://huggingface.co/prithivida/Splade_PP_en_v1) model is Apache License and hence, licensed for commercial use. \n",
"\n",
"## Outline:\n",
"1. [What is SPLADE?](#What-is-SPLADE?)\n",
"2. [Setting up the environment](#Setting-up-the-environment)\n",
"3. [Generating SPLADE vectors with FastEmbed](#Generating-SPLADE-vectors-with-FastEmbed)\n",
"4. [Understanding SPLADE vectors](#Understanding-SPLADE-vectors)\n",
"5. [Observations and Design Choices](#Observations-and-Model-Design-Choices)\n",
"\n",
"\n",
"## What is SPLADE?\n",
"\n",
"SPLADE was a novel method for _learning_ sparse vectors for text representation. This model beats BM25 -- the underlying approach for the Elastic/Lucene family of implementations. Thus making it highly effective for tasks such as information retrieval, document classification, and more. \n",
"\n",
"The key advantage of SPLADE is its ability to generate sparse vectors, which are more efficient and interpretable than dense vectors. This makes SPLADE a powerful tool for handling large-scale text data.\n",
"\n",
"## Setting up the environment\n",
"\n",
"This notebook uses few dependencies, which are installed below: "
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"# !pip install -q fastembed"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's get started! 🚀"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:20.516644Z",
"start_time": "2024-03-30T00:49:20.188543Z"
}
},
"outputs": [],
"source": [
"from fastembed import SparseTextEmbedding, SparseEmbedding\n",
"from typing import List"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> You can find the list of all supported Sparse Embedding models by calling this API: `SparseTextEmbedding.list_supported_models()`"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:22.366294Z",
"start_time": "2024-03-30T00:49:22.362384Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"[{'model': 'prithvida/Splade_PP_en_v1',\n",
" 'vocab_size': 30522,\n",
" 'description': 'Misspelled version of the model. Retained for backward compatibility. Independent Implementation of SPLADE++ Model for English',\n",
" 'size_in_GB': 0.532,\n",
" 'sources': {'hf': 'Qdrant/SPLADE_PP_en_v1'}},\n",
" {'model': 'prithivida/Splade_PP_en_v1',\n",
" 'vocab_size': 30522,\n",
" 'description': 'Independent Implementation of SPLADE++ Model for English',\n",
" 'size_in_GB': 0.532,\n",
" 'sources': {'hf': 'Qdrant/SPLADE_PP_en_v1'}}]"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"SparseTextEmbedding.list_supported_models()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:27.193530Z",
"start_time": "2024-03-30T00:49:26.139248Z"
}
},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "2aa47b26ab01475e8d3577433037f685",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Fetching 9 files: 0%| | 0/9 [00:00<?, ?it/s]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"model_name = \"prithvida/Splade_PP_en_v1\"\n",
"# This triggers the model download\n",
"model = SparseTextEmbedding(model_name=model_name)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:28.624109Z",
"start_time": "2024-03-30T00:49:28.399960Z"
}
},
"outputs": [],
"source": [
"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",
" \"Chandrayaan-3 will be launched by the Indian Space Research Organisation (ISRO)\",\n",
" \"The estimated cost of the mission is around $35 million\",\n",
" \"It will carry instruments to study the lunar surface and atmosphere\",\n",
" \"Chandrayaan-3 landed on the Moon's surface on 23rd August 2023\",\n",
" \"It consists of a lander named Vikram and a rover named Pragyan similar to Chandrayaan-2. Its propulsion module would act like an orbiter.\",\n",
" \"The propulsion module carries the lander and rover configuration until the spacecraft is in a 100-kilometre (62 mi) lunar orbit\",\n",
" \"The mission used GSLV Mk III rocket for its launch\",\n",
" \"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",
" model.embed(documents, batch_size=6)\n",
") # batch_size is optional, notice the generator"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:29.646340Z",
"start_time": "2024-03-30T00:49:29.643411Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"SparseEmbedding(values=array([0.05297208, 0.01963477, 0.36459631, 1.38508618, 0.71776593,\n",
" 0.12667948, 0.46230844, 0.446771 , 0.26897505, 1.01519883,\n",
" 1.5655334 , 0.29412213, 1.53102326, 0.59785569, 1.1001817 ,\n",
" 0.02079751, 0.09955651, 0.44249091, 0.09747757, 1.53519952,\n",
" 1.36765671, 0.15740395, 0.49882549, 0.38629025, 0.76612782,\n",
" 1.25805044, 0.39058095, 0.27236196, 0.45152301, 0.48262018,\n",
" 0.26085234, 1.35912788, 0.70710695, 1.71639752]), indices=array([ 1010, 1011, 1016, 1017, 2001, 2018, 2034, 2093, 2117,\n",
" 2319, 2353, 2509, 2634, 2686, 2796, 2817, 2922, 2959,\n",
" 3003, 3148, 3260, 3390, 3462, 3523, 3822, 4231, 4316,\n",
" 4774, 5590, 5871, 6416, 11926, 12076, 16469]))"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"index = 0\n",
"sparse_embeddings_list[index]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The previous output is a SparseEmbedding object for the first document in our list.\n",
"\n",
"It contains two arrays: values and indices. \n",
"- The 'values' array represents the weights of the features (tokens) in the document.\n",
"- The 'indices' array represents the indices of these features in the model's vocabulary.\n",
"\n",
"Each pair of corresponding values and indices represents a token and its weight in the document."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:31.549533Z",
"start_time": "2024-03-30T00:49:31.546398Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Token at index 1010 has weight 0.05297207832336426\n",
"Token at index 1011 has weight 0.01963476650416851\n",
"Token at index 1016 has weight 0.36459630727767944\n",
"Token at index 1017 has weight 1.385086178779602\n",
"Token at index 2001 has weight 0.7177659273147583\n"
]
}
],
"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]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Understanding SPLADE vectors\n",
"\n",
"This is still a little abstract, so let's use the tokenizer vocab to make sense of these indices."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:36.203640Z",
"start_time": "2024-03-30T00:49:34.889654Z"
}
},
"outputs": [],
"source": [
"import json\n",
"from transformers import AutoTokenizer\n",
"\n",
"tokenizer = AutoTokenizer.from_pretrained(SparseTextEmbedding.list_supported_models()[0][\"sources\"][\"hf\"])"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"ExecuteTime": {
"end_time": "2024-03-30T00:49:36.210049Z",
"start_time": "2024-03-30T00:49:36.206825Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"chandra\": 1.7163975238800049,\n",
" \"third\": 1.5655333995819092,\n",
" \"##ya\": 1.535199522972107,\n",
" \"india\": 1.5310232639312744,\n",
" \"3\": 1.385086178779602,\n",
" \"mission\": 1.3676567077636719,\n",
" \"lunar\": 1.3591278791427612,\n",
" \"moon\": 1.2580504417419434,\n",
" \"indian\": 1.1001816987991333,\n",
" \"##an\": 1.015198826789856,\n",
" \"3rd\": 0.7661278247833252,\n",
" \"was\": 0.7177659273147583,\n",
" \"spacecraft\": 0.7071069478988647,\n",
" \"space\": 0.5978556871414185,\n",
" \"flight\": 0.4988254904747009,\n",
" \"satellite\": 0.4826201796531677,\n",
" \"first\": 0.46230843663215637,\n",
" \"expedition\": 0.4515230059623718,\n",
" \"three\": 0.4467709958553314,\n",
" \"fourth\": 0.44249090552330017,\n",
" \"vehicle\": 0.390580952167511,\n",
" \"iii\": 0.3862902522087097,\n",
" \"2\": 0.36459630727767944,\n",
" \"##3\": 0.2941221296787262,\n",
" \"planet\": 0.27236196398735046,\n",
" \"second\": 0.26897504925727844,\n",
" \"missions\": 0.2608523368835449,\n",
" \"launched\": 0.15740394592285156,\n",
" \"had\": 0.12667948007583618,\n",
" \"largest\": 0.09955651313066483,\n",
" \"leader\": 0.09747757017612457,\n",
" \",\": 0.05297207832336426,\n",
" \"study\": 0.02079751156270504,\n",
" \"-\": 0.01963476650416851\n",
"}\n"
]
}
],
"source": [
"def get_tokens_and_weights(sparse_embedding, tokenizer):\n",
" token_weight_dict = {}\n",
" for i in range(len(sparse_embedding.indices)):\n",
" token = tokenizer.decode([sparse_embedding.indices[i]])\n",
" weight = sparse_embedding.values[i]\n",
" token_weight_dict[token] = weight\n",
"\n",
" # Sort the dictionary by weights\n",
" token_weight_dict = dict(sorted(token_weight_dict.items(), key=lambda item: item[1], reverse=True))\n",
" return token_weight_dict\n",
"\n",
"\n",
"# Test the function with the first SparseEmbedding\n",
"print(json.dumps(get_tokens_and_weights(sparse_embeddings_list[index], tokenizer), indent=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Observations and Model Design Choices\n",
"\n",
"1. The relative order of importance is quite useful. The most important tokens in the sentence have the highest weights.\n",
"1. **Term Expansion**: The model can expand the terms in the document. This means that the model can generate weights for tokens that are not present in the document but are related to the tokens in the document. This is a powerful feature that allows the model to capture the context of the document. Here, you'll see that the model has added the tokens '3' from 'third' and 'moon' from 'lunar' to the sparse vector.\n",
"\n",
"### Design Choices\n",
"\n",
"1. The weights are not normalized. This means that the sum of the weights is not 1 or 100. This is a common practice in sparse embeddings, as it allows the model to capture the importance of each token in the document.\n",
"1. Tokens are included in the sparse vector only if they are present in the model's vocabulary. This means that the model will not generate a weight for tokens that it has not seen during training.\n",
"1. Tokens do not map to words directly -- allowing you to gracefully handle typo errors and out-of-vocabulary tokens."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "fst",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+580 -29
View File
@@ -2,8 +2,65 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": 4,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:13:23.806907Z",
"start_time": "2024-05-31T18:13:23.797078Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The autoreload extension is already loaded. To reload it, use:\n",
" %reload_ext autoreload\n"
]
}
],
"source": [
"%load_ext autoreload\n",
"%autoreload 2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:14:31.147674Z",
"start_time": "2024-05-31T18:14:31.134015Z"
}
},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"from fastembed import (\n",
" SparseTextEmbedding,\n",
" TextEmbedding,\n",
" LateInteractionTextEmbedding,\n",
" ImageEmbedding,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Supported Text Embedding Models"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:13:25.863008Z",
"start_time": "2024-05-31T18:13:25.837795Z"
}
},
"outputs": [
{
"data": {
@@ -29,70 +86,559 @@
" <th>model</th>\n",
" <th>dim</th>\n",
" <th>description</th>\n",
" <th>size_in_GB</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>BAAI/bge-small-en</td>\n",
" <td>BAAI/bge-small-en-v1.5</td>\n",
" <td>384</td>\n",
" <td>Fast and Default English model</td>\n",
" <td>0.067</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>BAAI/bge-base-en</td>\n",
" <td>768</td>\n",
" <td>Base English model</td>\n",
" <td>BAAI/bge-small-zh-v1.5</td>\n",
" <td>512</td>\n",
" <td>Fast and recommended Chinese model</td>\n",
" <td>0.090</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>sentence-transformers/all-MiniLM-L6-v2</td>\n",
" <td>snowflake/snowflake-arctic-embed-xs</td>\n",
" <td>384</td>\n",
" <td>Sentence Transformer model, MiniLM-L6-v2</td>\n",
" <td>Based on all-MiniLM-L6-v2 model with only 22m ...</td>\n",
" <td>0.090</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>sentence-transformers/all-MiniLM-L6-v2</td>\n",
" <td>384</td>\n",
" <td>Sentence Transformer model, MiniLM-L6-v2</td>\n",
" <td>0.090</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>jinaai/jina-embeddings-v2-small-en</td>\n",
" <td>512</td>\n",
" <td>English embedding model supporting 8192 sequen...</td>\n",
" <td>0.120</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>BAAI/bge-small-en</td>\n",
" <td>384</td>\n",
" <td>Fast English model</td>\n",
" <td>0.130</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>snowflake/snowflake-arctic-embed-s</td>\n",
" <td>384</td>\n",
" <td>Based on infloat/e5-small-unsupervised, does n...</td>\n",
" <td>0.130</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>nomic-ai/nomic-embed-text-v1.5-Q</td>\n",
" <td>768</td>\n",
" <td>Quantized 8192 context length english model</td>\n",
" <td>0.130</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>BAAI/bge-base-en-v1.5</td>\n",
" <td>768</td>\n",
" <td>Base English model, v1.5</td>\n",
" <td>0.210</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
" <td>384</td>\n",
" <td>Sentence Transformer model, paraphrase-multili...</td>\n",
" <td>0.220</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>Qdrant/clip-ViT-B-32-text</td>\n",
" <td>512</td>\n",
" <td>CLIP text encoder</td>\n",
" <td>0.250</td>\n",
" </tr>\n",
" <tr>\n",
" <th>11</th>\n",
" <td>jinaai/jina-embeddings-v2-base-de</td>\n",
" <td>768</td>\n",
" <td>German embedding model supporting 8192 sequenc...</td>\n",
" <td>0.320</td>\n",
" </tr>\n",
" <tr>\n",
" <th>12</th>\n",
" <td>BAAI/bge-base-en</td>\n",
" <td>768</td>\n",
" <td>Base English model</td>\n",
" <td>0.420</td>\n",
" </tr>\n",
" <tr>\n",
" <th>13</th>\n",
" <td>snowflake/snowflake-arctic-embed-m</td>\n",
" <td>768</td>\n",
" <td>Based on intfloat/e5-base-unsupervised model, ...</td>\n",
" <td>0.430</td>\n",
" </tr>\n",
" <tr>\n",
" <th>14</th>\n",
" <td>nomic-ai/nomic-embed-text-v1.5</td>\n",
" <td>768</td>\n",
" <td>8192 context length english model</td>\n",
" <td>0.520</td>\n",
" </tr>\n",
" <tr>\n",
" <th>15</th>\n",
" <td>jinaai/jina-embeddings-v2-base-en</td>\n",
" <td>768</td>\n",
" <td>English embedding model supporting 8192 sequen...</td>\n",
" <td>0.520</td>\n",
" </tr>\n",
" <tr>\n",
" <th>16</th>\n",
" <td>nomic-ai/nomic-embed-text-v1</td>\n",
" <td>768</td>\n",
" <td>8192 context length english model</td>\n",
" <td>0.520</td>\n",
" </tr>\n",
" <tr>\n",
" <th>17</th>\n",
" <td>snowflake/snowflake-arctic-embed-m-long</td>\n",
" <td>768</td>\n",
" <td>Based on nomic-ai/nomic-embed-text-v1-unsuperv...</td>\n",
" <td>0.540</td>\n",
" </tr>\n",
" <tr>\n",
" <th>18</th>\n",
" <td>mixedbread-ai/mxbai-embed-large-v1</td>\n",
" <td>1024</td>\n",
" <td>MixedBread Base sentence embedding model, does...</td>\n",
" <td>0.640</td>\n",
" </tr>\n",
" <tr>\n",
" <th>19</th>\n",
" <td>jinaai/jina-embeddings-v2-base-code</td>\n",
" <td>768</td>\n",
" <td>Source code embedding model supporting 8192 se...</td>\n",
" <td>0.640</td>\n",
" </tr>\n",
" <tr>\n",
" <th>20</th>\n",
" <td>sentence-transformers/paraphrase-multilingual-...</td>\n",
" <td>768</td>\n",
" <td>Sentence-transformers model for tasks like clu...</td>\n",
" <td>1.000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>21</th>\n",
" <td>snowflake/snowflake-arctic-embed-l</td>\n",
" <td>1024</td>\n",
" <td>Based on intfloat/e5-large-unsupervised, large...</td>\n",
" <td>1.020</td>\n",
" </tr>\n",
" <tr>\n",
" <th>22</th>\n",
" <td>thenlper/gte-large</td>\n",
" <td>1024</td>\n",
" <td>Large general text embeddings model</td>\n",
" <td>1.200</td>\n",
" </tr>\n",
" <tr>\n",
" <th>23</th>\n",
" <td>BAAI/bge-large-en-v1.5</td>\n",
" <td>1024</td>\n",
" <td>Large English model, v1.5</td>\n",
" <td>1.200</td>\n",
" </tr>\n",
" <tr>\n",
" <th>24</th>\n",
" <td>intfloat/multilingual-e5-large</td>\n",
" <td>1024</td>\n",
" <td>Multilingual model, e5-large. Recommend using this model for non-English languages. Recommend using this via Torch implementation of FastEmbed</td>\n",
" <td>Multilingual model, e5-large. Recommend using ...</td>\n",
" <td>2.240</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" model dim \\\n",
"0 BAAI/bge-small-en 384 \n",
"1 BAAI/bge-base-en 768 \n",
"2 sentence-transformers/all-MiniLM-L6-v2 384 \n",
"3 intfloat/multilingual-e5-large 1024 \n",
" model dim \\\n",
"0 BAAI/bge-small-en-v1.5 384 \n",
"1 BAAI/bge-small-zh-v1.5 512 \n",
"2 snowflake/snowflake-arctic-embed-xs 384 \n",
"3 sentence-transformers/all-MiniLM-L6-v2 384 \n",
"4 jinaai/jina-embeddings-v2-small-en 512 \n",
"5 BAAI/bge-small-en 384 \n",
"6 snowflake/snowflake-arctic-embed-s 384 \n",
"7 nomic-ai/nomic-embed-text-v1.5-Q 768 \n",
"8 BAAI/bge-base-en-v1.5 768 \n",
"9 sentence-transformers/paraphrase-multilingual-... 384 \n",
"10 Qdrant/clip-ViT-B-32-text 512 \n",
"11 jinaai/jina-embeddings-v2-base-de 768 \n",
"12 BAAI/bge-base-en 768 \n",
"13 snowflake/snowflake-arctic-embed-m 768 \n",
"14 nomic-ai/nomic-embed-text-v1.5 768 \n",
"15 jinaai/jina-embeddings-v2-base-en 768 \n",
"16 nomic-ai/nomic-embed-text-v1 768 \n",
"17 snowflake/snowflake-arctic-embed-m-long 768 \n",
"18 mixedbread-ai/mxbai-embed-large-v1 1024 \n",
"19 jinaai/jina-embeddings-v2-base-code 768 \n",
"20 sentence-transformers/paraphrase-multilingual-... 768 \n",
"21 snowflake/snowflake-arctic-embed-l 1024 \n",
"22 thenlper/gte-large 1024 \n",
"23 BAAI/bge-large-en-v1.5 1024 \n",
"24 intfloat/multilingual-e5-large 1024 \n",
"\n",
" description \n",
"0 Fast and Default English model \n",
"1 Base English model \n",
"2 Sentence Transformer model, MiniLM-L6-v2 \n",
"3 Multilingual model, e5-large. Recommend using this model for non-English languages. Recommend using this via Torch implementation of FastEmbed "
" description size_in_GB \n",
"0 Fast and Default English model 0.067 \n",
"1 Fast and recommended Chinese model 0.090 \n",
"2 Based on all-MiniLM-L6-v2 model with only 22m ... 0.090 \n",
"3 Sentence Transformer model, MiniLM-L6-v2 0.090 \n",
"4 English embedding model supporting 8192 sequen... 0.120 \n",
"5 Fast English model 0.130 \n",
"6 Based on infloat/e5-small-unsupervised, does n... 0.130 \n",
"7 Quantized 8192 context length english model 0.130 \n",
"8 Base English model, v1.5 0.210 \n",
"9 Sentence Transformer model, paraphrase-multili... 0.220 \n",
"10 CLIP text encoder 0.250 \n",
"11 German embedding model supporting 8192 sequenc... 0.320 \n",
"12 Base English model 0.420 \n",
"13 Based on intfloat/e5-base-unsupervised model, ... 0.430 \n",
"14 8192 context length english model 0.520 \n",
"15 English embedding model supporting 8192 sequen... 0.520 \n",
"16 8192 context length english model 0.520 \n",
"17 Based on nomic-ai/nomic-embed-text-v1-unsuperv... 0.540 \n",
"18 MixedBread Base sentence embedding model, does... 0.640 \n",
"19 Source code embedding model supporting 8192 se... 0.640 \n",
"20 Sentence-transformers model for tasks like clu... 1.000 \n",
"21 Based on intfloat/e5-large-unsupervised, large... 1.020 \n",
"22 Large general text embeddings model 1.200 \n",
"23 Large English model, v1.5 1.200 \n",
"24 Multilingual model, e5-large. Recommend using ... 2.240 "
]
},
"execution_count": 1,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"%load_ext autoreload\n",
"%autoreload 2\n",
"\n",
"from fastembed.embedding import Embedding\n",
"import pandas as pd\n",
"pd.set_option('display.max_colwidth', None)\n",
"pd.DataFrame(Embedding.list_supported_models())"
"supported_models = (\n",
" pd.DataFrame(TextEmbedding.list_supported_models())\n",
" .sort_values(\"size_in_GB\")\n",
" .drop(columns=[\"sources\", \"model_file\", \"additional_files\"])\n",
" .reset_index(drop=True)\n",
")\n",
"supported_models"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Supported Sparse Text Embedding Models"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:13:27.124747Z",
"start_time": "2024-05-31T18:13:27.096212Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>model</th>\n",
" <th>vocab_size</th>\n",
" <th>description</th>\n",
" <th>size_in_GB</th>\n",
" <th>requires_idf</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Qdrant/bm25</td>\n",
" <td>NaN</td>\n",
" <td>BM25 as sparse embeddings meant to be used wit...</td>\n",
" <td>0.010</td>\n",
" <td>True</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Qdrant/bm42-all-minilm-l6-v2-attentions</td>\n",
" <td>30522.0</td>\n",
" <td>Light sparse embedding model, which assigns an...</td>\n",
" <td>0.090</td>\n",
" <td>True</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>prithvida/Splade_PP_en_v1</td>\n",
" <td>30522.0</td>\n",
" <td>Misspelled version of the model. Retained for ...</td>\n",
" <td>0.532</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>prithivida/Splade_PP_en_v1</td>\n",
" <td>30522.0</td>\n",
" <td>Independent Implementation of SPLADE++ Model f...</td>\n",
" <td>0.532</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" model vocab_size \\\n",
"0 Qdrant/bm25 NaN \n",
"1 Qdrant/bm42-all-minilm-l6-v2-attentions 30522.0 \n",
"2 prithvida/Splade_PP_en_v1 30522.0 \n",
"3 prithivida/Splade_PP_en_v1 30522.0 \n",
"\n",
" description size_in_GB requires_idf \n",
"0 BM25 as sparse embeddings meant to be used wit... 0.010 True \n",
"1 Light sparse embedding model, which assigns an... 0.090 True \n",
"2 Misspelled version of the model. Retained for ... 0.532 NaN \n",
"3 Independent Implementation of SPLADE++ Model f... 0.532 NaN "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(\n",
" pd.DataFrame(SparseTextEmbedding.list_supported_models())\n",
" .sort_values(\"size_in_GB\")\n",
" .drop(columns=[\"sources\", \"model_file\", \"additional_files\"])\n",
" .reset_index(drop=True)\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"## Supported Late Interaction Text Embedding Models"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:14:34.370252Z",
"start_time": "2024-05-31T18:14:34.354270Z"
},
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>model</th>\n",
" <th>dim</th>\n",
" <th>description</th>\n",
" <th>size_in_GB</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>colbert-ir/colbertv2.0</td>\n",
" <td>128</td>\n",
" <td>Late interaction model</td>\n",
" <td>0.44</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" model dim description size_in_GB\n",
"0 colbert-ir/colbertv2.0 128 Late interaction model 0.44"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(\n",
" pd.DataFrame(LateInteractionTextEmbedding.list_supported_models())\n",
" .sort_values(\"size_in_GB\")\n",
" .drop(columns=[\"sources\", \"model_file\"])\n",
" .reset_index(drop=True)\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": false
},
"source": [
"## Supported Image Embedding Models"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-31T18:14:42.501881Z",
"start_time": "2024-05-31T18:14:42.484726Z"
},
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>model</th>\n",
" <th>dim</th>\n",
" <th>description</th>\n",
" <th>size_in_GB</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Qdrant/resnet50-onnx</td>\n",
" <td>2048</td>\n",
" <td>ResNet-50 from `Deep Residual Learning for Ima...</td>\n",
" <td>0.10</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Qdrant/clip-ViT-B-32-vision</td>\n",
" <td>512</td>\n",
" <td>CLIP vision encoder based on ViT-B/32</td>\n",
" <td>0.34</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>Qdrant/Unicom-ViT-B-32</td>\n",
" <td>512</td>\n",
" <td>Unicom Unicom-ViT-B-32 from open-metric-learning</td>\n",
" <td>0.48</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>Qdrant/Unicom-ViT-B-16</td>\n",
" <td>768</td>\n",
" <td>Unicom Unicom-ViT-B-16 from open-metric-learning</td>\n",
" <td>0.82</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" model dim \\\n",
"0 Qdrant/resnet50-onnx 2048 \n",
"1 Qdrant/clip-ViT-B-32-vision 512 \n",
"2 Qdrant/Unicom-ViT-B-32 512 \n",
"3 Qdrant/Unicom-ViT-B-16 768 \n",
"\n",
" description size_in_GB \n",
"0 ResNet-50 from `Deep Residual Learning for Ima... 0.10 \n",
"1 CLIP vision encoder based on ViT-B/32 0.34 \n",
"2 Unicom Unicom-ViT-B-32 from open-metric-learning 0.48 \n",
"3 Unicom Unicom-ViT-B-16 from open-metric-learning 0.82 "
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(\n",
" pd.DataFrame(ImageEmbedding.list_supported_models()).sort_values(\"size_in_GB\")\n",
" .drop(columns=[\"sources\", \"model_file\"])\n",
" .reset_index(drop=True)\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "fst",
"display_name": "Python 3.8.18 ('base')",
"language": "python",
"name": "python3"
},
@@ -106,9 +652,14 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
"version": "3.11.8"
},
"orig_nbformat": 4
"orig_nbformat": 4,
"vscode": {
"interpreter": {
"hash": "c4a27af61e455bc18dcf16f5867a2ff0402fa12b01dd0f6ce3a79ae73ad15e91"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
File diff suppressed because one or more lines are too long
@@ -3,22 +3,7 @@
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Binary Quantization of OpenAI Embedding\n",
"---\n",
"\n",
"In the world of large-scale data retrieval and processing, efficiency is crucial. With the exponential growth of data, the ability to retrieve information quickly and accurately can significantly affect system performance. This blog post explores a technique known as binary quantization applied to OpenAI embeddings, demonstrating how it can enhance **retrieval latency by 20x** or more.\n",
"\n",
"## What Are OpenAI Embeddings?\n",
"OpenAI embeddings are numerical representations of textual information. They transform text into a vector space where semantically similar texts are mapped close together. This mathematical representation enables computers to understand and process human language more effectively.\n",
"\n",
"## Binary Quantization\n",
"Binary quantization is a method which converts continuous numerical values into binary values (0 or 1). It simplifies the data structure, allowing faster computations. Here's a brief overview of the binary quantization process applied to OpenAI embeddings:\n",
"\n",
"1. **Load Embeddings**: OpenAI embeddings are loaded from parquet files.\n",
"2. **Binary Transformation**: The continuous valued vectors are converted into binary form. Here, values greater than 0 are set to 1, and others remain 0.\n",
"3. **Comparison & Retrieval**: Binary vectors are used for comparison using logical XOR operations and other efficient algorithms."
]
"source": []
},
{
"cell_type": "markdown",
@@ -29,24 +14,33 @@
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"execution_count": 1,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:00:06.460001Z",
"start_time": "2024-06-06T17:00:04.214098Z"
}
},
"outputs": [],
"source": [
"!pip install matplotlib tqdm pandas numpy --quiet"
"!pip install matplotlib tqdm pandas numpy datasets --quiet --upgrade"
]
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 2,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:00:07.041784Z",
"start_time": "2024-06-06T17:00:06.461658Z"
},
"id": "WBVTItUX4yyr"
},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from datasets import load_dataset\n",
"from tqdm import tqdm"
]
},
@@ -68,8 +62,12 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 3,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:01:09.343230Z",
"start_time": "2024-06-06T17:00:07.042526Z"
},
"colab": {
"base_uri": "https://localhost:8080/",
"height": 250
@@ -77,58 +75,24 @@
"id": "REJpFqkG7EG2",
"outputId": "7a43c0ae-fbcc-45fe-fd58-bfe691297b22"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 26/26 [00:10<00:00, 2.45it/s]\n"
]
},
{
"data": {
"text/plain": [
"(1000000, 1536)"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"def get_openai_vectors(force_download: bool = False):\n",
" res = []\n",
" for i in tqdm(range(26)):\n",
" if force_download:\n",
" !wget https://huggingface.co/api/datasets/KShivendu/dbpedia-entities-openai-1M/parquet/KShivendu--dbpedia-entities-openai-1M/train/{i}.parquet\n",
" df = pd.read_parquet(f\"{i}.parquet\", engine=\"pyarrow\")\n",
" res.append(np.stack(df.openai))\n",
" del df\n",
"\n",
" openai_vectors = np.concatenate(res)\n",
" del res\n",
" return openai_vectors\n",
"\n",
"\n",
"openai_vectors = get_openai_vectors(force_download=False)\n",
"openai_vectors.shape"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## ㆓ Binary Conversion\n",
"\n",
"Here, we will use 0 as the threshold for the binary conversion. All values greater than 0 will be set to 1, and others will remain 0. This is a simple and effective way to convert continuous values into binary values for OpenAI embeddings."
"# Download from Huggingface Hub\n",
"ds = load_dataset(\n",
" \"Qdrant/dbpedia-entities-openai3-text-embedding-3-large-3072-100K\", split=\"train\"\n",
")\n",
"openai_vectors = np.array(ds[\"text-embedding-3-large-3072-embedding\"])\n",
"del ds"
]
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 4,
"metadata": {
"id": "0JM2-Bj2Jkab"
"ExecuteTime": {
"end_time": "2024-06-06T17:01:10.900963Z",
"start_time": "2024-06-06T17:01:09.344842Z"
}
},
"outputs": [],
"source": [
@@ -136,6 +100,30 @@
"openai_bin[openai_vectors > 0] = 1"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:01:10.906827Z",
"start_time": "2024-06-06T17:01:10.901820Z"
}
},
"outputs": [
{
"data": {
"text/plain": "3072"
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"n_dim = openai_vectors.shape[1]\n",
"n_dim"
]
},
{
"cell_type": "markdown",
"metadata": {},
@@ -147,8 +135,12 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 6,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:01:10.909730Z",
"start_time": "2024-06-06T17:01:10.908166Z"
},
"id": "FqshI-GlIERd"
},
"outputs": [],
@@ -157,7 +149,7 @@
" scores = np.dot(openai_vectors, openai_vectors[idx])\n",
" dot_results = np.argsort(scores)[-limit:][::-1]\n",
"\n",
" bin_scores = 1536 - np.logical_xor(openai_bin, openai_bin[idx]).sum(axis=1)\n",
" bin_scores = n_dim - np.logical_xor(openai_bin, openai_bin[idx]).sum(axis=1)\n",
" bin_results = np.argsort(bin_scores)[-(limit * oversampling) :][::-1]\n",
"\n",
" return len(set(dot_results).intersection(set(bin_results))) / limit"
@@ -172,8 +164,12 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": 7,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:01:25.206592Z",
"start_time": "2024-06-06T17:01:10.911971Z"
},
"colab": {
"base_uri": "https://localhost:8080/"
},
@@ -185,110 +181,128 @@
"name": "stderr",
"output_type": "stream",
"text": [
" 0%| | 0/4 [00:00<?, ?it/s]"
" 0%| | 0/4 [00:00<?, ?it/s]\n",
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
" 50%|█████ | 1/2 [00:02<00:02, 2.05s/it]\u001b[A"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 1, 'limit': 10, 'recall': 0.8}\n"
"{'sampling_rate': 1, 'limit': 3, 'mean_acc': 0.9}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 2/2 [00:33<00:00, 16.98s/it]\n",
" 25%|██▌ | 1/4 [00:33<01:41, 33.96s/it]"
"\n",
"100%|██████████| 2/2 [00:04<00:00, 2.02s/it]\u001b[A\n",
" 25%|██▌ | 1/4 [00:04<00:12, 4.05s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 1, 'limit': 100, 'recall': 0.708}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": []
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 2, 'limit': 10, 'recall': 0.95}\n"
"{'sampling_rate': 1, 'limit': 10, 'mean_acc': 0.8300000000000001}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 2/2 [00:32<00:00, 16.38s/it]\n",
" 50%|█████ | 2/4 [01:06<01:06, 33.26s/it]"
"\n",
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
" 50%|█████ | 1/2 [00:01<00:01, 1.72s/it]\u001b[A"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 2, 'limit': 100, 'recall': 0.877}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": []
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 3, 'limit': 10, 'recall': 0.96}\n"
"{'sampling_rate': 2, 'limit': 3, 'mean_acc': 1.0}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 2/2 [00:32<00:00, 16.49s/it]\n",
" 75%|███████▌ | 3/4 [01:39<00:33, 33.13s/it]"
"\n",
"100%|██████████| 2/2 [00:03<00:00, 1.76s/it]\u001b[A\n",
" 50%|█████ | 2/4 [00:07<00:07, 3.75s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 3, 'limit': 100, 'recall': 0.937}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": []
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 5, 'limit': 10, 'recall': 0.9800000000000001}\n"
"{'sampling_rate': 2, 'limit': 10, 'mean_acc': 0.9700000000000001}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"100%|██████████| 2/2 [00:32<00:00, 16.47s/it]\n",
"100%|██████████| 4/4 [02:12<00:00, 33.17s/it]"
"\n",
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
" 50%|█████ | 1/2 [00:01<00:01, 1.72s/it]\u001b[A"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 5, 'limit': 100, 'recall': 0.977}\n"
"{'sampling_rate': 3, 'limit': 3, 'mean_acc': 1.0}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n",
"100%|██████████| 2/2 [00:03<00:00, 1.69s/it]\u001b[A\n",
" 75%|███████▌ | 3/4 [00:10<00:03, 3.58s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 3, 'limit': 10, 'mean_acc': 0.9800000000000001}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n",
" 0%| | 0/2 [00:00<?, ?it/s]\u001b[A\n",
" 50%|█████ | 1/2 [00:01<00:01, 1.68s/it]\u001b[A"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 5, 'limit': 3, 'mean_acc': 1.0}\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"\n",
"100%|██████████| 2/2 [00:03<00:00, 1.65s/it]\u001b[A\n",
"100%|██████████| 4/4 [00:14<00:00, 3.57s/it]"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'sampling_rate': 5, 'limit': 10, 'mean_acc': 0.99}\n"
]
},
{
@@ -301,117 +315,53 @@
],
"source": [
"number_of_samples = 10\n",
"limits = [10, 100]\n",
"limits = [3, 10]\n",
"sampling_rate = [1, 2, 3, 5]\n",
"results = []\n",
"\n",
"\n",
"def mean_accuracy(number_of_samples, limit, sampling_rate):\n",
" return np.mean([accuracy(i, limit=limit, oversampling=sampling_rate) for i in range(number_of_samples)])\n",
" return np.mean(\n",
" [accuracy(i, limit=limit, oversampling=sampling_rate) for i in range(number_of_samples)]\n",
" )\n",
"\n",
"\n",
"for i in tqdm(sampling_rate):\n",
" for j in tqdm(limits):\n",
" result = {\"sampling_rate\": i, \"limit\": j, \"recall\": mean_accuracy(number_of_samples, j, i)}\n",
" result = {\n",
" \"sampling_rate\": i,\n",
" \"limit\": j,\n",
" \"mean_acc\": mean_accuracy(number_of_samples, j, i),\n",
" }\n",
" print(result)\n",
" results.append(result)"
]
},
{
"cell_type": "code",
"execution_count": 19,
"cell_type": "markdown",
"metadata": {},
"source": [
"## ㆓ Binary Conversion\n",
"\n",
"Here, we will use 0 as the threshold for the binary conversion. All values greater than 0 will be set to 1, and others will remain 0. This is a simple and effective way to convert continuous values into binary values for OpenAI embeddings."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"ExecuteTime": {
"end_time": "2024-06-06T17:01:25.247495Z",
"start_time": "2024-06-06T17:01:25.213508Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>sampling_rate</th>\n",
" <th>limit</th>\n",
" <th>recall</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>10</td>\n",
" <td>0.800</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>1</td>\n",
" <td>100</td>\n",
" <td>0.708</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>2</td>\n",
" <td>10</td>\n",
" <td>0.950</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>2</td>\n",
" <td>100</td>\n",
" <td>0.877</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>3</td>\n",
" <td>10</td>\n",
" <td>0.960</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>3</td>\n",
" <td>100</td>\n",
" <td>0.937</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>5</td>\n",
" <td>10</td>\n",
" <td>0.980</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>5</td>\n",
" <td>100</td>\n",
" <td>0.977</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" sampling_rate limit recall\n",
"0 1 10 0.800\n",
"1 1 100 0.708\n",
"2 2 10 0.950\n",
"3 2 100 0.877\n",
"4 3 10 0.960\n",
"5 3 100 0.937\n",
"6 5 10 0.980\n",
"7 5 100 0.977"
]
"text/html": "<div>\n<style scoped>\n .dataframe tbody tr th:only-of-type {\n vertical-align: middle;\n }\n\n .dataframe tbody tr th {\n vertical-align: top;\n }\n\n .dataframe thead th {\n text-align: right;\n }\n</style>\n<table border=\"1\" class=\"dataframe\">\n <thead>\n <tr style=\"text-align: right;\">\n <th></th>\n <th>sampling_rate</th>\n <th>limit</th>\n <th>mean_acc</th>\n </tr>\n </thead>\n <tbody>\n <tr>\n <th>0</th>\n <td>1</td>\n <td>3</td>\n <td>0.90</td>\n </tr>\n <tr>\n <th>1</th>\n <td>1</td>\n <td>10</td>\n <td>0.83</td>\n </tr>\n <tr>\n <th>2</th>\n <td>2</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>3</th>\n <td>2</td>\n <td>10</td>\n <td>0.97</td>\n </tr>\n <tr>\n <th>4</th>\n <td>3</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>5</th>\n <td>3</td>\n <td>10</td>\n <td>0.98</td>\n </tr>\n <tr>\n <th>6</th>\n <td>5</td>\n <td>3</td>\n <td>1.00</td>\n </tr>\n <tr>\n <th>7</th>\n <td>5</td>\n <td>10</td>\n <td>0.99</td>\n </tr>\n </tbody>\n</table>\n</div>",
"text/plain": " sampling_rate limit mean_acc\n0 1 3 0.90\n1 1 10 0.83\n2 2 3 1.00\n3 2 10 0.97\n4 3 3 1.00\n5 3 10 0.98\n6 5 3 1.00\n7 5 10 0.99"
},
"execution_count": 19,
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
@@ -422,22 +372,13 @@
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"| sampling_rate | limit | accuracy |\n",
"|---------------|-------|----------|\n",
"| 1 | 10 | 0.800 |\n",
"| 1 | 100 | 0.708 |\n",
"| 2 | 10 | 0.950 |\n",
"| 2 | 100 | 0.877 |\n",
"| 4 | 10 | 0.970 |\n",
"| 4 | 100 | 0.956 |\n",
"| 8 | 10 | 0.990 |\n",
"| 8 | 100 | 0.990 |\n",
"| 16 | 10 | 1.000 |\n",
"| 16 | 100 | 0.998 |"
]
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -446,7 +387,8 @@
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
@@ -459,7 +401,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
"version": "3.10.13"
}
},
"nbformat": 4,
File diff suppressed because it is too large Load Diff
+14 -15
View File
@@ -2,20 +2,20 @@
FastEmbed is a lightweight, fast, Python library built for embedding generation. We [support popular text models](https://qdrant.github.io/fastembed/examples/Supported_Models/). Please [open a Github issue](https://github.com/qdrant/fastembed/issues/new) if you want us to add a new model.
The default embedding supports "query" and "passage" prefixes for the input text. The default model is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard. Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/examples/Retrieval%20with%20FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/).
1. Light & Fast
- Quantized model weights
- ONNX Runtime for inference via [Optimum](github.com/huggingface/optimum)
- ONNX Runtime for inference
2. Accuracy/Recall
- Better than OpenAI Ada-002
- Default is Flag Embedding, which is top of the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard
- Default is Flag Embedding, which has shown good results on the [MTEB](https://huggingface.co/spaces/mteb/leaderboard) leaderboard
- List of [supported models](https://qdrant.github.io/fastembed/examples/Supported_Models/) - including multilingual models
Here is an example for [Retrieval Embedding Generation](https://qdrant.github.io/fastembed/examples/Retrieval%20with%20FastEmbed/) and how to use [FastEmbed with Qdrant](https://qdrant.github.io/fastembed/examples/Usage_With_Qdrant/).
## 🚀 Installation
To install the FastEmbed library, pip works:
To install the FastEmbed library, pip works:
```bash
pip install fastembed
@@ -24,16 +24,16 @@ pip install fastembed
## 📖 Usage
```python
from fastembed.embedding import FlagEmbedding as Embedding
from fastembed import TextEmbedding
documents: List[str] = [
"passage: Hello, World!",
"query: Hello, World!", # these are two different embedding
"query: Hello, World!",
"passage: This is an example passage.",
"fastembed is supported by and maintained by Qdrant." # You can leave out the prefix but it's recommended
"fastembed is supported by and maintained by Qdrant."
]
embedding_model = Embedding(model_name="BAAI/bge-base-en", max_length=512)
embeddings: List[np.ndarray] = embedding_model.embed(documents) # If you use
embedding_model = TextEmbedding()
embeddings: List[np.ndarray] = embedding_model.embed(documents)
```
## Usage with Qdrant
@@ -44,23 +44,22 @@ Installation with Qdrant Client in Python:
pip install qdrant-client[fastembed]
```
Might have to use ```pip install 'qdrant-client[fastembed]'``` on zsh.
Might have to use ```pip install 'qdrant-client[fastembed]'``` on zsh.
```python
from qdrant_client import QdrantClient
# Initialize the client
client = QdrantClient(":memory:") # or QdrantClient(path="path/to/db")
client = QdrantClient(":memory:") # Using an in-process Qdrant
# Prepare your documents, metadata, and IDs
docs = ["Qdrant has Langchain integrations", "Qdrant also has Llama Index integrations"]
metadata = [
{"source": "Langchain-docs"},
{"source": "Linkedin-docs"},
{"source": "Llama-index-docs"},
]
ids = [42, 2]
# Use the new add method
client.add(
collection_name="demo_collection",
documents=docs,
@@ -73,4 +72,4 @@ search_result = client.query(
query_text="This is a query document"
)
print(search_result)
```
```
+4 -3
View File
@@ -5,7 +5,7 @@
<a href="{{ page.nb_url }}" title="Download Notebook" class="md-content__button md-icon jp-DownloadNB">
{% include ".icons/material/download.svg" %}
</a>
{% endif %}
{% endif %}
{{ super() }}
@@ -20,7 +20,8 @@
</span>
<strong>Qdrant Discord server</strong>
</a>
to get help and share your work! Or check out <a rel="me" href="https://login.cloud.qdrant.io/">Qdrant Cloud</a> to
to get help and share your work! Or check out <a rel="me"
href="https://cloud.qdrant.io?utm_source=twitter&utm_medium=website&utm_campaign=fastembed">Qdrant Cloud</a> to
get started with vector search!
</div>
{% endblock %}
{% endblock %}
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
@@ -37,13 +37,13 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from typing import List\n",
"import numpy as np\n",
"from fastembed.embedding import FlagEmbedding as Embedding"
"from fastembed import TextEmbedding"
]
},
{
@@ -58,7 +58,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -84,7 +84,7 @@
" \"His life has been depicted in various films, TV shows, and books\",\n",
"]\n",
"# Initialize the DefaultEmbedding class with the desired parameters\n",
"embedding_model = Embedding(model_name=\"BAAI/bge-small-en\", max_length=512)\n",
"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",
@@ -105,7 +105,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
@@ -124,65 +124,27 @@
" print(f\"Rank {i+1}: {documents[sorted_scores[i]]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Running and Comparing Queries\n",
"Finally, we run our sample query using the `print_top_k` function.\n",
"\n",
"The differences between using query embeddings and plain embeddings can be observed in the retrieved ranks:\n",
"\n",
"Using query embeddings (from `query_embed` method):"
]
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rank 1: Maharana Pratap was a Rajput warrior king from Mewar\n",
"Rank 2: Maharana Pratap is considered a symbol of Rajput resistance against foreign rule\n",
"Rank 3: His legacy is celebrated in Rajasthan through festivals and monuments\n",
"Rank 4: His capital was Chittorgarh, which he lost to the Mughals\n",
"Rank 5: He fought against the Mughal Empire led by Akbar\n"
]
"data": {
"text/plain": [
"(array([-0.06002192, 0.04322132, -0.00545516, -0.04419701, -0.00542277],\n",
" dtype=float32),\n",
" array([-0.06002192, 0.04322132, -0.00545516, -0.04419701, -0.00542277],\n",
" dtype=float32))"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"print_top_k(query_embedding, embeddings, documents)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using plain embeddings (from `embed` method):"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rank 1: He died in 1597 at the age of 57\n",
"Rank 2: His life has been depicted in various films, TV shows, and books\n",
"Rank 3: Maharana Pratap was a Rajput warrior king from Mewar\n",
"Rank 4: He had 11 wives and 17 sons, including Amar Singh I who succeeded him as ruler of Mewar\n",
"Rank 5: He fought against the Mughal Empire led by Akbar\n"
]
}
],
"source": [
"print_top_k(plain_query_embedding, embeddings, documents)"
"query_embedding[:5], plain_query_embedding[:5]"
]
},
{
@@ -213,7 +175,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
"version": "3.10.13"
},
"orig_nbformat": 4
},
@@ -28,16 +28,7 @@
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[33mDEPRECATION: pytorch-lightning 1.6.5 has a non-standard dependency specifier torch>=1.8.*. pip 23.3 will enforce this behaviour change. A possible replacement is to upgrade to a newer version of pytorch-lightning or contact the author to suggest that they release a version with a conforming dependency specifiers. Discussion can be found at https://github.com/pypa/pip/issues/12063\u001b[0m\u001b[33m\n",
"\u001b[0m"
]
}
],
"outputs": [],
"source": [
"!pip install 'qdrant-client[fastembed]' --quiet --upgrade"
]
@@ -56,8 +47,6 @@
"outputs": [],
"source": [
"from typing import List\n",
"import numpy as np\n",
"from fastembed.embedding import FlagEmbedding as Embedding\n",
"from qdrant_client import QdrantClient"
]
},
@@ -117,22 +106,22 @@
"name": "stderr",
"output_type": "stream",
"text": [
"Asking to truncate to max_length but no maximum length is provided and the model has no predefined maximum length. Default to no truncation.\n"
"100%|██████████| 77.7M/77.7M [00:05<00:00, 14.6MiB/s]\n"
]
},
{
"data": {
"text/plain": [
"['77e1e4724dd243b08608f57d5692f6aa',\n",
" '74841e5dc3594646bda2c6a6d2795dbd',\n",
" '6ef39a9445604d0da84d04f760cd7cf7',\n",
" 'e659503d3b3748ef90f23c778274835b',\n",
" 'b999675068cd413f93faa0cc890c3819',\n",
" '8e452f2935cf4e4b80d8eea68c2aad58',\n",
" '28ed4fd4592c48c9a0519618d51bb86e',\n",
" '59378c784c5f49109bef65fdc4061334',\n",
" 'a78c9b598f7942749156334283a6f24f',\n",
" 'f72bb24701c64fabb0182c9e757b581b']"
"['4fa8b10c78da4b18ba0830ba8a57367a',\n",
" '2eae04b515ee4e9185a9a0e6be812bba',\n",
" 'c6039f88486f47f1835ae3b069c5823c',\n",
" 'c2c8c51e305144d1917b373125fb4d95',\n",
" '79fd23b9ec0648cdab38d1947c6b933e',\n",
" '036aa200d8c3492b8a438e4f825f5e7f',\n",
" 'c35c77f3ea37460a9a13723fb77b7367',\n",
" '6ebccbca571b40d0ab6e83e5e0f2f562',\n",
" '38048c2ccc1d4962a4f8f1bd89c8357a',\n",
" 'c6b09308360140c7b4f106af3658a31e']"
]
},
"execution_count": 4,
@@ -186,12 +175,14 @@
"ids = [42, 2]\n",
"\n",
"# Use the new add method\n",
"client.add(\n",
" collection_name=\"demo_collection\",\n",
" documents=docs,\n",
" metadata=metadata,\n",
" ids=ids\n",
")"
"client.add(collection_name=\"demo_collection\", documents=docs, metadata=metadata, ids=ids)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Behind the scenes, Qdrant Client uses the FastEmbed library to make a passage embedding and then uses the Qdrant API to upsert the documents with metadata, put together as a Points into the collection."
]
},
{
@@ -203,15 +194,12 @@
"name": "stdout",
"output_type": "stream",
"text": [
"[QueryResponse(id='42', embedding=None, metadata={'document': 'Qdrant has Langchain integrations', 'source': 'Langchain-docs'}, document='Qdrant has Langchain integrations', score=0.8496814051311954), QueryResponse(id='2', embedding=None, metadata={'document': 'Qdrant also has Llama Index integrations', 'source': 'Linkedin-docs'}, document='Qdrant also has Llama Index integrations', score=0.8478494193031256)]\n"
"[QueryResponse(id=42, embedding=None, metadata={'document': 'Qdrant has Langchain integrations', 'source': 'Langchain-docs'}, document='Qdrant has Langchain integrations', score=0.8276550115796268), QueryResponse(id=2, embedding=None, metadata={'document': 'Qdrant also has Llama Index integrations', 'source': 'Linkedin-docs'}, document='Qdrant also has Llama Index integrations', score=0.8265536935180283)]\n"
]
}
],
"source": [
"search_result = client.query(\n",
" collection_name=\"demo_collection\",\n",
" query_text=[\"This is a query document\"]\n",
")\n",
"search_result = client.query(collection_name=\"demo_collection\", query_text=\"This is a query document\")\n",
"print(search_result)"
]
},
@@ -245,7 +233,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.17"
"version": "3.11.5"
},
"orig_nbformat": 4
},
@@ -27,7 +27,6 @@
"from transformers import AutoTokenizer, AutoModel\n",
"\n",
"from optimum.onnxruntime import AutoOptimizationConfig, ORTModelForFeatureExtraction, ORTOptimizer\n",
"from optimum.onnxruntime.configuration import OptimizationConfig\n",
"from optimum.pipelines import pipeline\n",
"import torch.nn.functional as F"
]
@@ -149,7 +148,9 @@
"metadata": {},
"outputs": [],
"source": [
"onnx_quant_embed = pipeline(\"feature-extraction\", model=model, accelerator=\"ort\", tokenizer=tokenizer,return_tensors=True)"
"onnx_quant_embed = pipeline(\n",
" \"feature-extraction\", model=model, accelerator=\"ort\", tokenizer=tokenizer, return_tensors=True\n",
")"
]
},
{
@@ -159,9 +160,8 @@
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"embeddings = onnx_quant_embed(inputs=english_texts)\n",
"F.normalize(embeddings[4])[:,0], english_texts[4], len(embeddings), len(english_texts)"
"F.normalize(embeddings[4])[:, 0], english_texts[4], len(embeddings), len(english_texts)"
]
},
{
@@ -171,7 +171,6 @@
"metadata": {},
"outputs": [],
"source": [
"\n",
"def measure_pipeline_time(pipeline, input_texts: List[str], num_runs=10, **kwargs: Any) -> Tuple[float, float]:\n",
" \"\"\"Measures the time it takes to run the pipeline on the input texts.\"\"\"\n",
" times = []\n",
@@ -256,6 +255,7 @@
"\n",
"save_dir = Path(\"../local_cache/fast-bge-small-en-v1.5\")\n",
"\n",
"\n",
"def compress(directory_path):\n",
" directory_path = Path(directory_path)\n",
" assert directory_path.exists(), f\"{directory_path} does not exist\"\n",
@@ -304,9 +304,9 @@
}
],
"source": [
"import os\n",
"from google.cloud import storage\n",
"\n",
"\n",
"def upload(bucket_name, source_file_path):\n",
" storage_client = storage.Client(project=\"main\")\n",
" bucket = storage_client.bucket(bucket_name)\n",
+371
View File
@@ -0,0 +1,371 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import torch\n",
"from transformers import AutoModelForMaskedLM, AutoTokenizer"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Running the model with Transformers and Torch"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"sentences = [\n",
" \"Hello World\",\n",
" \"Built by Nirant Kasliwal\",\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## PyTorch Code from the [SPLADERunner](https://github.com/PrithivirajDamodaran/SPLADERunner) library"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"hf_token = \"<your_hf_token_here>\""
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output Logits shape: torch.Size([2, 10, 30522])\n",
"Output Attention mask shape: torch.Size([2, 10])\n",
"Sparse Vector shape: torch.Size([2, 30522])\n",
"SPLADE BOW rep for sentence:\tBuilt by Nirant Kasliwal\n",
"[('##rant', 2.02), ('built', 1.94), ('##wal', 1.79), ('##sl', 1.69), ('build', 1.57), ('ka', 1.4), ('ni', 1.26), ('made', 0.93), ('architect', 0.76), ('was', 0.69), ('who', 0.61), ('his', 0.5), ('wrote', 0.47), ('india', 0.45), ('company', 0.41), ('##i', 0.41), ('he', 0.37), ('manufacturer', 0.36), ('by', 0.35), ('engineer', 0.33), ('architecture', 0.33), ('ko', 0.23), ('him', 0.22), ('invented', 0.19), ('said', 0.14), ('k', 0.11), ('man', 0.11), ('statue', 0.11), ('bomb', 0.1), ('##wa', 0.1), ('builder', 0.09), ('.', 0.07), ('started', 0.06), (',', 0.04), ('ku', 0.03)]\n"
]
}
],
"source": [
"# Download the model and tokenizer\n",
"device = \"cuda:0\" if torch.cuda.is_available() else \"cpu\"\n",
"tokenizer = AutoTokenizer.from_pretrained(\"prithivida/Splade_PP_en_v1\", token=hf_token)\n",
"reverse_voc = {v: k for k, v in tokenizer.vocab.items()}\n",
"model = AutoModelForMaskedLM.from_pretrained(\"prithivida/Splade_PP_en_v1\", token=hf_token)\n",
"model.to(device)\n",
"\n",
"# Tokenize the input\n",
"inputs = tokenizer(sentences, return_tensors=\"pt\", padding=True, truncation=True, max_length=512)\n",
"inputs = {key: val.to(device) for key, val in inputs.items()}\n",
"input_ids = inputs[\"input_ids\"]\n",
"attention_mask = inputs[\"attention_mask\"]\n",
"token_type_ids = inputs[\"token_type_ids\"]\n",
"\n",
"# Run model and prepare sparse vector\n",
"outputs = model(**inputs)\n",
"logits = outputs.logits\n",
"print(\"Output Logits shape: \", logits.shape)\n",
"print(\"Output Attention mask shape: \", attention_mask.shape)\n",
"relu_log = torch.log(1 + torch.relu(logits))\n",
"weighted_log = relu_log * attention_mask.unsqueeze(-1)\n",
"max_val, _ = torch.max(weighted_log, dim=1)\n",
"vector = max_val.squeeze()\n",
"print(\"Sparse Vector shape: \", vector.shape)\n",
"# print(\"Number of Actual Dimensions: \", len(cols))\n",
"cols = [vec.nonzero().squeeze().cpu().tolist() for vec in vector]\n",
"weights = [vec[col].cpu().tolist() for vec, col in zip(vector, cols)]\n",
"\n",
"idx = 1\n",
"cols, weights = cols[idx], weights[idx]\n",
"# Print the BOW representation\n",
"d = {k: v for k, v in zip(cols, weights)}\n",
"sorted_d = {k: v for k, v in sorted(d.items(), key=lambda item: item[1], reverse=True)}\n",
"bow_rep = []\n",
"for k, v in sorted_d.items():\n",
" bow_rep.append((reverse_voc[k], round(v, 2)))\n",
"print(f\"SPLADE BOW rep for sentence:\\t{sentences[idx]}\\n{bow_rep}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Export with output_attentions and logits"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Exporting model to models/nirantk_SPLADE_PP_en_v1\n"
]
},
{
"data": {
"text/plain": [
"('models/nirantk_SPLADE_PP_en_v1/tokenizer_config.json',\n",
" 'models/nirantk_SPLADE_PP_en_v1/special_tokens_map.json',\n",
" 'models/nirantk_SPLADE_PP_en_v1/vocab.txt',\n",
" 'models/nirantk_SPLADE_PP_en_v1/added_tokens.json',\n",
" 'models/nirantk_SPLADE_PP_en_v1/tokenizer.json')"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from transformers import AutoTokenizer\n",
"\n",
"model_id = \"nirantk/SPLADE_PP_en_v1\"\n",
"output_dir = f\"models/{model_id.replace('/', '_')}\"\n",
"model_kwargs = {\"output_attentions\": True, \"return_dict\": True}\n",
"\n",
"print(f\"Exporting model to {output_dir}\")\n",
"tokenizer.save_pretrained(output_dir)\n",
"# main_export(\n",
"# model_id,\n",
"# output=output_dir,\n",
"# no_post_process=True,\n",
"# model_kwargs=model_kwargs,\n",
"# token=hf_token,\n",
"# )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Running the model with ONNX"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"from optimum.onnxruntime import ORTModelForMaskedLM\n",
"\n",
"model = ORTModelForMaskedLM.from_pretrained(\"nirantk/SPLADE_PP_en_v1\")\n",
"tokenizer = AutoTokenizer.from_pretrained(\"nirantk/SPLADE_PP_en_v1\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"inputs = tokenizer(sentences, return_tensors=\"pt\", padding=True, truncation=True, max_length=512)\n",
"inputs = {key: val.to(device) for key, val in inputs.items()}\n",
"input_ids = inputs[\"input_ids\"]\n",
"attention_mask = inputs[\"attention_mask\"]\n",
"token_type_ids = inputs[\"token_type_ids\"]\n",
"\n",
"onnx_input = {\n",
" \"input_ids\": input_ids.cpu().numpy(),\n",
" \"attention_mask\": attention_mask.cpu().numpy(),\n",
" \"token_type_ids\": token_type_ids.cpu().numpy(),\n",
"}\n",
"\n",
"logits = model(**onnx_input).logits"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(2, 10, 30522)"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"logits.shape"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Output Logits shape: (2, 10, 30522)\n",
"Sparse Vector shape: (2, 30522)\n",
"SPLADE BOW rep for sentence:\tBuilt by Nirant Kasliwal\n",
"[('##rant', 2.02), ('built', 1.94), ('##wal', 1.79), ('##sl', 1.69), ('build', 1.57), ('ka', 1.4), ('ni', 1.26), ('made', 0.93), ('architect', 0.76), ('was', 0.69), ('who', 0.61), ('his', 0.5), ('wrote', 0.47), ('india', 0.45), ('company', 0.41), ('##i', 0.41), ('he', 0.37), ('manufacturer', 0.36), ('by', 0.35), ('engineer', 0.33), ('architecture', 0.33), ('ko', 0.23), ('him', 0.22), ('invented', 0.19), ('said', 0.14), ('k', 0.11), ('man', 0.11), ('statue', 0.11), ('bomb', 0.1), ('##wa', 0.1), ('builder', 0.09), ('.', 0.07), ('started', 0.06), (',', 0.04), ('ku', 0.03)]\n"
]
}
],
"source": [
"print(\"Output Logits shape: \", logits.shape)\n",
"\n",
"relu_log = np.log(1 + np.maximum(logits, 0))\n",
"\n",
"# Equivalent to relu_log * attention_mask.unsqueeze(-1)\n",
"# For NumPy, you might need to explicitly expand dimensions if 'attention_mask' is not already 2D\n",
"weighted_log = relu_log * np.expand_dims(attention_mask, axis=-1)\n",
"\n",
"# Equivalent to torch.max(weighted_log, dim=1)\n",
"# NumPy's max function returns only the max values, not the indices, so we don't need to unpack two values\n",
"max_val = np.max(weighted_log, axis=1)\n",
"\n",
"# Equivalent to max_val.squeeze()\n",
"# This step may be unnecessary in NumPy if max_val doesn't have unnecessary dimensions\n",
"vector = np.squeeze(max_val)\n",
"print(\"Sparse Vector shape: \", vector.shape)\n",
"\n",
"# print(vector[0].nonzero())\n",
"\n",
"cols = [vec.nonzero()[0].squeeze().tolist() for vec in vector]\n",
"weights = [vec[col].tolist() for vec, col in zip(vector, cols)]\n",
"\n",
"idx = 1\n",
"cols, weights = cols[idx], weights[idx]\n",
"# Print the BOW representation\n",
"d = {k: v for k, v in zip(cols, weights)}\n",
"sorted_d = {k: v for k, v in sorted(d.items(), key=lambda item: item[1], reverse=True)}\n",
"bow_rep = []\n",
"for k, v in sorted_d.items():\n",
" bow_rep.append((reverse_voc[k], round(v, 2)))\n",
"print(f\"SPLADE BOW rep for sentence:\\t{sentences[idx]}\\n{bow_rep}\")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"35"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(cols)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[1010,\n",
" 1012,\n",
" 1047,\n",
" 2001,\n",
" 2002,\n",
" 2010,\n",
" 2011,\n",
" 2032,\n",
" 2040,\n",
" 2056,\n",
" 2072,\n",
" 2081,\n",
" 2158,\n",
" 2194,\n",
" 2318,\n",
" 2328,\n",
" 2626,\n",
" 2634,\n",
" 3857,\n",
" 3992,\n",
" 4213,\n",
" 4294,\n",
" 4944,\n",
" 5968,\n",
" 6231,\n",
" 7751,\n",
" 8826,\n",
" 9152,\n",
" 10556,\n",
" 12508,\n",
" 12849,\n",
" 13476,\n",
" 13970,\n",
" 14540,\n",
" 17884]"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cols"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "fst",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.13"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,122 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "4bdb2a91-fa2a-4cee-ad5a-176cc957394d",
"metadata": {
"ExecuteTime": {
"end_time": "2024-05-23T12:15:28.171586Z",
"start_time": "2024-05-23T12:15:28.076314Z"
}
},
"outputs": [
{
"ename": "ModuleNotFoundError",
"evalue": "No module named 'torch'",
"output_type": "error",
"traceback": [
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
"\u001B[0;31mModuleNotFoundError\u001B[0m Traceback (most recent call last)",
"Cell \u001B[0;32mIn[1], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorch\u001B[39;00m\n\u001B[1;32m 2\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorch\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01monnx\u001B[39;00m\n\u001B[1;32m 3\u001B[0m \u001B[38;5;28;01mimport\u001B[39;00m \u001B[38;5;21;01mtorchvision\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mmodels\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m \u001B[38;5;21;01mmodels\u001B[39;00m\n",
"\u001B[0;31mModuleNotFoundError\u001B[0m: No module named 'torch'"
]
}
],
"source": [
"import torch\n",
"import torch.onnx\n",
"import torchvision.models as models\n",
"import torchvision.transforms as transforms\n",
"from PIL import Image\n",
"import numpy as np\n",
"from tests.config import TEST_MISC_DIR\n",
"\n",
"# Load pre-trained ResNet-50 model\n",
"resnet = models.resnet50(pretrained=True)\n",
"resnet = torch.nn.Sequential(*(list(resnet.children())[:-1])) # Remove the last fully connected layer\n",
"resnet.eval()\n",
"\n",
"# Define preprocessing transform\n",
"preprocess = transforms.Compose([\n",
" transforms.Resize(256),\n",
" transforms.CenterCrop(224),\n",
" transforms.ToTensor(),\n",
" transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n",
"])\n",
"\n",
"# Load and preprocess the image\n",
"def preprocess_image(image_path):\n",
" input_image = Image.open(image_path)\n",
" input_tensor = preprocess(input_image)\n",
" input_batch = input_tensor.unsqueeze(0) # Add batch dimension\n",
" return input_batch\n",
"\n",
"# Example input for exporting\n",
"input_image = preprocess_image('example.jpg')\n",
"\n",
"# Export the model to ONNX with dynamic axes\n",
"torch.onnx.export(\n",
" resnet, \n",
" input_image, \n",
" \"model.onnx\", \n",
" export_params=True, \n",
" opset_version=9, \n",
" input_names=['input'], \n",
" output_names=['output'],\n",
" dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}\n",
")\n",
"\n",
"# Load ONNX model\n",
"import onnx\n",
"import onnxruntime as ort\n",
"\n",
"onnx_model = onnx.load(\"model.onnx\")\n",
"ort_session = ort.InferenceSession(\"model.onnx\")\n",
"\n",
"# Run inference and extract feature vectors\n",
"def extract_feature_vectors(image_paths):\n",
" input_images = [preprocess_image(image_path) for image_path in image_paths]\n",
" input_batch = torch.cat(input_images, dim=0) # Combine images into a single batch\n",
" ort_inputs = {ort_session.get_inputs()[0].name: input_batch.numpy()}\n",
" ort_outs = ort_session.run(None, ort_inputs)\n",
" return ort_outs[0]\n",
"\n",
"# Example usage\n",
"images = [TEST_MISC_DIR / \"image.jpeg\", str(TEST_MISC_DIR / \"small_image.jpeg\")] # Replace with your image paths\n",
"feature_vectors = extract_feature_vectors(images)\n",
"print(\"Feature vector shape:\", feature_vectors.shape)\n"
]
},
{
"cell_type": "code",
"outputs": [],
"source": [],
"metadata": {
"collapsed": false
},
"id": "baa650c4cb3e0e6d"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
File diff suppressed because one or more lines are too long
+17
View File
@@ -0,0 +1,17 @@
from optimum.exporters.onnx import main_export
from transformers import AutoTokenizer
model_id = "sentence-transformers/paraphrase-MiniLM-L6-v2"
output_dir = f"models/{model_id.replace('/', '_')}"
model_kwargs = {"output_attentions": True, "return_dict": True}
tokenizer = AutoTokenizer.from_pretrained(model_id)
# export if the output model does not exist
# try:
# sess = onnxruntime.InferenceSession(f"{output_dir}/model.onnx")
# print("Model already exported")
# except FileNotFoundError:
print(f"Exporting model to {output_dir}")
main_export(
model_id, output=output_dir, no_post_process=True, model_kwargs=model_kwargs
)
+33
View File
@@ -0,0 +1,33 @@
import numpy as np
import onnx
import onnxruntime
from transformers import AutoTokenizer
model_id = "sentence-transformers/paraphrase-MiniLM-L6-v2"
output_dir = f"models/{model_id.replace('/', '_')}"
model_kwargs = {"output_attentions": True, "return_dict": True}
tokenizer = AutoTokenizer.from_pretrained(model_id)
model_path = f"{output_dir}/model.onnx"
onnx_model = onnx.load(model_path)
ort_session = onnxruntime.InferenceSession(model_path)
text = "This is a test sentence"
tokenizer_output = tokenizer(text, return_tensors="np")
input_ids = tokenizer_output["input_ids"]
attention_mask = tokenizer_output["attention_mask"]
print(attention_mask)
# Prepare the input
input_ids = np.array(input_ids).astype(
np.int64
) # Replace your_input_ids with actual input data
# Run the ONNX model
outputs = ort_session.run(
None, {"input_ids": input_ids, "attention_mask": attention_mask}
)
# Get the attention weights
attentions = outputs[-1]
# Print the attention weights for the first layer and first head
print(attentions[0][0])
+20
View File
@@ -0,0 +1,20 @@
import importlib.metadata
from fastembed.image import ImageEmbedding
from fastembed.late_interaction import LateInteractionTextEmbedding
from fastembed.sparse import SparseEmbedding, SparseTextEmbedding
from fastembed.text import TextEmbedding
try:
version = importlib.metadata.version("fastembed")
except importlib.metadata.PackageNotFoundError as _:
version = importlib.metadata.version("fastembed-gpu")
__version__ = version
__all__ = [
"TextEmbedding",
"SparseTextEmbedding",
"SparseEmbedding",
"ImageEmbedding",
"LateInteractionTextEmbedding",
]
+3
View File
@@ -0,0 +1,3 @@
from fastembed.common.types import ImageInput, OnnxProvider, PathInput, PilInput
__all__ = ["OnnxProvider", "ImageInput", "PathInput", "PilInput"]
+262
View File
@@ -0,0 +1,262 @@
import os
import time
import shutil
import tarfile
from pathlib import Path
from typing import Any, Dict, List, Optional
import requests
from huggingface_hub import snapshot_download
from huggingface_hub.utils import RepositoryNotFoundError
from loguru import logger
from tqdm import tqdm
class ModelManagement:
@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.
"""
raise NotImplementedError()
@classmethod
def _get_model_description(cls, model_name: str) -> Dict[str, Any]:
"""
Gets the model description from the model_name.
Args:
model_name (str): The name of the model.
raises:
ValueError: If the model_name is not supported.
Returns:
Dict[str, Any]: The model description.
"""
for model in cls.list_supported_models():
if model_name.lower() == model["model"].lower():
return model
raise ValueError(f"Model {model_name} is not supported in {cls.__name__}.")
@classmethod
def download_file_from_gcs(cls, url: str, output_path: str, show_progress: bool = True) -> str:
"""
Downloads a file from Google Cloud Storage.
Args:
url (str): The URL to download the file from.
output_path (str): The path to save the downloaded file to.
show_progress (bool, optional): Whether to show a progress bar. Defaults to True.
Returns:
str: The path to the downloaded file.
"""
if os.path.exists(output_path):
return output_path
response = requests.get(url, stream=True)
# Handle HTTP errors
if response.status_code == 403:
raise PermissionError(
"Authentication Error: You do not have permission to access this resource. "
"Please check your credentials."
)
# Get the total size of the file
total_size_in_bytes = int(response.headers.get("content-length", 0))
# Warn if the total size is zero
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
with tqdm(
total=total_size_in_bytes,
unit="iB",
unit_scale=True,
disable=not show_progress,
) as progress_bar:
with open(output_path, "wb") as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk: # Filter out keep-alive new chunks
progress_bar.update(len(chunk))
file.write(chunk)
return output_path
@classmethod
def download_files_from_huggingface(
cls,
hf_source_repo: str,
cache_dir: Optional[str] = None,
extra_patterns: Optional[List[str]] = None,
**kwargs,
) -> 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
includes the required model files.
Returns:
Path: The path to the model directory.
"""
allow_patterns = [
"config.json",
"tokenizer.json",
"tokenizer_config.json",
"special_tokens_map.json",
"preprocessor_config.json",
]
if extra_patterns is not None:
allow_patterns.extend(extra_patterns)
return snapshot_download(
repo_id=hf_source_repo,
allow_patterns=allow_patterns,
cache_dir=cache_dir,
local_files_only=kwargs.get("local_files_only", False),
)
@classmethod
def decompress_to_cache(cls, targz_path: str, cache_dir: str):
"""
Decompresses a .tar.gz file to a cache directory.
Args:
targz_path (str): Path to the .tar.gz file.
cache_dir (str): Path to the cache directory.
Returns:
cache_dir (str): Path to the cache directory.
"""
# Check if targz_path exists and is a file
if not os.path.isfile(targz_path):
raise ValueError(f"{targz_path} does not exist or is not a file.")
# Check if targz_path is a .tar.gz file
if not targz_path.endswith(".tar.gz"):
raise ValueError(f"{targz_path} is not a .tar.gz file.")
try:
# Open the tar.gz file
with tarfile.open(targz_path, "r:gz") as tar:
# Extract all files into the cache directory
tar.extractall(path=cache_dir)
except tarfile.TarError as e:
# If any error occurs while opening or extracting the tar.gz file,
# delete the cache directory (if it was created in this function)
# and raise the error again
if "tmp" in cache_dir:
shutil.rmtree(cache_dir)
raise ValueError(f"An error occurred while decompressing {targz_path}: {e}")
return cache_dir
@classmethod
def retrieve_model_gcs(cls, model_name: str, source_url: str, cache_dir: str) -> Path:
fast_model_name = f"fast-{model_name.split('/')[-1]}"
cache_tmp_dir = Path(cache_dir) / "tmp"
model_tmp_dir = cache_tmp_dir / fast_model_name
model_dir = Path(cache_dir) / fast_model_name
# check if the model_dir and the model files are both present for macOS
if model_dir.exists() and len(list(model_dir.glob("*"))) > 0:
return model_dir
if model_tmp_dir.exists():
shutil.rmtree(model_tmp_dir)
cache_tmp_dir.mkdir(parents=True, exist_ok=True)
model_tar_gz = Path(cache_dir) / f"{fast_model_name}.tar.gz"
if model_tar_gz.exists():
model_tar_gz.unlink()
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}"
model_tar_gz.unlink()
# Rename from tmp to final name is atomic
model_tmp_dir.rename(model_dir)
return model_dir
@classmethod
def download_model(cls, model: Dict[str, Any], cache_dir: Path, retries=3, **kwargs) -> Path:
"""
Downloads a model from HuggingFace Hub or Google Cloud Storage.
Args:
model (Dict[str, Any]): The model description.
Example:
```
{
"model": "BAAI/bge-base-en-v1.5",
"dim": 768,
"description": "Base English model, v1.5",
"size_in_GB": 0.44,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
"hf": "qdrant/bge-base-en-v1.5-onnx-q",
}
}
```
cache_dir (str): The path to the cache directory.
retries: (int): The number of times to retry (including the first attempt)
Returns:
Path: The path to the downloaded model directory.
"""
hf_source = model.get("sources", {}).get("hf")
url_source = model.get("sources", {}).get("url")
sleep = 3.0
while retries > 0:
retries -= 1
if hf_source:
extra_patterns = [model["model_file"]]
extra_patterns.extend(model.get("additional_files", []))
try:
return Path(
cls.download_files_from_huggingface(
hf_source,
cache_dir=str(cache_dir),
extra_patterns=extra_patterns,
local_files_only=kwargs.get("local_files_only", False),
)
)
except (EnvironmentError, RepositoryNotFoundError, ValueError) as e:
logger.error(
f"Could not download model from HuggingFace: {e} "
"Falling back to other sources."
)
if url_source:
try:
return cls.retrieve_model_gcs(model["model"], url_source, str(cache_dir))
except Exception:
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."
)
time.sleep(sleep)
sleep *= 3
raise ValueError(f"Could not download model {model['model']} from any source.")
+123
View File
@@ -0,0 +1,123 @@
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import (
Any,
Dict,
Generic,
Iterable,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
)
import numpy as np
import onnxruntime as ort
from fastembed.common.types import OnnxProvider
from fastembed.parallel_processor import Worker
# Holds type of the embedding result
T = TypeVar("T")
@dataclass
class OnnxOutputContext:
model_output: np.ndarray
attention_mask: Optional[np.ndarray] = None
input_ids: Optional[np.ndarray] = None
class OnnxModel(Generic[T]):
@classmethod
def _get_worker_class(cls) -> Type["EmbeddingWorker"]:
raise NotImplementedError("Subclasses must implement this method")
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[T]:
raise NotImplementedError("Subclasses must implement this method")
def __init__(self) -> None:
self.model = None
self.tokenizer = None
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
return onnx_input
def load_onnx_model(
self,
model_dir: Path,
model_file: str,
threads: Optional[int],
providers: Optional[Sequence[OnnxProvider]] = None,
) -> 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)
)
available_providers = ort.get_available_providers()
requested_provider_names = []
for provider in onnx_providers:
# check providers available
provider_name = provider if isinstance(provider, str) else provider[0]
requested_provider_names.append(provider_name)
if provider_name not in available_providers:
raise ValueError(
f"Provider {provider_name} is not available. Available providers: {available_providers}"
)
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
if threads is not None:
so.intra_op_num_threads = threads
so.inter_op_num_threads = threads
self.model = ort.InferenceSession(
str(model_path), providers=onnx_providers, sess_options=so
)
if "CUDAExecutionProvider" in requested_provider_names:
current_providers = self.model.get_providers()
if "CUDAExecutionProvider" not in current_providers:
warnings.warn(
f"Attempt to set CUDAExecutionProvider failed. Current providers: {current_providers}."
"If you are using CUDA 12.x, install onnxruntime-gpu via "
"`pip install onnxruntime-gpu --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/`",
RuntimeWarning,
)
def onnx_embed(self, *args, **kwargs) -> OnnxOutputContext:
raise NotImplementedError("Subclasses must implement this method")
class EmbeddingWorker(Worker):
def init_embedding(
self,
model_name: str,
cache_dir: str,
**kwargs,
) -> OnnxModel:
raise NotImplementedError()
def __init__(
self,
model_name: str,
cache_dir: str,
**kwargs,
):
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
@classmethod
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "EmbeddingWorker":
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
raise NotImplementedError("Subclasses must implement this method")
+76
View File
@@ -0,0 +1,76 @@
import json
from pathlib import Path
from typing import Tuple
from tokenizers import AddedToken, Tokenizer
from fastembed.image.transform.operators import Compose
def load_special_tokens(model_dir: Path) -> dict:
tokens_map_path = model_dir / "special_tokens_map.json"
if not tokens_map_path.exists():
raise ValueError(f"Could not find special_tokens_map.json in {model_dir}")
with open(str(tokens_map_path)) as tokens_map_file:
tokens_map = json.load(tokens_map_file)
return tokens_map
def load_tokenizer(model_dir: Path, max_length: int = 512) -> 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}")
tokenizer_path = model_dir / "tokenizer.json"
if not tokenizer_path.exists():
raise ValueError(f"Could not find tokenizer.json in {model_dir}")
tokenizer_config_path = model_dir / "tokenizer_config.json"
if not tokenizer_config_path.exists():
raise ValueError(f"Could not find tokenizer_config.json in {model_dir}")
with open(str(config_path)) as config_file:
config = json.load(config_file)
with open(str(tokenizer_config_path)) as tokenizer_config_file:
tokenizer_config = json.load(tokenizer_config_file)
tokens_map = load_special_tokens(model_dir)
tokenizer = Tokenizer.from_file(str(tokenizer_path))
tokenizer.enable_truncation(
max_length=min(tokenizer_config["model_max_length"], max_length)
)
tokenizer.enable_padding(
pad_id=config.get("pad_token_id", 0), pad_token=tokenizer_config["pad_token"]
)
for token in tokens_map.values():
if isinstance(token, str):
tokenizer.add_special_tokens([token])
elif isinstance(token, dict):
tokenizer.add_special_tokens([AddedToken(**token)])
special_token_to_id = {}
for token in tokens_map.values():
if isinstance(token, str):
special_token_to_id[token] = tokenizer.token_to_id(token)
elif isinstance(token, dict):
token_str = token.get("content", "")
special_token_to_id[token_str] = tokenizer.token_to_id(token_str)
return tokenizer, special_token_to_id
def load_preprocessor(model_dir: Path) -> Compose:
preprocessor_config_path = model_dir / "preprocessor_config.json"
if not preprocessor_config_path.exists():
raise ValueError(f"Could not find preprocessor_config.json in {model_dir}")
with open(str(preprocessor_config_path)) as preprocessor_config_file:
preprocessor_config = json.load(preprocessor_config_file)
transforms = Compose.from_config(preprocessor_config)
return transforms
+16
View File
@@ -0,0 +1,16 @@
import os
import sys
from PIL import Image
from typing import Any, Dict, Iterable, Tuple, Union
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias
PathInput: TypeAlias = Union[str, os.PathLike]
PilInput: TypeAlias = Union[Image.Image, Iterable[Image.Image]]
ImageInput: TypeAlias = Union[PathInput, Iterable[PathInput], PilInput]
OnnxProvider: TypeAlias = Union[str, Tuple[str, Dict[Any, Any]]]
+43
View File
@@ -0,0 +1,43 @@
import os
import tempfile
from itertools import islice
from pathlib import Path
from typing import Generator, Iterable, Optional, Union
import numpy as np
def normalize(input_array, p=2, dim=1, eps=1e-12) -> np.ndarray:
# Calculate the Lp norm along the specified dimension
norm = np.linalg.norm(input_array, ord=p, axis=dim, keepdims=True)
norm = np.maximum(norm, eps) # Avoid division by zero
normalized_array = input_array / norm
return normalized_array
def iter_batch(iterable: Union[Iterable, Generator], size: int) -> Iterable:
"""
>>> list(iter_batch([1,2,3,4,5], 3))
[[1, 2, 3], [4, 5]]
"""
source_iter = iter(iterable)
while source_iter:
b = list(islice(source_iter, size))
if len(b) == 0:
break
yield b
def define_cache_dir(cache_dir: Optional[str] = None) -> Path:
"""
Define the cache directory for fastembed
"""
if cache_dir is None:
default_cache_dir = os.path.join(tempfile.gettempdir(), "fastembed_cache")
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
+18 -398
View File
@@ -1,404 +1,24 @@
import json
import os
import shutil
import tarfile
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, Iterable, List, Union
from typing import Optional
import onnxruntime as ort
import numpy as np
import requests
from tokenizers import Tokenizer, AddedToken
from tqdm import tqdm
from loguru import logger
from fastembed import TextEmbedding
logger.warning(
"DefaultEmbedding, FlagEmbedding, JinaEmbedding are deprecated."
"Use from fastembed import TextEmbedding instead."
)
DefaultEmbedding = TextEmbedding
FlagEmbedding = TextEmbedding
def normalize(input_array, p=2, dim=1, eps=1e-12):
# Calculate the Lp norm along the specified dimension
norm = np.linalg.norm(input_array, ord=p, axis=dim, keepdims=True)
norm = np.maximum(norm, eps) # Avoid division by zero
normalized_array = input_array / norm
return normalized_array
class Embedding(ABC):
"""
Abstract class for embeddings.
Args:
ABC ():
Raises:
NotImplementedError: Raised when you call an abstract method that has not been implemented.
PermissionError: _description_
ValueError: Several possible reasons: 1) targz_path does not exist or is not a file, 2) targz_path is not a .tar.gz file, 3) An error occurred while decompressing targz_path, 4) Could not find model_dir in cache_dir, 5) Could not find tokenizer.json in model_dir, 6) Could not find model.onnx in model_dir.
NotImplementedError: _description_
Returns:
_type_: _description_
Yields:
_type_: _description_
"""
@abstractmethod
def embed(self, texts: List[str]) -> List[np.ndarray]:
raise NotImplementedError
@classmethod
def list_supported_models(cls) -> List[Dict[str, Union[str, int]]]:
"""
Lists the supported models.
"""
return [
{
"model": "BAAI/bge-small-en",
"dim": 384,
"description": "Fast and Default English model",
},
{
"model": "BAAI/bge-base-en",
"dim": 768,
"description": "Base English model",
},
{
"model": "sentence-transformers/all-MiniLM-L6-v2",
"dim": 384,
"description": "Sentence Transformer model, MiniLM-L6-v2",
},
{
"model": "intfloat/multilingual-e5-large",
"dim": 1024,
"description": "Multilingual model, e5-large. Recommend using this model for non-English languages",
},
]
@classmethod
def download_file_from_gcs(cls, url: str, output_path: str, show_progress: bool = True) -> str:
"""
Downloads a file from Google Cloud Storage.
Args:
url (str): The URL to download the file from.
output_path (str): The path to save the downloaded file to.
show_progress (bool, optional): Whether to show a progress bar. Defaults to True.
Returns:
str: The path to the downloaded file.
"""
if os.path.exists(output_path):
return output_path
response = requests.get(url, stream=True)
# Handle HTTP errors
if response.status_code == 403:
raise PermissionError(
"Authentication Error: You do not have permission to access this resource. Please check your credentials."
)
# Get the total size of the file
total_size_in_bytes = int(response.headers.get("content-length", 0))
# Warn if the total size is zero
if total_size_in_bytes == 0:
print(f"Warning: Content-length header is missing or zero in the response from {url}.")
# Initialize the progress bar
progress_bar = (
tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True)
if total_size_in_bytes and show_progress
else None
)
# Attempt to download the file
try:
with open(output_path, "wb") as file:
for chunk in response.iter_content(chunk_size=1024): # Adjust chunk size to your preference
if chunk: # Filter out keep-alive new chunks
if progress_bar is not None:
progress_bar.update(len(chunk))
file.write(chunk)
except Exception as e:
print(f"An error occurred while trying to download the file: {str(e)}")
return
finally:
if progress_bar is not None:
progress_bar.close()
return output_path
@classmethod
def decompress_to_cache(cls, targz_path: str, cache_dir: str):
"""
Decompresses a .tar.gz file to a cache directory.
Args:
targz_path (str): Path to the .tar.gz file.
cache_dir (str): Path to the cache directory.
Returns:
cache_dir (str): Path to the cache directory.
"""
# Check if targz_path exists and is a file
if not os.path.isfile(targz_path):
raise ValueError(f"{targz_path} does not exist or is not a file.")
# Check if targz_path is a .tar.gz file
if not targz_path.endswith(".tar.gz"):
raise ValueError(f"{targz_path} is not a .tar.gz file.")
try:
# Open the tar.gz file
with tarfile.open(targz_path, "r:gz") as tar:
# Extract all files into the cache directory
tar.extractall(path=cache_dir)
except tarfile.TarError as e:
# If any error occurs while opening or extracting the tar.gz file,
# delete the cache directory (if it was created in this function)
# and raise the error again
if "tmp" in cache_dir:
shutil.rmtree(cache_dir)
raise ValueError(f"An error occurred while decompressing {targz_path}: {e}")
return cache_dir
def retrieve_model(self, model_name: str, cache_dir: str) -> Path:
"""
Retrieves a model from Google Cloud Storage.
Args:
model_name (str): The name of the model to retrieve.
cache_dir (str): The path to the cache directory.
Raises:
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
Returns:
Path: The path to the model directory.
"""
assert "/" in model_name, "model_name must be in the format <org>/<model> e.g. BAAI/bge-base-en"
fast_model_name = f"fast-{model_name.split('/')[-1]}"
model_dir = Path(cache_dir) / fast_model_name
if model_dir.exists():
return model_dir
model_tar_gz = Path(cache_dir) / f"{fast_model_name}.tar.gz"
try:
self.download_file_from_gcs(
f"https://storage.googleapis.com/qdrant-fastembed/{fast_model_name}.tar.gz",
output_path=str(model_tar_gz),
)
except PermissionError:
simple_model_name = model_name.replace("/", "-")
print(f"Was not able to download {fast_model_name}.tar.gz, trying {simple_model_name}.tar.gz")
self.download_file_from_gcs(
f"https://storage.googleapis.com/qdrant-fastembed/{simple_model_name}.tar.gz",
output_path=str(model_tar_gz),
)
self.decompress_to_cache(targz_path=str(model_tar_gz), cache_dir=cache_dir)
assert model_dir.exists(), f"Could not find {model_dir} in {cache_dir}"
model_tar_gz.unlink()
return model_dir
def passage_embed(self, texts: List[str], batch_size: int = 256) -> Iterable[np.ndarray]:
"""
Embeds a list of text passages into a list of embeddings.
Args:
texts (List[str]): The list of texts to embed.
batch_size (int, optional): The batch size. Defaults to 256.
Yields:
Iterable[np.ndarray]: The embeddings.
"""
for i in range(0, len(texts), batch_size):
# Prepend "passage: " to each text
yield from self.embed([f"passage: {t}" for t in texts[i : i + batch_size]])
def query_embed(self, query: str) -> Iterable[np.ndarray]:
"""
Embeds a query
Args:
query (str): The query to search for.
Returns:
Iterable[np.ndarray]: The embeddings.
"""
# Prepend "query: " to the query
query = f"query: {query}"
# Embed the query
query_embedding = self.embed([query])
# Compute the cosine similarity between the query embedding and the document embeddings
return query_embedding
class FlagEmbedding(Embedding):
"""
Implementation of the Flag Embedding model.
Args:
Embedding (_type_): _description_
"""
@classmethod
def load_tokenizer(cls, model_dir: Path, max_length: int = 512) -> Tokenizer:
config_path = model_dir / "config.json"
if not config_path.exists():
raise ValueError(f"Could not find config.json in {model_dir}")
tokenizer_path = model_dir / "tokenizer.json"
if not tokenizer_path.exists():
raise ValueError(f"Could not find tokenizer.json in {model_dir}")
tokenizer_config_path = model_dir / "tokenizer_config.json"
if not tokenizer_config_path.exists():
raise ValueError(f"Could not find tokenizer_config.json in {model_dir}")
tokens_map_path = model_dir / "special_tokens_map.json"
if not tokens_map_path.exists():
raise ValueError(f"Could not find special_tokens_map.json in {model_dir}")
config = json.load(open(str(config_path)))
tokenizer_config = json.load(open(str(tokenizer_config_path)))
tokens_map = json.load(open(str(tokens_map_path)))
tokenizer = Tokenizer.from_file(str(tokenizer_path))
tokenizer.enable_truncation(max_length=min(tokenizer_config["model_max_length"], max_length))
tokenizer.enable_padding(pad_id=config["pad_token_id"], pad_token=tokenizer_config["pad_token"])
for token in tokens_map.values():
if isinstance(token, str):
tokenizer.add_special_tokens([token])
elif isinstance(token, dict):
tokenizer.add_special_tokens([AddedToken(**token)])
return tokenizer
class JinaEmbedding(TextEmbedding):
def __init__(
self,
model_name: str = "BAAI/bge-small-en",
max_length: int = 512,
cache_dir: str = None,
model_name: str = "jinaai/jina-embeddings-v2-base-en",
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
**kwargs,
):
"""
Args:
model_name (str): The name of the model to use.
max_length (int, optional): The maximum number of tokens. Defaults to 512. Unknown behavior for values > 512.
cache_dir (str, optional): The path to the cache directory. Defaults to `local_cache` in the current directory.
Raises:
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
"""
self.model_name = model_name
if cache_dir is None:
cache_dir = Path(".").resolve() / "local_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
model_dir = self.retrieve_model(model_name, cache_dir)
model_path = model_dir / "model.onnx"
optimized_model_path = model_dir / "model_optimized.onnx"
# List of Execution Providers: https://onnxruntime.ai/docs/execution-providers
onnx_providers = ["CPUExecutionProvider"]
if not model_path.exists():
# Rename file model_optimized.onnx to model.onnx if it exists
if optimized_model_path.exists():
optimized_model_path.rename(model_path)
else:
raise ValueError(f"Could not find model.onnx in {model_dir}")
# Hacky support for multilingual model
self.exclude_token_type_ids = False
if model_name == "intfloat/multilingual-e5-large":
self.exclude_token_type_ids = True
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
self.tokenizer = self.load_tokenizer(model_dir, max_length=max_length)
self.model = ort.InferenceSession(str(model_path), providers=onnx_providers, sess_options=so)
def onnx_embed(self, documents: List[str]) -> np.ndarray:
encoded = self.tokenizer.encode_batch(documents)
input_ids = np.array([e.ids for e in encoded])
attention_mask = np.array([e.attention_mask for e in encoded])
onnx_input = {
"input_ids": np.array(input_ids, dtype=np.int64),
"attention_mask": np.array(attention_mask, dtype=np.int64),
}
if not self.exclude_token_type_ids:
onnx_input["token_type_ids"] = np.array(
[np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
)
model_output = self.model.run(None, onnx_input)
last_hidden_state = model_output[0][:, 0]
embeddings = normalize(last_hidden_state).astype(np.float32)
return embeddings
def embed(self, documents: List[str], batch_size: int = 256) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: List of documents to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
Returns:
List of embeddings, one per document
"""
if type(documents) == str:
documents = [documents]
# TODO: Replace loop with parallelized batching
if len(documents) >= batch_size:
for i in range(0, len(documents), batch_size):
batch = documents[i : i + batch_size]
yield from self.onnx_embed(batch)
else:
vectors = self.onnx_embed(documents)
yield from vectors
class DefaultEmbedding(FlagEmbedding):
"""
Implementation of the default Flag Embedding model.
Args:
FlagEmbedding (_type_): _description_
"""
def __init__(
self,
model_name: str = "BAAI/bge-small-en",
max_length: int = 512,
cache_dir: str = None,
):
super().__init__(model_name, max_length=max_length, cache_dir=cache_dir)
class OpenAIEmbedding(Embedding):
def __init__(self):
# Initialize your OpenAI model here
# self.model = ...
...
def embed(self, texts):
# Use your OpenAI model to embed the texts
# return self.model.embed(texts)
raise NotImplementedError
super().__init__(model_name, cache_dir, threads, **kwargs)
+3
View File
@@ -0,0 +1,3 @@
from fastembed.image.image_embedding import ImageEmbedding
__all__ = ["ImageEmbedding"]
+94
View File
@@ -0,0 +1,94 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type
import numpy as np
from fastembed.common import ImageInput, OnnxProvider
from fastembed.image.image_embedding_base import ImageEmbeddingBase
from fastembed.image.onnx_embedding import OnnxImageEmbedding
class ImageEmbedding(ImageEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[ImageEmbeddingBase]] = [OnnxImageEmbedding]
@classmethod
def list_supported_models(cls) -> List[Dict[str, Any]]:
"""
Lists the supported models.
Returns:
List[Dict[str, Any]]: A list of dictionaries containing the model information.
Example:
```
[
{
"model": "Qdrant/clip-ViT-B-32-vision",
"dim": 512,
"description": "CLIP vision encoder based on ViT-B/32",
"size_in_GB": 0.33,
"sources": {
"hf": "Qdrant/clip-ViT-B-32-vision",
},
"model_file": "model.onnx",
}
]
```
"""
result = []
for embedding in cls.EMBEDDINGS_REGISTRY:
result.extend(embedding.list_supported_models())
return result
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
super().__init__(model_name, cache_dir, threads, **kwargs)
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
if any(
model_name.lower() == model["model"].lower()
for model in supported_models
):
self.model = EMBEDDING_MODEL_TYPE(
model_name,
cache_dir,
threads=threads,
providers=providers,
**kwargs,
)
return
raise ValueError(
f"Model {model_name} is not supported in ImageEmbedding."
"Please check the supported models using `ImageEmbedding.list_supported_models()`"
)
def embed(
self,
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
images: Iterator of image paths or single image path to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self.model.embed(images, batch_size, parallel, **kwargs)
+44
View File
@@ -0,0 +1,44 @@
from typing import Iterable, Optional
import numpy as np
from fastembed.common.model_management import ModelManagement
from fastembed.common.types import ImageInput
class ImageEmbeddingBase(ModelManagement):
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
**kwargs,
):
self.model_name = model_name
self.cache_dir = cache_dir
self.threads = threads
self._local_files_only = kwargs.pop("local_files_only", False)
def embed(
self,
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Embeds a list of images into a list of embeddings.
Args:
images - The list of image paths to preprocess and embed.
batch_size: Batch size for encoding
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
**kwargs: Additional keyword argument to pass to the embed method.
Yields:
Iterable[np.ndarray]: The embeddings.
"""
raise NotImplementedError()
+151
View File
@@ -0,0 +1,151 @@
from typing import Any, Dict, Iterable, List, 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
from fastembed.image.image_embedding_base import ImageEmbeddingBase
from fastembed.image.onnx_image_model import ImageEmbeddingWorker, OnnxImageModel
supported_onnx_models = [
{
"model": "Qdrant/clip-ViT-B-32-vision",
"dim": 512,
"description": "CLIP vision encoder based on ViT-B/32",
"size_in_GB": 0.34,
"sources": {
"hf": "Qdrant/clip-ViT-B-32-vision",
},
"model_file": "model.onnx",
},
{
"model": "Qdrant/resnet50-onnx",
"dim": 2048,
"description": "ResNet-50 from `Deep Residual Learning for Image Recognition <https://arxiv.org/abs/1512.03385>`__.",
"size_in_GB": 0.1,
"sources": {
"hf": "Qdrant/resnet50-onnx",
},
"model_file": "model.onnx",
},
{
"model": "Qdrant/Unicom-ViT-B-16",
"dim": 768,
"description": "Unicom Unicom-ViT-B-16 from open-metric-learning",
"size_in_GB": 0.82,
"sources": {
"hf": "Qdrant/Unicom-ViT-B-16",
},
"model_file": "model.onnx",
},
{
"model": "Qdrant/Unicom-ViT-B-32",
"dim": 512,
"description": "Unicom Unicom-ViT-B-32 from open-metric-learning",
"size_in_GB": 0.48,
"sources": {
"hf": "Qdrant/Unicom-ViT-B-32",
},
"model_file": "model.onnx",
},
]
class OnnxImageEmbedding(ImageEmbeddingBase, OnnxImageModel[np.ndarray]):
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
"""
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.
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)
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,
)
@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 embed(
self,
images: ImageInput,
batch_size: int = 16,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of images into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
images: Iterator of image paths or single image path to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_images(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
images=images,
batch_size=batch_size,
parallel=parallel,
**kwargs,
)
@classmethod
def _get_worker_class(cls) -> Type["ImageEmbeddingWorker"]:
return OnnxImageEmbeddingWorker
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
return onnx_input
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[np.ndarray]:
return normalize(output.model_output).astype(np.float32)
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)
+113
View File
@@ -0,0 +1,113 @@
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
import numpy as np
from PIL import Image
from fastembed.common import ImageInput, OnnxProvider
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
from fastembed.common.preprocessor_utils import load_preprocessor
from fastembed.common.utils import iter_batch
from fastembed.parallel_processor import ParallelWorkerPool
# Holds type of the embedding result
class OnnxImageModel(OnnxModel[T]):
@classmethod
def _get_worker_class(cls) -> Type["ImageEmbeddingWorker"]:
raise NotImplementedError("Subclasses must implement this method")
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[T]:
raise NotImplementedError("Subclasses must implement this method")
def __init__(self) -> None:
super().__init__()
self.processor = None
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
return onnx_input
def load_onnx_model(
self,
model_dir: Path,
model_file: str,
threads: Optional[int],
providers: Optional[Sequence[OnnxProvider]] = None,
) -> None:
super().load_onnx_model(
model_dir=model_dir,
model_file=model_file,
threads=threads,
providers=providers,
)
self.processor = load_preprocessor(model_dir=model_dir)
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:
with contextlib.ExitStack():
image_files = [
Image.open(image) if not isinstance(image, Image.Image) else image
for image in images
]
encoded = self.processor(image_files)
onnx_input = self._build_onnx_input(encoded)
onnx_input = self._preprocess_onnx_input(onnx_input)
model_output = self.model.run(None, onnx_input)
embeddings = model_output[0].reshape(len(images), -1)
return OnnxOutputContext(model_output=embeddings)
def _embed_images(
self,
model_name: str,
cache_dir: str,
images: ImageInput,
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[T]:
is_small = False
if (
isinstance(images, str)
or isinstance(images, Path)
or (isinstance(images, 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 parallel is None or is_small:
for batch in iter_batch(images, 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}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), 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]]:
for idx, batch in items:
embeddings = self.model.onnx_embed(batch)
yield idx, embeddings
+124
View File
@@ -0,0 +1,124 @@
from typing import Sized, Tuple, Union
import numpy as np
from PIL import Image
def convert_to_rgb(image: Image.Image) -> Image.Image:
if image.mode == "RGB":
return image
image = image.convert("RGB")
return image
def center_crop(
image: Union[Image.Image, np.ndarray],
size: Tuple[int, int],
) -> np.ndarray:
if isinstance(image, np.ndarray):
_, orig_height, orig_width = image.shape
else:
orig_height, orig_width = image.height, image.width
# (H, W, C) -> (C, H, W)
image = np.array(image).transpose((2, 0, 1))
crop_height, crop_width = size
# left upper corner (0, 0)
top = (orig_height - crop_height) // 2
bottom = top + crop_height
left = (orig_width - crop_width) // 2
right = left + crop_width
# Check if cropped area is within image boundaries
if top >= 0 and bottom <= orig_height and left >= 0 and right <= orig_width:
image = image[..., top:bottom, left:right]
return image
# Padding with zeros
new_height = max(crop_height, orig_height)
new_width = max(crop_width, orig_width)
new_shape = image.shape[:-2] + (new_height, new_width)
new_image = np.zeros_like(image, shape=new_shape)
top_pad = (new_height - orig_height) // 2
bottom_pad = top_pad + orig_height
left_pad = (new_width - orig_width) // 2
right_pad = left_pad + orig_width
new_image[..., top_pad:bottom_pad, left_pad:right_pad] = image
top += top_pad
bottom += top_pad
left += left_pad
right += left_pad
new_image = new_image[
..., max(0, top) : min(new_height, bottom), max(0, left) : min(new_width, right)
]
return new_image
def normalize(
image: 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")
num_channels = image.shape[1] if len(image.shape) == 4 else image.shape[0]
if not np.issubdtype(image.dtype, np.floating):
image = image.astype(np.float32)
if isinstance(mean, Sized):
if len(mean) != num_channels:
raise ValueError(
f"mean must have {num_channels} elements if it is an iterable, got {len(mean)}"
)
else:
mean = [mean] * num_channels
mean = np.array(mean, dtype=image.dtype)
if isinstance(std, Sized):
if len(std) != num_channels:
raise ValueError(
f"std must have {num_channels} elements if it is an iterable, got {len(std)}"
)
else:
std = [std] * num_channels
std = np.array(std, dtype=image.dtype)
image = ((image.T - mean) / std).T
return image
def resize(
image: Image,
size: Union[int, Tuple[int, int]],
resample: Image.Resampling = Image.Resampling.BILINEAR,
) -> Image:
if isinstance(size, tuple):
return image.resize(size, resample)
height, width = image.height, image.width
short, long = (width, height) if width <= height else (height, width)
new_short, new_long = size, int(size * long / short)
if width <= height:
new_size = (new_short, new_long)
else:
new_size = (new_long, new_short)
return image.resize(new_size, resample)
def rescale(image: np.ndarray, scale: float, dtype=np.float32) -> np.ndarray:
return (image * scale).astype(dtype)
def pil2ndarray(image: Union[Image.Image, np.ndarray]):
if isinstance(image, Image.Image):
return np.asarray(image).transpose((2, 0, 1))
return image
+198
View File
@@ -0,0 +1,198 @@
from typing import Any, Dict, List, Tuple, Union
import numpy as np
from PIL import Image
from fastembed.image.transform.functional import (
center_crop,
convert_to_rgb,
normalize,
pil2ndarray,
rescale,
resize,
)
class Transform:
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]:
return [convert_to_rgb(image=image) for image in images]
class CenterCrop(Transform):
def __init__(self, size: Tuple[int, int]):
self.size = size
def __call__(self, images: List[Image.Image]) -> List[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]]):
self.mean = mean
self.std = std
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]],
resample: Image.Resampling = Image.Resampling.BICUBIC,
):
self.size = size
self.resample = resample
def __call__(self, images: List[Image.Image]) -> List[Image.Image]:
return [
resize(image, size=self.size, resample=self.resample) for image in images
]
class Rescale(Transform):
def __init__(self, scale: float = 1 / 255):
self.scale = scale
def __call__(self, images: List[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]:
return [pil2ndarray(image) for image in images]
class Compose:
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]]:
for transform in self.transforms:
images = transform(images)
return images
@classmethod
def from_config(cls, config: Dict[str, Any]) -> "Compose":
"""Creates processor from a config dict.
Args:
config (Dict[str, Any]): Configuration dictionary.
Valid keys:
- do_resize
- size
- do_center_crop
- crop_size
- do_rescale
- rescale_factor
- do_normalize
- image_mean
- image_std
Valid size keys (nested):
- {"height", "width"}
- {"shortest_edge"}
Returns:
Compose: Image processor.
"""
transforms = []
cls._get_convert_to_rgb(transforms, config)
cls._get_resize(transforms, config)
cls._get_center_crop(transforms, config)
cls._get_pil2ndarray(transforms, config)
cls._get_rescale(transforms, config)
cls._get_normalize(transforms, config)
return cls(transforms=transforms)
@staticmethod
def _get_convert_to_rgb(transforms: List[Transform], config: Dict[str, Any]):
transforms.append(ConvertToRGB())
@staticmethod
def _get_resize(transforms: List[Transform], config: Dict[str, Any]):
mode = config.get("image_processor_type", "CLIPImageProcessor")
if mode == "CLIPImageProcessor":
if config.get("do_resize", False):
size = config["size"]
if "shortest_edge" in size:
size = size["shortest_edge"]
elif "height" in size and "width" in size:
size = (size["height"], size["width"])
else:
raise ValueError(
"Size must contain either 'shortest_edge' or 'height' and 'width'."
)
transforms.append(
Resize(
size=size,
resample=config.get("resample", Image.Resampling.BICUBIC),
)
)
elif mode == "ConvNextFeatureExtractor":
if "size" in config and "shortest_edge" not in config["size"]:
raise ValueError(
f"Size dictionary must contain 'shortest_edge' key. Got {config['size'].keys()}"
)
shortest_edge = config["size"]["shortest_edge"]
crop_pct = config.get("crop_pct", 0.875)
if shortest_edge < 384:
# maintain same ratio, resizing shortest edge to shortest_edge/crop_pct
resize_shortest_edge = int(shortest_edge / crop_pct)
transforms.append(
Resize(
size=resize_shortest_edge,
resample=config.get("resample", Image.Resampling.BICUBIC),
)
)
transforms.append(CenterCrop(size=(shortest_edge, shortest_edge)))
else:
transforms.append(
Resize(
size=(shortest_edge, shortest_edge),
resample=config.get("resample", Image.Resampling.BICUBIC),
)
)
@staticmethod
def _get_center_crop(transforms: List[Transform], config: Dict[str, Any]):
mode = config.get("image_processor_type", "CLIPImageProcessor")
if mode == "CLIPImageProcessor":
if config.get("do_center_crop", False):
crop_size = config["crop_size"]
if isinstance(crop_size, int):
crop_size = (crop_size, crop_size)
elif isinstance(crop_size, dict):
crop_size = (crop_size["height"], crop_size["width"])
else:
raise ValueError(f"Invalid crop size: {crop_size}")
transforms.append(CenterCrop(size=crop_size))
elif mode == "ConvNextFeatureExtractor":
pass
else:
raise ValueError(f"Preprocessor {mode} is not supported")
@staticmethod
def _get_pil2ndarray(transforms: List[Transform], config: Dict[str, Any]):
transforms.append(PILtoNDarray())
@staticmethod
def _get_rescale(transforms: List[Transform], config: Dict[str, Any]):
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]):
if config.get("do_normalize", False):
transforms.append(
Normalize(mean=config["image_mean"], std=config["image_std"])
)
+5
View File
@@ -0,0 +1,5 @@
from fastembed.late_interaction.late_interaction_text_embedding import (
LateInteractionTextEmbedding,
)
__all__ = ["LateInteractionTextEmbedding"]
+194
View File
@@ -0,0 +1,194 @@
import string
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
import numpy as np
from tokenizers import Encoding
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import define_cache_dir
from fastembed.late_interaction.late_interaction_embedding_base import (
LateInteractionTextEmbeddingBase,
)
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
supported_colbert_models = [
{
"model": "colbert-ir/colbertv2.0",
"dim": 128,
"description": "Late interaction model",
"size_in_GB": 0.44,
"sources": {
"hf": "colbert-ir/colbertv2.0",
},
"model_file": "model.onnx",
}
]
class Colbert(LateInteractionTextEmbeddingBase, OnnxTextModel[np.ndarray]):
QUERY_MARKER_TOKEN_ID = 1
DOCUMENT_MARKER_TOKEN_ID = 2
MIN_QUERY_LENGTH = 32
MASK_TOKEN = "[MASK]"
def _post_process_onnx_output(
self, output: OnnxOutputContext, is_doc: bool = True
) -> Iterable[np.ndarray]:
if not is_doc:
return output.model_output.astype(np.float32)
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:
output.attention_mask[i, j] = 0
output.model_output *= np.expand_dims(output.attention_mask, 2).astype(np.float32)
norm = np.linalg.norm(output.model_output, ord=2, axis=2, keepdims=True)
norm_clamped = np.maximum(norm, 1e-12)
output.model_output /= norm_clamped
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
return onnx_input
def tokenize(self, documents: List[str], is_doc: bool = True) -> 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)
# 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
if self.tokenizer.padding:
prev_padding = self.tokenizer.padding
self.tokenizer.enable_padding(
pad_token=self.MASK_TOKEN,
pad_id=self.mask_token_id,
length=self.MIN_QUERY_LENGTH,
)
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]
encoded = self.tokenizer.encode_batch(documents)
return encoded
@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_colbert_models
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
"""
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.
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)
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.mask_token_id = self.special_token_to_id["[MASK]"]
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
}
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_documents(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
documents=documents,
batch_size=batch_size,
parallel=parallel,
**kwargs,
)
def query_embed(self, query: Union[str, List[str]], **kwargs) -> np.ndarray:
if isinstance(query, str):
query = [query]
for text in query:
yield from self._post_process_onnx_output(
self.onnx_embed([text], is_doc=False), is_doc=False
)
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return ColbertEmbeddingWorker
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)
@@ -0,0 +1,62 @@
from typing import Iterable, Optional, Union
import numpy as np
from fastembed.common.model_management import ModelManagement
class LateInteractionTextEmbeddingBase(ModelManagement):
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
**kwargs,
):
self.model_name = model_name
self.cache_dir = cache_dir
self.threads = threads
self._local_files_only = kwargs.pop("local_files_only", False)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
raise NotImplementedError()
def passage_embed(self, texts: Iterable[str], **kwargs) -> 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[np.ndarray]: The embeddings.
"""
# This is model-specific, so that different models can have specialized implementations
yield from self.embed(texts, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
) -> 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
if isinstance(query, str):
yield from self.embed([query], **kwargs)
if isinstance(query, Iterable):
yield from self.embed(query, **kwargs)
@@ -0,0 +1,109 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
import numpy as np
from fastembed.common import OnnxProvider
from fastembed.late_interaction.colbert import Colbert
from fastembed.late_interaction.late_interaction_embedding_base import (
LateInteractionTextEmbeddingBase,
)
class LateInteractionTextEmbedding(LateInteractionTextEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[LateInteractionTextEmbeddingBase]] = [
Colbert,
]
@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": "prithvida/SPLADE_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"size_in_GB": 0.532,
"sources": {
"hf": "qdrant/SPLADE_PP_en_v1",
},
}
]
```
"""
result = []
for embedding in cls.EMBEDDINGS_REGISTRY:
result.extend(embedding.list_supported_models())
return result
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
super().__init__(model_name, cache_dir, threads, **kwargs)
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
if any(
model_name.lower() == model["model"].lower()
for model in supported_models
):
self.model = EMBEDDING_MODEL_TYPE(
model_name, cache_dir, threads, providers=providers, **kwargs
)
return
raise ValueError(
f"Model {model_name} is not supported in SparseTextEmbedding."
"Please check the supported models using `SparseTextEmbedding.list_supported_models()`"
)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
) -> 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)
+213
View File
@@ -0,0 +1,213 @@
import logging
import os
from collections import defaultdict
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
# Single item should be processed in less than:
processing_timeout = 10 * 60 # seconds
max_internal_batch_size = 200
class QueueSignals(str, Enum):
stop = "stop"
confirm = "confirm"
error = "error"
class Worker:
@classmethod
def start(cls, **kwargs: Any) -> "Worker":
raise NotImplementedError()
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, Any]]:
raise NotImplementedError()
def _worker(
worker_class: Type[Worker],
input_queue: Queue,
output_queue: Queue,
num_active_workers: BaseValue,
worker_id: int,
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.
When there are no data pints left on the input queue, it decrements
num_active_workers to signal completion.
"""
if kwargs is None:
kwargs = {}
logging.info(f"Reader worker: {worker_id} PID: {os.getpid()}")
try:
worker = worker_class.start(**kwargs)
# Keep going until you get an item that's None.
def input_queue_iterable() -> Iterable[Any]:
while True:
item = input_queue.get()
if item == QueueSignals.stop:
break
yield item
for processed_item in worker.process(input_queue_iterable()):
output_queue.put(processed_item)
except Exception as e: # pylint: disable=broad-except
logging.exception(e)
output_queue.put(QueueSignals.error)
finally:
# It's important that we close and join the queue here before
# decrementing num_active_workers. Otherwise our parent may join us
# before the queue's feeder thread has passed all buffered items to
# the underlying pipe resulting in a deadlock.
#
# 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
output_queue.close()
output_queue.join_thread()
with num_active_workers.get_lock():
num_active_workers.value -= 1
logging.info(f"Reader worker {worker_id} finished")
class ParallelWorkerPool:
def __init__(
self, num_workers: int, worker: Type[Worker], start_method: Optional[str] = None
):
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.queue_size = self.num_workers * max_internal_batch_size
self.num_active_workers: Optional[BaseValue] = None
def start(self, **kwargs: Any) -> None:
self.input_queue = self.ctx.Queue(self.queue_size)
self.output_queue = self.ctx.Queue(self.queue_size)
ctx_value = self.ctx.Value("i", self.num_workers)
assert isinstance(ctx_value, BaseValue)
self.num_active_workers = ctx_value
for worker_id in range(0, self.num_workers):
assert hasattr(self.ctx, "Process")
process = self.ctx.Process(
target=_worker,
args=(
self.worker_class,
self.input_queue,
self.output_queue,
self.num_active_workers,
worker_id,
kwargs.copy(),
),
)
process.start()
self.processes.append(process)
def ordered_map(
self, stream: Iterable[Any], *args: Any, **kwargs: Any
) -> Iterable[Any]:
buffer = defaultdict(Any)
next_expected = 0
for idx, item in self.semi_ordered_map(stream, *args, **kwargs):
buffer[idx] = item
while next_expected in buffer:
yield buffer.pop(next_expected)
next_expected += 1
def semi_ordered_map(
self, stream: Iterable[Any], *args: Any, **kwargs: Any
) -> Iterable[Tuple[int, Any]]:
try:
self.start(**kwargs)
assert self.input_queue is not None, "Input queue was not initialized"
assert self.output_queue is not None, "Output queue was not initialized"
pushed = 0
read = 0
for idx, item in enumerate(stream):
if pushed - read < self.queue_size:
try:
out_item = self.output_queue.get_nowait()
except Empty:
out_item = None
else:
try:
out_item = self.output_queue.get(timeout=processing_timeout)
except Empty as e:
self.join_or_terminate()
raise e
if out_item is not None:
if out_item == QueueSignals.error:
self.join_or_terminate()
raise RuntimeError("Thread unexpectedly terminated")
yield out_item
read += 1
self.input_queue.put((idx, item))
pushed += 1
for _ in range(self.num_workers):
self.input_queue.put(QueueSignals.stop)
while read < pushed:
out_item = self.output_queue.get(timeout=processing_timeout)
if out_item == QueueSignals.error:
self.join_or_terminate()
raise RuntimeError("Thread unexpectedly terminated")
yield out_item
read += 1
finally:
assert self.input_queue is not None, "Input queue is None"
assert self.output_queue is not None, "Output queue is None"
self.input_queue.close()
self.output_queue.close()
def join_or_terminate(self, timeout: Optional[int] = 1) -> None:
"""
Emergency shutdown
@param timeout:
@return:
"""
for process in self.processes:
process.join(timeout=timeout)
if process.is_alive():
process.terminate()
self.processes.clear()
def join(self) -> None:
for process in self.processes:
process.join()
self.processes.clear()
def __del__(self) -> None:
"""
Terminate processes if the user hasn't joined. This is necessary as
leaving stray processes running can corrupt shared state. In brief,
we've observed shared memory counters being reused (when the memory was
free from the perspective of the parent process) while the stray
workers still held a reference to them.
For a discussion of using destructors in Python in this manner, see
https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python/.
"""
for process in self.processes:
process.terminate()
+4
View File
@@ -0,0 +1,4 @@
from fastembed.sparse.sparse_embedding_base import SparseEmbedding
from fastembed.sparse.sparse_text_embedding import SparseTextEmbedding
__all__ = ["SparseEmbedding", "SparseTextEmbedding"]
+285
View File
@@ -0,0 +1,285 @@
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
import mmh3
import numpy as np
from snowballstemmer import stemmer as get_stemmer
from fastembed.common.utils import define_cache_dir, iter_batch
from fastembed.parallel_processor import ParallelWorkerPool, Worker
from fastembed.sparse.sparse_embedding_base import (
SparseEmbedding,
SparseTextEmbeddingBase,
)
from fastembed.sparse.utils.tokenizer import WordTokenizer
supported_bm25_models = [
{
"model": "Qdrant/bm25",
"description": "BM25 as sparse embeddings meant to be used with Qdrant",
"size_in_GB": 0.01,
"sources": {
"hf": "Qdrant/bm25",
},
"model_file": "mock.file", # bm25 does not require a model, so we just use a mock
"additional_files": ["stopwords.txt"],
"requires_idf": True,
},
]
MODEL_TO_LANGUAGE = {
"Qdrant/bm25": "english",
}
class Bm25(SparseTextEmbeddingBase):
"""Implements traditional BM25 in a form of sparse embeddings.
Uses a count of tokens in the document to evaluate the importance of the token.
WARNING: This model is expected to be used with `modifier="idf"` in the sparse vector index of Qdrant.
BM25 formula:
score(q, d) = SUM[ IDF(q_i) * (f(q_i, d) * (k + 1)) / (f(q_i, d) + k * (1 - b + b * (|d| / avg_len))) ],
where IDF is the inverse document frequency, computed on Qdrant's side
f(q_i, d) is the term frequency of the token q_i in the document d
k, b, avg_len are hyperparameters, described below.
Args:
model_name (str): The name of the model to use.
cache_dir (str, optional): The path to the cache directory.
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
Defaults to `fastembed_cache` in the system's temp directory.
k (float, optional): The k parameter in the BM25 formula. Defines the saturation of the term frequency.
I.e. defines how fast the moment when additional terms stop to increase the score. Defaults to 1.2.
b (float, optional): The b parameter in the BM25 formula. Defines the importance of the document length.
Defaults to 0.75.
avg_len (float, optional): The average length of the documents in the corpus. Defaults to 256.0.
Raises:
ValueError: If the model_name is not in the format <org>/<model> e.g. BAAI/bge-base-en.
"""
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
k: float = 1.2,
b: float = 0.75,
avg_len: float = 256.0,
**kwargs,
):
super().__init__(model_name, cache_dir, **kwargs)
self.k = k
self.b = b
self.avg_len = avg_len
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.punctuation = set(string.punctuation)
self.stopwords = set(self._load_stopwords(model_dir))
self.stemmer = get_stemmer(MODEL_TO_LANGUAGE[model_name])
self.tokenizer = WordTokenizer
@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_bm25_models
@classmethod
def _load_stopwords(cls, model_dir: Path) -> List[str]:
stopwords_path = model_dir / "stopwords.txt"
if not stopwords_path.exists():
return []
with open(stopwords_path, "r") as f:
return f.read().splitlines()
def _embed_documents(
self,
model_name: str,
cache_dir: str,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
) -> Iterable[SparseEmbedding]:
is_small = False
if isinstance(documents, str):
documents = [documents]
is_small = True
if isinstance(documents, list):
if len(documents) < batch_size:
is_small = True
if parallel == 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:
start_method = "forkserver" if "forkserver" in get_all_start_methods() else "spawn"
params = {
"model_name": model_name,
"cache_dir": cache_dir,
"k": self.k,
"b": self.b,
"avg_len": self.avg_len,
}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), start_method=start_method
)
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
for record in batch:
yield record
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[SparseEmbedding]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_documents(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
documents=documents,
batch_size=batch_size,
parallel=parallel,
)
def _stem(self, tokens: List[str]) -> List[str]:
stemmed_tokens = []
for token in tokens:
if token in self.punctuation:
continue
if token in self.stopwords:
continue
stemmed_token = self.stemmer.stemWord(token)
if stemmed_token:
stemmed_tokens.append(stemmed_token)
return stemmed_tokens
def raw_embed(
self,
documents: List[str],
) -> List[SparseEmbedding]:
embeddings = []
for document in documents:
tokens = self.tokenizer.tokenize(document)
stemmed_tokens = self._stem(tokens)
token_id2value = self._term_frequency(stemmed_tokens)
embeddings.append(SparseEmbedding.from_dict(token_id2value))
return embeddings
def _term_frequency(self, tokens: List[str]) -> Dict[int, float]:
"""Calculate the term frequency part of the BM25 formula.
(
f(q_i, d) * (k + 1)
) / (
f(q_i, d) + k * (1 - b + b * (|d| / avg_len))
)
Args:
tokens (List[str]): The list of tokens in the document.
Returns:
Dict[int, float]: The token_id to term frequency mapping.
"""
tf_map = {}
counter = defaultdict(int)
for stemmed_token in tokens:
counter[stemmed_token] += 1
doc_len = len(tokens)
for stemmed_token in counter:
token_id = self.compute_token_id(stemmed_token)
num_occurrences = counter[stemmed_token]
tf_map[token_id] = num_occurrences * (self.k + 1)
tf_map[token_id] /= num_occurrences + self.k * (
1 - self.b + self.b * doc_len / self.avg_len
)
return tf_map
@classmethod
def compute_token_id(cls, token: str) -> int:
return abs(mmh3.hash(token))
def query_embed(self, query: Union[str, Iterable[str]], **kwargs) -> Iterable[SparseEmbedding]:
"""To emulate BM25 behaviour, we don't need to use weights in the query, and
it's enough to just hash the tokens and assign a weight of 1.0 to them.
"""
if isinstance(query, str):
query = [query]
for text in query:
tokens = self.tokenizer.tokenize(text)
stemmed_tokens = self._stem(tokens)
token_ids = np.array(
list(set(self.compute_token_id(token) for token in stemmed_tokens)),
dtype=np.int32,
)
values = np.ones_like(token_ids)
yield SparseEmbedding(indices=token_ids, values=values)
@classmethod
def _get_worker_class(cls) -> Type["Bm25Worker"]:
return Bm25Worker
class Bm25Worker(Worker):
def __init__(
self,
model_name: str,
cache_dir: str,
**kwargs,
):
self.model = self.init_embedding(model_name, cache_dir, **kwargs)
@classmethod
def start(cls, model_name: str, cache_dir: str, **kwargs: Any) -> "Bm25Worker":
return cls(model_name=model_name, cache_dir=cache_dir, **kwargs)
def process(self, items: Iterable[Tuple[int, Any]]) -> Iterable[Tuple[int, 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:
return Bm25(model_name=model_name, cache_dir=cache_dir, **kwargs)
+293
View File
@@ -0,0 +1,293 @@
import math
import string
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union
import mmh3
import numpy as np
from snowballstemmer import stemmer as get_stemmer
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import define_cache_dir
from fastembed.sparse.sparse_embedding_base import (
SparseEmbedding,
SparseTextEmbeddingBase,
)
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
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",
"size_in_GB": 0.09,
"sources": {
"hf": "Qdrant/all_miniLM_L6_v2_with_attentions",
},
"model_file": "model.onnx",
"additional_files": ["stopwords.txt"],
"requires_idf": True,
},
]
MODEL_TO_LANGUAGE = {
"Qdrant/bm42-all-minilm-l6-v2-attentions": "english",
}
class Bm42(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
"""
Bm42 is an extension of BM25, which tries to better evaluate importance of tokens in the documents,
by extracting attention weights from the transformer model.
Traditional BM25 uses a count of tokens in the document to evaluate the importance of the token,
but this approach doesn't work well with short documents or chunks of text, as almost all tokens
there are unique.
BM42 addresses this issue by replacing the token count with the attention weights from the transformer model.
This allows sparse embeddings to work well with short documents, handle rare tokens and leverage traditional NLP
techniques like stemming and stopwords.
WARNING: This model is expected to be used with `modifier="idf"` in the sparse vector index of Qdrant.
"""
ONNX_OUTPUT_NAMES = ["attention_6"]
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
alpha: float = 0.5,
**kwargs,
):
"""
Args:
model_name (str): The name of the model to use.
cache_dir (str, optional): The path to the cache directory.
Can be set using the `FASTEMBED_CACHE_PATH` env variable.
Defaults to `fastembed_cache` in the system's temp directory.
threads (int, optional): The number of threads single onnxruntime session can use. Defaults to None.
providers (Optional[Sequence[OnnxProvider]], optional): The providers to use for onnxruntime.
alpha (float, optional): Parameter, that defines the importance of the token weight in the document
versus the importance of the token frequency in the corpus. Defaults to 0.5, based on empirical testing.
It is recommended to only change this parameter based on training data for a specific dataset.
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)
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.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.punctuation = set(string.punctuation)
self.stopwords = set(self._load_stopwords(model_dir))
self.stemmer = get_stemmer(MODEL_TO_LANGUAGE[model_name])
self.alpha = alpha
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:
continue
result.append((token, value))
return result
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)
result.append((processed_token, value))
return result
@classmethod
def _aggregate_weights(
cls, tokens: List[Tuple[str, List[int]]], weights: List[float]
) -> List[Tuple[str, float]]:
result = []
for token, idxs in tokens:
sum_weight = sum(weights[idx] for idx in idxs)
result.append((token, sum_weight))
return result
def _reconstruct_bpe(
self, bpe_tokens: Iterable[Tuple[int, str]]
) -> List[Tuple[str, List[int]]]:
result = []
acc = ""
acc_idx = []
continuing_subword_prefix = self.tokenizer.model.continuing_subword_prefix
continuing_subword_prefix_len = len(continuing_subword_prefix)
for idx, token in bpe_tokens:
if token in self.special_tokens:
continue
if token.startswith(continuing_subword_prefix):
acc += token[continuing_subword_prefix_len:]
acc_idx.append(idx)
else:
if acc:
result.append((acc, acc_idx))
acc_idx = []
acc = token
acc_idx.append(idx)
if acc:
result.append((acc, acc_idx))
return result
def _rescore_vector(self, vector: Dict[str, float]) -> Dict[int, float]:
"""
Orders all tokens in the vector by their importance and generates a new score based on the importance order.
So that the scoring doesn't depend on absolute values assigned by the model, but on the relative importance.
"""
new_vector = {}
for token, value in vector.items():
token_id = abs(mmh3.hash(token))
# Examples:
# Num 0: Log(1/1 + 1) = 0.6931471805599453
# Num 1: Log(1/2 + 1) = 0.4054651081081644
# Num 2: Log(1/3 + 1) = 0.28768207245178085
new_vector[token_id] = math.log(1.0 + value) ** self.alpha # value
return new_vector
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[SparseEmbedding]:
token_ids_batch = output.input_ids
# attention_value shape: (batch_size, num_heads, num_tokens, num_tokens)
pooled_attention = np.mean(output.model_output[:, :, 0], axis=1) * output.attention_mask
for document_token_ids, attention_value in zip(token_ids_batch, pooled_attention):
document_tokens_with_ids = (
(idx, self.invert_vocab[token_id])
for idx, token_id in enumerate(document_token_ids)
)
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
filtered = self._filter_pair_tokens(reconstructed)
stemmed = self._stem_pair_tokens(filtered)
weighted = self._aggregate_weights(stemmed, attention_value)
max_token_weight = {}
for token, weight in weighted:
max_token_weight[token] = max(max_token_weight.get(token, 0), weight)
rescored = self._rescore_vector(max_token_weight)
yield SparseEmbedding.from_dict(rescored)
@classmethod
def list_supported_models(cls) -> List[Dict[str, Any]]:
"""Lists the supported models.
Returns:
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]:
stopwords_path = model_dir / "stopwords.txt"
if not stopwords_path.exists():
return []
with open(stopwords_path, "r") as f:
return f.read().splitlines()
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[SparseEmbedding]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_documents(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
documents=documents,
batch_size=batch_size,
parallel=parallel,
alpha=self.alpha,
)
@classmethod
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]:
"""
To emulate BM25 behaviour, we don't need to use smart weights in the query, and
it's enough to just hash the tokens and assign a weight of 1.0 to them.
It is also faster, as we don't need to run the model for the query.
"""
if isinstance(query, str):
query = [query]
for text in query:
encoded = self.tokenizer.encode(text)
document_tokens_with_ids = enumerate(encoded.tokens)
reconstructed = self._reconstruct_bpe(document_tokens_with_ids)
filtered = self._filter_pair_tokens(reconstructed)
stemmed = self._stem_pair_tokens(filtered)
yield SparseEmbedding.from_dict(self._query_rehash(token for token, _ in stemmed))
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return Bm42TextEmbeddingWorker
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)
+87
View File
@@ -0,0 +1,87 @@
from dataclasses import dataclass
from typing import Dict, Iterable, Optional, Union
import numpy as np
from fastembed.common.model_management import ModelManagement
@dataclass
class SparseEmbedding:
values: np.ndarray
indices: np.ndarray
def as_object(self) -> Dict[str, np.ndarray]:
return {
"values": self.values,
"indices": self.indices,
}
def as_dict(self) -> Dict[int, float]:
return {i: v for i, v in zip(self.indices, self.values)}
@classmethod
def from_dict(cls, data: Dict[int, float]) -> "SparseEmbedding":
if len(data) == 0:
return cls(values=np.array([]), indices=np.array([]))
indices, values = zip(*data.items())
return cls(values=np.array(values), indices=np.array(indices))
class SparseTextEmbeddingBase(ModelManagement):
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
**kwargs,
):
self.model_name = model_name
self.cache_dir = cache_dir
self.threads = threads
self._local_files_only = kwargs.pop("local_files_only", False)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[SparseEmbedding]:
raise NotImplementedError()
def passage_embed(
self, texts: Iterable[str], **kwargs
) -> Iterable[SparseEmbedding]:
"""
Embeds a list of text passages into a list of embeddings.
Args:
texts (Iterable[str]): The list of texts to embed.
**kwargs: Additional keyword argument to pass to the embed method.
Yields:
Iterable[SparseEmbedding]: The sparse embeddings.
"""
# This is model-specific, so that different models can have specialized implementations
yield from self.embed(texts, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
) -> Iterable[SparseEmbedding]:
"""
Embeds queries
Args:
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
Returns:
Iterable[SparseEmbedding]: The sparse embeddings.
"""
# This is model-specific, so that different models can have specialized implementations
if isinstance(query, str):
yield from self.embed([query], **kwargs)
if isinstance(query, Iterable):
yield from self.embed(query, **kwargs)
+110
View File
@@ -0,0 +1,110 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
from fastembed.common import OnnxProvider
from fastembed.sparse.bm25 import Bm25
from fastembed.sparse.bm42 import Bm42
from fastembed.sparse.sparse_embedding_base import (
SparseEmbedding,
SparseTextEmbeddingBase,
)
from fastembed.sparse.splade_pp import SpladePP
class SparseTextEmbedding(SparseTextEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[SparseTextEmbeddingBase]] = [SpladePP, Bm42, Bm25]
@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": "prithvida/SPLADE_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"size_in_GB": 0.532,
"sources": {
"hf": "qdrant/SPLADE_PP_en_v1",
},
}
]
```
"""
result = []
for embedding in cls.EMBEDDINGS_REGISTRY:
result.extend(embedding.list_supported_models())
return result
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
super().__init__(model_name, cache_dir, threads, **kwargs)
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
if any(
model_name.lower() == model["model"].lower()
for model in supported_models
):
self.model = EMBEDDING_MODEL_TYPE(
model_name,
cache_dir,
threads=threads,
providers=providers,
**kwargs,
)
return
raise ValueError(
f"Model {model_name} is not supported in SparseTextEmbedding."
"Please check the supported models using `SparseTextEmbedding.list_supported_models()`"
)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[SparseEmbedding]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
) -> Iterable[SparseEmbedding]:
"""
Embeds queries
Args:
query (Union[str, Iterable[str]]): The query to embed, or an iterable e.g. list of queries.
Returns:
Iterable[SparseEmbedding]: The sparse embeddings.
"""
yield from self.model.query_embed(query, **kwargs)
+135
View File
@@ -0,0 +1,135 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
import numpy as np
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import define_cache_dir
from fastembed.sparse.sparse_embedding_base import (
SparseEmbedding,
SparseTextEmbeddingBase,
)
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
supported_splade_models = [
{
"model": "prithvida/Splade_PP_en_v1",
"vocab_size": 30522,
"description": "Misspelled version of the model. Retained for backward compatibility. Independent Implementation of SPLADE++ Model for English",
"size_in_GB": 0.532,
"sources": {
"hf": "Qdrant/SPLADE_PP_en_v1",
},
"model_file": "model.onnx",
},
{
"model": "prithivida/Splade_PP_en_v1",
"vocab_size": 30522,
"description": "Independent Implementation of SPLADE++ Model for English",
"size_in_GB": 0.532,
"sources": {
"hf": "Qdrant/SPLADE_PP_en_v1",
},
"model_file": "model.onnx",
},
]
class SpladePP(SparseTextEmbeddingBase, OnnxTextModel[SparseEmbedding]):
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[SparseEmbedding]:
relu_log = np.log(1 + np.maximum(output.model_output, 0))
weighted_log = relu_log * np.expand_dims(output.attention_mask, axis=-1)
scores = np.max(weighted_log, axis=1)
# Score matrix of shape (batch_size, vocab_size)
# Most of the values are 0, only a few are non-zero
for row_scores in scores:
indices = row_scores.nonzero()[0]
scores = row_scores[indices]
yield SparseEmbedding(values=scores, indices=indices)
@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_splade_models
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
"""
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.
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)
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,
)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[SparseEmbedding]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_documents(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
documents=documents,
batch_size=batch_size,
parallel=parallel,
)
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return SpladePPEmbeddingWorker
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)
+113
View File
@@ -0,0 +1,113 @@
# This code is a modified copy of the `NLTKWordTokenizer` class from `NLTK` library.
import re
from typing import List
class WordTokenizer:
"""The tokenizer is "destructive" such that the regexes applied will munge the
input string to a state beyond re-construction.
"""
# Starting quotes.
STARTING_QUOTES = [
(re.compile("([«“‘„]|[`]+)", re.U), r" \1 "),
(re.compile(r"^\""), r"``"),
(re.compile(r"(``)"), r" \1 "),
(re.compile(r"([ \(\[{<])(\"|\'{2})"), r"\1 `` "),
(re.compile(r"(?i)(\')(?!re|ve|ll|m|t|s|d|n)(\w)\b", re.U), r"\1 \2"),
]
# Ending quotes.
ENDING_QUOTES = [
(re.compile("([»”’])", re.U), r" \1 "),
(re.compile(r"''"), " '' "),
(re.compile(r'"'), " '' "),
(re.compile(r"([^' ])('[sS]|'[mM]|'[dD]|') "), r"\1 \2 "),
(re.compile(r"([^' ])('ll|'LL|'re|'RE|'ve|'VE|n't|N'T) "), r"\1 \2 "),
]
# Punctuation.
PUNCTUATION = [
(re.compile(r'([^\.])(\.)([\]\)}>"\'' "»”’ " r"]*)\s*$", re.U), r"\1 \2 \3 "),
(re.compile(r"([:,])([^\d])"), r" \1 \2"),
(re.compile(r"([:,])$"), r" \1 "),
(
re.compile(r"\.{2,}", re.U),
r" \g<0> ",
),
(re.compile(r"[;@#$%&]"), r" \g<0> "),
(
re.compile(r'([^\.])(\.)([\]\)}>"\']*)\s*$'),
r"\1 \2\3 ",
), # Handles the final period.
(re.compile(r"[?!]"), r" \g<0> "),
(re.compile(r"([^'])' "), r"\1 ' "),
(
re.compile(r"[*]", re.U),
r" \g<0> ",
),
]
# Pads parentheses
PARENS_BRACKETS = (re.compile(r"[\]\[\(\)\{\}\<\>]"), r" \g<0> ")
DOUBLE_DASHES = (re.compile(r"--"), r" -- ")
# List of contractions adapted from Robert MacIntyre's tokenizer.
CONTRACTIONS2 = [
re.compile(pattern)
for pattern in (
r"(?i)\b(can)(?#X)(not)\b",
r"(?i)\b(d)(?#X)('ye)\b",
r"(?i)\b(gim)(?#X)(me)\b",
r"(?i)\b(gon)(?#X)(na)\b",
r"(?i)\b(got)(?#X)(ta)\b",
r"(?i)\b(lem)(?#X)(me)\b",
r"(?i)\b(more)(?#X)('n)\b",
r"(?i)\b(wan)(?#X)(na)(?=\s)",
)
]
CONTRACTIONS3 = [
re.compile(pattern)
for pattern in (r"(?i) ('t)(?#X)(is)\b", r"(?i) ('t)(?#X)(was)\b")
]
@classmethod
def tokenize(cls, text: str) -> List[str]:
"""Return a tokenized copy of `text`.
>>> s = '''Good muffins cost $3.88 (roughly 3,36 euros)\nin New York.'''
>>> WordTokenizer().tokenize(s)
['Good', 'muffins', 'cost', '$', '3.88', '(', 'roughly', '3,36', 'euros', ')', 'in', 'New', 'York', '.']
Args:
text: The text to be tokenized.
Returns:
A list of tokens.
"""
for regexp, substitution in cls.STARTING_QUOTES:
text = regexp.sub(substitution, text)
for regexp, substitution in cls.PUNCTUATION:
text = regexp.sub(substitution, text)
# Handles parentheses.
regexp, substitution = cls.PARENS_BRACKETS
text = regexp.sub(substitution, text)
# Handles double dash.
regexp, substitution = cls.DOUBLE_DASHES
text = regexp.sub(substitution, text)
# add extra space to make things easier
text = " " + text + " "
for regexp, substitution in cls.ENDING_QUOTES:
text = regexp.sub(substitution, text)
for regexp in cls.CONTRACTIONS2:
text = regexp.sub(r" \1 \2 ", text)
for regexp in cls.CONTRACTIONS3:
text = regexp.sub(r" \1 \2 ", text)
return text.split()
+3
View File
@@ -0,0 +1,3 @@
from fastembed.text.text_embedding import TextEmbedding
__all__ = ["TextEmbedding"]
+49
View File
@@ -0,0 +1,49 @@
from typing import Any, Dict, Iterable, List, Type
import numpy as np
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.text.onnx_embedding import OnnxTextEmbedding, OnnxTextEmbeddingWorker
from fastembed.text.onnx_text_model import TextEmbeddingWorker
supported_clip_models = [
{
"model": "Qdrant/clip-ViT-B-32-text",
"dim": 512,
"description": "CLIP text encoder",
"size_in_GB": 0.25,
"sources": {
"hf": "Qdrant/clip-ViT-B-32-text",
},
"model_file": "model.onnx",
},
]
class CLIPOnnxEmbedding(OnnxTextEmbedding):
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return CLIPEmbeddingWorker
@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_clip_models
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
) -> OnnxTextEmbedding:
return CLIPOnnxEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
)
+64
View File
@@ -0,0 +1,64 @@
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": "Multilingual model, e5-large. Recommend using this model for non-English languages",
"size_in_GB": 2.24,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
"hf": "qdrant/multilingual-e5-large-onnx",
},
"model_file": "model.onnx",
"additional_files": ["model.onnx_data"],
},
{
"model": "sentence-transformers/paraphrase-multilingual-mpnet-base-v2",
"dim": 768,
"description": "Sentence-transformers model for tasks like clustering or semantic search",
"size_in_GB": 1.00,
"sources": {
"hf": "xenova/paraphrase-multilingual-mpnet-base-v2",
},
"model_file": "onnx/model.onnx",
},
]
class E5OnnxEmbedding(OnnxTextEmbedding):
@classmethod
def _get_worker_class(cls) -> Type["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
)
+263
View File
@@ -0,0 +1,263 @@
from typing import Any, Dict, Iterable, List, Optional, Sequence, Type, Union
import numpy as np
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import OnnxOutputContext
from fastembed.common.utils import define_cache_dir, normalize
from fastembed.text.onnx_text_model import OnnxTextModel, TextEmbeddingWorker
from fastembed.text.text_embedding_base import TextEmbeddingBase
supported_onnx_models = [
{
"model": "BAAI/bge-base-en",
"dim": 768,
"description": "Base English model",
"size_in_GB": 0.42,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en.tar.gz",
},
"model_file": "model_optimized.onnx",
},
{
"model": "BAAI/bge-base-en-v1.5",
"dim": 768,
"description": "Base English model, v1.5",
"size_in_GB": 0.21,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-base-en-v1.5.tar.gz",
"hf": "qdrant/bge-base-en-v1.5-onnx-q",
},
"model_file": "model_optimized.onnx",
},
{
"model": "BAAI/bge-large-en-v1.5",
"dim": 1024,
"description": "Large English model, v1.5",
"size_in_GB": 1.20,
"sources": {
"hf": "qdrant/bge-large-en-v1.5-onnx",
},
"model_file": "model.onnx",
},
{
"model": "BAAI/bge-small-en",
"dim": 384,
"description": "Fast English model",
"size_in_GB": 0.13,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/BAAI-bge-small-en.tar.gz",
},
"model_file": "model_optimized.onnx",
},
{
"model": "BAAI/bge-small-en-v1.5",
"dim": 384,
"description": "Fast and Default English model",
"size_in_GB": 0.067,
"sources": {
"hf": "qdrant/bge-small-en-v1.5-onnx-q",
},
"model_file": "model_optimized.onnx",
},
{
"model": "BAAI/bge-small-zh-v1.5",
"dim": 512,
"description": "Fast and recommended Chinese model",
"size_in_GB": 0.09,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/fast-bge-small-zh-v1.5.tar.gz",
},
"model_file": "model_optimized.onnx",
},
{
"model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
"dim": 384,
"description": "Sentence Transformer model, paraphrase-multilingual-MiniLM-L12-v2",
"size_in_GB": 0.22,
"sources": {
"hf": "qdrant/paraphrase-multilingual-MiniLM-L12-v2-onnx-Q",
},
"model_file": "model_optimized.onnx",
},
{
"model": "thenlper/gte-large",
"dim": 1024,
"description": "Large general text embeddings model",
"size_in_GB": 1.20,
"sources": {
"hf": "qdrant/gte-large-onnx",
},
"model_file": "model.onnx",
},
{
"model": "mixedbread-ai/mxbai-embed-large-v1",
"dim": 1024,
"description": "MixedBread Base sentence embedding model, does well on MTEB",
"size_in_GB": 0.64,
"sources": {
"hf": "mixedbread-ai/mxbai-embed-large-v1",
},
"model_file": "onnx/model.onnx",
},
{
"model": "snowflake/snowflake-arctic-embed-xs",
"dim": 384,
"description": "Based on all-MiniLM-L6-v2 model with only 22m parameters, ideal for latency/TCO budgets.",
"size_in_GB": 0.09,
"sources": {
"hf": "snowflake/snowflake-arctic-embed-xs",
},
"model_file": "onnx/model.onnx",
},
{
"model": "snowflake/snowflake-arctic-embed-s",
"dim": 384,
"description": "Based on infloat/e5-small-unsupervised, does not trade off retrieval accuracy for its small size.",
"size_in_GB": 0.13,
"sources": {
"hf": "snowflake/snowflake-arctic-embed-s",
},
"model_file": "onnx/model.onnx",
},
{
"model": "snowflake/snowflake-arctic-embed-m",
"dim": 768,
"description": "Based on intfloat/e5-base-unsupervised model, provides the best retrieval without slowing down inference.",
"size_in_GB": 0.43,
"sources": {
"hf": "Snowflake/snowflake-arctic-embed-m",
},
"model_file": "onnx/model.onnx",
},
{
"model": "snowflake/snowflake-arctic-embed-m-long",
"dim": 768,
"description": "Based on nomic-ai/nomic-embed-text-v1-unsupervised model, 8192 context-length model",
"size_in_GB": 0.54,
"sources": {
"hf": "snowflake/snowflake-arctic-embed-m-long",
},
"model_file": "onnx/model.onnx",
},
{
"model": "snowflake/snowflake-arctic-embed-l",
"dim": 1024,
"description": "Based on intfloat/e5-large-unsupervised, large model for most accurate retrieval.",
"size_in_GB": 1.02,
"sources": {
"hf": "snowflake/snowflake-arctic-embed-l",
},
"model_file": "onnx/model.onnx",
},
]
class OnnxTextEmbedding(TextEmbeddingBase, OnnxTextModel[np.ndarray]):
"""Implementation of the Flag Embedding model."""
@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 = "BAAI/bge-small-en-v1.5",
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
"""
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.
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)
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,
)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self._embed_documents(
model_name=self.model_name,
cache_dir=str(self.cache_dir),
documents=documents,
batch_size=batch_size,
parallel=parallel,
**kwargs,
)
@classmethod
def _get_worker_class(cls) -> Type["TextEmbeddingWorker"]:
return OnnxTextEmbeddingWorker
def _preprocess_onnx_input(
self, onnx_input: Dict[str, np.ndarray], **kwargs
) -> Dict[str, np.ndarray]:
"""
Preprocess the onnx input.
"""
return onnx_input
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
embeddings = output.model_output
return normalize(embeddings[:, 0]).astype(np.float32)
class OnnxTextEmbeddingWorker(TextEmbeddingWorker):
def init_embedding(
self,
model_name: str,
cache_dir: str,
**kwargs,
) -> OnnxTextEmbedding:
return OnnxTextEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
)
+126
View File
@@ -0,0 +1,126 @@
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
import numpy as np
from tokenizers import Encoding
from fastembed.common import OnnxProvider
from fastembed.common.onnx_model import EmbeddingWorker, OnnxModel, OnnxOutputContext, T
from fastembed.common.preprocessor_utils import load_tokenizer
from fastembed.common.utils import iter_batch
from fastembed.parallel_processor import ParallelWorkerPool
class OnnxTextModel(OnnxModel[T]):
ONNX_OUTPUT_NAMES: Optional[List[str]] = None
@classmethod
def _get_worker_class(cls) -> Type["TextEmbeddingWorker"]:
raise NotImplementedError("Subclasses must implement this method")
def _post_process_onnx_output(self, output: OnnxOutputContext) -> Iterable[T]:
raise NotImplementedError("Subclasses must implement this method")
def __init__(self) -> None:
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]:
"""
Preprocess the onnx input.
"""
return onnx_input
def load_onnx_model(
self,
model_dir: Path,
model_file: str,
threads: Optional[int],
providers: Optional[Sequence[OnnxProvider]] = None,
) -> None:
super().load_onnx_model(
model_dir=model_dir,
model_file=model_file,
threads=threads,
providers=providers,
)
self.tokenizer, self.special_token_to_id = load_tokenizer(model_dir=model_dir)
def tokenize(self, documents: List[str], **kwargs) -> List[Encoding]:
return self.tokenizer.encode_batch(documents)
def onnx_embed(
self,
documents: List[str],
**kwargs,
) -> OnnxOutputContext:
encoded = self.tokenize(documents, **kwargs)
input_ids = np.array([e.ids for e in encoded])
attention_mask = np.array([e.attention_mask for e in encoded])
input_names = {node.name for node in self.model.get_inputs()}
onnx_input = {
"input_ids": np.array(input_ids, dtype=np.int64),
}
if "attention_mask" in input_names:
onnx_input["attention_mask"] = np.array(attention_mask, dtype=np.int64)
if "token_type_ids" in input_names:
onnx_input["token_type_ids"] = np.array(
[np.zeros(len(e), dtype=np.int64) for e in input_ids], dtype=np.int64
)
onnx_input = self._preprocess_onnx_input(onnx_input, **kwargs)
model_output = self.model.run(self.ONNX_OUTPUT_NAMES, onnx_input)
return OnnxOutputContext(
model_output=model_output[0],
attention_mask=onnx_input.get("attention_mask", attention_mask),
input_ids=onnx_input.get("input_ids", input_ids),
)
def _embed_documents(
self,
model_name: str,
cache_dir: str,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[T]:
is_small = False
if isinstance(documents, str):
documents = [documents]
is_small = True
if isinstance(documents, list):
if len(documents) < batch_size:
is_small = True
if parallel == 0:
parallel = os.cpu_count()
if parallel is None or is_small:
for batch in iter_batch(documents, batch_size):
yield from self._post_process_onnx_output(self.onnx_embed(batch))
else:
start_method = (
"forkserver" if "forkserver" in get_all_start_methods() else "spawn"
)
params = {"model_name": model_name, "cache_dir": cache_dir, **kwargs}
pool = ParallelWorkerPool(
parallel, self._get_worker_class(), start_method=start_method
)
for batch in pool.ordered_map(iter_batch(documents, batch_size), **params):
yield from self._post_process_onnx_output(batch)
class TextEmbeddingWorker(EmbeddingWorker):
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
+87
View File
@@ -0,0 +1,87 @@
from typing import Any, Dict, Iterable, List, 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
supported_pooled_models = [
{
"model": "nomic-ai/nomic-embed-text-v1.5",
"dim": 768,
"description": "8192 context length english model",
"size_in_GB": 0.52,
"sources": {
"hf": "nomic-ai/nomic-embed-text-v1.5",
},
"model_file": "onnx/model.onnx",
},
{
"model": "nomic-ai/nomic-embed-text-v1.5-Q",
"dim": 768,
"description": "Quantized 8192 context length english model",
"size_in_GB": 0.13,
"sources": {
"hf": "nomic-ai/nomic-embed-text-v1.5",
},
"model_file": "onnx/model_quantized.onnx",
},
{
"model": "nomic-ai/nomic-embed-text-v1",
"dim": 768,
"description": "8192 context length english model",
"size_in_GB": 0.52,
"sources": {
"hf": "nomic-ai/nomic-embed-text-v1",
},
"model_file": "onnx/model.onnx",
},
]
class PooledEmbedding(OnnxTextEmbedding):
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return PooledEmbeddingWorker
@classmethod
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 = 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)
pooled_embeddings = sum_embeddings / np.maximum(sum_mask, 1e-9)
return pooled_embeddings
@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_pooled_models
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
embeddings = output.model_output
attn_mask = output.attention_mask
return self.mean_pooling(embeddings, attn_mask).astype(np.float32)
class PooledEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self, model_name: str, cache_dir: str, **kwargs
) -> OnnxTextEmbedding:
return PooledEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
)
@@ -0,0 +1,86 @@
from typing import Any, Dict, Iterable, List, 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
from fastembed.text.pooled_embedding import PooledEmbedding
supported_pooled_normalized_models = [
{
"model": "sentence-transformers/all-MiniLM-L6-v2",
"dim": 384,
"description": "Sentence Transformer model, MiniLM-L6-v2",
"size_in_GB": 0.09,
"sources": {
"url": "https://storage.googleapis.com/qdrant-fastembed/sentence-transformers-all-MiniLM-L6-v2.tar.gz",
"hf": "qdrant/all-MiniLM-L6-v2-onnx",
},
"model_file": "model.onnx",
},
{
"model": "jinaai/jina-embeddings-v2-base-en",
"dim": 768,
"description": "English embedding model supporting 8192 sequence length",
"size_in_GB": 0.52,
"sources": {"hf": "xenova/jina-embeddings-v2-base-en"},
"model_file": "onnx/model.onnx",
},
{
"model": "jinaai/jina-embeddings-v2-small-en",
"dim": 512,
"description": "English embedding model supporting 8192 sequence length",
"size_in_GB": 0.12,
"sources": {"hf": "xenova/jina-embeddings-v2-small-en"},
"model_file": "onnx/model.onnx",
},
{
"model": "jinaai/jina-embeddings-v2-base-de",
"dim": 768,
"description": "German embedding model supporting 8192 sequence length",
"size_in_GB": 0.32,
"sources": {"hf": "jinaai/jina-embeddings-v2-base-de"},
"model_file": "onnx/model_fp16.onnx",
},
{
"model": "jinaai/jina-embeddings-v2-base-code",
"dim": 768,
"description": "Source code embedding model supporting 8192 sequence length",
"size_in_GB": 0.64,
"sources": {"hf": "jinaai/jina-embeddings-v2-base-code"},
"model_file": "onnx/model.onnx",
},
]
class PooledNormalizedEmbedding(PooledEmbedding):
@classmethod
def _get_worker_class(cls) -> Type[TextEmbeddingWorker]:
return PooledNormalizedEmbeddingWorker
@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_pooled_normalized_models
def _post_process_onnx_output(
self, output: OnnxOutputContext
) -> Iterable[np.ndarray]:
embeddings = output.model_output
attn_mask = output.attention_mask
return normalize(self.mean_pooling(embeddings, attn_mask)).astype(np.float32)
class PooledNormalizedEmbeddingWorker(OnnxTextEmbeddingWorker):
def init_embedding(
self, model_name: str, cache_dir: str, **kwargs
) -> OnnxTextEmbedding:
return PooledNormalizedEmbedding(
model_name=model_name, cache_dir=cache_dir, threads=1, **kwargs
)
+104
View File
@@ -0,0 +1,104 @@
from typing import Any, Dict, Iterable, List, 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.onnx_embedding import OnnxTextEmbedding
from fastembed.text.text_embedding_base import TextEmbeddingBase
class TextEmbedding(TextEmbeddingBase):
EMBEDDINGS_REGISTRY: List[Type[TextEmbeddingBase]] = [
OnnxTextEmbedding,
E5OnnxEmbedding,
CLIPOnnxEmbedding,
PooledNormalizedEmbedding,
PooledEmbedding,
]
@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": "intfloat/multilingual-e5-large",
"dim": 1024,
"description": "Multilingual model, e5-large. Recommend using this model for non-English languages",
"size_in_GB": 2.24,
"sources": {
"gcp": "https://storage.googleapis.com/qdrant-fastembed/fast-multilingual-e5-large.tar.gz",
"hf": "qdrant/multilingual-e5-large-onnx",
}
}
]
```
"""
result = []
for embedding in cls.EMBEDDINGS_REGISTRY:
result.extend(embedding.list_supported_models())
return result
def __init__(
self,
model_name: str = "BAAI/bge-small-en-v1.5",
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
providers: Optional[Sequence[OnnxProvider]] = None,
**kwargs,
):
super().__init__(model_name, cache_dir, threads, **kwargs)
for EMBEDDING_MODEL_TYPE in self.EMBEDDINGS_REGISTRY:
supported_models = EMBEDDING_MODEL_TYPE.list_supported_models()
if any(
model_name.lower() == model["model"].lower()
for model in supported_models
):
self.model = EMBEDDING_MODEL_TYPE(
model_name,
cache_dir,
threads=threads,
providers=providers,
**kwargs,
)
return
raise ValueError(
f"Model {model_name} is not supported in TextEmbedding."
"Please check the supported models using `TextEmbedding.list_supported_models()`"
)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
"""
Encode a list of documents into list of embeddings.
We use mean pooling with attention so that the model can handle variable-length inputs.
Args:
documents: Iterator of documents or single document to embed
batch_size: Batch size for encoding -- higher values will use more memory, but be faster
parallel:
If > 1, data-parallel encoding will be used, recommended for offline encoding of large datasets.
If 0, use all available cores.
If None, don't use data-parallel processing, use default onnxruntime threading instead.
Returns:
List of embeddings, one per document
"""
yield from self.model.embed(documents, batch_size, parallel, **kwargs)
+62
View File
@@ -0,0 +1,62 @@
from typing import Iterable, Optional, Union
import numpy as np
from fastembed.common.model_management import ModelManagement
class TextEmbeddingBase(ModelManagement):
def __init__(
self,
model_name: str,
cache_dir: Optional[str] = None,
threads: Optional[int] = None,
**kwargs,
):
self.model_name = model_name
self.cache_dir = cache_dir
self.threads = threads
self._local_files_only = kwargs.pop("local_files_only", False)
def embed(
self,
documents: Union[str, Iterable[str]],
batch_size: int = 256,
parallel: Optional[int] = None,
**kwargs,
) -> Iterable[np.ndarray]:
raise NotImplementedError()
def passage_embed(self, texts: Iterable[str], **kwargs) -> 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[np.ndarray]: The embeddings.
"""
# This is model-specific, so that different models can have specialized implementations
yield from self.embed(texts, **kwargs)
def query_embed(
self, query: Union[str, Iterable[str]], **kwargs
) -> 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
if isinstance(query, str):
yield from self.embed([query], **kwargs)
if isinstance(query, Iterable):
yield from self.embed(query, **kwargs)
+3 -3
View File
@@ -2,7 +2,7 @@ site_name: FastEmbed
site_url: https://qdrant.github.io/fastembed/
site_author: Nirant Kasliwal
repo_url: https://github.com/qdrant/fastembed/
repo_name: qdrant/fastembed
repo_name: qdrant/fastembed
remote_branch: gh-pages
remote_name: origin
@@ -33,11 +33,11 @@ theme:
# Text color for primary color
text: "#ffffff"
palette:
palette:
# Palette toggle for light mode
- scheme: default
toggle:
icon: material/brightness-7
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
Generated
-3069
View File
File diff suppressed because it is too large Load Diff
+31 -22
View File
@@ -1,8 +1,8 @@
[tool.poetry]
name = "fastembed"
version = "0.0.5"
version = "0.3.4"
description = "Fast, light, accurate library built for retrieval embedding generation"
authors = ["NirantK <nirant.bits@gmail.com>"]
authors = ["Qdrant Team <info@qdrant.tech>", "NirantK <nirant.bits@gmail.com>"]
license = "Apache License"
readme = "README.md"
packages = [{include = "fastembed"}]
@@ -11,34 +11,43 @@ repository = "https://github.com/qdrant/fastembed"
keywords = ["vector", "embedding", "neural", "search", "qdrant", "sentence-transformers"]
[tool.poetry.dependencies]
python = ">=3.8.0,<3.12"
onnx = "^1.11"
onnxruntime = "^1.15"
tqdm = "^4.65"
python = ">=3.8.0,<3.13"
onnx = "^1.15.0"
onnxruntime = "^1.17.0"
tqdm = "^4.66"
requests = "^2.31"
tokenizers = "^0.13"
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"
mmh3 = "^4.0"
PyStemmer = { version = "^2.2.0", optional = true }
[tool.poetry.dev-dependencies]
ruff = "^0.0.277"
isort = "^5.12.0"
black = "^23.7.0"
[tool.poetry.extras]
pystemmer = ["PyStemmer"]
[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
ruff = ">=0.3.1,<1.0"
notebook = ">=7.0.2"
mkdocs-material = "^9.1.21"
mkdocstrings = "^0.22.0"
pillow = "^10.0.0"
pre-commit = {version = "^3.6.2", python = ">=3.9,<3.12" }
[tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.5.10"
mkdocstrings = "^0.24.0"
pillow = "^10.2.0"
cairosvg = "^2.7.1"
mknotebooks = "^0.8.0"
pytest = "^7.4.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
[tool.isort]
profile = "black"
[tool.ruff]
line-length = 120
line-length = 99
+4
View File
@@ -0,0 +1,4 @@
from pathlib import Path
TEST_DIR = Path(__file__).parent
TEST_MISC_DIR = TEST_DIR / "misc"
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+27 -22
View File
@@ -1,27 +1,25 @@
# %% [markdown]
# # 🤗 Huggingface vs ⚡ FastEmbed
#
#
# Comparing the performance of Huggingface's 🤗 Transformers and ⚡ FastEmbed on a simple task on the following machine: Apple M2 Max, 32 GB RAM
#
#
# ## 📦 Imports
#
#
# Importing the necessary libraries for this comparison.
# %%
import time
from pathlib import Path
from typing import Any, Callable, List, Tuple
from typing import Callable, List, Tuple
import numpy as np
import matplotlib.pyplot as plt
import torch.nn.functional as F
from torch import Tensor
from transformers import AutoModel, AutoTokenizer
from fastembed.embedding import DefaultEmbedding
# %% [markdown]
# ## 📖 Data
#
#
# data is a list of strings, each string is a document.
# %%
@@ -43,9 +41,10 @@ len(documents)
# %% [markdown]
# ## Setting up 🤗 Huggingface
#
#
# We'll be using the [Huggingface Transformers](https://huggingface.co/transformers/) with PyTorch library to generate embeddings. We'll be using the same model across both libraries for a fair(er?) comparison.
# %%
class HF:
"""
@@ -58,18 +57,21 @@ class HF:
self.tokenizer = AutoTokenizer.from_pretrained(model_id)
def embed(self, texts: List[str]):
encoded_input = self.tokenizer(texts, max_length=512, padding=True, truncation=True, return_tensors="pt")
encoded_input = self.tokenizer(
texts, max_length=512, padding=True, truncation=True, return_tensors="pt"
)
model_output = self.model(**encoded_input)
sentence_embeddings = model_output[0][:, 0]
sentence_embeddings = F.normalize(sentence_embeddings)
return sentence_embeddings
hf = HF(model_id="BAAI/bge-small-en")
hf.embed(documents).shape
# %% [markdown]
# ## Setting up ⚡️FastEmbed
#
#
# Sorry, don't have a lot to set up here. We'll be using the default model, which is Flag Embedding, same as the Huggingface model.
# %%
@@ -77,18 +79,21 @@ embedding_model = DefaultEmbedding()
# %% [markdown]
# ## 📊 Comparison
#
#
# We'll be comparing the following metrics: Minimum, Maximum, Mean, across k runs. Let's write a function to do that:
#
#
# ### 🚀 Calculating Stats
# %%
def calculate_time_stats(embed_func: Callable, documents: list, k: int) -> Tuple[float, float, float]:
def calculate_time_stats(
embed_func: Callable, documents: list, k: int
) -> Tuple[float, float, float]:
times = []
for _ in range(k):
# Timing the embed_func call
start_time = time.time()
embeddings = embed_func(documents)
embed_func(documents)
end_time = time.time()
times.append(end_time - start_time)
@@ -96,19 +101,21 @@ def calculate_time_stats(embed_func: Callable, documents: list, k: int) -> Tuple
# Returning mean, max, and min time for the call
return (sum(times) / k, max(times), min(times))
# %%
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}")
# %%
import matplotlib.pyplot as plt
# %%
def plot_character_per_second_comparison(
hf_stats: Tuple[float, float, float], fst_stats: Tuple[float, float, float], documents: list
hf_stats: Tuple[float, float, float],
fst_stats: Tuple[float, float, float],
documents: list,
):
# Calculating total characters in documents
total_characters = sum(len(doc) for doc in documents)
@@ -141,5 +148,3 @@ def plot_character_per_second_comparison(
plot_character_per_second_comparison(hf_stats, fst_stats, documents)
+87
View File
@@ -0,0 +1,87 @@
import numpy as np
import pytest
from fastembed import SparseTextEmbedding
@pytest.mark.parametrize(
"model_name", ["Qdrant/bm42-all-minilm-l6-v2-attentions", "Qdrant/bm25"]
)
def test_attention_embeddings(model_name):
model = SparseTextEmbedding(model_name=model_name)
output = list(
model.query_embed(
[
"I must not fear. Fear is the mind-killer.",
]
)
)
assert len(output) == 1
for result in output:
assert len(result.indices) == len(result.values)
assert np.allclose(result.values, np.ones(len(result.values)))
quotes = [
"I must not fear. Fear is the mind-killer.",
"All animals are equal, but some animals are more equal than others.",
"It was a pleasure to burn.",
"The sky above the port was the color of television, tuned to a dead channel.",
"In the beginning, the universe was created."
" This has made a lot of people very angry and been widely regarded as a bad move.",
"It's a truth universally acknowledged that a zombie in possession of brains must be in want of more brains.",
"War is peace. Freedom is slavery. Ignorance is strength.",
"We're not in Infinity; we're in the suburbs.",
"I was a thousand times more evil than thou!",
"History is merely a list of surprises... It can only prepare us to be surprised yet again.",
".", # Empty string
]
output = list(model.embed(quotes))
assert len(output) == len(quotes)
for result in output[:-1]:
assert len(result.indices) == len(result.values)
assert len(result.indices) > 0
assert len(output[-1].indices) == 0
# Test support for unknown languages
output = list(
model.query_embed(
[
"привет мир!",
]
)
)
assert len(output) == 1
for result in output:
assert len(result.indices) == len(result.values)
assert len(result.indices) == 2
@pytest.mark.parametrize(
"model_name", ["Qdrant/bm42-all-minilm-l6-v2-attentions", "Qdrant/bm25"]
)
def test_parallel_processing(model_name):
model = SparseTextEmbedding(model_name=model_name)
docs = ["hello world", "attention embedding"] * 100
embeddings = list(model.embed(docs, batch_size=10, parallel=2))
embeddings_2 = list(model.embed(docs, batch_size=10, parallel=None))
embeddings_3 = list(model.embed(docs, batch_size=10, parallel=0))
assert len(embeddings) == len(docs)
for emb_1, emb_2, emb_3 in zip(embeddings, embeddings_2, embeddings_3):
assert np.allclose(emb_1.indices, emb_2.indices)
assert np.allclose(emb_1.indices, emb_3.indices)
assert np.allclose(emb_1.values, emb_2.values)
assert np.allclose(emb_1.values, emb_3.values)
+95
View File
@@ -0,0 +1,95 @@
import os
from io import BytesIO
import numpy as np
import pytest
import requests
from PIL import Image
from fastembed import ImageEmbedding
from tests.config import TEST_MISC_DIR
CANONICAL_VECTOR_VALUES = {
"Qdrant/clip-ViT-B-32-vision": np.array([-0.0098, 0.0128, -0.0274, 0.002, -0.0059]),
"Qdrant/resnet50-onnx": np.array(
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.01046245, 0.01171397, 0.00705971, 0.0]
),
"Qdrant/Unicom-ViT-B-16": np.array(
[0.0170, -0.0361, 0.0125, -0.0428, -0.0232, 0.0232, -0.0602, -0.0333, 0.0155, 0.0497]
),
"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]
),
}
def test_embedding():
is_ci = os.getenv("CI")
for model_desc in ImageEmbedding.list_supported_models():
if not is_ci and model_desc["size_in_GB"] > 1:
continue
dim = model_desc["dim"]
model = ImageEmbedding(model_name=model_desc["model"])
images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
Image.open((TEST_MISC_DIR / "small_image.jpeg")),
Image.open(BytesIO(requests.get("https://qdrant.tech/img/logo.png").content)),
]
embeddings = list(model.embed(images))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(images), dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_desc["model"]]
assert np.allclose(
embeddings[0, : canonical_vector.shape[0]], canonical_vector, atol=1e-3
), model_desc["model"]
assert np.allclose(embeddings[1], embeddings[2]), model_desc["model"]
@pytest.mark.parametrize("n_dims,model_name", [(512, "Qdrant/clip-ViT-B-32-vision")])
def test_batch_embedding(n_dims, model_name):
model = ImageEmbedding(model_name=model_name)
n_images = 32
test_images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
Image.open(TEST_MISC_DIR / "small_image.jpeg"),
]
images = test_images * n_images
embeddings = list(model.embed(images, batch_size=10))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (len(test_images) * n_images, n_dims)
@pytest.mark.parametrize("n_dims,model_name", [(512, "Qdrant/clip-ViT-B-32-vision")])
def test_parallel_processing(n_dims, model_name):
model = ImageEmbedding(model_name=model_name)
n_images = 32
test_images = [
TEST_MISC_DIR / "image.jpeg",
str(TEST_MISC_DIR / "small_image.jpeg"),
Image.open(TEST_MISC_DIR / "small_image.jpeg"),
]
images = test_images * n_images
embeddings = list(model.embed(images, batch_size=10, parallel=2))
embeddings = np.stack(embeddings, axis=0)
embeddings_2 = list(model.embed(images, batch_size=10, parallel=None))
embeddings_2 = np.stack(embeddings_2, axis=0)
embeddings_3 = list(model.embed(images, batch_size=10, parallel=0))
embeddings_3 = np.stack(embeddings_3, axis=0)
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)
+112
View File
@@ -0,0 +1,112 @@
import numpy as np
from fastembed.late_interaction.late_interaction_text_embedding import (
LateInteractionTextEmbedding,
)
# vectors are abridged and rounded for brevity
CANONICAL_COLUMN_VALUES = {
"colbert-ir/colbertv2.0": np.array(
[
[0.0759, 0.0841, -0.0299, 0.0374, 0.0254],
[0.0005, -0.0163, -0.0127, 0.2165, 0.1517],
[-0.0257, -0.0575, 0.0135, 0.2202, 0.1896],
[0.0846, 0.0122, 0.0032, -0.0109, -0.1041],
[0.0477, 0.1078, -0.0314, 0.016, 0.0156],
]
)
}
CANONICAL_QUERY_VALUES = {
"colbert-ir/colbertv2.0": np.array(
[
[0.0824, 0.0872, -0.0324, 0.0418, 0.024],
[-0.0007, -0.0154, -0.0113, 0.2277, 0.1528],
[-0.0251, -0.0565, 0.0136, 0.2236, 0.1838],
[0.0848, 0.0056, 0.0041, -0.0036, -0.1032],
[0.0574, 0.1072, -0.0332, 0.0233, 0.0209],
[0.1041, 0.0364, -0.0058, -0.027, -0.0704],
[0.106, 0.0371, -0.0055, -0.0339, -0.0719],
[0.1063, 0.0363, 0.0014, -0.0334, -0.0698],
[0.112, 0.036, 0.0026, -0.0355, -0.0675],
[0.1184, 0.0441, 0.0166, -0.0169, -0.0244],
[0.1033, 0.035, 0.0183, 0.0475, 0.0612],
[-0.0028, -0.014, -0.016, 0.2175, 0.1537],
[0.0547, 0.0219, -0.007, 0.1748, 0.1154],
[-0.001, -0.0184, -0.0112, 0.2197, 0.1523],
[-0.0012, -0.0149, -0.0119, 0.2147, 0.152],
[-0.0186, -0.0239, -0.014, 0.2196, 0.156],
[-0.017, -0.0232, -0.0108, 0.2212, 0.157],
[-0.0109, -0.0024, -0.003, 0.1972, 0.1391],
[0.0898, 0.0219, -0.0255, 0.0734, -0.0096],
[0.1143, 0.015, -0.022, 0.0417, -0.0421],
[0.1056, 0.0091, -0.0137, 0.0129, -0.0619],
[0.0234, 0.004, -0.0285, 0.1565, 0.0883],
[-0.0037, -0.0079, -0.0204, 0.1982, 0.1502],
[0.0988, 0.0377, 0.0226, 0.0309, 0.0508],
[-0.0103, -0.0128, -0.0035, 0.2114, 0.155],
[-0.0103, -0.0184, -0.011, 0.2252, 0.157],
[-0.0033, -0.0292, -0.0097, 0.2237, 0.1607],
[-0.0198, -0.0257, -0.0193, 0.2265, 0.165],
[-0.0227, -0.0028, -0.0084, 0.1995, 0.1306],
[0.0916, 0.0185, -0.0186, 0.0173, -0.0577],
[0.1022, 0.0228, -0.0174, -0.0102, -0.065],
[0.1043, 0.0231, -0.0144, -0.0246, -0.067],
]
)
}
docs = ["Hello World"]
def test_batch_embedding():
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)
result = list(model.embed(docs_to_embed, batch_size=6))
for value in result:
token_num, abridged_dim = expected_result.shape
assert np.allclose(value[:, :abridged_dim], expected_result, atol=10e-4)
def test_single_embedding():
docs_to_embed = docs
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
print("evaluating", model_name)
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)
def test_single_embedding_query():
queries_to_embed = docs
for model_name, expected_result in CANONICAL_QUERY_VALUES.items():
print("evaluating", model_name)
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)
def test_parallel_processing():
model = LateInteractionTextEmbedding(model_name="colbert-ir/colbertv2.0")
token_dim = 128
docs = ["hello world", "flag embedding"] * 100
embeddings = list(model.embed(docs, batch_size=10, parallel=2))
embeddings = np.stack(embeddings, axis=0)
embeddings_2 = list(model.embed(docs, batch_size=10, parallel=None))
embeddings_2 = np.stack(embeddings_2, axis=0)
embeddings_3 = list(model.embed(docs, batch_size=10, parallel=0))
embeddings_3 = np.stack(embeddings_3, axis=0)
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)
-35
View File
@@ -1,35 +0,0 @@
import numpy as np
from fastembed.embedding import DefaultEmbedding, Embedding
CANONICAL_VECTOR_VALUES = {
"BAAI/bge-small-en": np.array([-0.0232, -0.0255, 0.0174, -0.0639, -0.0006]),
"BAAI/bge-base-en": np.array([0.0115, 0.0372, 0.0295, 0.0121, 0.0346]),
"sentence-transformers/all-MiniLM-L6-v2": np.array([0.0259, 0.0058, 0.0114, 0.0380, -0.0233]),
"intfloat/multilingual-e5-large": np.array([0.0098, 0.0045, 0.0066, -0.0354, 0.0070]),
}
def test_default_embedding():
for model_desc in Embedding.list_supported_models():
dim = model_desc["dim"]
model = DefaultEmbedding(model_name=model_desc["model"])
docs = ["hello world", "flag embedding"]
embeddings = list(model.embed(docs))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (2, dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_desc["model"]]
assert np.allclose(embeddings[0, :canonical_vector.shape[0]], canonical_vector, atol=1e-3)
def test_batch_embedding():
model = DefaultEmbedding()
docs = ["hello world", "flag embedding"] * 100
embeddings = list(model.embed(docs, batch_size=10))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (200, 384)
+101
View File
@@ -0,0 +1,101 @@
import pytest
from fastembed.sparse.sparse_text_embedding import SparseTextEmbedding
CANONICAL_COLUMN_VALUES = {
"prithvida/Splade_PP_en_v1": {
"indices": [
2040,
2047,
2088,
2299,
2748,
3011,
3376,
3795,
4774,
5304,
5798,
6160,
7592,
7632,
8484,
],
"values": [
0.4219532012939453,
0.4320072531700134,
2.766580104827881,
0.3314574658870697,
1.395172119140625,
0.021595917642116547,
0.43770670890808105,
0.0008370947907678783,
0.5187209844589233,
0.17124654352664948,
0.14742016792297363,
0.8142819404602051,
2.803262710571289,
2.1904349327087402,
1.0531445741653442,
],
}
}
docs = ["Hello World"]
def test_batch_embedding():
docs_to_embed = docs * 10
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
model = SparseTextEmbedding(model_name=model_name)
result = next(iter(model.embed(docs_to_embed, batch_size=6)))
assert result.indices.tolist() == expected_result["indices"]
for i, value in enumerate(result.values):
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
def test_single_embedding():
for model_name, expected_result in CANONICAL_COLUMN_VALUES.items():
model = SparseTextEmbedding(model_name=model_name)
passage_result = next(iter(model.embed(docs, batch_size=6)))
query_result = next(iter(model.query_embed(docs)))
for result in [passage_result, query_result]:
assert result.indices.tolist() == expected_result["indices"]
for i, value in enumerate(result.values):
assert pytest.approx(value, abs=0.001) == expected_result["values"][i]
def test_parallel_processing():
import numpy as np
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))
sparse_embeddings_all = list(model.embed(docs, batch_size=10, parallel=0))
sparse_embeddings = list(model.embed(docs, batch_size=10, parallel=None))
assert (
len(sparse_embeddings)
== len(sparse_embeddings_duo)
== len(sparse_embeddings_all)
== len(docs)
)
for sparse_embedding, sparse_embedding_duo, sparse_embedding_all in zip(
sparse_embeddings, sparse_embeddings_duo, sparse_embeddings_all
):
assert (
sparse_embedding.indices.tolist()
== 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
)
+142
View File
@@ -0,0 +1,142 @@
import os
import numpy as np
import pytest
from fastembed.text.text_embedding import TextEmbedding
CANONICAL_VECTOR_VALUES = {
"BAAI/bge-small-en": np.array([-0.0232, -0.0255, 0.0174, -0.0639, -0.0006]),
"BAAI/bge-small-en-v1.5": np.array(
[0.01522374, -0.02271799, 0.00860278, -0.07424029, 0.00386434]
),
"BAAI/bge-small-en-v1.5-quantized": np.array(
[0.01522374, -0.02271799, 0.00860278, -0.07424029, 0.00386434]
),
"BAAI/bge-small-zh-v1.5": np.array(
[-0.01023294, 0.07634465, 0.0691722, -0.04458365, -0.03160762]
),
"BAAI/bge-base-en": np.array([0.0115, 0.0372, 0.0295, 0.0121, 0.0346]),
"BAAI/bge-base-en-v1.5": np.array(
[0.01129394, 0.05493144, 0.02615099, 0.00328772, 0.02996045]
),
"BAAI/bge-large-en-v1.5": np.array(
[0.03434538, 0.03316108, 0.02191251, -0.03713358, -0.01577825]
),
"BAAI/bge-large-en-v1.5-quantized": np.array(
[0.03434538, 0.03316108, 0.02191251, -0.03713358, -0.01577825]
),
"sentence-transformers/all-MiniLM-L6-v2": np.array(
[-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]
),
"intfloat/multilingual-e5-large": np.array(
[0.0098, 0.0045, 0.0066, -0.0354, 0.0070]
),
"sentence-transformers/paraphrase-multilingual-mpnet-base-v2": np.array(
[-0.01341097, 0.0416553, -0.00480805, 0.02844842, 0.0505299]
),
"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]
),
"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]
),
"thenlper/gte-large": np.array(
[-0.01920587, 0.00113156, -0.00708992, -0.00632304, -0.04025577]
),
"mixedbread-ai/mxbai-embed-large-v1": np.array(
[0.02295546, 0.03196154, 0.016512, -0.04031524, -0.0219634]
),
"snowflake/snowflake-arctic-embed-xs": np.array(
[0.0092, 0.0619, 0.0196, 0.009, -0.0114]
),
"snowflake/snowflake-arctic-embed-s": np.array(
[-0.0416, -0.0867, 0.0209, 0.0554, -0.0272]
),
"snowflake/snowflake-arctic-embed-m": np.array(
[-0.0329, 0.0364, 0.0481, 0.0016, 0.0328]
),
"snowflake/snowflake-arctic-embed-m-long": np.array(
[0.0080, -0.0266, -0.0335, 0.0282, 0.0143]
),
"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]),
}
def test_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
dim = model_desc["dim"]
model = TextEmbedding(model_name=model_desc["model"])
docs = ["hello world", "flag embedding"]
embeddings = list(model.embed(docs))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (2, dim)
canonical_vector = CANONICAL_VECTOR_VALUES[model_desc["model"]]
assert np.allclose(
embeddings[0, : canonical_vector.shape[0]], canonical_vector, atol=1e-3
), model_desc["model"]
@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):
model = TextEmbedding(model_name=model_name)
docs = ["hello world", "flag embedding"] * 100
embeddings = list(model.embed(docs, batch_size=10))
embeddings = np.stack(embeddings, axis=0)
assert embeddings.shape == (200, n_dims)
@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):
model = TextEmbedding(model_name=model_name)
docs = ["hello world", "flag embedding"] * 100
embeddings = list(model.embed(docs, batch_size=10, parallel=2))
embeddings = np.stack(embeddings, axis=0)
embeddings_2 = list(model.embed(docs, batch_size=10, parallel=None))
embeddings_2 = np.stack(embeddings_2, axis=0)
embeddings_3 = list(model.embed(docs, batch_size=10, parallel=0))
embeddings_3 = np.stack(embeddings_3, axis=0)
assert embeddings.shape == (200, n_dims)
assert np.allclose(embeddings, embeddings_2, atol=1e-3)
assert np.allclose(embeddings, embeddings_3, atol=1e-3)