Skip to content

Commit 351d639

Browse files
committed
Speed up spam classifier neighbor lookup on hot bands
1 parent fa6535c commit 351d639

6 files changed

Lines changed: 79 additions & 26 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Band rows only exist for neighbor-eligible fingerprints (token_count >= 20).
2+
-- Shorter snippets never pass the similarity threshold, and near-empty ones
3+
-- share a degenerate signature that made every lookup scan the same hot bands.
4+
DELETE FROM spam_classifier_bands b
5+
USING spam_classifier_fingerprints f
6+
WHERE f.snippet_id = b.snippet_id
7+
AND f.content_revision = b.content_revision
8+
AND f.algorithm_version = b.algorithm_version
9+
AND f.token_count < 20;

‎glot_backend/src/glot_backend/spam_classifier/domain/similarity.gleam‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,24 @@ pub fn jaccard(left: Fingerprint, right: Fingerprint) -> Float {
8181
}
8282

8383
pub fn is_neighbor(left: Fingerprint, right: Fingerprint) -> Bool {
84-
left.token_count >= 20
85-
&& right.token_count >= 20
84+
can_have_neighbors(left)
85+
&& can_have_neighbors(right)
8686
&& jaccard(left, right) >=. 0.8
8787
}
88+
89+
const neighbor_token_threshold = 20
90+
91+
/// Only fingerprints at or above the token threshold take part in neighbor
92+
/// matching. Shorter snippets have too little content to compare, and the
93+
/// near-empty ones share a degenerate signature that would form hot bands.
94+
pub fn can_have_neighbors(fingerprint: Fingerprint) -> Bool {
95+
fingerprint.token_count >= neighbor_token_threshold
96+
}
97+
98+
/// Bands to look up when searching for neighbors: none for short snippets.
99+
pub fn candidate_bands(fingerprint: Fingerprint) -> List(String) {
100+
case can_have_neighbors(fingerprint) {
101+
True -> fingerprint.bands
102+
False -> []
103+
}
104+
}

‎glot_backend/src/glot_backend/spam_classifier/effect/classification.gleam‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn run(
7171
classifier_provider.Local -> {
7272
use candidates <- result.try(ports.storage.candidates(
7373
scoring.version,
74-
signature.bands,
74+
similarity.candidate_bands(signature),
7575
request.snippet.id,
7676
))
7777
let verified =

‎glot_backend/src/glot_backend/spam_classifier/sql/fingerprints.sql‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,43 @@ WITH current_snippet AS (
1414
trigram_hashes = EXCLUDED.trigram_hashes, signature = EXCLUDED.signature,
1515
independently_suspicious = EXCLUDED.independently_suspicious,
1616
urls = EXCLUDED.urls, indexed_at = CURRENT_TIMESTAMP
17-
RETURNING snippet_id, content_revision, algorithm_version
17+
RETURNING snippet_id, content_revision, algorithm_version, token_count
1818
), bands AS (
19+
-- Only neighbor-eligible snippets get band rows: shorter ones never pass
20+
-- the token threshold, and their degenerate signatures would form hot bands.
1921
INSERT INTO spam_classifier_bands
2022
(snippet_id, content_revision, algorithm_version, band_number, band_value)
2123
SELECT stored.snippet_id, stored.content_revision, stored.algorithm_version,
2224
(band.ordinality - 1)::int, band.value
2325
FROM stored CROSS JOIN unnest(sqlc.arg(bands)::text[]) WITH ORDINALITY AS band(value, ordinality)
26+
WHERE stored.token_count >= 20
2427
ON CONFLICT (snippet_id, content_revision, algorithm_version, band_number)
2528
DO UPDATE SET band_value = EXCLUDED.band_value
2629
)
2730
SELECT EXISTS(SELECT 1 FROM stored)::boolean AS stored;
2831

2932
-- name: FindClassifierNeighbors :many
33+
-- Rank on the narrow band rows first; identical content shares every band, so
34+
-- hot band values can match tens of thousands of snippets. Joining the wide
35+
-- fingerprint rows before the LIMIT forced a large on-disk sort.
36+
WITH ranked AS (
37+
SELECT b.snippet_id, b.content_revision, b.algorithm_version,
38+
count(*)::int AS matching_bands
39+
FROM spam_classifier_bands b
40+
WHERE b.algorithm_version = sqlc.arg(algorithm_version)::text
41+
AND b.band_value = ANY(sqlc.arg(bands)::text[])
42+
AND b.snippet_id <> sqlc.arg(snippet_id)::uuid
43+
GROUP BY b.snippet_id, b.content_revision, b.algorithm_version
44+
ORDER BY matching_bands DESC, b.content_revision DESC, b.snippet_id DESC
45+
LIMIT 200
46+
)
3047
SELECT f.snippet_id, f.content_revision, s.slug, f.token_count,
31-
f.trigram_hashes, f.signature, f.independently_suspicious,
32-
count(*)::int AS matching_bands
33-
FROM spam_classifier_bands b
48+
f.trigram_hashes, f.signature, f.independently_suspicious, r.matching_bands
49+
FROM ranked r
3450
JOIN spam_classifier_fingerprints f USING (snippet_id, content_revision, algorithm_version)
3551
JOIN snippets s ON s.id = f.snippet_id AND s.updated_at = f.content_revision
36-
WHERE b.algorithm_version = sqlc.arg(algorithm_version)::text
37-
AND b.band_value = ANY(sqlc.arg(bands)::text[])
38-
AND b.snippet_id <> sqlc.arg(snippet_id)::uuid
39-
AND f.token_count >= 20
40-
GROUP BY f.snippet_id, f.content_revision, f.algorithm_version, s.slug
41-
ORDER BY matching_bands DESC, f.content_revision DESC, f.snippet_id DESC
42-
LIMIT 200;
52+
WHERE f.token_count >= 20
53+
ORDER BY r.matching_bands DESC, f.content_revision DESC, f.snippet_id DESC;
4354

4455
-- name: ListClassifierIndexBatch :many
4556
SELECT s.id, s.slug, s.user_id, s.language, s.title, s.visibility, s.stdin,
@@ -106,6 +117,7 @@ WITH progress AS MATERIALIZED (
106117
(band.ordinality - 1)::int, band.value
107118
FROM stored JOIN input i ON i.snippet_id = stored.snippet_id
108119
CROSS JOIN unnest(i.bands) WITH ORDINALITY AS band(value, ordinality)
120+
WHERE i.token_count >= 20
109121
ON CONFLICT (snippet_id, content_revision, algorithm_version, band_number)
110122
DO UPDATE SET band_value = EXCLUDED.band_value
111123
), advanced AS (

‎glot_backend/src/glot_backend/sql.gleam‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6918,13 +6918,16 @@ pub fn store_classifier_fingerprint(
69186918
trigram_hashes = EXCLUDED.trigram_hashes, signature = EXCLUDED.signature,
69196919
independently_suspicious = EXCLUDED.independently_suspicious,
69206920
urls = EXCLUDED.urls, indexed_at = CURRENT_TIMESTAMP
6921-
RETURNING snippet_id, content_revision, algorithm_version
6921+
RETURNING snippet_id, content_revision, algorithm_version, token_count
69226922
), bands AS (
6923+
-- Only neighbor-eligible snippets get band rows: shorter ones never pass
6924+
-- the token threshold, and their degenerate signatures would form hot bands.
69236925
INSERT INTO spam_classifier_bands
69246926
(snippet_id, content_revision, algorithm_version, band_number, band_value)
69256927
SELECT stored.snippet_id, stored.content_revision, stored.algorithm_version,
69266928
(band.ordinality - 1)::int, band.value
69276929
FROM stored CROSS JOIN unnest($9::text[]) WITH ORDINALITY AS band(value, ordinality)
6930+
WHERE stored.token_count >= 20
69286931
ON CONFLICT (snippet_id, content_revision, algorithm_version, band_number)
69296932
DO UPDATE SET band_value = EXCLUDED.band_value
69306933
)
@@ -6972,19 +6975,24 @@ pub fn find_classifier_neighbors(
69726975
snippet_id snippet_id: BitArray,
69736976
) {
69746977
let sql =
6975-
"SELECT f.snippet_id, f.content_revision, s.slug, f.token_count,
6976-
f.trigram_hashes, f.signature, f.independently_suspicious,
6977-
count(*)::int AS matching_bands
6978-
FROM spam_classifier_bands b
6978+
"WITH ranked AS (
6979+
SELECT b.snippet_id, b.content_revision, b.algorithm_version,
6980+
count(*)::int AS matching_bands
6981+
FROM spam_classifier_bands b
6982+
WHERE b.algorithm_version = $1::text
6983+
AND b.band_value = ANY($2::text[])
6984+
AND b.snippet_id <> $3::uuid
6985+
GROUP BY b.snippet_id, b.content_revision, b.algorithm_version
6986+
ORDER BY matching_bands DESC, b.content_revision DESC, b.snippet_id DESC
6987+
LIMIT 200
6988+
)
6989+
SELECT f.snippet_id, f.content_revision, s.slug, f.token_count,
6990+
f.trigram_hashes, f.signature, f.independently_suspicious, r.matching_bands
6991+
FROM ranked r
69796992
JOIN spam_classifier_fingerprints f USING (snippet_id, content_revision, algorithm_version)
69806993
JOIN snippets s ON s.id = f.snippet_id AND s.updated_at = f.content_revision
6981-
WHERE b.algorithm_version = $1::text
6982-
AND b.band_value = ANY($2::text[])
6983-
AND b.snippet_id <> $3::uuid
6984-
AND f.token_count >= 20
6985-
GROUP BY f.snippet_id, f.content_revision, f.algorithm_version, s.slug
6986-
ORDER BY matching_bands DESC, f.content_revision DESC, f.snippet_id DESC
6987-
LIMIT 200"
6994+
WHERE f.token_count >= 20
6995+
ORDER BY r.matching_bands DESC, f.content_revision DESC, f.snippet_id DESC"
69886996
#(
69896997
sql,
69906998
[
@@ -7175,6 +7183,7 @@ pub fn commit_classifier_index_batch(
71757183
(band.ordinality - 1)::int, band.value
71767184
FROM stored JOIN input i ON i.snippet_id = stored.snippet_id
71777185
CROSS JOIN unnest(i.bands) WITH ORDINALITY AS band(value, ordinality)
7186+
WHERE i.token_count >= 20
71787187
ON CONFLICT (snippet_id, content_revision, algorithm_version, band_number)
71797188
DO UPDATE SET band_value = EXCLUDED.band_value
71807189
), advanced AS (

‎glot_backend/test/spam_classifier/domain/features_test.gleam‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ pub fn url_substitutions_have_identical_fingerprints_test() {
6767
pub fn short_snippets_never_qualify_test() {
6868
let short = similarity.fingerprint(["hello", "world", "example"])
6969
assert similarity.is_neighbor(short, short) == False
70+
assert similarity.can_have_neighbors(short) == False
71+
assert similarity.candidate_bands(short) == []
72+
let long =
73+
similarity.fingerprint(list.repeat("word", 19) |> list.append(["end"]))
74+
assert similarity.can_have_neighbors(long)
75+
assert similarity.candidate_bands(long) == long.bands
7076
}
7177

7278
pub fn defanged_protocol_and_hidden_bare_domain_are_obfuscation_test() {

0 commit comments

Comments
 (0)