diff --git a/Cargo.lock b/Cargo.lock index ffc1f6229..a4cb07d0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1066,9 +1066,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1251,9 +1251,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fjall" -version = "3.0.4" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ebf22b812878dcd767879cb19e03124fd62563dce6410f96538175fba0c132d" +checksum = "b62b25b4d815ae178d7d9e4aa32ee59f072efd5431c736abede1e6ee13c8c453" dependencies = [ "byteorder-lite", "byteview", @@ -1261,7 +1261,7 @@ dependencies = [ "flume", "log", "lsm-tree", - "lz4_flex 0.11.6", + "lz4_flex", "tempfile", "xxhash-rust", ] @@ -2010,9 +2010,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lsm-tree" -version = "3.0.4" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9bfd2a6ea0c1d430c13643002f35800a87f200fc8ac4827f18a2db9d9fd0644" +checksum = "e447ac67ff6aef4ec07fc19e507b219336cbba90a697c0dbeb1bf51b91536b67" dependencies = [ "byteorder-lite", "byteview", @@ -2020,7 +2020,7 @@ dependencies = [ "enum_dispatch", "interval-heap", "log", - "lz4_flex 0.11.6", + "lz4_flex", "quick_cache", "rustc-hash", "self_cell", @@ -2030,20 +2030,14 @@ dependencies = [ "xxhash-rust", ] -[[package]] -name = "lz4_flex" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" -dependencies = [ - "twox-hash", -] - [[package]] name = "lz4_flex" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] [[package]] name = "matchit" @@ -2387,9 +2381,9 @@ dependencies = [ [[package]] name = "quickmatch" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848244615004bddb7273545dfe909ead495ed734f9faf130c43a7daccca2bf99" +checksum = "d6abd98fde5c9aa23590316caedc9e90be29da035d33905cdae71a54f034aaac" dependencies = [ "rustc-hash", ] @@ -3150,9 +3144,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags 2.11.1", @@ -3360,7 +3354,7 @@ dependencies = [ "itoa", "libc", "log", - "lz4_flex 0.13.1", + "lz4_flex", "parking_lot", "pco", "rawdb", diff --git a/Cargo.toml b/Cargo.toml index 783498c33..392dae299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ color-eyre = "0.6.5" corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false } corepc-types = { version = "0.13.0", features = ["std"], default-features = false } derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] } -fjall = "=3.0.4" +fjall = "3.1.4" indexmap = { version = "2.14.0", features = ["serde"] } jiff = { version = "0.2.24", features = ["perf-inline", "tz-system"], default-features = false } owo-colors = "4.3.0" @@ -79,7 +79,7 @@ serde_derive = "1.0.228" serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] } smallvec = "1.15.1" tokio = { version = "1.52.3", features = ["rt-multi-thread"] } -tower-http = { version = "0.6.10", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] } +tower-http = { version = "0.6.11", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] } tower-layer = "0.3" tracing = { version = "0.1", default-features = false, features = ["std"] } ureq = { version = "3.3.0", features = ["json"] } diff --git a/LICENSE b/LICENSE index 238f558de..f5bc84632 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 bitcoinresearchkit +Copyright (c) 2025 Bitcoin Research Kit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 08a09c2e1..a65f550ea 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8953,7 +8953,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.3.0-beta.9"; + pub const VERSION: &'static str = "v0.3.0-beta.11"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { diff --git a/crates/brk_query/Cargo.toml b/crates/brk_query/Cargo.toml index 70701ba49..b150c0173 100644 --- a/crates/brk_query/Cargo.toml +++ b/crates/brk_query/Cargo.toml @@ -26,7 +26,7 @@ derive_more = { workspace = true } jiff = { workspace = true } parking_lot = { workspace = true } # quickmatch = { path = "../../../quickmatch" } -quickmatch = "0.4.0" +quickmatch = "0.5.0" rustc-hash = { workspace = true } smallvec = { workspace = true } tokio = { workspace = true, optional = true } diff --git a/modules/quickmatch-js/0.4.1/src/index.js b/modules/quickmatch-js/0.5.0/src/index.js similarity index 87% rename from modules/quickmatch-js/0.4.1/src/index.js rename to modules/quickmatch-js/0.5.0/src/index.js index b238ee553..b18dd908d 100644 --- a/modules/quickmatch-js/0.4.1/src/index.js +++ b/modules/quickmatch-js/0.5.0/src/index.js @@ -102,6 +102,10 @@ export class QuickMatch { for (let i = 0; i < words.length - 1; i++) { const compound = words[i] + words[i + 1]; + // A joined-word query ("hashrate") can be longer than any single + // word. Capping at the longest index key keeps the DDoS guard + // data-bounded while still letting it match. + if (compound.length > maxWordLen) maxWordLen = compound.length; const from = words[i].length + 1; for (let len = from; len <= compound.length; len++) { addToIndex(this.wordIndex, compound.slice(0, len), idx); @@ -238,24 +242,29 @@ export class QuickMatch { */ _rank(indices, minScore, qwords, sep, limit) { const { items, _scores: scores } = this; - /** @type {[number[], number[], number[]]} */ - const buckets = [[], [], []]; // ps=0, ps=1, ps=2 + /** @type {[number, number][][]} */ + const buckets = Array.from({ length: qwords.length + 1 }, () => []); for (let i = 0; i < indices.length; i++) { const idx = indices[i]; if (minScore !== null && scores[idx] < minScore) continue; - buckets[prefixScore(items[idx], qwords, sep)].push(idx); + const [matched, position] = wordMatch(items[idx], qwords, sep); + buckets[matched].push([idx, position]); } const results = []; - for (let ps = 2; ps >= 0 && results.length < limit; ps--) { + for (let ps = buckets.length - 1; ps >= 0 && results.length < limit; ps--) { const bucket = buckets[ps]; if (!bucket.length) continue; bucket.sort( - (a, b) => scores[b] - scores[a] || items[a].length - items[b].length, + ([a, pa], [b, pb]) => + scores[b] - scores[a] || + pa - pb || + items[a].length - items[b].length || + (items[a] < items[b] ? -1 : 1), // item text, asc (total order) ); const take = Math.min(bucket.length, limit - results.length); - for (let i = 0; i < take; i++) results.push(items[bucket[i]]); + for (let i = 0; i < take; i++) results.push(items[bucket[i][0]]); } return results; @@ -377,29 +386,35 @@ function bsearch(arr, val) { return false; } -/** @param {string} item @param {string[]} qwords @param {Uint8Array} sep */ -function prefixScore(item, qwords, sep) { - let qi = 0, - pos = 0; +/** + * Aligns query words against the item's words, in order. + * @param {string} item @param {string[]} qwords @param {Uint8Array} sep + * @returns {[number, number]} `[matched, position]` - query words matched as + * an in-order subsequence, and the item-word index where that run starts + * (or the item's word count when nothing matched). + */ +function wordMatch(item, qwords, sep) { const len = item.length; + let matched = 0; + let position = 0; + let pos = 0; - while (qi < qwords.length) { + while (pos < len) { while (pos < len && sep[item.charCodeAt(pos)]) pos++; - if (pos >= len) return 0; + if (pos >= len) break; const ws = pos; while (pos < len && !sep[item.charCodeAt(pos)]) pos++; - const qw = qwords[qi]; - if (pos - ws < qw.length) return 0; - for (let j = 0; j < qw.length; j++) { - if (item.charCodeAt(ws + j) !== qw.charCodeAt(j)) return 0; + const qw = qwords[matched]; + if (qw !== undefined && pos - ws >= qw.length && item.startsWith(qw, ws)) { + matched++; + } else if (matched === 0) { + position++; } - qi++; } - while (pos < len && sep[item.charCodeAt(pos)]) pos++; - return pos >= len ? 2 : 1; + return [matched, position]; } /** @param {number} len @param {number} round */ diff --git a/website/scripts/panes/search.js b/website/scripts/panes/search.js index ddd274c45..80b9f4005 100644 --- a/website/scripts/panes/search.js +++ b/website/scripts/panes/search.js @@ -3,7 +3,7 @@ import { searchLabelElement, searchResultsElement, } from "../utils/elements.js"; -import { QuickMatch } from "../modules/quickmatch-js/0.4.1/src/index.js"; +import { QuickMatch } from "../modules/quickmatch-js/0.5.0/src/index.js"; import { brk } from "../utils/client.js"; /** @@ -64,14 +64,18 @@ export function init(options) { brk.getTx(needle, { signal }), ]); if (signal.aborted) return; - if (blockRes.status === "fulfilled") results.push(["Block", `/block/${needle}`]); - if (txRes.status === "fulfilled") results.push(["Transaction", `/tx/${needle}`]); + if (blockRes.status === "fulfilled") + results.push(["Block", `/block/${needle}`]); + if (txRes.status === "fulfilled") + results.push(["Transaction", `/tx/${needle}`]); } else if (ADDR_RE.test(needle)) { try { const { isvalid } = await brk.validateAddress(needle, { signal }); if (signal.aborted || !isvalid) return; results.push(["Address", `/address/${needle}`]); - } catch { return; } + } catch { + return; + } } else { return; }