feat(mcpedia): Phase 2 — semantic + hybrid search, tRPC/Hono API

- @mcpedia/embeddings: OpenRouter provider (9router /v1, encoding_format float),
  chunkText + embedChunks; EMBED_DIM=2048
- document_chunks table (real[] embedding) — pgvector NOT available on shared
  imrnes Postgres, so cosine is computed in-app (KB-scale fine); pgvector deferred
- indexer: chunk + embed + upsert per document
- @mcpedia/search: semanticSearch (cosine) + hybridSearch (FTS+cosine RRF)
- apps/api: Hono + tRPC v11 (6 procedures), serve on :4020
- MCP: semantic_search + hybrid_search tools (6 total)
- web: keyword/hybrid toggle; .env.example + README + PHASES updated
This commit is contained in:
asepharyana committed 2026-08-19 19:00:29 +07:00
1 parent ec3a2867d4
commit 9397303f01
32 files changed
+1120 -42

No files matched your search

+26
View File
@@ -17,8 +17,10 @@ async function main() {
const expected = [
"get_document",
"get_related_documents",
"hybrid_search",
"list_documents",
"search_documents",
"semantic_search",
].sort();
if (JSON.stringify(names) !== JSON.stringify(expected)) {
throw new Error(`tool set mismatch: ${names.join(",")}`);
@@ -66,6 +68,30 @@ async function main() {
if (docs.length !== 1) throw new Error("list_documents docs != 1");
console.log("list_documents(section=docs) =>", docs.length, "doc");
// 6) semantic_search
const sem = await client.callTool({
name: "semantic_search",
arguments: { query: "websocket connection closing unexpectedly", limit: 5 },
});
const semHits = JSON.parse((sem.content as any)[0].text);
if (!Array.isArray(semHits) || semHits.length < 1) {
throw new Error("semantic_search returned no hits");
}
console.log(
`semantic_search => ${semHits.length} chunks, top: ${semHits[0].slug}@${semHits[0].score.toFixed(3)}`,
);
// 7) hybrid_search
const hyb = await client.callTool({
name: "hybrid_search",
arguments: { query: "websocket timeout debugging", limit: 5 },
});
const hybHits = JSON.parse((hyb.content as any)[0].text);
if (!Array.isArray(hybHits) || hybHits.length < 1) {
throw new Error("hybrid_search returned no hits");
}
console.log(`hybrid_search => ${hybHits.length} docs, top: ${hybHits[0].doc.slug}`);
await client.close();
await server.close();
console.log("\nSMOKE OK");