diff --git a/apps/api/package.json b/apps/api/package.json index ca1942b..68ae90b 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -26,6 +26,7 @@ "@langchain/textsplitters": "^1.0.0", "@neondatabase/serverless": "^1.0.2", "@qdrant/js-client-rest": "^1.15.1", + "@sevinf/maybe": "^0.5.0", "better-auth": "^1.3.34", "bullmq": "^5.63.1", "cookie-parser": "^1.4.7", diff --git a/apps/api/src/db/qdrant.ts b/apps/api/src/db/qdrant.ts index c1e3c01..3e1f7b2 100644 --- a/apps/api/src/db/qdrant.ts +++ b/apps/api/src/db/qdrant.ts @@ -1,5 +1,6 @@ import { HuggingFaceInferenceEmbeddings } from "@langchain/community/embeddings/hf"; import { QdrantVectorStore } from "@langchain/qdrant"; +import { QdrantClient } from "@qdrant/js-client-rest"; import config from "../config/index.js"; let vectorStoreInstance: QdrantVectorStore | null = null; @@ -8,17 +9,65 @@ const embeddings = new HuggingFaceInferenceEmbeddings({ model: "sentence-transformers/all-MiniLM-L6-v2", }); +const COLLECTION_NAME = "resume"; +const VECTOR_SIZE = 384; // all-MiniLM-L6-v2 produces 384-dim vectors + +async function ensureCollectionExists() { + const client = new QdrantClient({ + url: config.qdrant.url!, + apiKey: config.qdrant.key!, + }); + + try { + const collections = await client.getCollections(); + const exists = collections.collections.some( + (c) => c.name === COLLECTION_NAME, + ); + + if (!exists) { + console.log(`Collection "${COLLECTION_NAME}" not found, creating...`); + await client.createCollection(COLLECTION_NAME, { + vectors: { + size: VECTOR_SIZE, + distance: "Cosine", + }, + }); + console.log(`✅ Collection "${COLLECTION_NAME}" created`); + + // Create index for candidateId + await client.createPayloadIndex(COLLECTION_NAME, { + field_name: "metadata.candidateId", + field_schema: "keyword", + }); + console.log("✅ Index created for candidateId"); + } + } catch (error) { + console.error("Error ensuring collection exists:", error); + throw error; + } +} + export const connectQdrant = async () => { - vectorStoreInstance = await QdrantVectorStore.fromExistingCollection( - embeddings, - { - url: config.qdrant.url!, - collectionName: "resume", - apiKey: config.qdrant.key!, - contentPayloadKey: "content", - }, - ); - console.log("✅ Qdrant vector store connected"); + try { + await ensureCollectionExists(); + + vectorStoreInstance = await QdrantVectorStore.fromExistingCollection( + embeddings, + { + url: config.qdrant.url!, + collectionName: COLLECTION_NAME, + apiKey: config.qdrant.key!, + contentPayloadKey: "content", + }, + ); + console.log("✅ Qdrant vector store connected"); + } catch (error) { + console.error("❌ Failed to connect to Qdrant:", error); + // Don't crash the server, just log the error + console.warn( + "⚠️ Server will continue without Qdrant. Resume features may not work.", + ); + } }; export const getVectorStore = async (): Promise => { @@ -26,11 +75,13 @@ export const getVectorStore = async (): Promise => { return vectorStoreInstance; } try { + await ensureCollectionExists(); + vectorStoreInstance = await QdrantVectorStore.fromExistingCollection( embeddings, { url: config.qdrant.url!, - collectionName: "resume", + collectionName: COLLECTION_NAME, apiKey: config.qdrant.key!, contentPayloadKey: "content", }, diff --git a/yarn.lock b/yarn.lock index 5192950..cb29d7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2061,7 +2061,7 @@ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.55.0.tgz#4964920229fcf649237ef13b1533dfc4b9f6b22e" integrity sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA== -"@sevinf/maybe@0.5.0": +"@sevinf/maybe@0.5.0", "@sevinf/maybe@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@sevinf/maybe/-/maybe-0.5.0.tgz#e59fcea028df615fe87d708bb30e1f338e46bb44" integrity sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==