add document search

This commit is contained in:
Matthias Nannt
2024-08-22 18:07:00 +02:00
parent 590f9305b8
commit 5e9df605e4
5 changed files with 193 additions and 3 deletions

View File

@@ -0,0 +1,40 @@
"use server";
import { embed, generateText } from "ai";
import { z } from "zod";
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
import { embeddingsModel, llmModel } from "@formbricks/lib/ai";
import { findNearestDocuments, getDocumentsByTypeAndReferenceId } from "@formbricks/lib/document/service";
import { getQuestionResponseReferenceId } from "@formbricks/lib/document/utils";
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
import { ZId } from "@formbricks/types/environment";
const ZSearchDocuments = z.object({
environmentId: ZId,
searchTerm: z.string(),
});
export const searchDocumentsAction = authenticatedActionClient
.schema(ZSearchDocuments)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
rules: ["environment", "read"],
});
const { text } = await generateText({
model: llmModel,
prompt: `Generate 4 example survey feedback responses from users in one line, separated by comma for the following topic: ${parsedInput.searchTerm}`,
});
const { embedding } = await embed({
model: embeddingsModel,
value: text,
});
const documents = findNearestDocuments(parsedInput.environmentId, embedding, 10);
return documents;
});

View File

@@ -0,0 +1,55 @@
"use client";
import { useState } from "react";
import { TDocument } from "@formbricks/types/documents";
import { Button } from "@formbricks/ui/Button";
import { Card } from "@formbricks/ui/Card";
import { Input } from "@formbricks/ui/Input";
import { searchDocumentsAction } from "../actions";
interface DocumentSearchProps {
environmentId: string;
}
export const DocumentSearch = ({ environmentId }: DocumentSearchProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [documents, setDocuments] = useState<TDocument[]>([]);
const searchDocuments = async () => {
const documents = await searchDocumentsAction({ environmentId, searchTerm });
if (documents?.data) {
setDocuments(documents.data);
} else {
console.error(documents);
}
};
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
searchDocuments();
}}
className="flex w-full space-x-2">
<Input
placeholder="What are users thinking the performance of my app?"
value={searchTerm}
onChange={(v) => setSearchTerm(v.target.value)}
/>
<Button className="h-10">Search</Button>
</form>
<div className="flex-col space-y-4">
{documents.map((document) => (
<div className="overflow-hidden rounded-lg bg-white shadow">
<div className="whitespace-pre-wrap px-4 py-5 sm:p-6">
<p>{document.text}</p>
<hr className="my-4 text-slate-300" />
<p className="text-xs">Survey Response</p>
</div>
</div>
))}
</div>
</>
);
};

View File

@@ -0,0 +1,7 @@
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
const Loading = () => {
return <LoadingSpinner />;
};
export default Loading;

View File

@@ -0,0 +1,45 @@
import { DocumentSearch } from "@/app/(app)/environments/[environmentId]/documents/components/DocumentSearch";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getUser } from "@formbricks/lib/user/service";
import { TTemplateRole } from "@formbricks/types/templates";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
export const metadata: Metadata = {
title: "Your Surveys",
};
interface SurveyTemplateProps {
params: {
environmentId: string;
};
searchParams: {
role?: TTemplateRole;
};
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Session not found");
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
}
return (
<PageContentWrapper>
<PageHeader pageTitle="Documents" />
<DocumentSearch environmentId={params.environmentId} />
</PageContentWrapper>
);
};
export default Page;

View File

@@ -9,6 +9,7 @@ import {
ZDocumentCreateInput,
ZDocumentType,
} from "@formbricks/types/documents";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError } from "@formbricks/types/errors";
import { cache } from "../cache";
import { validateInputs } from "../utils/validate";
@@ -72,9 +73,9 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
text,
"referenceId",
vector::text
FROM "Document" e
WHERE e."type" = ${type}::"DocumentType"
AND e."referenceId" = ${referenceId}
FROM "Document" d
WHERE d."type" = ${type}::"DocumentType"
AND d."referenceId" = ${referenceId}
`;
const documents = prismaDocuments.map((prismaDocument) => {
@@ -104,3 +105,45 @@ export const getDocumentsByTypeAndReferenceId = reactCache(
}
)()
);
export const findNearestDocuments = async (
environmentId: string,
vector: number[],
limit: number = 5
): Promise<TDocument[]> => {
validateInputs([environmentId, ZId]);
const threshold = 0.8; //0.2;
// Convert the embedding array to a JSON-like string representation
const vectorString = `[${vector.join(",")}]`;
// Execute raw SQL query to find nearest neighbors and exclude the vector column
const prismaDocuments: TPrismaDocument[] = await prisma.$queryRaw`
SELECT
id,
created_at AS "createdAt",
updated_at AS "updatedAt",
type,
text,
"referenceId",
vector::text
FROM "Document" d
WHERE d."environmentId" = ${environmentId}
AND d."vector" <=> ${vectorString}::vector(512) <= ${threshold}
ORDER BY d."vector" <=> ${vectorString}::vector(512)
LIMIT ${limit};
`;
const documents = prismaDocuments.map((prismaDocument) => {
// Convert the string representation of the vector back to an array of numbers
const vector = prismaDocument.vector
.slice(1, -1) // Remove the surrounding square brackets
.split(",") // Split the string into an array of strings
.map(Number); // Convert each string to a number
return {
...prismaDocument,
vector,
};
});
return documents;
};