feat(platform): forum search mesh merge API for Phase 128 (v0.8.93)

Add searchForumPostsWithMesh and _forumPostsFromMeshMessages to merge
local forum haystack hits with guild MESSAGE_SEARCH mesh responses via
pearcord-search.mergeForumSearchHits.

_resolveSearchResults uses mesh for forum scope and records
_lastSearchMeshMeta with scope forum for the desktop search UI.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 10:30:21 -04:00
co-authored by Cursor
parent 8ac673b2aa
commit af74d9af07
+95 -2
View File
@@ -156,6 +156,8 @@ const {
ingestGuildSearchIndexSlice, ingestGuildSearchIndexSlice,
buildIndexRow, buildIndexRow,
mergeSearchHits, mergeSearchHits,
mergeForumSearchHits,
rankAndEnrichForumSearchHits,
compactMeshSearchHit, compactMeshSearchHit,
meshHitToMessage meshHitToMessage
} = require('pearcord-search') } = require('pearcord-search')
@@ -1044,7 +1046,17 @@ class PearcordPlatform extends EventEmitter {
id: this.activeChannelId id: this.activeChannelId
}) })
if (!ch || !isForumChannel(ch)) return [] if (!ch || !isForumChannel(ch)) return []
return this.searchForumPosts(guildId, ch.id, q) const out = await this.searchForumPostsWithMesh(guildId, ch.id, q, {
limit: 50,
mesh: this._searchIncludeMesh
})
this._lastSearchMeshMeta = {
mesh: out.mesh,
meshHitCount: out.meshHitCount || 0,
localHitCount: out.localHitCount || 0,
scope: 'forum'
}
return out.hits
} }
if (this._searchScope === 'guild') { if (this._searchScope === 'guild') {
const out = await this.searchGuildMessagesWithMesh(this.guild.guild.id, q, { const out = await this.searchGuildMessagesWithMesh(this.guild.guild.id, q, {
@@ -1143,10 +1155,91 @@ class PearcordPlatform extends EventEmitter {
snippet: String(content || '').slice(0, 160) snippet: String(content || '').slice(0, 160)
}) })
} }
const { rankAndEnrichForumSearchHits } = require('pearcord-search')
return rankAndEnrichForumSearchHits(hits, q, { limit }) return rankAndEnrichForumSearchHits(hits, q, { limit })
} }
async _forumPostsFromMeshMessages (guildId, forumChannelId, messages, query) {
const q = String(query || '').trim()
if (!q || !guildId || !forumChannelId) return []
const channels =
(await this.guild?.listChannels?.()) ||
(await this.db.find(COLLECTIONS.CHANNELS, { guildId }))
const forumCh = channels.find((c) => c.id === forumChannelId)
const posts = listForumPostsForParent(channels, forumChannelId, {
includeArchived: true
})
const byThread = new Map(posts.map((p) => [p.id, p]))
const hits = []
for (const m of messages || []) {
const post = byThread.get(m.channelId)
if (!post) continue
let content = String(m.content || '')
if (!content && post.rootMessageId) {
const root = await this.db.get(COLLECTIONS.MESSAGES, {
channelId: post.id,
id: post.rootMessageId
})
if (root) content = this._messagePlaintext(enrichMessageRow(root))
}
const tags = this.forum
? await this.forum.getPostTags(guildId, post.id).catch(() => [])
: []
hits.push({
...post,
tags,
forumChannelId,
forumChannelName: forumCh?.name || 'forum',
searchHaystack: forumPostHaystack({
title: post.name,
content,
tags,
archived: post.archived
}),
snippet: String(content || '').slice(0, 160),
fromMesh: true
})
}
return rankAndEnrichForumSearchHits(hits, q, { limit: 50 })
}
async searchForumPostsWithMesh (guildId, forumChannelId, query, opts = {}) {
const local = await this.searchForumPosts(guildId, forumChannelId, query, opts)
if (opts.mesh === false || !this.guild?.guild) {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
try {
const meshMsgs = await this._fetchGuildSearchFromMeshOnce(guildId, query, opts)
const meshPosts = await this._forumPostsFromMeshMessages(
guildId,
forumChannelId,
meshMsgs.map((m) => enrichMessageRow(m)),
query
)
const merged = mergeForumSearchHits(local, meshPosts, {
limit: opts.limit || 50,
query: String(query || '').trim()
})
return {
hits: merged,
mesh: true,
meshHitCount: meshPosts.length,
localHitCount: local.length
}
} catch {
return {
hits: local,
mesh: false,
meshHitCount: 0,
localHitCount: local.length
}
}
}
_getSearchIndexStore () { _getSearchIndexStore () {
if (!this._searchIndexStore) { if (!this._searchIndexStore) {
this._searchIndexStore = new GuildSearchIndexStore(this.storagePath) this._searchIndexStore = new GuildSearchIndexStore(this.storagePath)