using SimilaritySearch, SimSearchManifoldLearning, Plots, StatsBase, LinearAlgebra, Markdown, RandomAll-kNN and center selection algorithms
by: Eric S. Téllez
This demo shows all-pairs kNN and dataset-level center selection algorithms available in SimilaritySearch.jl. These tools are useful for exploratory data analysis, prototype learning, and building fast approximations of large datasets.
Algorithms covered:
| Function | What it does |
|---|---|
allknn |
All-pairs k nearest neighbours graph |
fft |
Farthest-First Traversal — maximally separated centers |
dnet |
Density Net — centers balanced by local density |
randsel |
Random selection of centers |
multirandsel |
Multiple random selection rounds (better coverage) |
neardup |
Near-duplicate detection / data cleaning |
Dataset
We use two setups: a small 2D synthetic dataset for visualisation, and Fashion-MNIST for a realistic high-dimensional example.
# 2D synthetic: mixture of 5 Gaussians with different widths
Random.seed!(42)
n2d = 10_000
centers_gt = Float32[[-4,-4] [0,4] [4,-2] [-2,2] [3,3]]
σs = Float32[0.4, 0.8, 0.5, 1.2, 0.6]
M2d = reduce(hcat, [centers_gt[:, mod1(i,5)] .+ σs[mod1(i,5)] .* randn(Float32, 2)
for i in 1:n2d])
db2d = MatrixDatabase(M2d)
dist2d = Dist.SqL2()
G2d = SearchGraph(dist2d, db2d)
ctx2d = SearchGraphContext(hyperparameters_callback=OptimizeParameters(MinRecall(0.98)))
index!(G2d, ctx2d)All-kNN
allknn computes the k nearest neighbours of every item in the dataset simultaneously. It returns (ids, dists) matrices of size (k, n).
k = 15
gold_ids, gold_dists = allknn(ExhaustiveSearch(dist2d, db2d), GenericContext(), k)
approx_ids, approx_dists = allknn(G2d, ctx2d, k)
recall_val = macrorecall(gold_ids, approx_ids)
@info "All-kNN recall: $(round(recall_val * 100, digits=2))%"# Visualise the kNN graph (draw edges from each point to its nearest neighbour)
X, Y = view(M2d, 1, :), view(M2d, 2, :)
p = scatter(X, Y; fmt=:png, ms=2, msw=0, ma=0.4, c=:gray, label="data", size=(600,550))
for i in 1:min(500, n2d)
j = approx_ids[1, i]
plot!([M2d[1,i], M2d[1,j]], [M2d[2,i], M2d[2,j]]; lw=0.4, lc=:steelblue, la=0.3, label="")
end
plot!(p; title="All-kNN graph (showing 500 edges)")Farthest-First Traversal (fft)
fft greedily picks m points that are as far apart as possible — good for seeding k-means or for selecting representative prototypes.
m = 30
sel_fft = fft(dist2d, db2d, m)
centers_fft = sel_fft.centers # indices of selected centersp = scatter(X, Y; fmt=:png, ms=2, msw=0, ma=0.3, c=:lightgray, label="data", size=(600,550))
scatter!(M2d[1, centers_fft], M2d[2, centers_fft];
ms=10, msw=1, c=:red, ma=0.9, label="FFT centers (m=$m)")
plot!(p; title="Farthest-First Traversal ($m centers)")Density Net (dnet)
dnet selects centers that are balanced by local density, so dense regions get more representatives and sparse regions fewer.
sel_dnet = dnet(dist2d, db2d, m)
centers_dnet = sel_dnet.centersp = scatter(X, Y; fmt=:png, ms=2, msw=0, ma=0.3, c=:lightgray, label="data", size=(600,550))
scatter!(M2d[1, centers_dnet], M2d[2, centers_dnet];
ms=10, msw=1, c=:darkorange, ma=0.9, label="DNet centers (m=$m)")
plot!(p; title="Density Net ($m centers)")Random selection (randsel / multirandsel)
sel_rand = randsel(dist2d, db2d, m)
sel_multi = multirandsel(dist2d, db2d, m)p1 = scatter(X, Y; fmt=:png, ms=2, msw=0, ma=0.3, c=:lightgray, label="", size=(580,500))
scatter!(M2d[1, sel_rand.centers], M2d[2, sel_rand.centers];
ms=10, msw=1, c=:purple, ma=0.9, label="randsel")
plot!(p1; title="randsel")
p2 = scatter(X, Y; fmt=:png, ms=2, msw=0, ma=0.3, c=:lightgray, label="", size=(580,500))
scatter!(M2d[1, sel_multi.centers], M2d[2, sel_multi.centers];
ms=10, msw=1, c=:darkgreen, ma=0.9, label="multirandsel")
plot!(p2; title="multirandsel")
plot(p1, p2; layout=(1,2), size=(1100, 480))Near-duplicate detection (neardup)
neardup groups items by distance threshold ε, finding each item’s representative center. Useful for dataset deduplication and compression.
# Use a tight ε: ~1% quantile of the 1-NN distance distribution
_, nn1_dists = searchbatch(G2d, ctx2d, db2d, 2)
ε = quantile(vec(nn1_dists[2, :]), 0.02)
sel_nd = neardup(dist2d, db2d, ε)
println("ε = $(round(ε, digits=4))")
println("Unique groups: $(length(sel_nd.centers)) / $n2d items")# Color each point by group (up to 50 groups shown distinctly)
assign_colors = map(sel_nd.assign) do a
HSL(360.0f0 * mod(a, 50) / 50, 0.7f0, 0.5f0)
end
scatter(X, Y; color=assign_colors, fmt=:png, ms=2.5, msw=0, ma=0.6,
label="", title="Near-dup groups (ε=$(round(ε,digits=4)))",
xticks=nothing, yticks=nothing)Application to Fashion-MNIST: prototype selection
using MLDatasets
fmnist = FashionMNIST(split=:train)
X_fmn = Float32.(reshape(fmnist.features, 784, :)) ./ 255f0
labels = fmnist.targets
db_fmn = MatrixDatabase(X_fmn)
dist_fmn = Dist.SqL2()
G_fmn = SearchGraph(dist_fmn, db_fmn)
ctx_fmn = SearchGraphContext(hyperparameters_callback=OptimizeParameters(MinRecall(0.95)))
index!(G_fmn, ctx_fmn)# Select 100 prototypes via FFT — these are the most diverse/representative images
m_fmn = 100
sel_fmn = fft(dist_fmn, db_fmn, m_fmn)
proto_ids = sel_fmn.centers# Show the 48 most representative Fashion-MNIST images
function show_grid(imgs, ids, ncols=12)
nrows = ceil(Int, length(ids) / ncols)
tiles = [Gray.(reshape(X_fmn[:, i], 28, 28)') for i in ids[1:min(end, ncols*nrows)]]
grid = reduce(hcat, [reduce(vcat, tiles[(r-1)*ncols+1:min(r*ncols, end)]) for r in 1:nrows])
plot(Gray.(grid); fmt=:png, axis=nothing, size=(ncols*28*3, nrows*28*3),
title="FFT prototypes ($m_fmn most representative)")
end
show_grid(X_fmn, proto_ids[1:48])# Near-duplicates in Fashion-MNIST (very tight threshold)
_, nn1_dists_fmn = searchbatch(G_fmn, ctx_fmn, SubDatabase(db_fmn, rand(1:60_000, 2000)), 2)
ε_fmn = quantile(vec(nn1_dists_fmn[2, :]), 0.005)
sel_nd_fmn = neardup(dist_fmn, db_fmn, ε_fmn)
println("Fashion-MNIST ε = $(round(ε_fmn, digits=5))")
println("Near-dup groups: $(length(sel_nd_fmn.centers)) / 60_000")# Show some near-duplicate pairs
fmnist_classes = ["T-shirt", "Trouser", "Pullover", "Dress", "Coat",
"Sandal", "Shirt", "Sneaker", "Bag", "Ankle boot"]
pairs_shown = 0
pair_imgs = []
for (ci, cid) in enumerate(sel_nd_fmn.centers)
members = findall(==(ci), sel_nd_fmn.assign)
length(members) < 2 && continue
push!(pair_imgs, cid, members[findfirst(!=(cid), members)])
pairs_shown += 1
pairs_shown >= 6 && break
end
if !isempty(pair_imgs)
tiles = [Gray.(reshape(X_fmn[:, i], 28, 28)') for i in pair_imgs]
grid = reduce(hcat, tiles)
plot(Gray.(grid); fmt=:png, axis=nothing, title="Near-duplicate pairs (left=center, right=member)",
size=(length(pair_imgs)*84, 90))
endEnvironment and dependencies
Julia Version 1.12.7 Commit 6d172b025e4 (2026-08-15 08:05 UTC) Build Info: Official https://julialang.org release Platform Info: OS: Linux (x86_64-linux-gnu) CPU: 64 × Intel(R) Xeon(R) Silver 4216 CPU @ 2.10GHz WORD_SIZE: 64 LLVM: libLLVM-18.1.7 (ORCJIT, cascadelake) GC: Built with stock GC Threads: 64 default, 1 interactive, 64 GC (on 64 virtual cores) Environment: JULIA_PROJECT = @. JULIA_NUM_THREADS = auto JULIA_LOAD_PATH = @:@stdlib Status `~/Research/SimilaritySearchDemos/Project.toml` [aaaa29a8] Clustering v0.15.8 [944b1d66] CodecZlib v0.7.9 [a93c6f00] DataFrames v1.8.2 [f67ccb44] HDF5 v0.17.3 [0f8b85d8] JSON3 v1.14.3 [23fbe1c1] Latexify v0.16.12 [eb30cadb] MLDatasets v0.7.21 [06eb3307] ManifoldLearning v0.9.0 ⌅ [ca7969ec] PlotlyLight v0.11.1 [91a5bcdd] Plots v1.41.7 [27ebfcd6] Primes v0.5.7 [92933f4c] ProgressMeter v1.11.0 [ca7ab67e] SimSearchManifoldLearning v0.4.0 `../SimSearchManifoldLearning.jl` [053f045d] SimilaritySearch v1.2.0 `../SimilaritySearch.jl` ⌅ [2913bbd2] StatsBase v0.33.21 [f3b207a7] StatsPlots v0.15.8 [7f6f6c8a] TextSearch v1.1.1 `../TextSearch.jl` Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated`