From 68ec71db112bc9f659b1cffa5aa5f9f9084333a1 Mon Sep 17 00:00:00 2001 From: Guillaume Hivert Date: Sun, 19 May 2024 01:12:06 +0200 Subject: [PATCH] feat: add ability to search in docs & select search Signed-off-by: Guillaume Hivert --- .../src/backend/postgres/queries.gleam | 34 ++++++++- apps/backend/src/backend/router.gleam | 72 +++++++++++++++---- apps/frontend/src/data/model.gleam | 13 ++-- apps/frontend/src/data/search_result.gleam | 6 +- apps/frontend/src/frontend.gleam | 2 +- apps/frontend/src/frontend/strings.gleam | 2 + .../src/frontend/view/body/body.gleam | 4 +- .../src/frontend/view/body/cache.gleam | 7 ++ 8 files changed, 111 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/backend/postgres/queries.gleam b/apps/backend/src/backend/postgres/queries.gleam index 972c9cc..f3fdb8a 100644 --- a/apps/backend/src/backend/postgres/queries.gleam +++ b/apps/backend/src/backend/postgres/queries.gleam @@ -519,8 +519,8 @@ pub fn type_search_to_json(item) { ]) } -pub fn search(db: pgo.Connection, q: String) { - let query = pgo.text("'" <> q <> "'") +pub fn signature_search(db: pgo.Connection, q: String) { + let query = pgo.text(q) "SELECT DISTINCT ON (package_rank, type_name, signature_kind, module_name) s.name type_name, s.documentation, @@ -539,7 +539,35 @@ pub fn search(db: pgo.Connection, q: String) { ON m.package_release_id = r.id JOIN package p ON p.id = r.package_id - WHERE to_tsvector('english', s.signature_) @@ to_tsquery($1) + WHERE to_tsvector('english', s.signature_) @@ websearch_to_tsquery($1) + ORDER BY package_rank DESC, type_name, signature_kind, module_name, ordering DESC + LIMIT 100" + |> pgo.execute(db, [query], decode_type_search) + |> result.map_error(error.DatabaseError) + |> result.map(fn(r) { r.rows }) +} + +pub fn documentation_search(db: pgo.Connection, q: String) { + let query = pgo.text(q) + "SELECT DISTINCT ON (package_rank, type_name, signature_kind, module_name) + s.name type_name, + s.documentation, + s.kind signature_kind, + s.metadata, + s.json_signature, + m.name module_name, + p.name, + r.version, + p.rank package_rank, + string_to_array(regexp_replace(r.version, '([0-9]+).([0-9]+).([0-9]+).*', '\\1.\\2.\\3'), '.')::int[] AS ordering + FROM package_type_fun_signature s + JOIN package_module m + ON m.id = s.package_module_id + JOIN package_release r + ON m.package_release_id = r.id + JOIN package p + ON p.id = r.package_id + WHERE to_tsvector('english', s.documentation) @@ websearch_to_tsquery($1) ORDER BY package_rank DESC, type_name, signature_kind, module_name, ordering DESC LIMIT 100" |> pgo.execute(db, [query], decode_type_search) diff --git a/apps/backend/src/backend/router.gleam b/apps/backend/src/backend/router.gleam index 35dca17..ef71c09 100644 --- a/apps/backend/src/backend/router.gleam +++ b/apps/backend/src/backend/router.gleam @@ -4,10 +4,13 @@ import backend/error import backend/postgres/queries import backend/web import cors_builder as cors +import gleam/bool import gleam/http import gleam/json import gleam/list +import gleam/pair import gleam/result +import gleam/string import gleam/string_builder import tasks/hex as syncing import wisp.{type Request, type Response} @@ -19,28 +22,67 @@ fn empty_json() { |> wisp.json_response(200) } +fn isolate_filters(query: String) -> #(String, List(String)) { + string.split(query, " ") + |> list.fold(#([], []), fn(acc, val) { + case val { + "in:signature" | "in:name" | "in:documentation" -> #(acc.0, [val, ..acc.1]) + _ -> #([val, ..acc.0], acc.1) + } + }) + |> pair.map_first(list.reverse) + |> pair.map_first(string.join(_, " ")) + |> pair.map_second(fn(filters) { + let no_filters = list.is_empty(filters) + use <- bool.guard(when: no_filters, return: ["in:signature", "in:name"]) + filters + }) +} + fn search(query: String, ctx: Context) { wisp.log_notice("Searching for " <> query) - let exact_matches = - queries.name_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - let matches = - queries.content_search(ctx.db, query) - |> result.map_error(error.debug_log) - |> result.unwrap([]) - |> list.filter(fn(i) { !list.contains(exact_matches, i) }) - json.object([ - #("exact-matches", json.array(exact_matches, queries.type_search_to_json)), - #("matches", json.array(matches, queries.type_search_to_json)), - #("searches", { - queries.search(ctx.db, query) + let #(query, filters) = isolate_filters(query) + let exact_matches = case list.contains(filters, "in:name") { + False -> [] + True -> + queries.name_search(ctx.db, query) + |> result.map_error(error.debug_log) + |> result.unwrap([]) + } + let matches = case list.contains(filters, "in:signature") { + False -> [] + True -> + queries.content_search(ctx.db, query) + |> result.map_error(error.debug_log) + |> result.unwrap([]) + |> list.filter(fn(i) { !list.contains(exact_matches, i) }) + } + let signature_searches = case list.contains(filters, "in:signature") { + False -> [] + True -> + queries.signature_search(ctx.db, query) |> result.map_error(error.debug_log) |> result.unwrap([]) |> list.filter(fn(i) { !list.contains(list.append(exact_matches, matches), i) }) - |> json.array(queries.type_search_to_json) + } + let documentation_searches = case list.contains(filters, "in:documentation") { + False -> [] + True -> + queries.documentation_search(ctx.db, query) + |> result.map_error(error.debug_log) + |> result.unwrap([]) + |> list.filter(fn(i) { + !list.contains(list.append(exact_matches, matches), i) + }) + } + json.object([ + #("exact-matches", json.array(exact_matches, queries.type_search_to_json)), + #("matches", json.array(matches, queries.type_search_to_json)), + #("searches", json.array(signature_searches, queries.type_search_to_json)), + #("docs-searches", { + json.array(documentation_searches, queries.type_search_to_json) }), ]) } diff --git a/apps/frontend/src/data/model.gleam b/apps/frontend/src/data/model.gleam index daeea85..80ac464 100644 --- a/apps/frontend/src/data/model.gleam +++ b/apps/frontend/src/data/model.gleam @@ -25,7 +25,7 @@ pub fn init() { let search_results = search_result.Start let index = compute_index(search_results) Model( - input: "in:name in:signature ", + input: "", search_results: search_results, index: index, loading: False, @@ -50,8 +50,8 @@ pub fn update_search_results(model: Model, search_results: SearchResults) { let index = compute_index(search_results) let view_cache = case search_results { search_result.Start | search_result.InternalServerError -> element.none() - search_result.SearchResults(e, m, s) -> - cache.cache_search_results(index, e, m, s) + search_result.SearchResults(e, m, s, d) -> + cache.cache_search_results(index, e, m, s, d) } Model( ..model, @@ -63,8 +63,8 @@ pub fn update_search_results(model: Model, search_results: SearchResults) { pub fn reset(_model: Model) { Model( - search_results: search_result.SearchResults([], [], []), - input: "in:name in:signature ", + search_results: search_result.SearchResults([], [], [], []), + input: "", index: [], loading: False, view_cache: element.none(), @@ -75,11 +75,12 @@ pub fn reset(_model: Model) { fn compute_index(search_results: SearchResults) -> Index { case search_results { search_result.Start | search_result.InternalServerError -> [] - search_result.SearchResults(exact, others, searches) -> { + search_result.SearchResults(exact, others, searches, docs) -> { [] |> insert_module_names(exact) |> insert_module_names(others) |> insert_module_names(searches) + |> insert_module_names(docs) |> list.map(fn(i) { pair.map_second(i, list.reverse) }) } } diff --git a/apps/frontend/src/data/search_result.gleam b/apps/frontend/src/data/search_result.gleam index c020729..17c2d28 100644 --- a/apps/frontend/src/data/search_result.gleam +++ b/apps/frontend/src/data/search_result.gleam @@ -23,7 +23,8 @@ pub type SearchResults { SearchResults( exact_matches: List(SearchResult), matches: List(SearchResult), - searches: List(SearchResult), + signature_searches: List(SearchResult), + docs_searches: List(SearchResult), ) } @@ -46,11 +47,12 @@ pub fn decode_search_results(dyn) { dynamic.decode1(fn(_) { InternalServerError }, { dynamic.field("error", dynamic.string) }), - dynamic.decode3( + dynamic.decode4( SearchResults, dynamic.field("exact-matches", dynamic.list(decode_search_result)), dynamic.field("matches", dynamic.list(decode_search_result)), dynamic.field("searches", dynamic.list(decode_search_result)), + dynamic.field("docs-searches", dynamic.list(decode_search_result)), ), ])(dyn) } diff --git a/apps/frontend/src/frontend.gleam b/apps/frontend/src/frontend.gleam index cd87ba8..d04811e 100644 --- a/apps/frontend/src/frontend.gleam +++ b/apps/frontend/src/frontend.gleam @@ -143,7 +143,7 @@ fn handle_search_results( fn handle_route_change(model: Model, route: router.Route) { let model = model.update_route(model, route) update.none(case route { - router.Home -> model.update_input(model, "in:name in:signature ") + router.Home -> model.update_input(model, "") router.Search(q) -> model.update_input(model, q) }) } diff --git a/apps/frontend/src/frontend/strings.gleam b/apps/frontend/src/frontend/strings.gleam index f4c2143..20ae062 100644 --- a/apps/frontend/src/frontend/strings.gleam +++ b/apps/frontend/src/frontend/strings.gleam @@ -6,6 +6,8 @@ pub const partial_match = "Partly matched from the signature of functions, const pub const searches_match = "Matched from a document search in functions, constants or types." +pub const docs_match = "Matched from a documentation search in functions, constants or types." + pub const retry_query = "Retry with a different query. You can match functions, types or constants names, as well as functions types directly!" pub const internal_server_error = "Internal server error. The error should be fixed soon. Please, retry later." diff --git a/apps/frontend/src/frontend/view/body/body.gleam b/apps/frontend/src/frontend/view/body/body.gleam index 673cbd2..5554db7 100644 --- a/apps/frontend/src/frontend/view/body/body.gleam +++ b/apps/frontend/src/frontend/view/body/body.gleam @@ -63,13 +63,13 @@ pub fn body(model: Model) { title: "Internal server error", content: frontend_strings.internal_server_error, ) - search_result.SearchResults([], [], []) -> + search_result.SearchResults([], [], [], []) -> empty_state( image: images.shadow_lucy, title: "No match found!", content: frontend_strings.retry_query, ) - search_result.SearchResults(_, _, _) -> model.view_cache + search_result.SearchResults(_, _, _, _) -> model.view_cache } }, ]) diff --git a/apps/frontend/src/frontend/view/body/cache.gleam b/apps/frontend/src/frontend/view/body/cache.gleam index 4aa71a7..cac03f7 100644 --- a/apps/frontend/src/frontend/view/body/cache.gleam +++ b/apps/frontend/src/frontend/view/body/cache.gleam @@ -111,6 +111,7 @@ pub fn cache_search_results( exact: List(search_result.SearchResult), others: List(search_result.SearchResult), searches: List(search_result.SearchResult), + docs_searches: List(search_result.SearchResult), ) { s.search_results_wrapper([], [ sidebar(index), @@ -121,6 +122,12 @@ pub fn cache_search_results( view_search_results(others), match_title(searches, "Searches matches", frontend_strings.searches_match), view_search_results(searches), + match_title( + docs_searches, + "Documentation matches", + frontend_strings.docs_match, + ), + view_search_results(docs_searches), ]), ]) }