mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 02:16:08 +00:00
feat: embed pooling
This commit is contained in:
1 parent
de22be5a21
commit
ef51ee97b3
5 files changed
+326
-18
No files matched your search
@@ -18,6 +18,19 @@ def _clear_registration():
|
||||
embeddings_local._registered.clear()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _no_hub_reads():
|
||||
"""Keep unit tests off the network.
|
||||
|
||||
``_spec_for`` now asks a repository how it pools; without this every test
|
||||
naming an unregistered model would reach the Hugging Face hub. ``None`` is
|
||||
the "declares nothing" answer, which is the behaviour these tests were
|
||||
written against. Tests that exercise the metadata patch it themselves.
|
||||
"""
|
||||
with patch.object(embeddings_local, "_read_repo_json", return_value=None):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_fastembed():
|
||||
"""Patch FastEmbed so no model is downloaded or run."""
|
||||
@@ -263,3 +276,118 @@ class TestTokenizerPadding:
|
||||
_, instance = fake_fastembed
|
||||
instance.model = None
|
||||
EmbeddingsWrapper(GRANITE_97M.name)
|
||||
|
||||
|
||||
def _repo_json(pooling_file, modules_file):
|
||||
"""Stub ``_read_repo_json`` returning canned repository metadata."""
|
||||
|
||||
def read(repo, filename):
|
||||
return pooling_file if filename == embeddings_local._POOLING_CONFIG else modules_file
|
||||
|
||||
return read
|
||||
|
||||
|
||||
class TestPoolingReadFromTheRepository:
|
||||
"""A model's pooling is a fact its repository states, not a default.
|
||||
|
||||
Assuming mean pooling for a CLS model returns vectors at cosine ~0.95 to
|
||||
the correct ones: no error, no dimension mismatch, just quietly worse
|
||||
retrieval. These cover the shapes seen on the hub.
|
||||
"""
|
||||
|
||||
def test_cls_pooling_is_read_rather_than_assumed(self):
|
||||
with patch.object(
|
||||
embeddings_local,
|
||||
"_read_repo_json",
|
||||
_repo_json(
|
||||
{"pooling_mode_cls_token": True, "word_embedding_dimension": 384},
|
||||
[{"type": "sentence_transformers.models.Transformer"},
|
||||
{"type": "sentence_transformers.models.Pooling"},
|
||||
{"type": "sentence_transformers.models.Normalize"}],
|
||||
),
|
||||
):
|
||||
spec = embeddings_local._spec_for("BAAI/bge-small-en-v1.5")
|
||||
assert spec.pooling == "cls"
|
||||
assert spec.normalize is True
|
||||
# Declared width, so no probe run is needed to learn it.
|
||||
assert spec.dimension == 384
|
||||
|
||||
def test_missing_normalize_module_means_unnormalised(self):
|
||||
"""multi-qa-mpnet-base-dot-v1 is trained on unnormalised vectors."""
|
||||
with patch.object(
|
||||
embeddings_local,
|
||||
"_read_repo_json",
|
||||
_repo_json(
|
||||
{"pooling_mode_cls_token": True, "word_embedding_dimension": 768},
|
||||
[{"type": "sentence_transformers.models.Transformer"},
|
||||
{"type": "sentence_transformers.models.Pooling"}],
|
||||
),
|
||||
):
|
||||
spec = embeddings_local._spec_for("sentence-transformers/multi-qa-mpnet-base-dot-v1")
|
||||
assert spec.pooling == "cls"
|
||||
assert spec.normalize is False
|
||||
|
||||
def test_dense_projection_head_is_refused(self):
|
||||
"""FastEmbed would skip the projection and emit the wrong vectors."""
|
||||
with patch.object(
|
||||
embeddings_local,
|
||||
"_read_repo_json",
|
||||
_repo_json(
|
||||
{"pooling_mode_cls_token": True, "word_embedding_dimension": 768},
|
||||
[{"type": "sentence_transformers.models.Transformer"},
|
||||
{"type": "sentence_transformers.models.Pooling"},
|
||||
{"type": "sentence_transformers.models.Dense"},
|
||||
{"type": "sentence_transformers.models.Normalize"}],
|
||||
),
|
||||
):
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
embeddings_local._spec_for("sentence-transformers/LaBSE")
|
||||
message = str(excinfo.value)
|
||||
assert "LaBSE" in message
|
||||
assert "Dense" in message
|
||||
|
||||
def test_unsupported_pooling_mode_falls_back_rather_than_lying(self):
|
||||
with patch.object(
|
||||
embeddings_local,
|
||||
"_read_repo_json",
|
||||
_repo_json({"pooling_mode_max_tokens": True}, []),
|
||||
):
|
||||
spec = embeddings_local._spec_for("some-org/max-pooled")
|
||||
assert spec.pooling == embeddings_local._FALLBACK_POOLING
|
||||
assert spec.dimension == 0
|
||||
|
||||
def test_repository_without_metadata_keeps_the_assumption(self):
|
||||
spec = embeddings_local._spec_for("some-org/plain-onnx-export")
|
||||
assert spec.pooling == embeddings_local._FALLBACK_POOLING
|
||||
assert spec.normalize is True
|
||||
assert spec.dimension == 0
|
||||
|
||||
def test_registry_wins_over_the_repository(self):
|
||||
"""A described model is never re-read; the registry is the answer."""
|
||||
read = MagicMock()
|
||||
with patch.object(embeddings_local, "_read_repo_json", read):
|
||||
spec = embeddings_local._spec_for(MPNET.name)
|
||||
assert spec is MPNET
|
||||
read.assert_not_called()
|
||||
|
||||
|
||||
class TestPoolingOverrides:
|
||||
def test_settings_override_what_the_repository_declares(self):
|
||||
with patch.object(
|
||||
embeddings_local,
|
||||
"_read_repo_json",
|
||||
_repo_json(
|
||||
{"pooling_mode_mean_tokens": True, "word_embedding_dimension": 768},
|
||||
[{"type": "sentence_transformers.models.Normalize"}],
|
||||
),
|
||||
):
|
||||
with patch.object(embeddings_local.settings, "EMBEDDINGS_POOLING", "cls"), \
|
||||
patch.object(embeddings_local.settings, "EMBEDDINGS_NORMALIZE", False):
|
||||
spec = embeddings_local._spec_for("some-org/mislabelled")
|
||||
assert spec.pooling == "cls"
|
||||
assert spec.normalize is False
|
||||
|
||||
def test_a_meaningless_override_is_ignored(self):
|
||||
with patch.object(embeddings_local.settings, "EMBEDDINGS_POOLING", "banana"):
|
||||
spec = embeddings_local._spec_for("some-org/plain-onnx-export")
|
||||
assert spec.pooling == embeddings_local._FALLBACK_POOLING
|
||||
@@ -1,5 +1,7 @@
|
||||
"""The registry is the single source of truth for embedding-model facts."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from application.vectorstore import model_registry as reg
|
||||
@@ -86,3 +88,30 @@ class TestHelpers:
|
||||
for key in (model.name, *model.aliases):
|
||||
assert key.lower() not in seen, f"{key} claimed twice"
|
||||
seen[key.lower()] = model
|
||||
|
||||
|
||||
class TestRegistryMatchesTheHub:
|
||||
"""Every registry entry restates facts the model's repository already holds.
|
||||
|
||||
Restating them is what makes an offline install work, but a restatement can
|
||||
drift from its source and nothing else would notice: wrong pooling is not a
|
||||
crash, only worse retrieval. Opt in with ``DOCSGPT_HUB_TESTS=1``; needs
|
||||
network.
|
||||
"""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("DOCSGPT_HUB_TESTS") != "1",
|
||||
reason="set DOCSGPT_HUB_TESTS=1 to check the registry against the hub",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"model", [m for m in reg.MODELS if m.provider == "fastembed"]
|
||||
)
|
||||
def test_entry_matches_repository_metadata(self, model):
|
||||
from application.vectorstore.embeddings_local import _describe_from_repo
|
||||
|
||||
described = _describe_from_repo(model.repo)
|
||||
assert described is not None, f"{model.repo} declares no pooling metadata"
|
||||
assert described.pooling == model.pooling
|
||||
assert described.normalize == model.normalize
|
||||
if described.dimension:
|
||||
assert described.dimension == model.dimension
|
||||
Reference in new issue
Block a user