From 8b938396a05b260f5fcf95f85f6bf6d8863b142f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 21:11:26 +0000 Subject: [PATCH] refactor(quartz): move FTS toggle into IndexingStrategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold the full-text-search on/off switch into `IndexingStrategy` as `indexFullTextSearch` (default `true`) instead of a separate top-level `enableFullTextSearch` constructor param on `EventStore`/`SQLiteEventStore`. `IndexingStrategy` is already the single place that decides which indexes the store builds — every field is a per-index toggle with a size/speed tradeoff, and `QueryBuilder` already receives it — so FTS, being just another index, belongs there rather than split across two config surfaces. Behaviour is unchanged: the module's no-op path and the QueryBuilder "search matches nothing" guards now read the flag via the strategy. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BjzUpY8H31c7ux669zytWg --- quartz/RELAY.md | 3 +++ .../quartz/nip01Core/store/sqlite/EventStore.kt | 10 +--------- .../nip01Core/store/sqlite/IndexingStrategy.kt | 16 ++++++++++++++++ .../quartz/nip01Core/store/sqlite/README.md | 10 ++++++++-- .../nip01Core/store/sqlite/SQLiteEventStore.kt | 10 +--------- .../store/sqlite/NoFullTextSearchTest.kt | 9 +++++++-- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/quartz/RELAY.md b/quartz/RELAY.md index dda794bae9..8ff7ee7dcb 100644 --- a/quartz/RELAY.md +++ b/quartz/RELAY.md @@ -76,12 +76,15 @@ val store = EventStore( indexTagsByCreatedAtAlone = false, indexTagsWithKindAndPubkey = false, useAndIndexIdOnOrderBy = false, + indexFullTextSearch = true, ), ) ``` By default, all single-letter tags with values are indexed. Override `shouldIndex(kind, tag)` for custom behavior. More indexes = faster queries but larger database. +`indexFullTextSearch` defaults to `true` and controls the NIP-50 full-text index (`event_fts`). Set it to `false` when search is served elsewhere (e.g. a Vespa backend, or a `SearchEventSource` as shown below): inserts skip the FTS tokenization cost, no `event_fts` table/trigger is created, and any filter carrying a non-empty `search` term returns no matches. + ## Non-Storage Relays (search, redirector, computed) A relay's job is to *answer REQs*. When the answer doesn't come from a stored diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt index b75869f500..7e1c912d55 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -38,16 +38,8 @@ class EventStore( dbName: String? = "events.db", override val relay: NormalizedRelayUrl? = "wss://quartz.local/".normalizeRelayUrl(), val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), - /** - * When `false`, NIP-50 full-text search indexing is turned off: no - * `event_fts` table or delete trigger is created, inserts skip the - * FTS tokenization cost, and `search` filters return no matches. - * Use for stores that never serve search from SQLite (e.g. a relay - * that offloads NIP-50 to an external engine like Vespa). - */ - val enableFullTextSearch: Boolean = true, ) : IEventStore { - val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy, enableFullTextSearch = enableFullTextSearch) + val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy) override suspend fun insert(event: Event) = store.insertEvent(event) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt index 9c0f197847..bcb36f6140 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/IndexingStrategy.kt @@ -81,6 +81,21 @@ interface IndexingStrategy { */ val useAndIndexIdOnOrderBy: Boolean + /** + * Maintain the NIP-50 full-text search index (`event_fts`). + * + * Unlike the other flags this defaults to **on**: search is a core + * feature and every searchable event is tokenized into an FTS virtual + * table on insert, with an `AFTER DELETE` trigger keeping it in sync. + * + * Turn it **off** when this store never serves search from SQLite — + * e.g. a relay that offloads NIP-50 to an external engine like Vespa. + * With it off, no `event_fts` table or trigger is created, inserts + * skip the tokenization cost, deletes skip the trigger, and any filter + * carrying a non-empty `search` term returns no matches. + */ + val indexFullTextSearch: Boolean + fun shouldIndex( kind: Int, tag: Tag, @@ -95,6 +110,7 @@ class DefaultIndexingStrategy( override val indexTagsByCreatedAtAlone: Boolean = false, override val indexTagsWithKindAndPubkey: Boolean = false, override val useAndIndexIdOnOrderBy: Boolean = false, + override val indexFullTextSearch: Boolean = true, ) : IndexingStrategy { override fun shouldIndex( kind: Int, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/README.md index 42781d1b77..8a7fa59cbb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/README.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/README.md @@ -195,10 +195,16 @@ FTS indexing has a write-time cost: every inserted `SearchableEvent` is tokenized into the `event_fts` virtual table, and an `AFTER DELETE` trigger keeps that table in sync on every deletion. If you never serve NIP-50 search from this store — e.g. a relay that offloads search to an -external engine like Vespa — pass `enableFullTextSearch = false`: +external engine like Vespa — turn it off through the `IndexingStrategy`, +alongside the other per-index toggles. `indexFullTextSearch` defaults to +`true` (search is a core feature); set it to `false`: ```kotlin -val eventStore = EventStore("dbname.db", relayUrlIdentifier, enableFullTextSearch = false) +val eventStore = EventStore( + "dbname.db", + relayUrlIdentifier, + indexStrategy = DefaultIndexingStrategy(indexFullTextSearch = false), +) ``` With FTS off: diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index a9ef4b5c8f..bbbcead7b9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -43,14 +43,6 @@ class SQLiteEventStore( val relay: NormalizedRelayUrl? = null, val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(), val numReaders: Int = 4, - /** - * When `false`, NIP-50 full-text search indexing is turned off: no - * `event_fts` table or delete trigger is created, inserts skip the - * FTS tokenization cost, and `search` filters return no matches. - * Use for stores that never serve search from SQLite (e.g. a relay - * that offloads NIP-50 to an external engine like Vespa). - */ - val enableFullTextSearch: Boolean = true, ) { companion object { const val DATABASE_VERSION = 2 @@ -58,7 +50,7 @@ class SQLiteEventStore( val seedModule = SeedModule() - val fullTextSearchModule = FullTextSearchModule(enableFullTextSearch) + val fullTextSearchModule = FullTextSearchModule(indexStrategy.indexFullTextSearch) val eventIndexModule = EventIndexesModule( seedModule::hasher, diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/NoFullTextSearchTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/NoFullTextSearchTest.kt index bcd0063f21..a3f7406e47 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/NoFullTextSearchTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/NoFullTextSearchTest.kt @@ -35,7 +35,8 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue /** - * Exercises a [SQLiteEventStore] created with `enableFullTextSearch = false`. + * Exercises a [SQLiteEventStore] built with an [IndexingStrategy] whose + * `indexFullTextSearch` is `false`. * * The store must behave exactly like a normal store for every non-search * operation while (a) never creating the `event_fts` table or its delete @@ -51,7 +52,11 @@ class NoFullTextSearchTest { Secp256k1Instance } - private fun store() = SQLiteEventStore(dbName = null, enableFullTextSearch = false) + private fun store() = + SQLiteEventStore( + dbName = null, + indexStrategy = DefaultIndexingStrategy(indexFullTextSearch = false), + ) private fun objectExists( db: SQLiteConnection,