Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ gleamsver = ">= 1.0.1 and < 2.0.0"
porter_stemmer = ">= 1.0.0 and < 2.0.0"
gleam_time = ">= 1.2.0 and < 2.0.0"
gleam_httpc = ">= 5.0.0 and < 6.0.0"
edit_distance = ">= 3.0.0 and < 4.0.0"
cell = ">= 1.0.0 and < 2.0.0"

[dev-dependencies]
gleeunit = "~> 1.0"
4 changes: 4 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

packages = [
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
{ name = "cell", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "cell", source = "hex", outer_checksum = "4200D6FD95E7E720F9376FD4670E7ACC2E6847CF129514D91EB1583218E1C351" },
{ name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" },
{ name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" },
{ name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
Expand Down Expand Up @@ -38,6 +40,8 @@ packages = [

[requirements]
argv = { version = "~> 1.0" }
cell = { version = ">= 1.0.0 and < 2.0.0" }
edit_distance = { version = ">= 3.0.0 and < 4.0.0" }
envoy = { version = ">= 1.0.2 and < 2.0.0" }
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
gleam_hexpm = { version = ">= 3.0.0 and < 4.0.0" }
Expand Down
3 changes: 2 additions & 1 deletion src/packages/error.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ pub type Error {
HttpClientError(httpc.HttpError)
JsonDecodeError(json.DecodeError)
StorageError(storail.StorailError)
EtsTableError
TextIndexEtsTableError
KnownWordsEtsTableError
}
18 changes: 14 additions & 4 deletions src/packages/router.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,23 @@ fn internet_points(ctx: Context) -> Response {

fn search(request: Request, context: Context) -> Response {
let search_term = get_search_parameter(request)
let assert Ok(packages) = case search_term {
"" -> storage.packages_most_recent_first(context.db)
let assert Ok(search_outcome) = case search_term {
"" ->
storage.packages_most_recent_first(context.db)
|> result.map(storage.Packages)

_ -> storage.search_packages(context.db, context.search_index, search_term)
}

page.packages_list(packages, search_term)
|> wisp.html_response(200)
case search_outcome {
storage.Packages(packages:) ->
page.packages_list(packages, search_term)
|> wisp.html_response(200)

storage.DidYouMean(suggestion:) ->
page.did_you_mean(suggestion, search_term)
|> wisp.html_response(200)
}
}

fn get_search_parameter(request: Request) -> String {
Expand Down
87 changes: 62 additions & 25 deletions src/packages/storage.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -370,39 +370,44 @@ pub fn list_packages(database: Database) -> Result(List(String), Error) {
}
}

pub type SearchOutcome {
Packages(packages: List(Package))
DidYouMean(suggestion: String)
}

pub fn search_packages(
db: Database,
search: text_search.TextSearchIndex,
search_term: String,
) -> Result(List(Package), Error) {
let bool = fn(b) {
case b {
True -> 1
False -> 0
}
}
) -> Result(SearchOutcome, Error) {
use found <- result.try(text_search.lookup(search, search_term))

case found {
[_, ..] ->
rank_found_results(found, db, search_term)
|> result.map(Packages)

// If no results are found we try and suggest a fix for the search term.
[] ->
case text_search.did_you_mean(search, search_term) {
Ok(suggestion) -> Ok(DidYouMean(suggestion:))
Error(_) -> Ok(Packages(packages: []))
}
}
}

/// Given a list of `text_search` results, this returns a list of the matching
/// packages, ranked from most relevant to least relevant.
///
fn rank_found_results(
found: List(text_search.Found),
db: Database,
search_term: String,
) -> Result(List(Package), Error) {
use packages <- result.map(
list.try_map(found, fn(found) {
use package <- result.map(get_package(db, found.name))

let exact_package_name_match = bool(search_term == package.name)
let is_not_v0 = bool(!string.starts_with(package.latest_version, "0."))
let is_core_package = bool(override.is_core_package(package.name))
let updated_at =
float.round(timestamp.to_unix_seconds(package.updated_in_hex_at))

// This is the value we use to determine what order packages should be
// shown by. Later list values only take effect if the earlier ones are
// equal.
let ordering_key = [
exact_package_name_match,
is_not_v0,
found.match_count,
is_core_package,
package.downloads_recent,
updated_at,
]
let ordering_key = package_ordering_key(search_term, package, found)
#(ordering_key, package)
}),
)
Expand All @@ -412,6 +417,38 @@ pub fn search_packages(
|> list.map(fn(pair) { pair.1 })
}

/// This is the value we use to determine what order packages should be shown
/// by.
///
fn package_ordering_key(
search_term: String,
package: Package,
found: text_search.Found,
) -> List(Int) {
let bool = fn(bool) {
case bool {
True -> 1
False -> 0
}
}

let exact_package_name_match = bool(search_term == package.name)
let is_not_v0 = bool(!string.starts_with(package.latest_version, "0."))
let is_core_package = bool(override.is_core_package(package.name))
let updated_at =
float.round(timestamp.to_unix_seconds(package.updated_in_hex_at))

// Later list values only take effect if the earlier ones are equal.
[
exact_package_name_match,
is_not_v0,
found.match_count,
is_core_package,
package.downloads_recent,
updated_at,
]
}

fn list_compare(
a: List(t),
b: List(t),
Expand Down
Loading