feat: index natural-language fields of more event kinds for FTS

Several event kinds carry human-readable text (titles, summaries,
descriptions, names, free-text content) but were never added to the
full-text search index. Implement SearchableEvent on them so their
natural-language fields become searchable, while keeping non-prose data
(hex ids, URLs, relay hints, hashtags, geohashes, JSON config) out of the
index.

Kinds added:
- Classifieds (30402): title + summary + content
- Calendar (31924) + date/time slots (31922/31923): title + summary + content
- Community Definition (34550): name + description + rules
- Live Activities (30311): title + summary + content
- Meeting Space (30312): room + summary
- Status (30315): content
- Picture (20) and Video (NIP-71, all variants): title + content
- Goal (9041): summary + content
- Torrent (2003): title + content
- Git Repository (30617): name + description
- Git Pull Request (1618): subject + content
- Git Patch (1617): content
- Badge Definition (30009): name + description
- Emoji Pack (30030): title + description
- Feed Definition (31890): title only (content is JSON config)

JSON-content kinds (profile, channel, app handler) are intentionally left
out for now since they require parsing the content JSON to extract only
the natural-language fields. Existing v2->v3 FTS reindex repopulates the
index for already-cached events of these kinds on upgrade.

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 16:34:27 +00:00
parent 6a2d8baf43
commit 8f6074e85b
20 changed files with 131 additions and 19 deletions
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
/**
* Kind 31890: Feed Definition Event (draft NIP, PR #1181).
@@ -40,7 +41,10 @@ class FeedDefinitionEvent(
tags: TagArray,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty()
fun title(): String? = tags.firstOrNull { it.size >= 2 && it[0] == "title" }?.get(1)
fun emoji(): String? = tags.firstOrNull { it.size >= 2 && it[0] == "emoji" }?.get(1)
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
@@ -46,7 +47,10 @@ class EmojiPackEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + titleOrName().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
@Deprecated("NIP-51 has deprecated name. Use title instead", ReplaceWith("title()"))
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
@@ -44,6 +44,7 @@ import com.vitorpamplona.quartz.nip34Git.patch.tags.Committer
import com.vitorpamplona.quartz.nip34Git.patch.tags.CommitterTag
import com.vitorpamplona.quartz.nip34Git.patch.tags.ParentCommitTag
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -57,7 +58,10 @@ class GitPatchEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider,
EventHintProvider,
AddressHintProvider {
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = content
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
@@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.nip34Git.pr.tags.CurrentCommitTag
import com.vitorpamplona.quartz.nip34Git.pr.tags.MergeBaseTag
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/**
@@ -63,7 +64,10 @@ class GitPullRequestEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider,
EventHintProvider,
AddressHintProvider {
AddressHintProvider,
SearchableEvent {
override fun indexableContent() = "Subject: " + subject().orEmpty() + "\n" + content
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.nip34Git.repository.tags.MaintainersTag
import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag
import com.vitorpamplona.quartz.nip34Git.repository.tags.RelaysTag
import com.vitorpamplona.quartz.nip34Git.repository.tags.WebTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -49,7 +50,10 @@ class GitRepositoryEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip35Torrents.tags.FileTag
import com.vitorpamplona.quartz.nip35Torrents.tags.InfoHashTag
import com.vitorpamplona.quartz.nip35Torrents.tags.TrackerTag
import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.UrlEncoder
@@ -49,7 +50,10 @@ class TorrentEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun btih() = tags.firstNotNullOfOrNull(BtihTag::parse)
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -39,7 +40,10 @@ class StatusEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = content
fun firstTaggedUrl() = tags.firstTagValue("r")
companion object {
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.tags.LocationTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
@@ -48,7 +49,10 @@ class CalendarDateSlotEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
fun title() = tags.firstNotNullOfOrNull(TitleTag.Companion::parse)
fun location() = tags.firstNotNullOfOrNull(LocationTag.Companion::parse)
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.tags.LocationTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
@@ -49,7 +50,10 @@ class CalendarTimeSlotEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
fun title() = tags.firstNotNullOfOrNull(TitleTag.Companion::parse)
fun location() = tags.firstNotNullOfOrNull(LocationTag.Companion::parse)
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -41,7 +42,10 @@ class CalendarEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun calendarEventAddresses() = taggedAddresses()
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.EndpointUrlTag
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.RelayListTag
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.RoomNameTag
@@ -52,7 +53,10 @@ class MeetingSpaceEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PubKeyHintProvider {
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "room: " + room().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun pubKeyHints() = tags.mapNotNull(ParticipantTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(ParticipantTag::parseKey)
@@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip53LiveActivities.LiveStreamLike
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.CurrentParticipantsTag
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.EndsTag
@@ -62,7 +63,10 @@ class LiveActivitiesEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
PubKeyHintProvider,
LiveStreamLike {
LiveStreamLike,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
override fun eventHints(): List<EventIdHint> {
val pinnedEvents = pinned()
if (pinnedEvents.isEmpty()) return emptyList()
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip58Badges.definition.tags.ImageTag
import com.vitorpamplona.quartz.nip58Badges.definition.tags.ThumbTag
import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag
@@ -40,7 +41,10 @@ class BadgeDefinitionEvent(
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\n" + content
fun name() = tags.badgeName()
fun image() = tags.badgeImageUrl()
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip68Picture.tags.LocationTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag
@@ -45,7 +46,10 @@ class PictureEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
RootScope {
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
var iMetas: List<PictureMeta>? = null
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip71Video.tags.DurationTag
import com.vitorpamplona.quartz.nip71Video.tags.SegmentTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
@@ -48,7 +49,10 @@ abstract class AddressableVideoEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig),
PublishedAtProvider,
VideoEvent,
RootScope {
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
var iMetas: List<VideoMeta>? = null
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip71Video.tags.DurationTag
import com.vitorpamplona.quartz.nip71Video.tags.SegmentTag
import com.vitorpamplona.quartz.nip92IMeta.imetas
@@ -48,7 +49,10 @@ abstract class RegularVideoEvent(
) : Event(id, pubKey, createdAt, kind, tags, content, sig),
PublishedAtProvider,
VideoEvent,
RootScope {
RootScope,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\n" + content
@kotlinx.serialization.Transient
@kotlin.jvm.Transient
var iMetas: List<VideoMeta>? = null
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ImageTag
import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag
@@ -55,7 +56,10 @@ class CommunityDefinitionEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "name: " + name().orEmpty() + "\ndescription: " + description().orEmpty() + "\nrules: " + rules().orEmpty() + "\n" + content
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint)
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId)
@@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.references.reference
import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip75ZapGoals.tags.AmountTag
import com.vitorpamplona.quartz.nip75ZapGoals.tags.ClosedAtTag
import com.vitorpamplona.quartz.nip75ZapGoals.tags.RelayListTag
@@ -57,7 +58,10 @@ class GoalEvent(
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
PubKeyHintProvider,
SearchableEvent {
override fun indexableContent() = "summary: " + summary().orEmpty() + "\n" + content
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
@@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip92IMeta.imetas
import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag
import com.vitorpamplona.quartz.nip99Classifieds.tags.LocationTag
@@ -52,7 +53,10 @@ class ClassifiedsEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
PublishedAtProvider {
PublishedAtProvider,
SearchableEvent {
override fun indexableContent() = "title: " + title().orEmpty() + "\nsummary: " + summary().orEmpty() + "\n" + content
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
@@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.test.Test
@@ -129,4 +131,38 @@ class SearchTest : BaseDBTest() {
db.assertQuery(null, Filter(search = "uniqalpha"))
db.assertQuery(v2, Filter(search = "uniqbeta"))
}
@Test
fun testNewlySearchableKinds() =
forEachDB { db ->
// CalendarEvent indexes the title tag plus the free-text content.
val cal =
signer.sign(
CalendarEvent.build(
title = "uniqtitle Meetup",
content = "annual uniqbody gathering",
),
)
// GitRepositoryEvent has an empty content field — its searchable
// text comes entirely from the name and description tags.
val repo =
signer.sign(
GitRepositoryEvent.build(
name = "uniqname",
description = "uniqdesc nostr client",
),
)
db.store.insertEvent(cal)
db.store.insertEvent(repo)
// Title tag and content are both indexed for the calendar event.
db.assertQuery(cal, Filter(search = "uniqtitle"))
db.assertQuery(cal, Filter(search = "uniqbody"))
// Name and description tags are indexed even with empty content.
db.assertQuery(repo, Filter(search = "uniqname"))
db.assertQuery(repo, Filter(search = "uniqdesc"))
db.assertQuery(repo, Filter(kinds = listOf(GitRepositoryEvent.KIND), search = "uniqdesc"))
}
}