Skip to content

Commit 324ee36

Browse files
authored
refactor: reduce semantic search overhead … (#1128)
* refactor: reduce semantic search overhead - Strip (title, description, score, and text) from the python microservice, remain only neccessary (subject, courseNumber) - Replace post-Python CatalogClassModel.find() with the in-memory catalog cache already populated by fuzzy search, eliminating a edundant MongoDB round-trip on warm cache * fix: codex code review comment
1 parent 73d799c commit 324ee36

5 files changed

Lines changed: 19 additions & 35 deletions

File tree

apps/backend/src/modules/catalog/catalog-cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const evictOldest = () => {
3131
}
3232
};
3333

34-
const getCachedCatalog = async (
34+
export const getCachedCatalog = async (
3535
year: number,
3636
semester: string
3737
): Promise<ICatalogClassItem[]> => {

apps/backend/src/modules/catalog/controller.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { formatCourse } from "../course/formatter";
3232
import { formatEnrollment } from "../enrollment/formatter";
3333
import type { EnrollmentModule } from "../enrollment/generated-types/module-types";
3434
import type { GradeDistributionModule } from "../grade-distribution/generated-types/module-types";
35-
import { getSearchIndex } from "./catalog-cache";
35+
import { getCachedCatalog, getSearchIndex } from "./catalog-cache";
3636

3737
export interface CatalogQueryParams {
3838
year: number;
@@ -160,40 +160,39 @@ const getCatalogWithSemanticSearch = async ({
160160
limit: number;
161161
skip: number;
162162
}) => {
163-
// Throws if the semantic service is unavailable — surfaces as GraphQL error
164163
const response = await searchSemantic(searchTerm, year, semester);
165164

166165
if (response.results.length === 0) {
167166
return { results: [], totalCount: 0 };
168167
}
169168

170-
// Throws if the semantic service is unavailable — surfaces as GraphQL error
171169
// Python already returns results sorted by score descending — preserve that order.
172170
// Build a rank map: "subject-courseNumber" → position in Python's ranked list
173171
const rankMap = new Map<string, number>();
174172
response.results.forEach(({ subject, courseNumber }, index) => {
175173
rankMap.set(`${subject}-${courseNumber}`, index);
176174
});
177175

178-
const query = buildFilterQuery(year, semester, filters);
179-
query.$or = response.results.map(({ subject, courseNumber }) => ({
180-
subject,
181-
courseNumber,
182-
})) as unknown as CatalogFilterCondition[];
176+
// Reuse the server-side catalog cache (same one fuzzy search uses) to avoid
177+
// a redundant MongoDB round-trip.
178+
const allCatalog = await getCachedCatalog(year, semester);
179+
180+
const semanticMatches = allCatalog.filter((item) =>
181+
rankMap.has(`${item.subject}-${item.courseNumber}`)
182+
);
183183

184-
// Fetch all matching docs — semantic result sets are small (bounded by threshold)
185-
const allResults = await CatalogClassModel.find(query).lean();
184+
const filtered = applyInMemoryFilters(semanticMatches, filters);
186185

187186
// Sort by Python's relevance rank, then by section number within the same course
188-
allResults.sort((a, b) => {
187+
filtered.sort((a, b) => {
189188
const rankA = rankMap.get(`${a.subject}-${a.courseNumber}`) ?? 999;
190189
const rankB = rankMap.get(`${b.subject}-${b.courseNumber}`) ?? 999;
191190
if (rankA !== rankB) return rankA - rankB;
192191
return a.number.localeCompare(b.number);
193192
});
194193

195-
const totalCount = allResults.length;
196-
const results = allResults.slice(skip, skip + limit);
194+
const totalCount = filtered.length;
195+
const results = filtered.slice(skip, skip + limit);
197196

198197
return { results, totalCount };
199198
};

apps/backend/src/modules/semantic-search/client.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ import { config } from "../../../../../packages/common/src/utils/config";
33
interface SemanticSearchResult {
44
subject: string;
55
courseNumber: string;
6-
title: string;
7-
description: string;
8-
score: number;
9-
text: string;
106
}
117

128
interface SemanticSearchResponse {

apps/backend/src/modules/semantic-search/controller.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@ export async function searchCourses(req: Request, res: Response) {
2626
thresholdNum
2727
);
2828

29-
// Return lightweight response: only subject + courseNumber + score
3029
const courseIds = results.results.map((r) => ({
3130
subject: r.subject,
3231
courseNumber: r.courseNumber,
33-
score: r.score,
3432
}));
3533

3634
return res.json({

apps/semantic-search/app/engine.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -325,34 +325,25 @@ def search(
325325
vq = VectorQuery(
326326
vector=query_vec.tolist(),
327327
vector_field_name="embedding",
328-
return_fields=["subject", "course_number", "title", "description", "course_text"],
328+
return_fields=["subject", "course_number"],
329329
num_results=search_k,
330330
)
331331

332332
raw_results = entry.index.query(vq)
333333

334334
# RedisVL COSINE distance = 1 - cosine_similarity, so convert threshold accordingly
335335
distance_threshold = 1.0 - threshold
336-
results = []
336+
scored = []
337337
for r in raw_results:
338338
dist = float(r.get("vector_distance", 1.0))
339339
if dist > distance_threshold:
340340
continue
341-
results.append(
342-
{
343-
"subject": r.get("subject") or None,
344-
"courseNumber": r.get("course_number") or None,
345-
"title": r.get("title") or "",
346-
"description": r.get("description") or "",
347-
"score": 1.0 - dist,
348-
"text": r.get("course_text") or "",
349-
}
350-
)
341+
scored.append((1.0 - dist, r.get("subject") or None, r.get("course_number") or None))
351342

352-
# Sort by score only - semantic similarity is more accurate than keyword matching
353-
results.sort(key=lambda r: r["score"], reverse=True)
343+
# Sort by score descending — backend preserves this order for ranking
344+
scored.sort(key=lambda x: x[0], reverse=True)
354345

355-
# Return all results above threshold
346+
results = [{"subject": subj, "courseNumber": cn} for _, subj, cn in scored]
356347
return results, entry
357348

358349
def describe_indices(self) -> List[Dict]:

0 commit comments

Comments
 (0)