using SimilaritySearch, SimSearchManifoldLearning, Primes, Plots, StatsBase, LinearAlgebra, Markdown, RandomPrime numbers and set distances
by: Eric S. Téllez
This demonstration represents integers by their prime factor sets and uses set-distance functions to find numerically similar numbers. It is a self-contained demo — no dataset download needed — that shows SearchGraph with sparse set data, UMAP visualization, and the allknn operation.
Note: set the JULIA_NUM_THREADS=auto environment variable (or pass -t auto to Julia) for best performance.
Dataset
Each integer is represented as the sorted vector of its distinct prime factors.
n = 100_000
F = Vector{Vector{Int32}}(undef, n + 1)
encode_factors(num) = sort!(Int32[convert(Int32, f) for f in factor(Set, num)])
F[1] = Int32[1]
for i in 2:n+1
F[i] = encode_factors(i)
end
db = VectorDatabase(F)
dist = Dist.Sets.Dice() # other options:
# dist = Dist.Sets.Jaccard()
# dist = Dist.Sets.Intersection()
# dist = Dist.Sets.CosineSet()Index construction
- 1
-
Positional constructor
SearchGraph(dist, db)with the set distance. - 2
- Indexes all 100 001 integers.
- 3
- Optimizes beam-search hyperparameters for 90 % recall.
Searching
function search_and_render(index, ctx, qnum, qenc, res, k)
res = reuse!(res, k)
@time search(index, ctx, qenc, res)
L = [
"""## Result list for num: $(qnum), factors: $qenc""",
"""| nn | num | factors | dist |""",
"""|----|-----|---------|------|"""
]
for (j, p) in enumerate(IdDistView(res))
push!(L, """| $j | $(p.id) | $(index[p.id]) | $(round(p.dist, digits=3)) |""")
end
Markdown.parse(join(L, "\n"))
end
res = knnqueue(ctx, 12)
for qnum in [42, 51, 1000, 1492, 3192]
qenc = encode_factors(qnum)
search_and_render(G, ctx, qnum, qenc, res, maxlength(res)) |> display
end 0.000308 seconds (3 allocations: 64 bytes)
0.000676 seconds (3 allocations: 64 bytes)
0.000340 seconds (3 allocations: 64 bytes)
0.000675 seconds (3 allocations: 64 bytes)
0.000105 seconds (3 allocations: 64 bytes)
Result list for num: 42, factors: Int32[2, 3, 7]
| nn | num | factors | dist |
|---|---|---|---|
| 1 | 42 | Int32[2, 3, 7] | 0.0 |
| 2 | 84 | Int32[2, 3, 7] | 0.0 |
| 3 | 126 | Int32[2, 3, 7] | 0.0 |
| 4 | 168 | Int32[2, 3, 7] | 0.0 |
| 5 | 252 | Int32[2, 3, 7] | 0.0 |
| 6 | 294 | Int32[2, 3, 7] | 0.0 |
| 7 | 336 | Int32[2, 3, 7] | 0.0 |
| 8 | 378 | Int32[2, 3, 7] | 0.0 |
| 9 | 504 | Int32[2, 3, 7] | 0.0 |
| 10 | 756 | Int32[2, 3, 7] | 0.0 |
| 11 | 588 | Int32[2, 3, 7] | 0.0 |
| 12 | 1008 | Int32[2, 3, 7] | 0.0 |
Result list for num: 51, factors: Int32[3, 17]
| nn | num | factors | dist |
|---|---|---|---|
| 1 | 51 | Int32[3, 17] | 0.0 |
| 2 | 2601 | Int32[3, 17] | 0.0 |
| 3 | 23409 | Int32[3, 17] | 0.0 |
| 4 | 153 | Int32[3, 17] | 0.0 |
| 5 | 459 | Int32[3, 17] | 0.0 |
| 6 | 867 | Int32[3, 17] | 0.0 |
| 7 | 1377 | Int32[3, 17] | 0.0 |
| 8 | 4131 | Int32[3, 17] | 0.0 |
| 9 | 7803 | Int32[3, 17] | 0.0 |
| 10 | 12393 | Int32[3, 17] | 0.0 |
| 11 | 14739 | Int32[3, 17] | 0.0 |
| 12 | 37179 | Int32[3, 17] | 0.0 |
Result list for num: 1000, factors: Int32[2, 5]
| nn | num | factors | dist |
|---|---|---|---|
| 1 | 800 | Int32[2, 5] | 0.0 |
| 2 | 10 | Int32[2, 5] | 0.0 |
| 3 | 20 | Int32[2, 5] | 0.0 |
| 4 | 40 | Int32[2, 5] | 0.0 |
| 5 | 50 | Int32[2, 5] | 0.0 |
| 6 | 80 | Int32[2, 5] | 0.0 |
| 7 | 100 | Int32[2, 5] | 0.0 |
| 8 | 160 | Int32[2, 5] | 0.0 |
| 9 | 200 | Int32[2, 5] | 0.0 |
| 10 | 250 | Int32[2, 5] | 0.0 |
| 11 | 320 | Int32[2, 5] | 0.0 |
| 12 | 400 | Int32[2, 5] | 0.0 |
Result list for num: 1492, factors: Int32[2, 373]
| nn | num | factors | dist |
|---|---|---|---|
| 1 | 11936 | Int32[2, 373] | 0.0 |
| 2 | 23872 | Int32[2, 373] | 0.0 |
| 3 | 47744 | Int32[2, 373] | 0.0 |
| 4 | 746 | Int32[2, 373] | 0.0 |
| 5 | 1492 | Int32[2, 373] | 0.0 |
| 6 | 2984 | Int32[2, 373] | 0.0 |
| 7 | 5968 | Int32[2, 373] | 0.0 |
| 8 | 95488 | Int32[2, 373] | 0.0 |
| 9 | 2238 | Int32[2, 3, 373] | 0.2 |
| 10 | 10444 | Int32[2, 7, 373] | 0.2 |
| 11 | 9698 | Int32[2, 13, 373] | 0.2 |
| 12 | 17158 | Int32[2, 23, 373] | 0.2 |
Result list for num: 3192, factors: Int32[2, 3, 7, 19]
| nn | num | factors | dist |
|---|---|---|---|
| 1 | 798 | Int32[2, 3, 7, 19] | 0.0 |
| 2 | 1596 | Int32[2, 3, 7, 19] | 0.0 |
| 3 | 2394 | Int32[2, 3, 7, 19] | 0.0 |
| 4 | 3192 | Int32[2, 3, 7, 19] | 0.0 |
| 5 | 4788 | Int32[2, 3, 7, 19] | 0.0 |
| 6 | 5586 | Int32[2, 3, 7, 19] | 0.0 |
| 7 | 6384 | Int32[2, 3, 7, 19] | 0.0 |
| 8 | 7182 | Int32[2, 3, 7, 19] | 0.0 |
| 9 | 9576 | Int32[2, 3, 7, 19] | 0.0 |
| 10 | 11172 | Int32[2, 3, 7, 19] | 0.0 |
| 11 | 12768 | Int32[2, 3, 7, 19] | 0.0 |
| 12 | 14364 | Int32[2, 3, 7, 19] | 0.0 |
All-kNN and recall evaluation
allknn computes the exact kNN for every item in the dataset. Here we compare the approximate SearchGraph result against ExhaustiveSearch to measure recall.
k = 10
# Exact baseline (only feasible on a small sample for demo purposes)
sample_n = 3_000
sample_idx = randperm(n + 1)[1:sample_n]
db_sample = SubDatabase(db, sample_idx)
exact_idx = ExhaustiveSearch(dist, db_sample)
exact_ctx = GenericContext()
gold_ids, gold_dists = allknn(exact_idx, exact_ctx, k)
# Approximate with SearchGraph built on the same sample
G_sample = SearchGraph(dist, db_sample)
ctx_sample = SearchGraphContext(hyperparameters_callback=OptimizeParameters(MinRecall(0.95)))
index!(G_sample, ctx_sample)
approx_ids, _ = allknn(G_sample, ctx_sample, k)
recall = macrorecall(gold_ids, approx_ids)
@info "All-kNN recall: $(round(recall, digits=4))"UMAP visualization
e2, e3 = let min_dist=0.5f0, k=20, n_epochs=75, neg_sample_rate=3, tol=1e-3,
layout=SpectralLayout()
@time "2D UMAP" U2 = fit(UMAP, G; k, neg_sample_rate, layout, n_epochs, tol, min_dist)
@time "3D UMAP" U3 = fit(U2, 3; neg_sample_rate, n_epochs, tol)
@time "predict 2D" e2 = clamp.(predict(U2), -10f0, 10f0)
@time "predict 3D" e3 = clamp.(predict(U3), -10f0, 10f0)
e2, e3
endfunction normcolors(V)
mn, mx = extrema(V)
@. V = clamp((V - mn) / (mx - mn), 0f0, 1f0)
end
normcolors(@view e3[1, :])
normcolors(@view e3[2, :])
normcolors(@view e3[3, :])
let C = [RGB(c[1], c[2], c[3]) for c in eachcol(e3)],
X = view(e2, 1, :), Y = view(e2, 2, :)
scatter(X, Y; color=C, fmt=:png, alpha=0.2, size=(600, 600),
ma=0.3, ms=2, msw=0, label="",
yticks=nothing, xticks=nothing, xaxis=false, yaxis=false,
title="Prime factors (n=100k) — UMAP coloured by 3D embedding")
end
plot!()Environment 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`