Packages

Fuzzy string matching, distance, similarity, ranking, and entity resolution in Gleam

Current section

Files

Jump to
starfuzz src starfuzz@search.erl
Raw

src/starfuzz@search.erl

-module(starfuzz@search).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/starfuzz/search.gleam").
-export([new/2, by/2, using/2, with_normalizer/2, with_minimum_score/2, with_limit/2, run/2, strings/2]).
-export_type([match/1, search_builder/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A module providing fuzzy search collections matching capabilities.\n"
" Collections of custom candidates can be searched in-memory with custom selectors,\n"
" similarity scoring functions, score thresholds, and limits.\n"
).
-type match(EII) :: {match, EII, float(), integer()}.
-type search_builder(EIJ) :: {search_builder,
list(EIJ),
fun((EIJ) -> binary()),
fun((binary(), binary()) -> float()),
gleam@option:option(starfuzz@normalize:normalizer()),
float(),
gleam@option:option(integer())}.
-file("src/starfuzz/search.gleam", 37).
?DOC(
" Creates a new SearchBuilder with the list of candidates and a string selector function.\n"
" Defaults to using Levenshtein similarity, no normalizer, minimum score of 0.0, and no limit.\n"
).
-spec new(list(EIK), fun((EIK) -> binary())) -> search_builder(EIK).
new(Candidates, Selector) ->
{search_builder,
Candidates,
Selector,
fun starfuzz@similarity:levenshtein/2,
none,
+0.0,
none}.
-file("src/starfuzz/search.gleam", 49).
?DOC(" Configures a custom selector function to retrieve the string value to match from candidates.\n").
-spec by(search_builder(EIN), fun((EIN) -> binary())) -> search_builder(EIN).
by(Builder, Selector) ->
{search_builder,
erlang:element(2, Builder),
Selector,
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder)}.
-file("src/starfuzz/search.gleam", 54).
?DOC(" Configures the similarity scoring function to use for comparison (e.g. `similarity.jaro_winkler`).\n").
-spec using(search_builder(EIQ), fun((binary(), binary()) -> float())) -> search_builder(EIQ).
using(Builder, Scorer) ->
{search_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
Scorer,
erlang:element(5, Builder),
erlang:element(6, Builder),
erlang:element(7, Builder)}.
-file("src/starfuzz/search.gleam", 59).
?DOC(" Configures the normalizer to clean up both query and candidate strings before scoring.\n").
-spec with_normalizer(search_builder(EIT), starfuzz@normalize:normalizer()) -> search_builder(EIT).
with_normalizer(Builder, Normalizer) ->
{search_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
{some, Normalizer},
erlang:element(6, Builder),
erlang:element(7, Builder)}.
-file("src/starfuzz/search.gleam", 64).
?DOC(" Configures the minimum score threshold. Matches below this score are discarded.\n").
-spec with_minimum_score(search_builder(EIW), float()) -> search_builder(EIW).
with_minimum_score(Builder, Min_score) ->
{search_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
Min_score,
erlang:element(7, Builder)}.
-file("src/starfuzz/search.gleam", 69).
?DOC(" Configures a limit on the number of matches returned.\n").
-spec with_limit(search_builder(EIZ), integer()) -> search_builder(EIZ).
with_limit(Builder, Limit) ->
{search_builder,
erlang:element(2, Builder),
erlang:element(3, Builder),
erlang:element(4, Builder),
erlang:element(5, Builder),
erlang:element(6, Builder),
{some, Limit}}.
-file("src/starfuzz/search.gleam", 106).
-spec compare_matches(match(EJG), match(EJG)) -> gleam@order:order().
compare_matches(A, B) ->
case erlang:element(3, A) > erlang:element(3, B) of
true ->
lt;
false ->
case erlang:element(3, A) < erlang:element(3, B) of
true ->
gt;
false ->
case erlang:element(4, A) < erlang:element(4, B) of
true ->
lt;
false ->
gt
end
end
end.
-file("src/starfuzz/search.gleam", 75).
?DOC(
" Runs the fuzzy search query against the candidates list.\n"
" Returns matched candidates sorted descending by score, with original index-based tie-breaking.\n"
).
-spec run(search_builder(EJC), binary()) -> list(match(EJC)).
run(Builder, Query) ->
Norm_query = case erlang:element(5, Builder) of
{some, Normalizer} ->
starfuzz@normalize:apply(Normalizer, Query);
none ->
Query
end,
Candidates_indexed = gleam@list:index_map(
erlang:element(2, Builder),
fun(C, I) -> {C, I} end
),
Matches = gleam@list:fold(
Candidates_indexed,
[],
fun(Acc, Item) ->
{Candidate, I@1} = Item,
Raw_val = (erlang:element(3, Builder))(Candidate),
Val = case erlang:element(5, Builder) of
{some, Normalizer@1} ->
starfuzz@normalize:apply(Normalizer@1, Raw_val);
none ->
Raw_val
end,
Score = (erlang:element(4, Builder))(Norm_query, Val),
case Score >= erlang:element(6, Builder) of
true ->
[{match, Candidate, Score, I@1} | Acc];
false ->
Acc
end
end
),
Sorted = gleam@list:sort(Matches, fun compare_matches/2),
case erlang:element(7, Builder) of
{some, L} ->
gleam@list:take(Sorted, L);
none ->
Sorted
end.
-file("src/starfuzz/search.gleam", 124).
?DOC(" Quick direct search over a list of strings using default Levenshtein similarity.\n").
-spec strings(binary(), list(binary())) -> list(match(binary())).
strings(Query, Candidates) ->
_pipe = new(Candidates, fun(X) -> X end),
run(_pipe, Query).