perf(quartz): direct-slot driver for replaceable + addressable queries

`profileOf(pk)` and `relaysOf(pk)` are the two hottest read patterns
in any Nostr client, both expressed as `Filter(authors=[pk],
kinds=[0|10002])`. Before this change every such query walked
`idx/kind/<k>/`, sorted the listing, and probed candidates — O(N)
in the kind's total event count even though we always wanted exactly
one event.

The planner now intercepts before the directory walk: when a filter
pins us to replaceable / addressable kinds with `authors` (and
`d`-tag for addressables), we read the slot file directly. One
`Files.exists()` + one `readString()` per (kind, author[, d]) triple,
no listing, no sort.

Falls through to the generic walk for anything that doesn't fit
(non-uniqueness kinds, missing d-tag, search clause, etc.). Two
new tests in FsSlotsTest assert the shortcut serves correctly
even when `idx/` has been externally wiped — proving the
shortcut isn't accidentally relying on the index.

115 fs tests green.
This commit is contained in:
Claude
2026-04-25 03:22:56 +00:00
parent 7ed525fe65
commit d460584d81
2 changed files with 124 additions and 0 deletions
@@ -21,6 +21,8 @@
package com.vitorpamplona.quartz.nip01Core.store.fs
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.isAddressable
import com.vitorpamplona.quartz.nip01Core.core.isReplaceable
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.sqlite.TagNameValueHasher
import java.nio.file.Files
@@ -59,6 +61,12 @@ internal class FsQueryPlanner(
return idsDriver(ids)
}
// Slot shortcut: for replaceable/addressable kinds with authors
// pinned (and d-tag for addressable), the filesystem already holds
// the winning event at a deterministic path. One stat + one read
// per (kind, pubkey[, d]) triple — no directory walk, no sort.
slotShortcut(filter)?.let { return it }
// NIP-50 search drives by FTS first when present so the AND
// intersection of token sets is the smallest possible candidate
// pool. All other predicates are post-filtered via Filter.match.
@@ -81,6 +89,77 @@ internal class FsQueryPlanner(
return allKindsDriver()
}
/**
* Direct-slot driver for replaceable / addressable queries. Returns
* null when the filter doesn't fit the shortcut, so the caller can
* fall through to the generic directory walk. Applies when:
*
* - `authors` is set,
* - `kinds` is set and every kind is either replaceable or
* addressable (not a mix with non-unique kinds),
* - for addressable kinds, a `d`-tag filter is supplied,
* - no `search` clause (FTS wouldn't benefit from this).
*
* The biggest win is for `profileOf` / `relaysOf` / `contactsOf`
* style reads (`Filter(authors=[pk], kinds=[0])` etc.) — those go
* from "list `idx/kind/0/` and sort" to "one `exists()` + one
* `readText`", even against a store with millions of kind-0 events.
*/
private fun slotShortcut(filter: Filter): Sequence<Candidate>? {
val kinds = filter.kinds?.takeIf { it.isNotEmpty() } ?: return null
val authors = filter.authors?.takeIf { it.isNotEmpty() } ?: return null
if (!filter.search.isNullOrBlank()) return null
val allReplaceable = kinds.all { it.isReplaceable() }
val allAddressable = kinds.all { it.isAddressable() }
if (!allReplaceable && !allAddressable) return null
val dTags: List<String>? =
if (allAddressable) {
(filter.tagsAll?.get("d") ?: filter.tags?.get("d"))?.takeIf { it.isNotEmpty() }
?: return null // addressable without d-tag → can't resolve a slot
} else {
null
}
return sequence {
val found = ArrayList<Candidate>(kinds.size * authors.size)
for (kind in kinds) {
for (pk in authors) {
if (allReplaceable) {
readSlot(layout.replaceableSlot(kind, pk))?.let(found::add)
} else {
for (d in dTags!!) {
readSlot(layout.addressableSlot(kind, pk, d))?.let(found::add)
}
}
}
}
found.sortByDescending { it.createdAt }
yieldAll(found)
}
}
/**
* Read a slot file into a `Candidate`. Parses enough of the JSON to
* extract `id` and `createdAt`. Returns null for missing slots or
* unreadable files — stale/corrupt slots are dropped silently, same
* behaviour as the directory walkers.
*/
private fun readSlot(path: Path): Candidate? {
if (!Files.exists(path)) return null
return try {
val event =
com.vitorpamplona.quartz.nip01Core.core.Event
.fromJson(Files.readString(path))
Candidate(event.createdAt, event.id)
} catch (_: java.io.IOException) {
null
} catch (_: com.fasterxml.jackson.core.JacksonException) {
null
}
}
// ---- drivers ------------------------------------------------------
/**
@@ -161,6 +161,51 @@ class FsSlotsTest {
assertTrue(entries.single().endsWith("-${v2.id}"), "author index entry must point at winner")
}
@Test
fun `slot shortcut serves replaceable queries even when idx is wiped`() {
// Belt-and-suspenders for the planner shortcut: a query pinned to
// (kinds=[0], authors=[pk]) must hit the slot directly without
// touching idx/. Wipe idx/ to prove the shortcut isn't relying on
// it.
val v = metadata("p", 100)
store.insert(v)
java.nio.file.Files
.walk(root.resolve("idx"))
.use { s ->
s.sorted(Comparator.reverseOrder()).forEach {
java.nio.file.Files
.deleteIfExists(it)
}
}
val got = store.query<MetadataEvent>(Filter(authors = listOf(signer.pubKey), kinds = listOf(MetadataEvent.KIND)))
assertEquals(listOf(v.id), got.map { it.id }, "slot shortcut should serve from replaceable/, not idx/")
}
@Test
fun `slot shortcut serves addressable queries when d-tag supplied`() {
val v = article("intro", "v", 10)
store.insert(v)
java.nio.file.Files
.walk(root.resolve("idx"))
.use { s ->
s.sorted(Comparator.reverseOrder()).forEach {
java.nio.file.Files
.deleteIfExists(it)
}
}
val got =
store.query<LongTextNoteEvent>(
Filter(
authors = listOf(signer.pubKey),
kinds = listOf(LongTextNoteEvent.KIND),
tags = mapOf("d" to listOf("intro")),
),
)
assertEquals(listOf(v.id), got.map { it.id })
}
@Test
fun `delete of current replaceable winner clears the slot`() {
val v = metadata("only", 100)