-
Notifications
You must be signed in to change notification settings - Fork 231
Feat/persian bm25 support #684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
| import pytest | ||
| import numpy as np | ||
|
|
||
| from fastembed.sparse.bm25 import Bm25 | ||
| from fastembed.sparse.bm25 import Bm25, normalize_persian | ||
| from fastembed.sparse.sparse_text_embedding import SparseTextEmbedding | ||
| from tests.utils import delete_model_cache, should_test_model | ||
|
|
||
|
|
@@ -157,6 +157,31 @@ def get_model(model_name: str): | |
| docs = ["Hello World"] | ||
|
|
||
|
|
||
| @pytest.fixture() | ||
| def local_bm25_model_dir(tmp_path): | ||
| (tmp_path / "mock.file").write_text("", encoding="utf-8") | ||
|
|
||
| (tmp_path / "persian.txt").write_text( | ||
| """این | ||
| است | ||
| می | ||
| به | ||
| از | ||
| که | ||
| برای | ||
| با | ||
| در | ||
| را | ||
| و | ||
| اما | ||
| یک | ||
| """, | ||
| encoding="utf-8", | ||
| ) | ||
|
|
||
| return tmp_path | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "model_name", | ||
| ["prithivida/Splade_PP_en_v1", "Qdrant/minicoil-v1"], | ||
|
|
@@ -311,6 +336,127 @@ def test_disable_stemmer_behavior(disable_stemmer: bool) -> None: | |
| assert result == expected, f"Expected {expected}, but got {result}" | ||
|
|
||
|
|
||
| def test_persian_normalization() -> None: | ||
| assert normalize_persian("\u0639\u0644\u064a") == "\u0639\u0644\u06cc" | ||
| assert normalize_persian("\u0643\u0631\u064a\u0645") == "\u06a9\u0631\u06cc\u0645" | ||
|
|
||
|
|
||
| def test_bm25_persian_resource_is_requested() -> None: | ||
| model_description = Bm25._get_model_description("Qdrant/bm25") | ||
|
|
||
| assert "persian.txt" in model_description.additional_files | ||
|
|
||
|
|
||
| def test_bm25_accepts_persian_without_snowball_stemmer(local_bm25_model_dir) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
|
|
||
| assert model.stemmer is None | ||
| assert {"این", "است", "می"} <= model.stopwords | ||
|
|
||
|
|
||
| def test_bm25_persian_stopwords_are_loaded(local_bm25_model_dir) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
|
|
||
| assert model._stem(["این", "یک", "متن"]) == [ | ||
| "متن", | ||
| ] | ||
|
|
||
| assert model._stem(["کریم", "متن"]) == [ | ||
| "کریم", | ||
| "متن", | ||
| ] | ||
|
|
||
|
|
||
| def test_bm25_persian_embedding_works(local_bm25_model_dir) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
|
|
||
| embedding = next( | ||
| iter( | ||
| model.embed( | ||
| [ | ||
| "\u0627\u06cc\u0646 \u06cc\u06a9 \u0645\u062a\u0646 " | ||
| "\u0641\u0627\u0631\u0633\u06cc \u0627\u0633\u062a" | ||
| ] | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| assert len(embedding.indices) > 0 | ||
| assert len(embedding.indices) == len(embedding.values) | ||
|
|
||
|
|
||
| def test_bm25_persian_normalization_keeps_query_and_document_consistent( | ||
| local_bm25_model_dir, | ||
| ) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
|
|
||
| document_embedding = next( | ||
| iter( | ||
| model.embed( | ||
| ["\u062f\u0627\u0646\u0634\u06af\u0627\u0647 \u062a\u0647\u0631\u0627\u0646"] | ||
| ) | ||
| ) | ||
| ) | ||
| query_embedding = next(iter(model.query_embed(["\u062f\u0627\u0646\u0634\u06af\u0627\u0647"]))) | ||
|
|
||
| assert set(document_embedding.indices).intersection(query_embedding.indices) | ||
|
|
||
|
|
||
| def test_bm25_persian_normalizes_arabic_yeh_and_kaf_to_same_sparse_dimension( | ||
| local_bm25_model_dir, | ||
| ) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
|
|
||
| arabic_forms = next(iter(model.query_embed(["\u0643\u064a\u0627\u0646"]))) | ||
| persian_forms = next(iter(model.query_embed(["\u06a9\u06cc\u0627\u0646"]))) | ||
|
|
||
| assert arabic_forms.indices.tolist() == persian_forms.indices.tolist() | ||
| assert len(arabic_forms.indices) == 1 | ||
|
|
||
|
|
||
| def test_bm25_persian_parallel_embedding(local_bm25_model_dir) -> None: | ||
| model = Bm25( | ||
| "Qdrant/bm25", | ||
| language="persian", | ||
| specific_model_path=str(local_bm25_model_dir), | ||
| ) | ||
| documents = [ | ||
| "\u062f\u0627\u0646\u0634\u06af\u0627\u0647 \u062a\u0647\u0631\u0627\u0646", | ||
| "\u0627\u06cc\u0646 \u0645\u062a\u0646 \u0641\u0627\u0631\u0633\u06cc \u0627\u0633\u062a", | ||
| ] * 3 | ||
|
|
||
| parallel_embeddings = list(model.embed(documents, batch_size=1, parallel=2)) | ||
| single_process_embeddings = list(model.embed(documents, batch_size=1, parallel=None)) | ||
|
|
||
| assert len(parallel_embeddings) == len(single_process_embeddings) == len(documents) | ||
| for parallel_embedding, single_process_embedding in zip( | ||
| parallel_embeddings, | ||
| single_process_embeddings, | ||
| ): | ||
|
Comment on lines
+452
to
+455
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
fd -HI -t f \
-g 'pyproject.toml' -g 'setup.cfg' -g 'tox.ini' \
-g '.python-version' -g '.tool-versions' . \
-x sh -c '
echo "== $1 =="
rg -n "requires-python|target-version|python_version|python-version" "$1" || true
' sh {}Repository: qdrant/fastembed Length of output: 410 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '== repository configuration files =='
git ls-files | rg '(^|/)(pyproject\.toml|setup\.cfg|tox\.ini|\.python-version|\.tool-versions|Pipfile|setup\.py)$' || true
printf '%s\n' '== Python target declarations =='
for f in pyproject.toml setup.cfg tox.ini .python-version .tool-versions setup.py; do
if [ -f "$f" ]; then
echo "== $f =="
rg -n -C 2 'requires-python|target-version|python_version|python-version|classifiers|Programming Language :: Python' "$f" || true
fi
done
printf '%s\n' '== relevant test lines =='
test_file='tests/test_sparse_embeddings.py'
if [ -f "$test_file" ]; then
sed -n '440,462p' "$test_file"
fi
printf '%s\n' '== Ruff B905 references =='
rg -n -C 2 'B905|strict\s*=' . --glob '*.toml' --glob '*.cfg' --glob '*.ini' --glob '*.py' || trueRepository: qdrant/fastembed Length of output: 1309 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '== pyproject.toml =='
cat -n pyproject.toml
printf '%s\n' '== Python-version references in tracked configuration and documentation =='
rg -n -i -C 2 \
'python( |-|_)version|requires-python|python_requires|Programming Language :: Python|setup-python|python:' \
. \
--glob '!*.lock' \
--glob '!*.json' \
--glob '!*.ipynb' \
--glob '!*.mdx' || trueRepository: qdrant/fastembed Length of output: 3432 Add
🧰 Tools🪛 Ruff (0.16.1)[warning] 452-455: Add explicit value for parameter (B905) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| assert np.array_equal(parallel_embedding.indices, single_process_embedding.indices) | ||
| assert np.allclose(parallel_embedding.values, single_process_embedding.values) | ||
|
|
||
|
|
||
| def test_if_splade_query_embed_is_inference_free() -> None: | ||
| is_ci = os.getenv("CI") | ||
| model = SparseTextEmbedding( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: qdrant/fastembed
Length of output: 154
🏁 Script executed:
Repository: qdrant/fastembed
Length of output: 146
🏁 Script executed:
Repository: qdrant/fastembed
Length of output: 1457
Add
persian.txtto the publishedQdrant/bm25artifact.The resource is currently missing, so Persian model initialization will fail when it attempts to download the required stopword file.
🤖 Prompt for AI Agents