sezaru

sezaru

Suggestions to improve my group by string similarity algorithm performance

Hey everyone, I made an algorithm which will take a list of strings with its correspondent trigrams and group them by similarity. It basically returns the same similarity score as the PostgreSQL pg_trgm extension.

For my inputs, I have something like this:

values = [
  %{name: "Eduardo", trigram: MapSet.new(["  e"," ed","ard","do ","dua","edu","rdo","uar"])},
  %{name: "Jeferson", trigram: MapSet.new(["  j"," je","efe","ers","fer","jef","on ","rso","son"])},
  %{name: "Eduardo B.", trigram: MapSet.new(["  b","  e"," b "," ed","ard","do ","dua","edu","rdo","uar"])},
  %{name: "Jefferson", trigram: MapSet.new(["  j"," je","eff","ers","fer","ffe","jef","on ","rso","son"])},
  %{name: "Maria", trigram: MapSet.new(["  m"," ma","ari","ia ","mar","ria"])}
]

The trigrams are generated directly by PostgreSQL using the show_trgm function (ex. select show_trgm('Eduardo');)

defmodule GroupBySimilarity do
  def group(values, threshold \\ 0.9)
  
  def group([], _threshold), do: []

  def group([value | rest], threshold) do
    %{trigram: trigram} = value

    {similar, rest} =
      Enum.split_with(rest, fn %{trigram: rhs_trigram} -> similarity(trigram, rhs_trigram) >= threshold end)

    [[value] ++ similar] ++ group(rest, threshold)
  end

  defp similarity(lhs, rhs) do
    intersection_count = lhs |> MapSet.intersection(rhs) |> MapSet.size()
    union_count = lhs |> MapSet.union(rhs) |> MapSet.size()

    intersection_count / union_count
  end
end

As you can see, the algorithm is pretty simple, it splits the values list based on the similarity/2 function and then repeats the process with the remaining values until the list is empty.

So, for the example values above, the result would be:

iex(194)> GroupBySimilarity.group(values, 0.7)
[
  [
    %{
      name: "Eduardo",
      trigram: MapSet.new(["  e", " ed", "ard", "do ", "dua", "edu", "rdo",
       "uar"])
    },
    %{
      name: "Eduardo B.",
      trigram: MapSet.new(["  b", "  e", " b ", " ed", "ard", "do ", "dua",
       "edu", "rdo", "uar"])
    }
  ],
  [
    %{
      name: "Jeferson",
      trigram: MapSet.new(["  j", " je", "efe", "ers", "fer", "jef", "on ",
       "rso", "son"])
    },
    %{
      name: "Jefferson",
      trigram: MapSet.new(["  j", " je", "eff", "ers", "fer", "ffe", "jef",
       "on ", "rso", "son"])
    }
  ],
  [
    %{
      name: "Maria",
      trigram: MapSet.new(["  m", " ma", "ari", "ia ", "mar", "ria"])
    }
  ]
]

This works great, but the problem is that the algorithm is very slow when the input is bigger, for example, for 10_000 values, it will take around 279 seconds.

I would love any suggestion on how to improve its performance and lower its complexity.

Most Liked

RudManusachi

RudManusachi

Hi @sezaru

the lowest hanging fruits to harvest I see is following some recommendation from Erlang Efficiency Guide # List Handling

  1. prepend to list with | instead of “concatenating” with ++

    [[value] ++ similar] ++ group(rest, threshold)
    

    into

    [[value | similar] | group(rest, threshold)]
    
  2. tail-call optimization might also show some improvement. However, as mentioned in the section Recursive List Functions

    measure before rewriting your code.


Now the bigger question is if it’s possible to reduce the complexity of the algorithm, because currently as I see it’s O(N^2) (if I understood it correctly)…

Maybe we could come up with some algorithm that could build the result in at least O(N Log N) or ideally O(N) (I’m not sure if it’s possible, didn’t give it too much thought yet)
But usually for these type of problems we might want to have a map where we would store some intermediate info along by iterating through the list and lookup in that store if we could reuse some results from there.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

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
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
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
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement