Repository navigation
Python: relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant. #14295
Description
Language: Python. The .NET defaults differ and are recorded at the end.
Verified against main at b39d95a on 2026-08-25.
Hi SK team. I just finished a construct-validity audit of embedding-cosine gates across agent
frameworks, and SK's memory-recall path carries the same test blind spot I found in four other
suites. Preprint: https://arxiv.org/abs/2608.10216.
The pattern. TextMemoryPlugin.recall gates on DEFAULT_RELEVANCE = 0.75
(text_memory_plugin.py#L22),
and SemanticTextMemory.search gates on min_relevance_score, defaulting to 0.0
(semantic_text_memory.py#L129),
which it passes through to the store at L153. Both implement one decision, "is this stored text
relevant to, meaning does it answer or agree with, this query?", and both answer it by thresholding
embedding similarity. That score measures shared wording rather than shared meaning, and in the pairs
a recall gate most needs to rank correctly the two run opposite ways: reversing an instruction is a
one-token edit that keeps similarity near maximal, while a faithful restatement in fresh words drops
it substantially. To be precise about framing, nothing here is a bug in SK's code, and the gate does
exactly what it says. The blind spot sits in what the test suite asserts the gate means.
Specimen from my audit corpus (cosine under nomic-embed-text-v1.5, MRL-256, frozen in the paper's
artifact; the mutation class scored 0.83 to 0.9997 across all nine encoders tested, so this is not
one model's quirk):
- Stored memory: "Withhold the study drug from any participant who reports chest tightness."
- Query: "Administer the study drug to any participant who reports chest tightness."
- Cosine 0.9608. It clears 0.75 comfortably, and clears any plausible relevance floor. At the 0.0
default it is not even a speed bump. - Mirror case: faithful low-overlap restatements in the same corpus average about 0.76 under the same
encoder, for example "Retry the request at most three times." against "Give the call up to three
attempts, then stop.". That average lands right at the plugin cut, so faithful restatements sit on
the knife edge while reversals clear it comfortably.
What the suite covers today. recall has no behavioral unit test. Across python/tests,
TextMemoryPlugin appears only in test_serialization.py, and the one test that exercises the gate
is
test_embedding_service_with_memory.py#L155-L169,
an integration test needing live credentials, which calls memory.search(...) four times and passes
min_relevance_score=0.0 at every call site. The threshold is never exercised at a non-zero value
anywhere in the suite, and the fixtures are taxonomic facts, so no pair shares wording while
disagreeing about what it decides.
Proposal, sized to be PR-able, and I am happy to write it:
- Add polarity cases: pairs sharing wording with opposite decisions (negation,
musttomay,
quantity changes, scope inversion) asserted at the shipped defaults, and pairs sharing the
decision with no shared wording. Section 11.3 of the paper gives the two families. The corpus
pairs are released and were authored blind to any encoder. - Record the vectors from a named encoder and check them in with provenance (model id, dimension,
date, generating script) rather than hand-authoring them. A hand-authored vector asserts only the
fixture author's arithmetic, and passes whatever the sentences happen to say. - Optionally, one line in the
relevanceandmin_relevance_scoredocstrings noting that the score
is surface-form similarity, so "relevant" should not be read as "in agreement".
.NET, and a correction to my original filing. @journaltraces checked the C# side and found two things
I had wrong.
TextMemoryPlugin.cs#L47
has private const double DefaultRelevance = 0.0;, so the .NET plugin ships an open gate where the
Python one ships 0.75. And VectorStoreTextSearch carries no minRelevanceScore at all:
ExecuteVectorSearchCoreAsync builds a VectorSearchOptions<TRecord> with Filter only and passes
top. I originally wrote that the deprecation migration ports the blind spot forward. It does not
port it, it drops the gate: a caller who migrates moves from a threshold that can at least be raised
to a top-k that always returns its k, with no floor left to argue about. Thanks to them for both.
The audit found the identical gap in LlamaIndex, LangChain, and GPTCache-style caches. SK is in good
company, which is rather the point: the failure mode ships because no framework's tests can see it.
Activity
DariusHQ commentedon Aug 17, 2026
Hi Scott, I took a look at the current C#
mainimplementation before attempting a PR, and noticed a couple of differences from the current issue description.In the current
TextMemoryPlugin,DefaultRelevanceis0.0, not0.75, whileSemanticTextMemory.SearchAsyncdoes passminRelevanceScorethrough toIMemoryStore.I also couldn't find an equivalent relevance-score gate in the current
VectorStoreTextSearchsuccessor path, it appears to useVectorSearchOptions<TRecord>and vector-store search without the sameminRelevanceScoreparameterI may be looking at a newer/different path than the one used for the audit. Could you clarify which current C# implementation/commit the
DEFAULT_RELEVANCE = 0.75andcollection.as_text_searchreferences are targeting?I wanted to confirm the intended path before preparing a PR so that the tests match the current SK implementation.
poeticize commentedon Aug 25, 2026
Darius,
Thanks for actually reading the code before writing the PR, and sorry for the slow reply.
You are looking at the right code. I pointed you at the wrong language: the audit ran against the
Python implementation, and
DEFAULT_RELEVANCEandcollection.as_text_searchare Pythonidentifiers. The issue body never said so, which is my fault, and I have fixed it. Re-verified
against
mainatb39d95atoday:python/semantic_kernel/core_plugins/text_memory_plugin.py#L22:DEFAULT_RELEVANCE: Final[float] = 0.75, the default forTextMemoryPlugin.recall(...)at L55.The class deprecation at L27 is the one pointing at
collection.as_text_search.python/semantic_kernel/memory/semantic_text_memory.py#L129:SemanticTextMemory.search(..., min_relevance_score: float = 0.0, ...), passed through to thestore at L153, exactly as you described the C# behaving.
Your C# readings are both correct.
TextMemoryPlugin.cs#L47has
private const double DefaultRelevance = 0.0;, so the .NET plugin ships an open gate where thePython one ships 0.75. I will add it that divergence in the issue.
Your
VectorStoreTextSearchobservation is the more interesting one, and strengthens thecase. There is no
minRelevanceScorethere:ExecuteVectorSearchCoreAsyncbuilds aVectorSearchOptions<TRecord>carryingFilteronly, and passestop. So the successor path does not carry the score gate forward, it drops it. A caller who migrates offTextMemoryPluginmoves from a threshold that can at least be raised to a top-k that always returns its k, and the "administer the study drug" record comes back as the top hit with no floor left to argue about. My issue said the migration ports the blind spot forward. That was too generous, and I would rather state it your way.On where the Python gap actually sits, my original wording was loose.
recallhas no behavioral unittest at all. Across
python/tests,TextMemoryPluginappears only intest_serialization.py, andthe one test that exercises the gate is
test_embedding_service_with_memory.py#L155-L169,an integration test needing live credentials, which calls
memory.search(...)four times and passesmin_relevance_score=0.0at every call site. The threshold is never exercised at a non-zero valueanywhere in the suite, and the fixtures are taxonomic facts (Sharks are fish, Penguins are birds), so
no pair in the suite shares wording while disagreeing about what it decides.
One caution if you do write the PR, because there is already an open one against this issue (#14319)
that ran into it. A unit test cannot call an embedding model, so the tempting move is to hand-author
the vectors, and a hand-authored vector asserts only the fixture author's arithmetic. The test then
passes whatever the sentences say, which is the failure this issue is about, reproduced one level up.
What I think does work:
provenance in the fixture (model id, dimension, date, generating script). The assertion then says
something about the world: the opposite-meaning record clears the shipped default while the
faithful paraphrase does not. It stays frozen and deterministic, and it fails if the claim stops
being true.
text-embedding-3-small, over the one inmy paper. The effect does not hinge on the choice; the mutation class scored 0.83 to 0.9997 across
all nine encoders I tested.
unit tests.
I am happy to generate the recorded vectors and hand them over so you keep authorship of the PR, in
Python or C# or both. Say which and I will produce them.
[-]Relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant.[/-][+]Python: relevance-score gates (min_relevance_score / DEFAULT_RELEVANCE) have no polarity cases in tests: a memory that says the opposite of the query is recalled as highly relevant.[/+]