Repository navigation
Expand file tree
/
Copy pathtest_dedup.py
More file actions
324 lines (236 loc) · 10.1 KB
/
Copy pathtest_dedup.py
File metadata and controls
324 lines (236 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""Tests for content deduplication."""
import pytest
import numpy as np
from unittest.mock import Mock, MagicMock
from sentence_transformers import SentenceTransformer
from feedrr.processor.dedup import (
serialize_embedding,
deserialize_embedding,
cosine_similarity,
generate_article_embedding,
find_duplicate,
mark_as_duplicate,
)
from feedrr.storage.models import Article, Source
def test_serialize_deserialize_embedding():
"""Test embedding serialization and deserialization."""
# Create a sample embedding
original = np.array([0.1, 0.2, 0.3, 0.4, 0.5])
# Serialize
serialized = serialize_embedding(original)
assert isinstance(serialized, bytes)
# Deserialize
deserialized = deserialize_embedding(serialized)
assert isinstance(deserialized, np.ndarray)
np.testing.assert_array_equal(original, deserialized)
def test_cosine_similarity_identical():
"""Test cosine similarity with identical vectors."""
vec = np.array([1.0, 2.0, 3.0])
similarity = cosine_similarity(vec, vec)
assert abs(similarity - 1.0) < 0.0001 # Should be 1.0 (identical)
def test_cosine_similarity_orthogonal():
"""Test cosine similarity with orthogonal vectors."""
vec1 = np.array([1.0, 0.0, 0.0])
vec2 = np.array([0.0, 1.0, 0.0])
similarity = cosine_similarity(vec1, vec2)
assert abs(similarity - 0.0) < 0.0001 # Should be 0.0 (orthogonal)
def test_cosine_similarity_opposite():
"""Test cosine similarity with opposite vectors."""
vec1 = np.array([1.0, 2.0, 3.0])
vec2 = np.array([-1.0, -2.0, -3.0])
similarity = cosine_similarity(vec1, vec2)
assert abs(similarity - (-1.0)) < 0.0001 # Should be -1.0 (opposite)
def test_cosine_similarity_similar():
"""Test cosine similarity with similar vectors."""
vec1 = np.array([1.0, 2.0, 3.0])
vec2 = np.array([1.1, 2.1, 2.9])
similarity = cosine_similarity(vec1, vec2)
assert similarity > 0.99 # Should be very similar
def test_generate_article_embedding():
"""Test generating embedding for an article."""
# Mock model
mock_model = Mock(spec=SentenceTransformer)
mock_embedding = np.array([0.1, 0.2, 0.3])
mock_model.encode.return_value = mock_embedding
# Create mock article
article = Mock(spec=Article)
article.title = "Test Article"
article.content = "This is test content"
# Generate embedding
embedding = generate_article_embedding(mock_model, article)
# Verify
mock_model.encode.assert_called_once_with("Test Article This is test content")
np.testing.assert_array_equal(embedding, mock_embedding)
def test_generate_article_embedding_no_content():
"""Test generating embedding for article without content."""
mock_model = Mock(spec=SentenceTransformer)
mock_embedding = np.array([0.1, 0.2, 0.3])
mock_model.encode.return_value = mock_embedding
article = Mock(spec=Article)
article.title = "Test Article"
article.content = None
embedding = generate_article_embedding(mock_model, article)
mock_model.encode.assert_called_once_with("Test Article ")
np.testing.assert_array_equal(embedding, mock_embedding)
def test_find_duplicate_exact_match():
"""Test finding duplicate with exact match."""
# Mock model
mock_model = Mock(spec=SentenceTransformer)
identical_embedding = np.array([0.5, 0.5, 0.5])
mock_model.encode.return_value = identical_embedding
# New article
new_article = Mock(spec=Article)
new_article.title = "Breaking News"
new_article.content = "Important story"
# Existing article with same embedding
existing = Mock(spec=Article)
existing.id = 1
existing.embedding = serialize_embedding(identical_embedding)
# Find duplicate
duplicate = find_duplicate(mock_model, new_article, [existing])
assert duplicate == existing
def test_find_duplicate_similar_content():
"""Test finding duplicate with similar content (above threshold)."""
mock_model = Mock(spec=SentenceTransformer)
# New article embedding
new_embedding = np.array([1.0, 0.0, 0.0])
mock_model.encode.return_value = new_embedding
new_article = Mock(spec=Article)
new_article.title = "Test Article"
new_article.content = "Test content"
# Existing article with very similar embedding (90% similar)
similar_embedding = np.array([0.9, 0.1, 0.0])
existing = Mock(spec=Article)
existing.id = 1
existing.embedding = serialize_embedding(similar_embedding)
duplicate = find_duplicate(mock_model, new_article, [existing], threshold=0.85)
assert duplicate == existing
def test_find_duplicate_below_threshold():
"""Test that articles below similarity threshold are not marked as duplicates."""
mock_model = Mock(spec=SentenceTransformer)
# New article embedding
new_embedding = np.array([1.0, 0.0, 0.0])
mock_model.encode.return_value = new_embedding
new_article = Mock(spec=Article)
new_article.title = "Different Article"
new_article.content = "Different content"
# Existing article with different embedding (50% similar)
different_embedding = np.array([0.5, 0.5, 0.5])
existing = Mock(spec=Article)
existing.id = 1
existing.embedding = serialize_embedding(different_embedding)
duplicate = find_duplicate(mock_model, new_article, [existing], threshold=0.85)
assert duplicate is None
def test_find_duplicate_no_existing_articles():
"""Test finding duplicate when no existing articles."""
mock_model = Mock(spec=SentenceTransformer)
mock_model.encode.return_value = np.array([0.1, 0.2, 0.3])
new_article = Mock(spec=Article)
new_article.title = "New Article"
new_article.content = "Fresh content"
duplicate = find_duplicate(mock_model, new_article, [])
assert duplicate is None
def test_find_duplicate_no_embeddings():
"""Test finding duplicate when existing articles have no embeddings."""
mock_model = Mock(spec=SentenceTransformer)
mock_model.encode.return_value = np.array([0.1, 0.2, 0.3])
new_article = Mock(spec=Article)
new_article.title = "New Article"
new_article.content = "Fresh content"
# Existing article without embedding
existing = Mock(spec=Article)
existing.id = 1
existing.embedding = None
duplicate = find_duplicate(mock_model, new_article, [existing])
assert duplicate is None
def test_find_duplicate_multiple_existing():
"""Test finding duplicate among multiple existing articles."""
mock_model = Mock(spec=SentenceTransformer)
new_embedding = np.array([1.0, 0.0, 0.0])
mock_model.encode.return_value = new_embedding
# First existing - not similar
existing1 = Mock(spec=Article)
existing1.id = 1
existing1.embedding = serialize_embedding(np.array([0.0, 1.0, 0.0]))
# Second existing - very similar (duplicate)
existing2 = Mock(spec=Article)
existing2.id = 2
existing2.embedding = serialize_embedding(np.array([0.95, 0.05, 0.0]))
# Third existing - not similar
existing3 = Mock(spec=Article)
existing3.id = 3
existing3.embedding = serialize_embedding(np.array([0.0, 0.0, 1.0]))
duplicate = find_duplicate(
mock_model,
new_article := Mock(spec=Article),
[existing1, existing2, existing3],
threshold=0.85
)
# Should find the second article as duplicate
assert duplicate == existing2
def test_find_duplicate_corrupted_embedding():
"""Test that corrupted embeddings are skipped gracefully."""
mock_model = Mock(spec=SentenceTransformer)
mock_model.encode.return_value = np.array([1.0, 0.0, 0.0])
new_article = Mock(spec=Article)
new_article.title = "New Article"
new_article.content = "Content"
# Article with corrupted embedding
existing = Mock(spec=Article)
existing.id = 1
existing.embedding = b"corrupted data"
# Should not crash, should return None
duplicate = find_duplicate(mock_model, new_article, [existing])
assert duplicate is None
def test_mark_as_duplicate():
"""Test marking an article as duplicate."""
article = Mock(spec=Article)
article.is_duplicate = False
article.duplicate_of_id = None
original = Mock(spec=Article)
original.id = 42
mark_as_duplicate(article, original)
assert article.is_duplicate is True
assert article.duplicate_of_id == 42
def test_mark_as_duplicate_already_marked():
"""Test marking an already-duplicate article."""
article = Mock(spec=Article)
article.is_duplicate = True
article.duplicate_of_id = 10
original = Mock(spec=Article)
original.id = 42
mark_as_duplicate(article, original)
# Should update to new original
assert article.is_duplicate is True
assert article.duplicate_of_id == 42
def test_integration_duplicate_workflow():
"""Integration test for complete duplicate detection workflow."""
# Create mock model with realistic behavior
mock_model = Mock(spec=SentenceTransformer)
# Simulate two similar articles about the same story
article1_embedding = np.array([0.8, 0.2, 0.1, 0.05])
article2_embedding = np.array([0.82, 0.18, 0.12, 0.04]) # Very similar
# First article (original)
article1 = Mock(spec=Article)
article1.id = 1
article1.title = "Netflix acquires Warner Bros"
article1.content = "Netflix has struck a deal to acquire Warner Bros..."
article1.embedding = serialize_embedding(article1_embedding)
article1.is_duplicate = False
# Second article (duplicate from different source)
article2 = Mock(spec=Article)
article2.id = 2
article2.title = "Netflix seals Warner Bros deal"
article2.content = "In a major acquisition, Netflix has acquired Warner Bros..."
article2.is_duplicate = False
article2.duplicate_of_id = None
# Simulate encoding the second article
mock_model.encode.return_value = article2_embedding
# Find if article2 is duplicate of article1
duplicate_of = find_duplicate(mock_model, article2, [article1], threshold=0.85)
# Should find article1 as the original
assert duplicate_of == article1
# Mark article2 as duplicate
mark_as_duplicate(article2, duplicate_of)
assert article2.is_duplicate is True
assert article2.duplicate_of_id == 1