From 44708bc226ca8155ed64a9fae96d57c8cf474072 Mon Sep 17 00:00:00 2001 From: anunayajoshi Date: Mon, 4 Nov 2024 16:12:50 +0800 Subject: [PATCH 1/3] fallbacks for random qn --- .../src/services/get/get-random-question.ts | 129 +++++++++++------- 1 file changed, 77 insertions(+), 52 deletions(-) diff --git a/backend/question/src/services/get/get-random-question.ts b/backend/question/src/services/get/get-random-question.ts index 2cb5f62ba4..7484dc72ef 100644 --- a/backend/question/src/services/get/get-random-question.ts +++ b/backend/question/src/services/get/get-random-question.ts @@ -88,9 +88,6 @@ type Params = { // Fetch an unattempted question or fallback to the least attempted one export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }: Params) => { - /** - * 1. Both Unattempted - */ // If an attempt contains either user's ID const ids = [userId1, userId2]; const userIdClause = [ @@ -103,62 +100,90 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty } or(...userIdClause), ]; - // Build the filter clause - // - attempt ID null: No attempts - // - topics: If specified, must intersect using Array Intersect - const filterClause = []; - - if (topics) { - filterClause.push(arrayOverlaps(QUESTIONS_TABLE.topic, topics)); - } + // Try different filter combinations in order of specificity + const filterCombinations = [ + // Exact match + topics && difficulty + ? [arrayOverlaps(QUESTIONS_TABLE.topic, topics), eq(QUESTIONS_TABLE.difficulty, difficulty)] + : // Topic only + topics + ? [arrayOverlaps(QUESTIONS_TABLE.topic, topics)] + : // Difficulty only + difficulty + ? [eq(QUESTIONS_TABLE.difficulty, difficulty)] + : // No filters + [], + ]; - if (difficulty) { - filterClause.push(eq(QUESTIONS_TABLE.difficulty, difficulty)); + // Additional combinations if both topic and difficulty are provided + if (topics && difficulty) { + filterCombinations.push( + // Topic only + [arrayOverlaps(QUESTIONS_TABLE.topic, topics)], + // Difficulty only + [eq(QUESTIONS_TABLE.difficulty, difficulty)], + // No filters + [] + ); } - const bothUnattempted = await db - .select({ question: QUESTIONS_TABLE }) - .from(QUESTIONS_TABLE) - .leftJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) - .where(and(isNull(QUESTION_ATTEMPTS_TABLE.attemptId), ...filterClause)) - .orderBy(sql`RANDOM()`) - .limit(1); + for (const filterClause of filterCombinations) { + // Check if questions exist with current filters + const questionExists = await db + .select({ count: sql`count(*)` }) + .from(QUESTIONS_TABLE) + .where(and(...filterClause)) + .then((result) => Number(result[0].count) > 0); - if (bothUnattempted && bothUnattempted.length > 0) { - return bothUnattempted[0].question; - } + if (!questionExists) { + continue; + } - // 2. At least one user has attempted. - // - Fetch all questions, summing attempts by both users, ranking and selecting the lowest count. - const attempts = db.$with('at').as( - db - .select({ - ...getTableColumns(QUESTIONS_TABLE), - user1Count: - sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId1}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId1}::uuid THEN 1 END)`.as( - 'user1_attempts' - ), - user2Count: - sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId2}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId2}::uuid THEN 1 END)`.as( - 'user2_attempts' - ), - }) + // Try to find an unattempted question with current filters + const bothUnattempted = await db + .select({ question: QUESTIONS_TABLE }) .from(QUESTIONS_TABLE) - .innerJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) - .where(and(...filterClause)) - .groupBy(QUESTIONS_TABLE.id) - ); - const result = await db - .with(attempts) - .select() - .from(attempts) - .orderBy(asc(sql`COALESCE(user1_attempts,0) + COALESCE(user2_attempts,0)`)) - .limit(1); + .leftJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) + .where(and(isNull(QUESTION_ATTEMPTS_TABLE.attemptId), ...filterClause)) + .orderBy(sql`RANDOM()`) + .limit(1); + + if (bothUnattempted && bothUnattempted.length > 0) { + return bothUnattempted[0].question; + } + + // If no unattempted question, try least attempted + const attempts = db.$with('at').as( + db + .select({ + ...getTableColumns(QUESTIONS_TABLE), + user1Count: + sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId1}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId1}::uuid THEN 1 END)`.as( + 'user1_attempts' + ), + user2Count: + sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId2}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId2}::uuid THEN 1 END)`.as( + 'user2_attempts' + ), + }) + .from(QUESTIONS_TABLE) + .innerJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) + .where(and(...filterClause)) + .groupBy(QUESTIONS_TABLE.id) + ); + + const result = await db + .with(attempts) + .select() + .from(attempts) + .orderBy(asc(sql`COALESCE(user1_attempts,0) + COALESCE(user2_attempts,0)`)) + .limit(1); - if (result && result.length > 0) { - return { ...result[0], user1Count: undefined, user2Count: undefined }; + if (result && result.length > 0) { + return { ...result[0], user1Count: undefined, user2Count: undefined }; + } } - // This branch should not be reached - logger.info('Unreachable Branch - If first query fails, second query must return something'); + logger.error('No questions found with any filter combination'); + return null; }; From 8015f4953bc274d33219a1827f2212dccbc7a995 Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Mon, 4 Nov 2024 17:14:26 +0800 Subject: [PATCH 2/3] PEER-219: Add question history dynamic query builder for attempted qns Signed-off-by: SeeuSim --- backend/matching/src/services/collab.ts | 3 +- .../matching/src/services/get-match-items.ts | 7 +- backend/matching/src/types/index.ts | 1 + .../src/services/get/get-random-question.ts | 71 ++++++++++++------- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/backend/matching/src/services/collab.ts b/backend/matching/src/services/collab.ts index 4f78954bda..892a516b22 100644 --- a/backend/matching/src/services/collab.ts +++ b/backend/matching/src/services/collab.ts @@ -3,7 +3,8 @@ import { collabServiceClient, routes } from './_hosts'; export async function createRoom( userId1: string, userId2: string, - questionId: string + questionId: string, + attemptCounts: number ): Promise { const response = await collabServiceClient.get<{ roomName: string }>( routes.COLLAB_SERVICE.GET_ROOM.path, diff --git a/backend/matching/src/services/get-match-items.ts b/backend/matching/src/services/get-match-items.ts index f5b206635e..2bae1b1822 100644 --- a/backend/matching/src/services/get-match-items.ts +++ b/backend/matching/src/services/get-match-items.ts @@ -33,7 +33,12 @@ export async function getMatchItems( return undefined; } - const roomId = await createRoom(userId1, userId2, question.id.toString()); + const roomId = await createRoom( + userId1, + userId2, + question.id.toString(), + question.attemptCount + ); logger.info('Successfully got match items'); return { diff --git a/backend/matching/src/types/index.ts b/backend/matching/src/types/index.ts index 64ab520787..6f08d588dc 100644 --- a/backend/matching/src/types/index.ts +++ b/backend/matching/src/types/index.ts @@ -58,6 +58,7 @@ export interface IQuestion { // description: string; // difficulty: string; // topic: string[]; + attemptCount: number; } export interface IGetRandomQuestionPayload { diff --git a/backend/question/src/services/get/get-random-question.ts b/backend/question/src/services/get/get-random-question.ts index 7484dc72ef..b5b0dbce9a 100644 --- a/backend/question/src/services/get/get-random-question.ts +++ b/backend/question/src/services/get/get-random-question.ts @@ -5,6 +5,7 @@ import { eq, getTableColumns, inArray, + InferSelectModel, isNull, or, sql, @@ -86,8 +87,17 @@ type Params = { difficulty?: string; }; +type IGetRandomQuestionResponse = InferSelectModel & { + attemptCount: number; +}; + // Fetch an unattempted question or fallback to the least attempted one -export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty }: Params) => { +export const getRandomQuestion = async ({ + userId1, + userId2, + topics, + difficulty, +}: Params): Promise => { // If an attempt contains either user's ID const ids = [userId1, userId2]; const userIdClause = [ @@ -128,14 +138,15 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty } } for (const filterClause of filterCombinations) { - // Check if questions exist with current filters - const questionExists = await db - .select({ count: sql`count(*)` }) + // Check if AT LEAST 1 question exists with current filters + const questionCounts = await db + .select({ id: QUESTIONS_TABLE.id }) .from(QUESTIONS_TABLE) .where(and(...filterClause)) - .then((result) => Number(result[0].count) > 0); + .limit(1); - if (!questionExists) { + // No questions exist with the filter. + if (!questionCounts || !questionCounts.length) { continue; } @@ -149,28 +160,33 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty } .limit(1); if (bothUnattempted && bothUnattempted.length > 0) { - return bothUnattempted[0].question; + return { ...bothUnattempted[0].question, attemptCount: 0 }; } // If no unattempted question, try least attempted - const attempts = db.$with('at').as( - db - .select({ - ...getTableColumns(QUESTIONS_TABLE), - user1Count: - sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId1}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId1}::uuid THEN 1 END)`.as( - 'user1_attempts' - ), - user2Count: - sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId2}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId2}::uuid THEN 1 END)`.as( - 'user2_attempts' - ), - }) - .from(QUESTIONS_TABLE) - .innerJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) - .where(and(...filterClause)) - .groupBy(QUESTIONS_TABLE.id) - ); + let nestedQuery = db + .select({ + ...getTableColumns(QUESTIONS_TABLE), + user1Count: + sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId1}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId1}::uuid THEN 1 END)`.as( + 'user1_attempts' + ), + user2Count: + sql`SUM(CASE WHEN ${QUESTION_ATTEMPTS_TABLE.userId1} = ${userId2}::uuid OR ${QUESTION_ATTEMPTS_TABLE.userId2} = ${userId2}::uuid THEN 1 END)`.as( + 'user2_attempts' + ), + }) + .from(QUESTIONS_TABLE) + .innerJoin(QUESTION_ATTEMPTS_TABLE, and(...joinClause)) + .$dynamic(); + + if (filterClause.length) { + nestedQuery = nestedQuery.where(and(...filterClause)); + } + + nestedQuery = nestedQuery.groupBy(QUESTIONS_TABLE.id); + + const attempts = db.$with('at').as(nestedQuery); const result = await db .with(attempts) @@ -180,7 +196,10 @@ export const getRandomQuestion = async ({ userId1, userId2, topics, difficulty } .limit(1); if (result && result.length > 0) { - return { ...result[0], user1Count: undefined, user2Count: undefined }; + const { user1Count, user2Count, ...details } = result[0]; + const attemptCount = + (user1Count ? (user1Count as number) : 0) + (user2Count ? (user2Count as number) : 0); + return { ...details, attemptCount }; } } From 7f2ae2f669f8a6d420019ed0045be7cbfe13011e Mon Sep 17 00:00:00 2001 From: SeeuSim Date: Mon, 4 Nov 2024 17:26:20 +0800 Subject: [PATCH 3/3] PEER-219: Remove build error for unused param Signed-off-by: SeeuSim --- backend/matching/src/services/collab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/matching/src/services/collab.ts b/backend/matching/src/services/collab.ts index 892a516b22..3d06ca34c4 100644 --- a/backend/matching/src/services/collab.ts +++ b/backend/matching/src/services/collab.ts @@ -4,7 +4,7 @@ export async function createRoom( userId1: string, userId2: string, questionId: string, - attemptCounts: number + _attemptCounts: number ): Promise { const response = await collabServiceClient.get<{ roomName: string }>( routes.COLLAB_SERVICE.GET_ROOM.path,