Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions fastembed/common/model_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@

class ModelManagement(Generic[T]):
METADATA_FILE = "files_metadata.json"
# Placeholder ``model_file`` for models that have no real ONNX weight file
# (e.g. ``Qdrant/bm25``); such a file is never present on disk and must not be
# required by the offline cache probe.
MOCK_MODEL_FILE = "mock.file"

@classmethod
def list_supported_models(cls) -> list[dict[str, Any]]:
Expand Down Expand Up @@ -421,9 +425,10 @@ def download_model(cls, model: T, cache_dir: str, retries: int = 3, **kwargs: An
**cache_kwargs,
)
)
if (resolved_path / model.model_file).exists() and all(
(resolved_path / file).exists() for file in extra_patterns
):
required_files = [
file for file in extra_patterns if file != cls.MOCK_MODEL_FILE
]
if all((resolved_path / file).exists() for file in required_files):
return resolved_path
except Exception:
pass
Expand Down
47 changes: 47 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

import numpy as np

from fastembed import (
Expand All @@ -7,7 +9,9 @@
LateInteractionMultimodalEmbedding,
LateInteractionTextEmbedding,
)
from fastembed.common.model_management import ModelManagement
from fastembed.common.utils import last_token_pooling
from fastembed.sparse.bm25 import Bm25, supported_bm25_models


def test_text_list_supported_models():
Expand Down Expand Up @@ -59,3 +63,46 @@ def test_last_token_pooling_with_left_padding():
pooled = last_token_pooling(token_embeddings, attention_mask)

assert np.allclose(pooled, [[2.0, 2.0], [6.0, 6.0]])


def test_bm25_resolves_offline_without_network_fetch(tmp_path, monkeypatch):
"""A mock model (``Qdrant/bm25``) whose real required files are already cached
must short-circuit locally and never trigger a network fetch.

``bm25`` uses ``model_file="mock.file"`` as a placeholder that never exists on
disk; its real required files are ``additional_files`` (the ``{lang}.txt``
stop-word lists). The offline cache probe must therefore ignore the mock file.
"""
model = supported_bm25_models[0]
assert model.model_file == ModelManagement.MOCK_MODEL_FILE

hf_repo = model.sources.hf
snapshot_dir = tmp_path / f"models--{hf_repo.replace('/', '--')}"
snapshot_dir.mkdir(parents=True)
# Seed the cache with the *real* required files only (NOT the mock model_file).
for fname in model.additional_files:
(snapshot_dir / fname).write_text("stopword\n")

seen_local_files_only: list[bool] = []

def fake_download_files_from_huggingface(hf_source_repo, cache_dir, extra_patterns, **kwargs):
local_files_only = bool(kwargs.get("local_files_only"))
seen_local_files_only.append(local_files_only)
if local_files_only:
# The offline probe: hand back the already-populated snapshot dir.
return str(snapshot_dir)
# Reaching the online branch means the probe wrongly failed and a real
# network fetch was attempted despite every required file being present.
raise AssertionError("network fetch attempted despite cached files")

monkeypatch.setattr(
ModelManagement,
"download_files_from_huggingface",
staticmethod(fake_download_files_from_huggingface),
)

result = Bm25.download_model(model, str(tmp_path), local_files_only=False)

assert Path(result) == snapshot_dir
# Only the offline probe ran; the online (network) branch was never reached.
assert seen_local_files_only == [True]