feat: add semantic document search endpoint
- Implement POST /search with query and result limit validation - Return matching document chunks and similarity scores - Support configurable result limits from 1 to 20 - Return sanitized validation and search failure responses - Add endpoint tests for successful searches, invalid inputs, and error handling
This commit is contained in:
parent
da134a1689
commit
9fa99b84b6
2 changed files with 120 additions and 0 deletions
|
|
@ -296,4 +296,97 @@ describe("API endpoints", () => {
|
|||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /search returns matching results", async () => {
|
||||
const expectedResults = [
|
||||
{
|
||||
documentId: "1",
|
||||
title: "RAG smoke test",
|
||||
content: "Relevant knowledge base content.",
|
||||
chunkIndex: 0,
|
||||
similarity: 0.92,
|
||||
},
|
||||
];
|
||||
|
||||
const app = buildApp(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
async (query, limit) => {
|
||||
assert.equal(query, "knowledge base search");
|
||||
assert.equal(limit, 3);
|
||||
return expectedResults;
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/search",
|
||||
payload: {
|
||||
query: "knowledge base search",
|
||||
limit: 3,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.deepEqual(response.json(), { results: expectedResults });
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /search rejects invalid input", async () => {
|
||||
const app = buildApp(undefined, undefined, undefined, async () => {
|
||||
throw new Error("Search should not run");
|
||||
});
|
||||
|
||||
try {
|
||||
for (const payload of [
|
||||
{ query: " " },
|
||||
{ query: "valid query", limit: 0 },
|
||||
{ query: "valid query", limit: 21 },
|
||||
{ query: "valid query", limit: 1.5 },
|
||||
{ query: 123 },
|
||||
]) {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/search",
|
||||
payload,
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 400);
|
||||
assert.deepEqual(response.json(), {
|
||||
error: "Invalid search request",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /search returns 503 when search fails", async () => {
|
||||
const app = buildApp(undefined, undefined, undefined, async () => {
|
||||
throw new Error("Internal database connection details");
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/search",
|
||||
payload: { query: "knowledge base search" },
|
||||
});
|
||||
|
||||
assert.equal(response.statusCode, 503);
|
||||
assert.deepEqual(response.json(), {
|
||||
error: "Document search failed",
|
||||
});
|
||||
assert.equal(
|
||||
response.body.includes("Internal database connection details"),
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,10 +15,16 @@ const documentSchema = z.object({
|
|||
content: z.string().trim().min(1).max(100_000),
|
||||
});
|
||||
|
||||
const searchSchema = z.object({
|
||||
query: z.string().trim().min(1).max(2_000),
|
||||
limit: z.number().int().min(1).max(20).optional(),
|
||||
});
|
||||
|
||||
export const buildApp = (
|
||||
generate = generateResponse,
|
||||
checkHealth = checkOllamaHealth,
|
||||
ingest = ingestDocumentDefault,
|
||||
search = searchDocumentsDefault,
|
||||
) => {
|
||||
const app = Fastify({
|
||||
logger: true,
|
||||
|
|
@ -126,6 +132,27 @@ export const buildApp = (
|
|||
}
|
||||
});
|
||||
|
||||
app.post("/search", async (request, reply) => {
|
||||
const result = searchSchema.safeParse(request.body);
|
||||
|
||||
if (!result.success) {
|
||||
return reply.code(400).send({
|
||||
error: "Invalid search request",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await search(result.data.query, result.data.limit);
|
||||
return { results };
|
||||
} catch (error) {
|
||||
request.log.error({ err: error }, "Document search failed");
|
||||
|
||||
return reply.code(503).send({
|
||||
error: "Document search failed",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/test-error", async () => {
|
||||
throw new Error("Sensitive internal detail");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue