chat-ui / src /lib /server /database.ts
nsarrazin's picture
nsarrazin HF staff
Move vars to dynamic, add metrics (#1085)
98b1c51 unverified
import { env } from "$env/dynamic/private";
import { GridFSBucket, MongoClient } from "mongodb";
import type { Conversation } from "$lib/types/Conversation";
import type { SharedConversation } from "$lib/types/SharedConversation";
import type { AbortedGeneration } from "$lib/types/AbortedGeneration";
import type { Settings } from "$lib/types/Settings";
import type { User } from "$lib/types/User";
import type { MessageEvent } from "$lib/types/MessageEvent";
import type { Session } from "$lib/types/Session";
import type { Assistant } from "$lib/types/Assistant";
import type { Report } from "$lib/types/Report";
import type { ConversationStats } from "$lib/types/ConversationStats";
import type { MigrationResult } from "$lib/types/MigrationResult";
import type { Semaphore } from "$lib/types/Semaphore";
import type { AssistantStats } from "$lib/types/AssistantStats";
import { logger } from "$lib/server/logger";
import { building } from "$app/environment";
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
export class Database {
private client: MongoClient;
private static instance: Database;
private constructor() {
if (!env.MONGODB_URL) {
throw new Error(
"Please specify the MONGODB_URL environment variable inside .env.local. Set it to mongodb://localhost:27017 if you are running MongoDB locally, or to a MongoDB Atlas free instance for example."
);
}
this.client = new MongoClient(env.MONGODB_URL, {
directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
});
this.client.connect().catch((err) => {
logger.error("Connection error", err);
process.exit(1);
});
this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
this.client.on("open", () => this.initDatabase());
// Disconnect DB on process kill
process.on("SIGINT", async () => {
await this.client.close(true);
// https://github.com/sveltejs/kit/issues/9540
setTimeout(() => {
process.exit(0);
}, 100);
});
}
public static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
/**
* Return mongoClient
*/
public getClient(): MongoClient {
return this.client;
}
/**
* Return map of database's collections
*/
public getCollections() {
const db = this.client.db(
env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
);
const conversations = db.collection<Conversation>("conversations");
const conversationStats = db.collection<ConversationStats>(CONVERSATION_STATS_COLLECTION);
const assistants = db.collection<Assistant>("assistants");
const assistantStats = db.collection<AssistantStats>("assistants.stats");
const reports = db.collection<Report>("reports");
const sharedConversations = db.collection<SharedConversation>("sharedConversations");
const abortedGenerations = db.collection<AbortedGeneration>("abortedGenerations");
const settings = db.collection<Settings>("settings");
const users = db.collection<User>("users");
const sessions = db.collection<Session>("sessions");
const messageEvents = db.collection<MessageEvent>("messageEvents");
const bucket = new GridFSBucket(db, { bucketName: "files" });
const migrationResults = db.collection<MigrationResult>("migrationResults");
const semaphores = db.collection<Semaphore>("semaphores");
return {
conversations,
conversationStats,
assistants,
assistantStats,
reports,
sharedConversations,
abortedGenerations,
settings,
users,
sessions,
messageEvents,
bucket,
migrationResults,
semaphores,
};
}
/**
* Init database once connected: Index creation
* @private
*/
private initDatabase() {
const {
conversations,
conversationStats,
assistants,
assistantStats,
reports,
sharedConversations,
abortedGenerations,
settings,
users,
sessions,
messageEvents,
semaphores,
} = this.getCollections();
conversations
.createIndex(
{ sessionId: 1, updatedAt: -1 },
{ partialFilterExpression: { sessionId: { $exists: true } } }
)
.catch(logger.error);
conversations
.createIndex(
{ userId: 1, updatedAt: -1 },
{ partialFilterExpression: { userId: { $exists: true } } }
)
.catch(logger.error);
conversations
.createIndex(
{ "message.id": 1, "message.ancestors": 1 },
{ partialFilterExpression: { userId: { $exists: true } } }
)
.catch(logger.error);
// Not strictly necessary, could use _id, but more convenient. Also for stats
// To do stats on conversation messages
conversations.createIndex({ "messages.createdAt": 1 }, { sparse: true }).catch(logger.error);
// Unique index for stats
conversationStats
.createIndex(
{
type: 1,
"date.field": 1,
"date.span": 1,
"date.at": 1,
distinct: 1,
},
{ unique: true }
)
.catch(logger.error);
// Allow easy check of last computed stat for given type/dateField
conversationStats
.createIndex({
type: 1,
"date.field": 1,
"date.at": 1,
})
.catch(logger.error);
abortedGenerations
.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 30 })
.catch(logger.error);
abortedGenerations.createIndex({ conversationId: 1 }, { unique: true }).catch(logger.error);
sharedConversations.createIndex({ hash: 1 }, { unique: true }).catch(logger.error);
settings.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
settings.createIndex({ userId: 1 }, { unique: true, sparse: true }).catch(logger.error);
settings.createIndex({ assistants: 1 }).catch(logger.error);
users.createIndex({ hfUserId: 1 }, { unique: true }).catch(logger.error);
users.createIndex({ sessionId: 1 }, { unique: true, sparse: true }).catch(logger.error);
// No unicity because due to renames & outdated info from oauth provider, there may be the same username on different users
users.createIndex({ username: 1 }).catch(logger.error);
messageEvents.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(logger.error);
sessions.createIndex({ sessionId: 1 }, { unique: true }).catch(logger.error);
assistants.createIndex({ createdById: 1, userCount: -1 }).catch(logger.error);
assistants.createIndex({ userCount: 1 }).catch(logger.error);
assistants.createIndex({ featured: 1, userCount: -1 }).catch(logger.error);
assistants.createIndex({ modelId: 1, userCount: -1 }).catch(logger.error);
assistants.createIndex({ searchTokens: 1 }).catch(logger.error);
assistants.createIndex({ last24HoursCount: 1 }).catch(logger.error);
assistantStats
// Order of keys is important for the queries
.createIndex({ "date.span": 1, "date.at": 1, assistantId: 1 }, { unique: true })
.catch(logger.error);
reports.createIndex({ assistantId: 1 }).catch(logger.error);
reports.createIndex({ createdBy: 1, assistantId: 1 }).catch(logger.error);
// Unique index for semaphore and migration results
semaphores.createIndex({ key: 1 }, { unique: true }).catch(logger.error);
semaphores.createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }).catch(logger.error);
}
}
export const collections = building
? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
: Database.getInstance().getCollections();