Files
modules/indexes-search/hyper-p2p-trie-prefix/index.js
T
Raven ScottandCursor e43e2e83f1 Deepen indexes-search: APIs, tests, and category docs
Extend all eight index/search packages with practical helpers for app
development and fix semantic-vector getStats() to expose real metrics.

Per-module code:
- bloom-gossip: listKeys(), clear()
- inverted-index: searchAll(and|or), getDocTerms()
- fulltext-lite: search(and|or), suggest(prefix)
- trie-prefix: countPrefix(), autocomplete(limit)
- graph-index: removeEdge(), outDegree(), edgeCount()
- similarity-lsh: nearVector(), bucketCount()
- semantic-vector-index: getStats() delegates to getMetrics() + PROTOCOL export
- spatial-index: listLocalPoints(), pointCount(), protocol in getStats()

Tests added for new APIs across six packages; all indexes-search npm test
suites pass (8/8).

Docs:
- Rewrite indexes-search category README (module table, composition)
- Expand bloom-gossip and semantic-vector architecture notes
- modules/README.md: link indexes-search doc hub

Parent workspace docs/indexes-search/README.md and MODULE_DOC_PASS.md
updated separately (outside this git root).

Co-authored-by: Cursor <[email protected]>
2026-05-21 00:20:56 -04:00

115 lines
2.6 KiB
JavaScript

require('bare-process/global')
const EventEmitter = require('bare-events')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const PROTOCOL = 'trie-prefix/v1'
class TrieNode {
constructor () {
this.children = new Map()
this.terminal = false
this.count = 0
}
}
class HyperP2PTriePrefix extends EventEmitter {
constructor (opts = {}) {
super()
this._root = new TrieNode()
this._words = new Set()
this._stats = { inserted: 0, queries: 0 }
}
insert (word) {
assertNonEmpty(word, 'word')
const w = String(word).toLowerCase()
let node = this._root
for (const ch of w) {
if (!node.children.has(ch)) node.children.set(ch, new TrieNode())
node = node.children.get(ch)
node.count++
}
if (!node.terminal) {
node.terminal = true
this._words.add(w)
this._stats.inserted++
this.emit('insert', { word: w })
}
return true
}
prefixSearch (prefix) {
this._stats.queries++
const p = String(prefix || '').toLowerCase()
let node = this._root
for (const ch of p) {
if (!node.children.has(ch)) return []
node = node.children.get(ch)
}
const out = []
const walk = (n, path) => {
if (n.terminal) out.push(path)
for (const [ch, child] of n.children) walk(child, path + ch)
}
walk(node, p)
return out.sort()
}
has (word) {
return this._words.has(String(word).toLowerCase())
}
remove (word) {
const w = String(word).toLowerCase()
if (!this._words.has(w)) return false
let node = this._root
const stack = []
for (const ch of w) {
stack.push([node, ch])
node = node.children.get(ch)
if (!node) return false
}
if (!node.terminal) return false
node.terminal = false
this._words.delete(w)
for (let i = stack.length - 1; i >= 0; i--) {
const [parent, ch] = stack[i]
const child = parent.children.get(ch)
child.count--
if (child.count === 0 && !child.terminal) parent.children.delete(ch)
}
return true
}
list () {
return [...this._words].sort()
}
countPrefix (prefix) {
return this.prefixSearch(prefix).length
}
autocomplete (prefix, limit = 10) {
const all = this.prefixSearch(prefix)
return all.slice(0, Math.max(0, limit | 0))
}
getStats () {
return {
...this._stats,
words: this._words.size,
protocol: PROTOCOL
}
}
async ready () { return this }
async close () {
this._root = new TrieNode()
this._words.clear()
this.emit('closed')
}
}
module.exports = { HyperP2PTriePrefix, PROTOCOL }