refactor(quartz): move FTS toggle into IndexingStrategy

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjzUpY8H31c7ux669zytWg
This commit is contained in:
Claude
2026-07-01 21:11:26 +00:00
parent cd4edefce0
commit 8b938396a0
6 changed files with 36 additions and 22 deletions
+3
View File
@@ -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
@@ -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)
@@ -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,
@@ -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:
@@ -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,
@@ -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,