jaybe78

jaybe78

Search random elements in a big ETS table (> 1M)

Hey,

I need help to find a scalable and efficient solution to:

  1. Store large number of elements (username)
  2. Table is not static => element gets removed from the table when process is demonitored
  3. Being able to return random elements efficiently

The context is that I’m storing some users based on criterias in different ETS tables and wants to be able to return those in my UI as part of a search.

First thing:

I’m dealing with username as keys, and at the same time, I think the easiest way to get random elements from an ETS table is to use sequential number as keys

i.e

:ets.lookup(tab, Enum.random(1..1000)) 

So I though about using 3 ETS tables:

  1. The first to store the username with corresponding index “usernames”
  2. The second to store all the user indexes “user_indexes”
  3. the last one to increment the total count of user added to get the next index “count”

Add user scenario

  1. Get the next index from “count”
  2. Add a new index in “user_indexes”
  3. Add corresponding username in “usernames” with that index
  4. Monitor the process

That’s my initial idea but the problem is that when users gets removed from the table, the indexes are not sequential anymore ({1..4,..6..400..})

In that situation the lookup function I use to get random elements would not return the expected results

:ets.lookup(tab, Enum.random(1..200)) 

There’s an other solution which is to work directly on the username keys

first = :ets.first(tab)
:ets.lookup(tab, first)
func = fn key->
    if function_that_may_return_true() do
        key = case :ets.next(tab, key) do
         :'$end_of_table' -> throw :reached_end_of_table
         key -> func.(key)
        end
    else 
        :ets.lookup(tab, key)
    end
end

Though that solution is not efficient for large tables.

At this point I’m not sure what I could do ?

Cheers

Marked As Solved

jstimps

jstimps

What if you were to insert into an :ordered_set table with a uniformly random key? Then the first N items in the table would be properly shuffled ahead of time. When you want to sample N items, you just choose the first N you encounter with :ets.first/:ets.next and then reshuffle them (by deleting and re-inserting).

Some disadvantages

  • deleting and re-inserting should be atomic, hence the GenServer
  • collisions theoretically possible

Also Liked

garrison

garrison

Lol I read the OP and knew there had to be matchmaking involved. I don’t think I fully understand the use-case here but putting that aside for a moment…

This is what I was going to suggest, but I see you figured it out first :slight_smile: I was feeling so clever, too!

But why do you have to sample from the start? Sample from a random index in the uniform space and just scan until you hit the quota. Wrap around if you hit the end.

I don’t think this is equivalent to a true random sample but for this use-case I don’t think anyone will be able to tell.

Edit: Actually if you choose 100 random indices and call :ets.next_lookup() with them (instead of scanning from the first) I think this would be a truly random sample. Right?

1..100
|> Enum.map(fn _ -> Enum.random(1..1_000_000) end)
|> Enum.map(fn i ->
  {_, [{_, value}]} = :ets.next_lookup(table, i)
  value
end)

Of course you could get duplicates but there are ways to deal with that.

Since the keys are already truly random there should be no bias from adding/removing users, no matter which users they are.

al2o3cr

al2o3cr

I meant “draw” in the lottery sense:

  • get an index with Enum.random(1..current_max)
  • fetch the element with that key
  • if no element, try again

This would work fine for systems with infrequent deletions, since most of the time the first lookup succeeds.

It would be extremely BAD for a high-churn system, since current_max would be increasing constantly but the table is mostly unoccupied. The average runtime of a “pick a random user” would just get worse and worse…

jstimps

jstimps

I think it should be pretty efficient due to the :ordered_set, which should get you O(log n). Only way to know for sure is to test in your specific use case.

As @garrison pointed out though this scheme might not be rigorously uniform. Some simulation testing might be helpful to confirm it’s fairness.

LostKobrakai

LostKobrakai

For matchmaking wouldn’t you want to prefer people having waited longer over people just having joined the queue? Also would there really be a million people waiting in the queue or rather be a million people playing? If you form couples quickly enought the queue might stay at a reasonable size.

jstimps

jstimps

Ah yes, you can start at a random location by generating a fake key and then wrap around. That seems to fix the problem. I updated the GitHub gist above with this idea… code was done in haste, so don’t judge me too hard :slight_smile:

Where Next?

Popular in Questions Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement