Packages

A blazing fast, memory-efficient Trie (prefix tree) implementation for Elixir with autocomplete support, powered by a high-performance Rust NIF.

Current section

Files

Jump to
trie_hard lib trie_hard.ex
Raw

lib/trie_hard.ex

defmodule TrieHard do
@moduledoc """
A blazing fast, memory-efficient Trie (prefix tree) implementation for Elixir
with autocomplete support, powered by the trie_hard_rs Rust library.
## Features
- **Fast operations**: Sub-microsecond autocomplete performance
- **Memory efficient**: Shared prefix storage
- **Unicode support**: Full UTF-8 character support
- **Generic values**: Store any Erlang term as values
- **Batch operations**: Efficient bulk insertions
- **Thread-safe**: Concurrent access support
## Example
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "cat", :feline)
:ok
iex> TrieHard.insert(trie, "car", %{type: "vehicle", wheels: 4})
:ok
iex> TrieHard.insert(trie, "card", {"payment", "plastic"})
:ok
iex> TrieHard.get(trie, "cat")
{:ok, :feline}
iex> {:ok, results} = TrieHard.auto_complete(trie, "ca", 10)
iex> Enum.sort(results)
["car", "card", "cat"]
"""
alias TrieHard.Native
@type trie :: reference()
@type key :: String.t()
@type value :: term()
@doc """
Creates a new empty Trie.
## Examples
iex> trie = TrieHard.new()
iex> is_reference(trie)
true
"""
@spec new() :: trie()
def new do
Native.new_trie()
end
@doc """
Inserts a key-value pair into the trie.
## Parameters
- `trie` - The trie reference
- `key` - The key to insert (string)
- `value` - The value to associate with the key (any Erlang term)
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "hello", "world")
:ok
iex> TrieHard.insert(trie, "config", %{timeout: 5000, retry: true})
:ok
iex> TrieHard.insert(trie, "data", {:ok, [1, 2, 3]})
:ok
"""
@spec insert(trie(), key(), value()) :: :ok | :error
def insert(trie, key, value) when is_binary(key) do
Native.insert(trie, key, value)
end
@doc """
Retrieves a value by exact key match.
## Parameters
- `trie` - The trie reference
- `key` - The key to look up
## Returns
- `{:ok, value}` if the key exists
- `{:not_found, nil}` if the key doesn't exist
- `{:error, nil}` on internal error
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "hello", "world")
:ok
iex> TrieHard.get(trie, "hello")
{:ok, "world"}
iex> TrieHard.insert(trie, "data", {:ok, %{id: 123}})
:ok
iex> TrieHard.get(trie, "data")
{:ok, {:ok, %{id: 123}}}
iex> TrieHard.get(trie, "missing")
{:not_found, nil}
"""
@spec get(trie(), key()) :: {:ok, value()} | {:not_found, nil} | {:error, nil}
def get(trie, key) when is_binary(key) do
case Native.get(trie, key) do
{:ok, value} when is_binary(value) ->
# Try to decode if it's an Erlang external term format
try do
decoded = :erlang.binary_to_term(value)
{:ok, decoded}
rescue
ArgumentError ->
# If decoding fails, it's just a binary value
{:ok, value}
end
other ->
other
end
end
@doc """
Removes a key and its associated value from the trie.
## Parameters
- `trie` - The trie reference
- `key` - The key to remove
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "hello", "world")
:ok
iex> TrieHard.delete(trie, "hello")
:ok
iex> TrieHard.get(trie, "hello")
{:not_found, nil}
"""
@spec delete(trie(), key()) :: :ok | :error
def delete(trie, key) when is_binary(key) do
Native.delete(trie, key)
end
@doc """
Checks if any words in the trie start with the given prefix.
## Parameters
- `trie` - The trie reference
- `prefix` - The prefix to search for
## Returns
- `{:ok, true}` if words with the prefix exist
- `{:ok, false}` if no words with the prefix exist
- `{:error, false}` on internal error
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "cat", "feline")
:ok
iex> TrieHard.prefix_search(trie, "ca")
{:ok, true}
iex> TrieHard.prefix_search(trie, "dog")
{:ok, false}
"""
@spec prefix_search(trie(), String.t()) :: {:ok, boolean()} | {:error, false}
def prefix_search(trie, prefix) when is_binary(prefix) do
Native.prefix_search(trie, prefix)
end
@doc """
Returns words that start with the given prefix, up to max_results.
## Parameters
- `trie` - The trie reference
- `prefix` - The prefix to search for
- `max_results` - Maximum number of results to return (default: 10)
## Returns
- `{:ok, [String.t()]}` - List of matching words
- `{:error, []}` on internal error
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.insert(trie, "cat", "1")
:ok
iex> TrieHard.insert(trie, "car", "2")
:ok
iex> TrieHard.insert(trie, "card", "3")
:ok
iex> {:ok, results} = TrieHard.auto_complete(trie, "ca", 2)
iex> length(results) <= 2
true
"""
@spec auto_complete(trie(), String.t(), non_neg_integer()) :: {:ok, [String.t()]} | {:error, []}
def auto_complete(trie, prefix, max_results \\ 10)
when is_binary(prefix) and is_integer(max_results) do
Native.auto_complete(trie, prefix, max_results)
end
@doc """
Efficiently inserts multiple words at once.
Each word will be inserted with itself as the value.
## Parameters
- `trie` - The trie reference
- `words` - List of words to insert
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.add_word_list(trie, ["cat", "car", "card"])
:ok
iex> TrieHard.get(trie, "cat")
{:ok, "cat"}
"""
@spec add_word_list(trie(), [String.t()]) :: :ok | :error
def add_word_list(trie, words) when is_list(words) do
Native.add_word_list(trie, words)
end
@doc """
Returns the number of words with a given prefix.
This is a convenience function that uses auto_complete with unlimited results.
## Examples
iex> trie = TrieHard.new()
iex> TrieHard.add_word_list(trie, ["cat", "car", "card", "dog"])
:ok
iex> TrieHard.count_prefix(trie, "ca")
{:ok, 3}
iex> TrieHard.count_prefix(trie, "do")
{:ok, 1}
"""
@spec count_prefix(trie(), String.t()) :: {:ok, non_neg_integer()} | {:error, 0}
def count_prefix(trie, prefix) when is_binary(prefix) do
case auto_complete(trie, prefix, :erlang.system_info(:wordsize) * 8) do
{:ok, results} -> {:ok, length(results)}
{:error, _} -> {:error, 0}
end
end
end