import Hyperbee from 'hyperbee'
class SearchIndex {
constructor (store) {
this._store = store
this._db = null
this._ready = false
}
async ready () {
if (this._ready) return
const core = this._store.get({ name: 'wiki-search-index' })
this._db = new Hyperbee(core, {
keyEncoding: 'utf-8',
valueEncoding: 'utf-8'
})
await this._db.ready()
this._ready = true
}
async index (slug, content, meta = {}) {
await this.ready()
const title = meta.title || slug
const text = `${title}\n${content}`.toLowerCase()
// Extract unique words (simple tokenizer)
const words = new Set(text.match(/\w{2,}/g) || [])
// Store word → slug mappings
for (const word of words) {
const key = `word/${word}/${slug}`
await this._db.put(key, title)
}
// Store slug → word count for ranking
await this._db.put(`doc/${slug}`, JSON.stringify({
title,
wordCount: words.size,
updated: meta.updated || Date.now()
}))
}
async remove (slug) {
await this.ready()
// Remove all word entries for this slug
const batch = this._db.batch()
for await (const entry of this._db.createReadStream({
gte: 'word/',
lt: 'word/\xff'
})) {
if (entry.key.endsWith(`/${slug}`)) {
await batch.del(entry.key)
}
}
await batch.del(`doc/${slug}`)
await batch.flush()
}
async search (query, limit = 20) {
await this.ready()
const terms = query.toLowerCase().match(/\w{2,}/g) || []
if (terms.length === 0) return []
// Find pages matching any term
const scores = new Map()
for (const term of terms) {
for await (const entry of this._db.createReadStream({
gte: `word/${term}/`,
lt: `word/${term}/\xff`
})) {
const slug = entry.key.split('/').pop()
scores.set(slug, (scores.get(slug) || 0) + 1)
}
}
// Sort by score (more matching terms = higher)
const results = [...scores.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
// Fetch metadata
const pages = []
for (const [slug, score] of results) {
const docEntry = await this._db.get(`doc/${slug}`)
let meta = { title: slug }
if (docEntry) {
try { meta = JSON.parse(docEntry.value) } catch {}
}
pages.push({ slug, score, title: meta.title })
}
return pages
}
async close () {
// Hyperbee closes with corestore
}
}
export { SearchIndex }