mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat: index parsed JSON fields of profile/channel/app-handler events
Kinds 0 (profile), 40/41 (channel create/metadata) and 31990 (app handler) store their data as JSON in content. Implement SearchableEvent on them by parsing the JSON (via the existing UserMetadata/ChannelData/AppMetadata accessors) and indexing only the meaningful fields — names, bio/about, and the addresses people search by: nip05 email, lightning addresses (lud06/lud16), and website/picture/banner URLs. This avoids indexing the JSON keys and structural punctuation that raw-content indexing would add. Updates the FsSearchTest "non-searchable" case to use an unknown kind, since MetadataEvent is now searchable, and adds SearchTest coverage for profile and channel JSON fields (name, about, email, lightning, URL). 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:
+22
-1
@@ -48,6 +48,7 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.githubClaim
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.mastodonClaim
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.replaceClaims
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.twitterClaim
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.utils.text
|
||||
@@ -64,9 +65,29 @@ class MetadataEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
SearchableEvent {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
// Profile content is JSON, so we parse it and index only the
|
||||
// human-meaningful fields: names, bio, plus the addresses people search
|
||||
// by (nip05 email, lightning addresses, website/picture/banner URLs).
|
||||
// Mirrors the in-memory searchable set in UserMetadata.anyPropertyContains.
|
||||
override fun indexableContent() =
|
||||
contactMetaData()?.let {
|
||||
listOfNotNull(
|
||||
it.name,
|
||||
it.displayName,
|
||||
it.about,
|
||||
it.nip05,
|
||||
it.lud06,
|
||||
it.lud16,
|
||||
it.website,
|
||||
it.picture,
|
||||
it.banner,
|
||||
).joinToString(" ")
|
||||
} ?: ""
|
||||
|
||||
fun contactMetadataJson() =
|
||||
try {
|
||||
Json.parseToJsonElement(content) as JsonObject
|
||||
|
||||
+5
-1
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -45,7 +46,10 @@ class ChannelCreateEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider {
|
||||
EventHintProvider,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = channelInfo().let { listOfNotNull(it.name, it.about, it.picture).joinToString(" ") }
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var cache: ChannelDataNorm? = null
|
||||
|
||||
+5
-1
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.channel
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -48,7 +49,10 @@ class ChannelMetadataEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider {
|
||||
EventHintProvider,
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = channelInfo().let { listOfNotNull(it.name, it.about, it.picture).joinToString(" ") }
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var cache: ChannelDataNorm? = null
|
||||
|
||||
+22
-1
@@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.tags.PlatformLinkTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -50,7 +51,27 @@ class AppDefinitionEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PublishedAtProvider {
|
||||
PublishedAtProvider,
|
||||
SearchableEvent {
|
||||
// App-handler content is JSON; parse it and index the human-meaningful
|
||||
// fields plus the addresses/URLs people search by.
|
||||
override fun indexableContent() =
|
||||
appMetaData()?.let {
|
||||
listOfNotNull(
|
||||
it.name,
|
||||
it.username,
|
||||
it.displayName,
|
||||
it.about,
|
||||
it.nip05,
|
||||
it.lud06,
|
||||
it.lud16,
|
||||
it.website,
|
||||
it.picture,
|
||||
it.banner,
|
||||
it.image,
|
||||
).joinToString(" ")
|
||||
} ?: ""
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
private var cachedMetadata: AppMetadata? = null
|
||||
|
||||
+45
@@ -26,6 +26,7 @@ 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.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
|
||||
import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
@@ -165,4 +166,48 @@ class SearchTest : BaseDBTest() {
|
||||
db.assertQuery(repo, Filter(search = "uniqdesc"))
|
||||
db.assertQuery(repo, Filter(kinds = listOf(GitRepositoryEvent.KIND), search = "uniqdesc"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProfileJsonFieldsAreSearchable() =
|
||||
forEachDB { db ->
|
||||
// Kind-0 content is JSON; we parse it and index names, bio, and the
|
||||
// addresses people search by (nip05 email, lightning address, URLs).
|
||||
val p =
|
||||
signer.sign(
|
||||
MetadataEvent.createNew(
|
||||
name = "uniqalice",
|
||||
about = "loves uniqbio and coffee",
|
||||
nip05 = "alice@uniqmail.example",
|
||||
lnAddress = "alice@uniqln.example",
|
||||
website = "https://uniqsite.example",
|
||||
),
|
||||
)
|
||||
db.store.insertEvent(p)
|
||||
|
||||
db.assertQuery(p, Filter(search = "uniqalice")) // name
|
||||
db.assertQuery(p, Filter(search = "uniqbio")) // about
|
||||
db.assertQuery(p, Filter(search = "uniqmail")) // nip05 email
|
||||
db.assertQuery(p, Filter(search = "uniqln")) // lightning address
|
||||
db.assertQuery(p, Filter(search = "uniqsite")) // website URL
|
||||
db.assertQuery(p, Filter(kinds = listOf(MetadataEvent.KIND), search = "uniqalice"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChannelJsonFieldsAreSearchable() =
|
||||
forEachDB { db ->
|
||||
val chan =
|
||||
signer.sign(
|
||||
ChannelCreateEvent.build(
|
||||
name = "uniqchan",
|
||||
about = "a uniqtopic discussion",
|
||||
picture = "https://uniqpic.example/c.jpg",
|
||||
relays = null,
|
||||
),
|
||||
)
|
||||
db.store.insertEvent(chan)
|
||||
|
||||
db.assertQuery(chan, Filter(search = "uniqchan")) // name
|
||||
db.assertQuery(chan, Filter(search = "uniqtopic")) // about
|
||||
db.assertQuery(chan, Filter(search = "uniqpic")) // picture URL
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.store.fs
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
@@ -117,17 +117,18 @@ class FsSearchTest {
|
||||
@Test
|
||||
fun `non-searchable event does not produce fts entries`() =
|
||||
runBlocking {
|
||||
val meta =
|
||||
signer.sign<MetadataEvent>(
|
||||
// An event whose kind has no SearchableEvent mapping must not be indexed.
|
||||
val nonSearchable =
|
||||
signer.sign<Event>(
|
||||
createdAt = 1,
|
||||
kind = MetadataEvent.KIND,
|
||||
kind = 9999,
|
||||
tags = emptyArray(),
|
||||
content = "{\"name\":\"vitor\"}",
|
||||
content = "this text must not be indexed",
|
||||
)
|
||||
store.insert(meta)
|
||||
store.insert(nonSearchable)
|
||||
|
||||
val ftsRoot = root.resolve("idx/fts")
|
||||
assertEquals(0, ftsRoot.listDirectoryEntries().size, "MetadataEvent is not SearchableEvent")
|
||||
assertEquals(0, ftsRoot.listDirectoryEntries().size, "unknown kind is not SearchableEvent")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user