-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).