feat(explorer): fuzzy search — filter the block list by partial hash/height
Add a fuzzy mode to the explorer search: a non-numeric, non-full-hash query now filters the list to cached blocks whose hash (or height text) contains the query substring, live as you type. Backed by a new ExplorerBlockCache::searchBlocks() (SQLite LIKE with escaped wildcards), memoized per query so it doesn't hit the DB every frame. Exact queries still navigate precisely: a block height re-anchors the list, and a full 64-char hash is resolved via RPC. Row clicks still open the detail modal. Empty results show "No matching cached blocks". Note: fuzzy matching covers cached (browsed/prefetched) blocks only — the daemon has no partial-hash index — while exact height/hash lookups reach any block. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,45 @@ std::map<int, ExplorerBlockSummary> ExplorerBlockCache::loadRange(int minHeight,
|
||||
return blocks;
|
||||
}
|
||||
|
||||
std::vector<ExplorerBlockSummary> ExplorerBlockCache::searchBlocks(const std::string& query, int limit)
|
||||
{
|
||||
std::vector<ExplorerBlockSummary> results;
|
||||
if (query.empty() || limit < 1 || !ensureOpen()) return results;
|
||||
|
||||
// Escape LIKE wildcards in the user input so '%' / '_' are matched literally.
|
||||
std::string escaped;
|
||||
escaped.reserve(query.size());
|
||||
for (char c : query) {
|
||||
if (c == '%' || c == '_' || c == '\\') escaped.push_back('\\');
|
||||
escaped.push_back(c);
|
||||
}
|
||||
std::string pattern = "%" + escaped + "%";
|
||||
|
||||
Statement statement(db_,
|
||||
"SELECT height, hash, tx_count, size, time, difficulty "
|
||||
"FROM explorer_blocks "
|
||||
"WHERE CAST(height AS TEXT) LIKE ?1 ESCAPE '\\' OR hash LIKE ?1 ESCAPE '\\' "
|
||||
"ORDER BY height DESC LIMIT ?2");
|
||||
if (!statement.handle) return results;
|
||||
|
||||
sqlite3_bind_text(statement.handle, 1, pattern.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(statement.handle, 2, limit);
|
||||
|
||||
while (sqlite3_step(statement.handle) == SQLITE_ROW) {
|
||||
ExplorerBlockSummary block;
|
||||
block.height = sqlite3_column_int(statement.handle, 0);
|
||||
const unsigned char* hashText = sqlite3_column_text(statement.handle, 1);
|
||||
if (hashText) block.hash = reinterpret_cast<const char*>(hashText);
|
||||
block.tx_count = sqlite3_column_int(statement.handle, 2);
|
||||
block.size = sqlite3_column_int(statement.handle, 3);
|
||||
block.time = static_cast<std::int64_t>(sqlite3_column_int64(statement.handle, 4));
|
||||
block.difficulty = sqlite3_column_double(statement.handle, 5);
|
||||
if (block.height > 0 && !block.hash.empty()) results.push_back(std::move(block));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
bool ExplorerBlockCache::storeBlock(const ExplorerBlockSummary& block)
|
||||
{
|
||||
if (block.height < 1 || block.hash.empty() || !ensureOpen()) return false;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct sqlite3;
|
||||
|
||||
@@ -41,6 +42,9 @@ public:
|
||||
const std::string& databasePath() const { return database_path_; }
|
||||
|
||||
std::map<int, ExplorerBlockSummary> loadRange(int minHeight, int maxHeight);
|
||||
// Fuzzy search over cached blocks: matches when the query is a substring of the height (as text)
|
||||
// or the block hash (case-insensitive). Returns newest-first, capped at `limit`.
|
||||
std::vector<ExplorerBlockSummary> searchBlocks(const std::string& query, int limit);
|
||||
bool storeBlock(const ExplorerBlockSummary& block);
|
||||
int cachedBlockCount();
|
||||
void clearBlocks();
|
||||
|
||||
Reference in New Issue
Block a user