Skip to content

Commit

Permalink
Add /api/v2/search/quick method
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitosing committed Aug 16, 2023
1 parent 14c2a7c commit 13274d6
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- [#8218](https://github.com/blockscout/blockscout/pull/8218) - Add `/api/v2/search/quick` method
- [#8156](https://github.com/blockscout/blockscout/pull/8156) - Add `is_verified_via_admin_panel` property to tokens table
- [#8165](https://github.com/blockscout/blockscout/pull/8165), [#8201](https://github.com/blockscout/blockscout/pull/8201) - Add broadcast of updated address_current_token_balances
- [#7952](https://github.com/blockscout/blockscout/pull/7952) - Add parsing constructor arguments for sourcify contracts
Expand Down
1 change: 1 addition & 0 deletions apps/block_scout_web/lib/block_scout_web/api_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ defmodule BlockScoutWeb.ApiRouter do
scope "/search" do
get("/", V2.SearchController, :search)
get("/check-redirect", V2.SearchController, :check_redirect)
get("/quick", V2.SearchController, :quick_search)
end

scope "/config" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ defmodule BlockScoutWeb.API.V2.SearchController do

import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1, from_param: 1]

alias Explorer.Chain
alias Explorer.{Chain, PagingOptions}

@api_true [api?: true]

def search(conn, %{"q" => query} = params) do
[paging_options: paging_options] = paging_options(params)
offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size

search_results_plus_one =
paging_options
|> Chain.joint_search(offset, query, api?: true)
|> Chain.joint_search(offset, query, @api_true)

{search_results, next_page} = split_list_by_page(search_results_plus_one)

Expand All @@ -32,4 +34,12 @@ defmodule BlockScoutWeb.API.V2.SearchController do
|> put_status(200)
|> render(:search_results, %{result: result})
end

def quick_search(conn, %{"q" => query}) do
search_results = Chain.balanced_unpaginated_search(%PagingOptions{page_size: 50}, query, @api_true)

conn
|> put_status(200)
|> render(:search_results, %{search_results: search_results})
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ defmodule BlockScoutWeb.API.V2.SearchView do
use BlockScoutWeb, :view

alias BlockScoutWeb.Endpoint
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Chain.{Address, Block, Hash, Transaction}

def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do
%{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params}
end

def render("search_results.json", %{search_results: search_results}) do
Enum.map(search_results, &prepare_search_result/1)
end

def render("search_results.json", %{result: {:ok, result}}) do
Map.merge(%{"redirect" => true}, redirect_search_results(result))
end
Expand Down Expand Up @@ -69,6 +73,7 @@ defmodule BlockScoutWeb.API.V2.SearchView do
}
end

defp hash_to_string(%Hash{bytes: bytes}), do: hash_to_string(bytes)
defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)

defp redirect_search_results(%Address{} = item) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do

alias Explorer.Chain.{Address, Block}
alias Explorer.Repo
alias Explorer.Tags.AddressTag

setup do
insert(:block)
Expand Down Expand Up @@ -288,4 +289,44 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do
%{"redirect" => false, "type" => nil, "parameter" => nil} = json_response(request, 200)
end
end

describe "/search/quick" do
test "check that all categories are in response list", %{conn: conn} do
name = "156000"

tags =
for _ <- 0..50 do
insert(:address_to_tag, tag: build(:address_tag, display_name: name))
end

contracts = insert_list(50, :smart_contract, name: name)
tokens = insert_list(50, :token, name: name)
blocks = [insert(:block, number: name, consensus: false), insert(:block, number: name)]

request = get(conn, "/api/v2/search/quick?q=#{name}")
assert response = json_response(request, 200)
assert Enum.count(response) == 50

assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address"] end) ==
tags |> Enum.reverse() |> Enum.take(16) |> Enum.map(fn tag -> Address.checksum(tag.address.hash) end)

assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address"] end) ==
contracts
|> Enum.reverse()
|> Enum.take(16)
|> Enum.map(fn contract -> Address.checksum(contract.address_hash) end)

assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address"] end) ==
tokens
|> Enum.reverse()
|> Enum.sort_by(fn x -> x.is_verified_via_admin_panel end, :desc)
|> Enum.take(16)
|> Enum.map(fn token -> Address.checksum(token.contract_address_hash) end)

block_hashes = response |> Enum.filter(fn x -> x["type"] == "block" end) |> Enum.map(fn x -> x["block_hash"] end)

assert block_hashes == blocks |> Enum.reverse() |> Enum.map(fn block -> to_string(block.hash) end) ||
block_hashes == blocks |> Enum.map(fn block -> to_string(block.hash) end)
end
end
end
121 changes: 121 additions & 0 deletions apps/explorer/lib/explorer/chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,127 @@ defmodule Explorer.Chain do
end
end

def balanced_unpaginated_search(paging_options, raw_search_query, options \\ []) do
search_query = String.trim(raw_search_query)

case prepare_search_term(search_query) do
{:some, term} ->
tokens_result =
term
|> search_token_query()
|> order_by([token],
desc_nulls_last: token.circulating_market_cap,
desc_nulls_last: token.fiat_value,
desc_nulls_last: token.is_verified_via_admin_panel,
desc_nulls_last: token.holder_count,
asc: token.name,
desc: token.inserted_at
)
|> limit(^paging_options.page_size)
|> select_repo(options).all()

contracts_result =
term
|> search_contract_query()
|> order_by([items], asc: items.name, desc: items.inserted_at)
|> limit(^paging_options.page_size)
|> select_repo(options).all()

labels_result =
term
|> search_label_query()
|> order_by([att, at], asc: at.display_name, desc: att.inserted_at)
|> limit(^paging_options.page_size)
|> select_repo(options).all()

tx_result =
if query = search_tx_query(search_query) do
query
|> select_repo(options).all()
else
[]
end

address_result =
if query = search_address_query(search_query) do
query
|> select_repo(options).all()
else
[]
end

blocks_result =
if query = search_block_query(search_query) do
query
|> limit(^paging_options.page_size)
|> select_repo(options).all()
else
[]
end

non_empty_lists =
[tokens_result, contracts_result, labels_result, tx_result, address_result, blocks_result]
|> Enum.filter(fn list -> Enum.count(list) > 0 end)
|> Enum.sort_by(fn list -> Enum.count(list) end, :asc)

to_take =
non_empty_lists
|> Enum.map(fn list -> Enum.count(list) end)
|> take_all_categories(List.duplicate(0, Enum.count(non_empty_lists)), paging_options.page_size)

non_empty_lists
|> Enum.zip_reduce(to_take, [], fn x, y, acc -> acc ++ Enum.take(x, y) end)
|> Enum.map(fn result ->
result
|> compose_result_checksummed_address_hash()
|> format_timestamp()
end)

_ ->
[]
end
end

defp take_all_categories(lengths, taken_lengths, remained) do
non_zero_count = count_non_zero(lengths)

target = if(remained < non_zero_count, do: 1, else: div(remained, non_zero_count))

{lengths_updated, %{result: taken_lengths_reversed}} =
Enum.map_reduce(lengths, %{result: [], sum: 0}, fn el, acc ->
taken =
cond do
acc[:sum] >= remained ->
0

el < target ->
el

true ->
target
end

{el - taken, %{result: [taken | acc[:result]], sum: acc[:sum] + taken}}
end)

taken_lengths =
taken_lengths
|> Enum.zip_reduce(Enum.reverse(taken_lengths_reversed), [], fn x, y, acc -> [x + y | acc] end)
|> Enum.reverse()

remained = remained - Enum.sum(taken_lengths_reversed)

if remained > 0 and count_non_zero(lengths_updated) > 0 do
take_all_categories(lengths_updated, taken_lengths, remained)
else
taken_lengths
end
end

defp count_non_zero(list) do
Enum.reduce(list, 0, fn el, acc -> acc + if el > 0, do: 1, else: 0 end)
end

defp compose_result_checksummed_address_hash(result) do
if result.address_hash do
result
Expand Down
12 changes: 8 additions & 4 deletions apps/explorer/test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,18 @@ defmodule Explorer.Factory do

def address_to_tag_factory do
%AddressToTag{
tag: %AddressTag{
label: sequence("label"),
display_name: sequence("display_name")
},
tag: build(:address_tag),
address: build(:address)
}
end

def address_tag_factory do
%AddressTag{
label: sequence("label"),
display_name: sequence("display_name")
}
end

def account_watchlist_address_factory do
hash = build(:address).hash

Expand Down

0 comments on commit 13274d6

Please sign in to comment.