Skip to content

Commit 2a772b4

Browse files
davidhooclaude
andcommitted
fix: include high-similarity stranger candidates in merge suggestions
Remove the `score >= attachThreshold` skip in buildAssignments. Candidates scoring above the attach threshold were silently dropped, but since these faces are already assigned to existing persons, auto-attach never fires for them. This created a blind spot where stranger persons with 70%+ similarity to acquaintance/friend/family targets were never suggested for merge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0d4393b commit 2a772b4

2 files changed

Lines changed: 15 additions & 10 deletions

File tree

backend/internal/service/person_merge_suggestion_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ func (s *personMergeSuggestionService) buildAssignments(targets []*model.Person)
679679
score1 := averageBestSuggestionSimilarity(tgtEmb, candidateEmbeddings)
680680
score2 := averageBestSuggestionSimilarity(candidateEmbeddings, tgtEmb)
681681
score := (score1 + score2) / 2
682-
if score < threshold || score >= s.attachThreshold() {
682+
if score < threshold {
683683
continue
684684
}
685685

backend/internal/service/person_merge_suggestion_service_test.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,11 @@ func TestPersonMergeSuggestionService_AllowsFamilyAndFriendCandidates(t *testing
258258
assert.Contains(t, allCandidateIDs, familyCandidate.ID)
259259
}
260260

261-
func TestPersonMergeSuggestionService_DoesNotCreateSuggestionAtOrAboveAttachThreshold(t *testing.T) {
261+
func TestPersonMergeSuggestionService_CreatesSuggestionEvenAboveAttachThreshold(t *testing.T) {
262+
// Candidates above the attach threshold should still produce merge suggestions.
263+
// Previously they were skipped (logic: "above attach = auto-attach"), but since
264+
// these faces are already assigned to existing persons, auto-attach never fires,
265+
// creating a blind spot where high-similarity stranger pairs are never suggested.
262266
svc, _, repos, _ := newPersonMergeSuggestionServiceWithConfigForTest(t, config.PeopleConfig{
263267
MergeSuggestionThreshold: 0.90,
264268
AttachThreshold: 0.95,
@@ -268,29 +272,30 @@ func TestPersonMergeSuggestionService_DoesNotCreateSuggestionAtOrAboveAttachThre
268272
})
269273

270274
target := createSuggestionTestPerson(t, repos, model.PersonCategoryFamily, []float32{1, 0}, []float32{0.99, 0.01})
271-
tooCloseCandidate := createSuggestionTestPerson(t, repos, model.PersonCategoryStranger, []float32{1, 0})
275+
highSimilarityCandidate := createSuggestionTestPerson(t, repos, model.PersonCategoryStranger, []float32{1, 0})
272276

273277
require.NoError(t, svc.MarkDirty("attach-threshold-upper-bound"))
274278
require.NoError(t, svc.RunBackgroundSlice())
275279

276280
got := pendingSuggestionCandidatesByTarget(t, repos.MergeSuggestion)
277-
assert.NotContains(t, got, target.ID)
278-
assert.NotContains(t, got[target.ID], tooCloseCandidate.ID)
281+
assert.Contains(t, got[target.ID], highSimilarityCandidate.ID, "candidates above attach threshold should still produce suggestions")
279282
}
280283

281284
func TestPersonMergeSuggestionService_UsesAverageBestSimilarityInsteadOfSingleMaxPair(t *testing.T) {
285+
// Verify that a candidate with one high-similarity prototype and one low-similarity
286+
// prototype stays below threshold when using average-best (not max-pair).
282287
svc, _, repos, _ := newPersonMergeSuggestionServiceWithConfigForTest(t, config.PeopleConfig{
283-
MergeSuggestionThreshold: 0.80,
284-
AttachThreshold: 0.95,
288+
MergeSuggestionThreshold: 0.90,
289+
AttachThreshold: 1.10, // effectively disabled - never skip above attach
285290
MergeSuggestionMaxPairsPerRun: 100,
286291
MergeSuggestionBatchSize: 10,
287292
MergeSuggestionCooldownSeconds: 1,
288293
})
289294

290295
target := createSuggestionTestPerson(t, repos, model.PersonCategoryFamily, []float32{1, 0}, []float32{0, 1})
291-
// One prototype matches perfectly, one is nearly orthogonal.
292-
// Max-pair logic would suggest; average-best should stay below threshold.
293-
candidate := createSuggestionTestPerson(t, repos, model.PersonCategoryStranger, []float32{1, 0}, []float32{0.2, 0.98})
296+
// One prototype matches target[0] perfectly, one is nearly orthogonal to both.
297+
// average-best is well below 0.90 threshold.
298+
candidate := createSuggestionTestPerson(t, repos, model.PersonCategoryStranger, []float32{1, 0}, []float32{0.5, 0.5})
294299

295300
require.NoError(t, svc.MarkDirty("average-best-similarity"))
296301
require.NoError(t, svc.RunBackgroundSlice())

0 commit comments

Comments
 (0)