feat(geode): --no-search / [options].full_text_search — run without NIP-50

strfry implements no NIP-50 at all, so geode's default setup pays FTS
tokenization on every searchable event for a feature the other side of a
benchmark isn't providing. The new switch removes it cleanly:

- geode: --no-search CLI flag and [options].full_text_search TOML key
  (default true). When off, the store skips the FTS index entirely,
  NIP-11 stops advertising 50 (an explicit [info] nips list stays
  operator-authoritative), and search filters match nothing via the
  existing QueryBuilder guard. RelayIndexingStrategy becomes
  relayIndexingStrategy(fullTextSearch) with the stock val kept.

- relayBench: --geode-no-search runs the geode entry with the flag (relay
  named geode-nosearch in reports); README documents the apples-to-apples
  rationale and a recipe for benchmarking both geode flavors side by side.

Alternating A/B on the 10k corpus: 4,767/4,792 ev/s without search vs
4,111/3,977 with — NIP-50 costs ~18% of ingest throughput at current
write-path speed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeoCvXnTxsKzqurkmjdC46
This commit is contained in:
Claude
2026-07-03 18:37:39 +00:00
parent 001d5f0eb7
commit 58c580cd34
7 changed files with 88 additions and 17 deletions
+6
View File
@@ -59,6 +59,12 @@ verify_signatures = true
# legacy in-policy verify path.
# parallel_verify = true
# NIP-50 full-text search. Default: true. When false no FTS index is
# built or maintained (inserts skip tokenization — a measurable share
# of ingest cost), NIP-11 stops advertising NIP-50, and REQ filters
# with a `search` term match nothing. CLI: --no-search.
# full_text_search = true
# Require clients to NIP-42 AUTH before REQ/EVENT/COUNT.
require_auth = false
@@ -71,6 +71,11 @@ import java.io.File
* --no-verify DO NOT verify event signatures (off by default
* verify is on; use only for trusted-input
* scenarios like fixture replay).
* --no-search disable NIP-50 full-text search (sets
* options.full_text_search = false): no FTS index is
* built or maintained, NIP-11 stops advertising 50,
* and `search` filters match nothing. Skips the
* per-event tokenization cost on ingest.
*/
fun main(args: Array<String>) {
val a = parseArgs(args)
@@ -101,6 +106,10 @@ fun main(args: Array<String>) {
// `[options].parallel_verify = false`.
val parallelVerify =
verifySigs && !a.flag("--no-parallel-verify") && config.options.parallel_verify
// NIP-50 search is on by default; `--no-search` (or
// `[options].full_text_search = false`) trades it for cheaper ingest —
// e.g. to match relays that don't implement NIP-50 at all.
val fullTextSearch = !a.flag("--no-search") && config.options.full_text_search
// Advertised URL: explicit `info.relay_url` wins, then build from
// host/port/path. 0.0.0.0 bind → 127.0.0.1 in the URL so NIP-42
@@ -111,9 +120,9 @@ fun main(args: Array<String>) {
val info =
cliInfoFile?.let { RelayInfo.fromFile(it) }
?: config.resolveInfo()
?: config.resolveInfo(fullTextSearch)
val store: IEventStore = EventStore(dbName = dbFile, relay = advertisedUrl, indexStrategy = RelayIndexingStrategy)
val store: IEventStore = EventStore(dbName = dbFile, relay = advertisedUrl, indexStrategy = relayIndexingStrategy(fullTextSearch))
val policyBuilder: () -> IRelayPolicy = {
composePolicy(config, advertisedUrl, requireAuth, optionalAuth, verifySigs, parallelVerify)
@@ -35,8 +35,17 @@ import com.vitorpamplona.quartz.nip01Core.store.sqlite.DefaultIndexingStrategy
* the gap grows linearly with the table. The extra index costs one B-tree
* insert per event, which the same benchmark shows is noise next to the
* Schnorr verify + tag indexing already paid on the write path.
*
* @param fullTextSearch maintain the NIP-50 FTS index. Off skips the
* per-event tokenization on ingest (relayBench measured it at roughly a
* quarter of write cost) at the price of `search` filters matching
* nothing — pair it with a NIP-11 doc that doesn't advertise 50.
*/
val RelayIndexingStrategy =
fun relayIndexingStrategy(fullTextSearch: Boolean = true) =
DefaultIndexingStrategy(
indexEventsByCreatedAtAlone = true,
indexFullTextSearch = fullTextSearch,
)
/** Stock relay strategy — everything on, matching geode's defaults. */
val RelayIndexingStrategy = relayIndexingStrategy()
@@ -46,7 +46,7 @@ data class StaticConfig(
val admin: AdminSection = AdminSection(),
val negentropy: NegentropySection = NegentropySection(),
) {
fun resolveInfo(): RelayInfo =
fun resolveInfo(fullTextSearch: Boolean = true): RelayInfo =
RelayInfo(
Nip11RelayInformation(
name = info.name ?: RelayInfo.NAME,
@@ -58,7 +58,9 @@ data class StaticConfig(
version = info.version ?: RelayInfo.VERSION,
supported_nips =
info.supported_nips?.map(Int::toString)
?: RelayInfo.SUPPORTED_NIPS,
// An explicit [info] nips list is operator-authoritative;
// the default list stays honest about search.
?: if (fullTextSearch) RelayInfo.SUPPORTED_NIPS else RelayInfo.SUPPORTED_NIPS - "50",
privacy_policy = info.privacy_policy,
terms_of_service = info.terms_of_service,
relay_countries = info.relay_countries,
@@ -123,6 +125,15 @@ data class StaticConfig(
val verify_signatures: Boolean = true,
/** CPU fan-out verification in the IngestQueue. No-op when [verify_signatures] is false. */
val parallel_verify: Boolean = true,
/**
* NIP-50 full-text search. On by default. When off, no FTS index
* is created or maintained (inserts skip tokenization — a
* measurable share of ingest cost), NIP-11 stops advertising
* NIP-50, and REQ filters carrying a `search` term match nothing.
* Turn off when search isn't needed, or to compare against relays
* that don't offer NIP-50 at all (strfry, for example).
*/
val full_text_search: Boolean = true,
)
/**
+21
View File
@@ -80,6 +80,27 @@ benchmark artifact.
> and manifest/fingerprint convention are designed so other relay authors can
> run the exact same workload and publish comparable numbers.
## Feature parity: NIP-50 search
geode maintains a NIP-50 full-text index by default; strfry has no search
at all. That skews the *ingest* comparison — geode tokenizes every
searchable event into the FTS index (~a quarter of its write cost) for a
feature strfry isn't providing. `--geode-no-search` runs geode with
`--no-search` (no FTS index, NIP-11 stops advertising 50) for the
apples-to-apples write path:
```bash
./relayBench/run.sh --geode-no-search # geode(no NIP-50) vs strfry
```
To see the price of search inline, run both geode flavors side by side:
```bash
GEODE=geode/build/install/geode/bin/geode
./relayBench/run.sh --geode-no-search \
--relay "geode-fts=$GEODE --host 127.0.0.1 --port {port} --db {dir}/geode.sqlite"
```
## Adding another relay
No code needed if the relay can be launched from a command line:
@@ -57,6 +57,7 @@ import kotlin.system.exitProcess
*/
class Options(
val geodeBin: String?,
val geodeNoSearch: Boolean,
val strfryBin: String?,
val customRelays: List<Pair<String, String>>,
val events: Int,
@@ -87,6 +88,10 @@ private val USAGE =
Relays (at least one):
--geode-bin PATH geode launcher (from :geode:installDist)
--geode-no-search run geode with --no-search (NIP-50 off) so the
write path matches relays that have no FTS at
all, e.g. strfry — the apples-to-apples ingest
comparison
--strfry-bin PATH strfry binary
--relay NAME=CMD any other relay; CMD may use {port} and {dir}
(repeatable)
@@ -199,7 +204,7 @@ fun main(args: Array<String>) {
private fun buildRelays(options: Options): List<RelayUnderTest> =
buildList {
options.geodeBin?.let { add(GeodeRelay(it)) }
options.geodeBin?.let { add(GeodeRelay(it, noSearch = options.geodeNoSearch)) }
options.strfryBin?.let { add(StrfryRelay(it, options.maxEventBytes, options.maxTags)) }
options.customRelays.forEach { (name, cmd) -> add(CustomRelay(name, cmd)) }
}
@@ -307,6 +312,7 @@ private fun parseArgs(args: Array<String>): Options? {
val moduleDir = File("relayBench").takeIf { it.isDirectory } ?: File(".")
return Options(
geodeBin = one("--geode-bin"),
geodeNoSearch = "--geode-no-search" in flags,
strfryBin = one("--strfry-bin"),
customRelays =
(map["--relay"] ?: emptyList()).map {
@@ -133,23 +133,32 @@ data class Nip11Info(
val version: String?,
)
/** Geode — the standalone JVM relay from this repo (`:geode:installDist`). */
/**
* Geode — the standalone JVM relay from this repo (`:geode:installDist`).
*
* [noSearch] passes `--no-search`, dropping NIP-50 (no FTS index, no
* per-event tokenization on ingest). strfry has no NIP-50 at all, so this
* is the apples-to-apples ingest configuration against it; the default
* keeps geode's stock feature set and eats the indexing cost.
*/
class GeodeRelay(
private val bin: String,
) : RelayUnderTest("geode") {
private val noSearch: Boolean = false,
) : RelayUnderTest(if (noSearch) "geode-nosearch" else "geode") {
override fun command(
port: Int,
dataDir: File,
): List<String> =
listOf(
bin,
"--host",
"127.0.0.1",
"--port",
"$port",
"--db",
File(dataDir, "geode.sqlite").absolutePath,
)
buildList {
add(bin)
add("--host")
add("127.0.0.1")
add("--port")
add("$port")
add("--db")
add(File(dataDir, "geode.sqlite").absolutePath)
if (noSearch) add("--no-search")
}
}
/**