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
2026-08-19 19:00:29 +07:00
parent ec3a2867d4
commit 9397303f01
32 changed files with 1120 additions and 42 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@mcpedia/api",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
"start": "bun run src/index.ts",
"lint": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hono/node-server": "^1.13.0",
"@mcpedia/config": "workspace:*",
"@mcpedia/core": "workspace:*",
"@trpc/server": "^11.0.0",
"hono": "^4.6.0",
"zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}
+26
View File
@@ -0,0 +1,26 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { db } from "@mcpedia/db";
import { appRouter } from "./router";
import type { Context } from "./trpc";
const app = new Hono();
// Health check.
app.get("/health", (c) => c.json({ ok: true }));
// Mount tRPC at /trpc/*. The fetch adapter is the canonical Bun/Hono adapter.
app.all("/trpc/*", (c) =>
fetchRequestHandler({
endpoint: "/trpc",
req: c.req.raw,
router: appRouter,
createContext: (): Context => ({ db }),
}),
);
const port = Number(process.env.API_PORT ?? 4020);
serve({ fetch: app.fetch, port }, (info) => {
console.log(`MCPedia API listening on http://localhost:${info.port}`);
});
+38
View File
@@ -0,0 +1,38 @@
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
import {
getDocument,
getRelated,
hybridSearch,
keywordSearch,
listDocuments,
semanticSearch,
} from "@mcpedia/core";
export const appRouter = router({
search: publicProcedure
.input(z.object({ q: z.string(), limit: z.number().int().min(1).max(50).default(20) }))
.query(({ input }) => keywordSearch(input.q, input.limit)),
semanticSearch: publicProcedure
.input(z.object({ q: z.string(), limit: z.number().int().min(1).max(50).default(10) }))
.query(async ({ input }) => semanticSearch(input.q, input.limit)),
hybridSearch: publicProcedure
.input(z.object({ q: z.string(), limit: z.number().int().min(1).max(50).default(10) }))
.query(async ({ input }) => hybridSearch(input.q, input.limit)),
getDocument: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input }) => getDocument(input.slug)),
listDocuments: publicProcedure
.input(z.object({ section: z.string().optional(), status: z.string().optional() }).optional())
.query(async ({ input }) => listDocuments(input ?? {})),
related: publicProcedure
.input(z.object({ slug: z.string(), limit: z.number().int().min(1).max(20).default(5) }))
.query(async ({ input }) => getRelated(input.slug, input.limit)),
});
export type AppRouter = typeof appRouter;
+11
View File
@@ -0,0 +1,11 @@
import { initTRPC } from "@trpc/server";
import { db } from "@mcpedia/db";
export interface Context {
db: typeof db;
}
export const t = initTRPC.context<Context>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"paths": {
"@mcpedia/db": ["../../packages/db/src/index.ts"],
"@mcpedia/db/schema": ["../../packages/db/src/schema.ts"],
"@mcpedia/*": ["../../packages/*"]
}
},
"include": ["src/**/*.ts"]
}