refactor: drop structural labels from FTS indexable content

The "title: ", "summary: ", "name: ", "Subject: ", "Option: " etc. prefixes
were tokenized into the single FTS content column as literal words, so every
event of a type matched bare terms like "title" or "summary" and the index
carried useless tokens — the same field-name pollution we avoid for JSON
kinds. The FTS table has one content column and search is plain-text MATCH,
so the labels enabled no fielded search; they were pure noise.

Index bare field values instead (listOfNotNull(...).joinToString("\n")),
which also drops the "null" token the older one-liners produced for absent
fields. Applied across all SearchableEvent implementations for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
This commit is contained in:
Claude
2026-06-18 19:41:09 +00:00
parent e400a48dbf
commit d87de452e6
27 changed files with 29 additions and 40 deletions
@@ -88,12 +88,7 @@ class MusicPlaylistEvent(
if (address.kind == MusicTrackEvent.KIND) address else null
}
override fun indexableContent(): String =
buildString {
append("title: ").append(title().orEmpty()).append('\n')
description()?.let { append("description: ").append(it).append('\n') }
append(content)
}
override fun indexableContent(): String = listOfNotNull(title(), description(), content).joinToString("\n")
companion object {
const val KIND = 34139
@@ -59,13 +59,7 @@ class MusicTrackEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent(): String =
buildString {
append("title: ").append(title().orEmpty()).append('\n')
append("artist: ").append(artist().orEmpty()).append('\n')
album()?.let { append("album: ").append(it).append('\n') }
append(content)
}
override fun indexableContent(): String = listOfNotNull(title(), artist(), album(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@@ -66,7 +66,7 @@ class NipTextEvent(
AddressHintProvider,
IForkableEvent,
SearchableEvent {
override fun indexableContent() = "title: " + title() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
override fun dTag() = tags.dTag()
@@ -66,7 +66,7 @@ class ZapPollEvent(
override fun indexableContent() =
buildString {
append(content)
pollOptionsArray().forEach { append("\nOption: ").append(it.descriptor) }
pollOptionsArray().forEach { append('\n').append(it.descriptor) }
}
override fun eventHints(): List<EventIdHint> {
@@ -43,7 +43,7 @@ class FeedDefinitionEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty()
override fun indexableContent() = title().orEmpty()
fun title(): String? = tags.firstOrNull { it.size >= 2 && it[0] == "title" }?.get(1)
@@ -66,7 +66,7 @@ class TextNoteEvent(
PubKeyHintProvider,
IForkableEvent,
SearchableEvent {
override fun indexableContent() = "Subject: " + subject() + "\n" + content
override fun indexableContent() = listOfNotNull(subject(), content).joinToString("\n")
override fun eventHints(): List<EventIdHint> {
val eHints = tags.mapNotNull(MarkedETag::parseAsHint)
@@ -71,7 +71,7 @@ class LongTextNoteEvent(
PublishedAtProvider,
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
override fun eventHints(): List<EventIdHint> {
val qHints = tags.mapNotNull(QTag::parseEventAsHint)
@@ -49,7 +49,7 @@ class EmojiPackEvent(
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + titleOrName().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(titleOrName(), description(), content).joinToString("\n")
@Deprecated("NIP-51 has deprecated name. Use title instead", ReplaceWith("title()"))
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
@@ -61,7 +61,7 @@ class GitIssueEvent(
EventHintProvider,
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = "Subject: " + subject() + "\n" + content
override fun indexableContent() = listOfNotNull(subject(), content).joinToString("\n")
override fun eventHints(): List<EventIdHint> {
val qHints = tags.mapNotNull(QTag::parseEventAsHint)
@@ -66,7 +66,7 @@ class GitPullRequestEvent(
EventHintProvider,
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = "Subject: " + subject().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(subject(), content).joinToString("\n")
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
@@ -52,7 +52,7 @@ class GitRepositoryEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(name(), description(), content).joinToString("\n")
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
@@ -52,7 +52,7 @@ class TorrentEvent(
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@@ -51,7 +51,7 @@ class CalendarDateSlotEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag.Companion::parse)
@@ -52,7 +52,7 @@ class CalendarTimeSlotEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag.Companion::parse)
@@ -44,7 +44,7 @@ class CalendarEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@@ -55,7 +55,7 @@ class MeetingSpaceEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "room: " + room().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(room(), summary(), content).joinToString("\n")
override fun pubKeyHints() = tags.mapNotNull(ParticipantTag::parseAsHint)
@@ -65,7 +65,7 @@ class LiveActivitiesEvent(
PubKeyHintProvider,
LiveStreamLike,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
override fun eventHints(): List<EventIdHint> {
val pinnedEvents = pinned()
@@ -43,7 +43,7 @@ class BadgeDefinitionEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(name(), description(), content).joinToString("\n")
fun name() = tags.badgeName()
@@ -48,7 +48,7 @@ class PictureEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
@@ -51,7 +51,7 @@ abstract class AddressableVideoEvent(
VideoEvent,
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
@@ -51,7 +51,7 @@ abstract class RegularVideoEvent(
VideoEvent,
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
@@ -58,7 +58,7 @@ class CommunityDefinitionEvent(
AddressHintProvider,
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\nrules: " + rules().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(name(), description(), rules(), content).joinToString("\n")
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
@@ -60,7 +60,7 @@ class GoalEvent(
AddressHintProvider,
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "summary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(summary(), content).joinToString("\n")
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
@@ -41,7 +41,7 @@ class ThreadEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), content).joinToString("\n")
fun title() = tags.title()
@@ -66,7 +66,7 @@ class HighlightEvent(
AddressHintProvider,
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "comment: " + comment() + "\ncontext: " + context() + "\n" + content
override fun indexableContent() = listOfNotNull(comment(), context(), content).joinToString("\n")
override fun eventHints(): List<EventIdHint> {
val eHints = tags.mapNotNull(ETag::parseAsHint)
@@ -55,7 +55,7 @@ class ClassifiedsEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PublishedAtProvider,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun indexableContent() = listOfNotNull(title(), summary(), content).joinToString("\n")
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@@ -105,9 +105,9 @@ class FsSearchTest {
val ftsRoot = root.resolve("idx/fts")
val tokenDirs = ftsRoot.listDirectoryEntries().map { it.fileName.toString() }.toSet()
// TextNoteEvent.indexableContent() prepends a "Subject: " prefix so
// we get the content tokens plus the subject ones. What matters is
// that each unique token yields exactly one entry under its dir.
// This note has no subject, so indexableContent() is just the
// content. What matters is that each unique token yields exactly
// one entry under its dir.
assertTrue("bitcoin" in tokenDirs)
assertTrue("nostr" in tokenDirs)
assertEquals(1, ftsRoot.resolve("bitcoin").listDirectoryEntries().size)