Current section
Files
Jump to
Current section
Files
test/angular_index_test.exs
defmodule AnnoyExAngularIndexTest do
use ExUnit.Case, async: true
import AnnoyTestHelper
defp check_precision(n, n_trees \\ 10, n_points \\ 10000, n_rounds \\ 10, search_k \\ 100_000) do
founds =
for _r <- 0..(n_rounds - 1) do
f = 10
i = AnnoyEx.new(f, :angular)
for j <- 0..(n_points - 1) do
p = normal_list(f - 1)
norm = :math.pow(Enum.sum(Enum.map(p, fn pi -> :math.pow(pi, 2) end)), 0.5)
x = Enum.map(p, fn pi -> pi / norm * j end) |> List.insert_at(0, 1000)
AnnoyEx.add_item(i, j, x)
end
AnnoyEx.build(i, n_trees)
v = Enum.map(1..(f - 1), fn _ -> 0 end) |> List.insert_at(0, 1000)
{nns, _} = AnnoyEx.get_nns_by_vector(i, v, n, search_k)
assert nns == Enum.sort(nns)
length(Enum.filter(nns, fn x -> x < n end))
end
1.0 * Enum.sum(founds) / (n * n_rounds)
end
test "get_nns_by_vector" do
f = 3
i = AnnoyEx.new(f, :angular)
AnnoyEx.add_item(i, 0, [0, 0, 1])
AnnoyEx.add_item(i, 1, [0, 1, 0])
AnnoyEx.add_item(i, 2, [1, 0, 0])
AnnoyEx.build(i, 10)
{res, _} = AnnoyEx.get_nns_by_vector(i, [3, 2, 1], 3)
assert res == [2, 1, 0]
{res, _} = AnnoyEx.get_nns_by_vector(i, [1, 2, 3], 3)
assert res == [0, 1, 2]
{res, _} = AnnoyEx.get_nns_by_vector(i, [2, 0, 1], 3)
assert res == [2, 0, 1]
end
test "get_nns_by_item" do
f = 3
i = AnnoyEx.new(f, :angular)
AnnoyEx.add_item(i, 0, [2, 1, 0])
AnnoyEx.add_item(i, 1, [1, 2, 0])
AnnoyEx.add_item(i, 2, [0, 0, 1])
AnnoyEx.build(i, 10)
{res, _} = AnnoyEx.get_nns_by_item(i, 0, 3)
assert res == [0, 1, 2]
{res, _} = AnnoyEx.get_nns_by_item(i, 1, 3)
assert res == [1, 0, 2]
{res, _} = AnnoyEx.get_nns_by_item(i, 2, 3)
assert res == [2, 0, 1] || res == [2, 1, 0]
end
test "dist" do
i = AnnoyEx.new(2, :angular)
AnnoyEx.add_item(i, 0, [0, 1])
AnnoyEx.add_item(i, 1, [1, 1])
assert_in_delta(
AnnoyEx.get_distance(i, 0, 1),
:math.pow(2 * (1.0 - :math.pow(2, -0.5)), 0.5),
0.1
)
end
test "dist_2" do
i = AnnoyEx.new(2, :angular)
AnnoyEx.add_item(i, 0, [1000, 0])
AnnoyEx.add_item(i, 1, [10, 0])
assert_in_delta(AnnoyEx.get_distance(i, 0, 1), 0, 0.01)
end
test "dist_3" do
i = AnnoyEx.new(2, :angular)
AnnoyEx.add_item(i, 0, [97, 0])
AnnoyEx.add_item(i, 1, [42, 42])
dist =
:math.pow(:math.pow(1.0 - :math.pow(2, -0.5), 2) + :math.pow(:math.pow(2, -0.5), 2), 0.5)
assert_in_delta(AnnoyEx.get_distance(i, 0, 1), dist, 0.01)
end
test "dist_degen" do
i = AnnoyEx.new(2, :angular)
AnnoyEx.add_item(i, 0, [1, 0])
AnnoyEx.add_item(i, 1, [0, 0])
assert_in_delta(AnnoyEx.get_distance(i, 0, 1), :math.pow(2.0, 0.5), 0.01)
end
test "large index" do
f = 10
i = AnnoyEx.new(f, :angular)
for j <- 0..9999//2 do
p = Enum.map(0..(f - 1), fn _ -> random_gauss() end)
f1 = :rand.uniform() + 1
f2 = :rand.uniform() + 1
x = Enum.map(p, fn pi -> f1 * pi + random_gauss(0, 0.01) end)
y = Enum.map(p, fn pi -> f2 * pi + random_gauss(0, 0.01) end)
AnnoyEx.add_item(i, j, x)
AnnoyEx.add_item(i, j + 1, y)
end
AnnoyEx.build(i, 10)
for j <- 0..9999//2 do
{res, _} = AnnoyEx.get_nns_by_item(i, j, 2)
assert res == [j, j + 1]
{res, _} = AnnoyEx.get_nns_by_item(i, j + 1, 2)
assert res == [j + 1, j]
end
end
test "precision_1" do
assert check_precision(1) >= 0.98
end
test "precision_10" do
assert check_precision(10) >= 0.98
end
test "precision_100" do
assert check_precision(100) >= 0.98
end
test "precision_1000" do
assert check_precision(1000) >= 0.98
end
@tag :tmp_dir
test "load save get item vector", %{tmp_dir: tmp_dir} do
f = 3
i = AnnoyEx.new(f, :angular)
AnnoyEx.add_item(i, 0, [1.1, 2.2, 3.3])
AnnoyEx.add_item(i, 1, [4.4, 5.5, 6.6])
AnnoyEx.add_item(i, 2, [7.7, 8.8, 9.9])
v = AnnoyEx.get_item_vector(i, 0)
assert_in_delta(Enum.at(v, 0), 1.1, 0.01)
assert_in_delta(Enum.at(v, 1), 2.2, 0.01)
assert_in_delta(Enum.at(v, 2), 3.3, 0.01)
assert AnnoyEx.build(i, 10)
assert AnnoyEx.save(i, Path.join(tmp_dir, "blah.ann"))
v = AnnoyEx.get_item_vector(i, 1)
assert_in_delta(Enum.at(v, 0), 4.4, 0.01)
assert_in_delta(Enum.at(v, 1), 5.5, 0.01)
assert_in_delta(Enum.at(v, 2), 6.6, 0.01)
j = AnnoyEx.new(f, :angular)
assert AnnoyEx.load(j, Path.join(tmp_dir, "blah.ann"))
v = AnnoyEx.get_item_vector(i, 2)
assert_in_delta(Enum.at(v, 0), 7.7, 0.01)
assert_in_delta(Enum.at(v, 1), 8.8, 0.01)
assert_in_delta(Enum.at(v, 2), 9.9, 0.01)
end
test "get_nns_search_k" do
i = AnnoyEx.new(3, :angular)
AnnoyEx.add_item(i, 0, [0, 0, 1])
AnnoyEx.add_item(i, 1, [0, 1, 0])
AnnoyEx.add_item(i, 2, [1, 0, 0])
AnnoyEx.build(i, 10)
{res, _} = AnnoyEx.get_nns_by_item(i, 0, 3, 10)
assert res == [0, 1, 2]
{res, _} = AnnoyEx.get_nns_by_vector(i, [3, 2, 1], 3, 10)
assert res == [2, 1, 0]
end
test "include dists" do
f = 40
i = AnnoyEx.new(f, :angular)
l1 = normal_list(f)
l2 = Enum.map(l1, fn x -> -x end)
AnnoyEx.add_item(i, 0, l1)
AnnoyEx.add_item(i, 1, l2)
AnnoyEx.build(i, 10)
{indices, dists} = AnnoyEx.get_nns_by_item(i, 0, 2, 10, true)
assert indices == [0, 1]
assert_in_delta(Enum.at(dists, 0), 0.0, 0.01)
assert_in_delta(Enum.at(dists, 1), 2.0, 0.01)
end
test "include_dists_check_ranges" do
f = 3
i = AnnoyEx.new(f, :angular)
for j <- 0..99999 do
v = normal_list(f)
AnnoyEx.add_item(i, j, v)
end
AnnoyEx.build(i, 10)
{_indices, dists} = AnnoyEx.get_nns_by_item(i, 0, 100_000, -1, true)
assert Enum.max(dists) <= 2.0
assert_in_delta(Enum.min(dists), 0.0, 0.01)
end
test "distance consistency" do
{n, f} = {1000, 3}
i = AnnoyEx.new(f, :angular)
for j <- 0..(n - 1) do
AnnoyEx.add_item(i, j, normal_list(f))
end
AnnoyEx.build(i, 10)
for a <- Enum.take_random(0..(n - 1), 100) do
{indices, dists} = AnnoyEx.get_nns_by_item(i, a, 100)
for {b, dist} <- Enum.zip(indices, dists) do
assert_in_delta(dist, AnnoyEx.get_distance(i, a, b), 0.01)
u = AnnoyEx.get_item_vector(i, a)
v = AnnoyEx.get_item_vector(i, b)
u_norm = Enum.map(u, fn x -> x * :math.pow(dot_product(u, u), -0.5) end)
v_norm = Enum.map(v, fn x -> x * :math.pow(dot_product(v, v), -0.5) end)
# self.assertAlmostEqual(dist ** 2, numpy.dot(u_norm - v_norm, u_norm - v_norm))
norm_diff = Enum.zip_reduce(u_norm, v_norm, [], fn x, y, acc -> [x - y | acc] end)
assert_in_delta(:math.pow(dist, 2), dot_product(norm_diff, norm_diff), 0.01)
# self.assertAlmostEqual(dist ** 2, sum([(x-y)**2 for x, y in zip(u_norm, v_norm)]))
assert_in_delta(
:math.pow(dist, 2),
Enum.zip_with(u_norm, v_norm, fn x, y -> :math.pow(x - y, 2) end) |> Enum.sum(),
0.01
)
end
end
end
@tag :tmp_dir
test "only_one_item", %{tmp_dir: tmp_dir} do
index_file = Path.join(tmp_dir, "foo.idx")
idx = AnnoyEx.new(100, :angular)
v = normal_list(100)
AnnoyEx.add_item(idx, 0, v)
AnnoyEx.build(idx, 10)
AnnoyEx.save(idx, index_file)
idx = AnnoyEx.new(100, :angular)
AnnoyEx.load(idx, index_file)
assert AnnoyEx.get_n_items(idx) == 1
v = normal_list(100)
{res, _} = AnnoyEx.get_nns_by_vector(idx, v, 50)
assert res == [0]
end
@tag :tmp_dir
test "no_items", %{tmp_dir: tmp_dir} do
index_file = Path.join(tmp_dir, "foo.idx")
idx = AnnoyEx.new(100, :angular)
AnnoyEx.build(idx, 10)
AnnoyEx.save(idx, index_file)
idx = AnnoyEx.new(100, :angular)
AnnoyEx.load(idx, index_file)
assert AnnoyEx.get_n_items(idx) == 0
v = normal_list(100)
{res, _} = AnnoyEx.get_nns_by_vector(idx, v, 50, -1, false)
assert res == []
end
@tag :tmp_dir
test "single_vector", %{tmp_dir: tmp_dir} do
a = AnnoyEx.new(3, :angular)
AnnoyEx.add_item(a, 0, [1, 0, 0])
AnnoyEx.build(a, 10)
AnnoyEx.save(a, Path.join(tmp_dir, "1.ann"))
{indices, dists} = AnnoyEx.get_nns_by_vector(a, [1, 0, 0], 3, -1, true)
assert indices == [0]
assert_in_delta(:math.pow(Enum.at(dists, 0), 2), 0.0, 0.01)
end
end