From 8b522ed7e74365fe23a6668fbd1561d85c135e92 Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Wed, 25 Mar 2026 13:24:11 +0800 Subject: [PATCH 1/7] SSD-1160 add quoted exact keyword mode for search --- src/controllers/search.controller.ts | 3 +-- src/services/search.svc.ts | 2 +- src/utils/query/fts.util.ts | 27 +++++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 77460d6..3c32ba2 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -36,11 +36,10 @@ export async function getSearchResults(request: FastifyRequest<{ Querystring: Se const startDate = await startDatePromise const endDate = request.query.end_date ?? new Date().toISOString().slice(0, 10) const windowSize = Number(request.query.window_size ?? 120) - const q = (request.query.q ?? '').toString().trim().toLowerCase() + const q = (request.query.q ?? '').toString().trim() const uid = request.query.uid ? Number(request.query.uid) : undefined const pageSize = Number(request.query.page_size ?? DEFAULT_PAGE_SIZE) const pageInput = Math.max(1, Number(request.query.page ?? 1)) - const searchSvc = new SearchService() const serviceResponse = await searchSvc.search(sequelize, request.query, { startDate, diff --git a/src/services/search.svc.ts b/src/services/search.svc.ts index f28fbda..efc5809 100644 --- a/src/services/search.svc.ts +++ b/src/services/search.svc.ts @@ -120,7 +120,7 @@ export class SearchService { selectHeadline = headlineFragment.select selectRank = headlineFragment.rankSelect orderBy = headlineFragment.order - repl.q = parameters.q + Object.assign(repl, headlineFragment.params) whereParts.push(headlineFragment.condition) } diff --git a/src/utils/query/fts.util.ts b/src/utils/query/fts.util.ts index 29f3029..9db9a71 100644 --- a/src/utils/query/fts.util.ts +++ b/src/utils/query/fts.util.ts @@ -1,17 +1,40 @@ import type { SqlBindings } from '@/types' +function escapeLikePattern(value: string): string { + return value.replace(/[\\%_]/g, '\\$&') +} + export function buildHeadlineFragment( q: string, windowSize: number, ): { select: string; rankSelect: string; order: string; params: SqlBindings; condition: string } | null { - const trimmed = q.trim().toLowerCase() + const trimmed = q.trim() if (!trimmed) return null const adjustedWindow = Math.max(10, windowSize - 10) + + const isQuoted = trimmed.startsWith('"') && trimmed.endsWith('"') + if (isQuoted) { + const literal = trimmed.slice(1, -1).trim().toLowerCase() + if (!literal) return null + + return { + select: `, ts_headline('english', s.speech, plainto_tsquery('english', :q), 'StartSel===, StopSel===, MinWords=${adjustedWindow}, MaxWords=${windowSize}') as headline`, + rankSelect: ", ts_rank(s.speech_vector, plainto_tsquery('english', :q)) as rank", + order: 'si.date DESC, rank DESC', + params: { + q: literal, + qLiteral: `%${escapeLikePattern(literal)}%`, + }, + condition: "LOWER(s.speech) LIKE :qLiteral ESCAPE '\\'", + } + } + + const normalized = trimmed.toLowerCase() return { select: `, ts_headline('english', s.speech, plainto_tsquery('english', :q), 'StartSel===, StopSel===, MinWords=${adjustedWindow}, MaxWords=${windowSize}') as headline`, rankSelect: ", ts_rank(s.speech_vector, plainto_tsquery('english', :q)) as rank", order: 'si.date DESC, rank DESC', - params: { q: trimmed }, + params: { q: normalized }, condition: "s.speech_vector @@ plainto_tsquery('english', :q)", } } From 3f073c64236cf43e7d32e3cf31d084a85fcaf1f8 Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Thu, 26 Mar 2026 15:39:33 +0800 Subject: [PATCH 2/7] SSD-1156 new keyword search counter --- src/controllers/search.controller.ts | 37 +++++++++- src/routes/search.route.ts | 17 ++++- src/services/search.svc.ts | 103 +++++++++++++++++++++++++++ src/types/schema-derived/requests.ts | 3 +- 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 3c32ba2..81a1084 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -1,7 +1,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' import { SearchService } from '@/services/search.svc' -import type { SearchMPDocResultsResponse, SearchQuery, SearchResultsResponse } from '@/types' +import type { SearchCounterQuery, SearchCounterResponse, SearchMPDocResultsResponse, SearchQuery, SearchResultsResponse } from '@/types' import { type House, HOUSE_CODE, HOUSE_TO_CODE } from '@/types/enum' import { deriveDefaultStartDateDR } from '@/utils' @@ -117,6 +117,41 @@ export async function getSearchMPDocResults(request: FastifyRequest<{ Querystrin } } +export async function getSearchCounter(request: FastifyRequest<{ Querystring: SearchCounterQuery }>, reply: FastifyReply) { + try { + const { sequelize } = request.server + const startDate = request.query.start_date ?? (await deriveDefaultStartDateDR(request.server.models)) + const endDate = request.query.end_date ?? new Date().toISOString().slice(0, 10) + const windowSize = Number(request.query.window_size ?? 120) + const q = (request.query.q ?? '').toString().trim() + const uid = request.query.uid ? Number(request.query.uid) : undefined + + const searchSvc = new SearchService() + const serviceResponse = await searchSvc.searchCounter(sequelize, request.query, { + startDate, + endDate, + windowSize, + q, + uid, + }) + + if (serviceResponse.error || !serviceResponse.success) { + const { code, type, message } = serviceResponse.error ?? { + code: 500, + type: 'text/plain', + message: 'Internal Server Error', + } + return reply.code(code).type(type).send(message) + } + + const response: SearchCounterResponse = serviceResponse.success + return reply.send(response) + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Bad Request' + return reply.code(400).send({ error: message }) + } +} + export async function getSearchPlot(request: FastifyRequest<{ Querystring: SearchQuery }>, reply: FastifyReply) { try { const { sequelize } = request.server diff --git a/src/routes/search.route.ts b/src/routes/search.route.ts index ced12da..845b61f 100644 --- a/src/routes/search.route.ts +++ b/src/routes/search.route.ts @@ -1,7 +1,9 @@ import type { FastifyInstance } from 'fastify' -import { getSearchMPDocResults, getSearchPlot, getSearchResults } from '@/controllers/search.controller' +import { getSearchCounter, getSearchMPDocResults, getSearchPlot, getSearchResults } from '@/controllers/search.controller' import { + searchCounterQuerySchema, + searchCounterResponseSchema, searchMPDocResultsResponseSchema, searchPlotQuerySchema, searchPlotResponseSchema, @@ -23,6 +25,19 @@ export async function registerSearchRoutes(app: FastifyInstance) { getSearchResults, ) + app.get( + '/search/counter', + { + schema: { + tags: ['Search'], + summary: 'Keyword result counter by house', + querystring: searchCounterQuerySchema, + response: { 200: searchCounterResponseSchema }, + }, + }, + getSearchCounter, + ) + app.get( '/search-mp-doc', { diff --git a/src/services/search.svc.ts b/src/services/search.svc.ts index 1751e68..3b15aff 100644 --- a/src/services/search.svc.ts +++ b/src/services/search.svc.ts @@ -1,8 +1,11 @@ import { QueryTypes, type Sequelize } from 'sequelize' import type { + SearchCounterQuery, + SearchCounterResponse, SearchCountRow, SearchFrequencyRow, + SearchHouseCountRow, SearchMPDocResultsResponse, SearchPlotResponse, SearchQuery, @@ -12,6 +15,7 @@ import type { SearchTopSpeakerRow, SqlBindings, } from '@/types' +import { HOUSE_CODE } from '@/types/enum' import { buildHeadlineFragment, paginate, resampleSeries, translateAgeGroupToBirthYearBounds } from '@/utils' export interface SearchServiceResponse { @@ -41,6 +45,15 @@ export interface SearchServicePlotResponse { } } +export interface SearchCounterServiceResponse { + success?: SearchCounterResponse + error?: { + code: number + type: string + message: string + } +} + export class SearchService { private async generateYearRange(startDate: Date, endDate: Date, yearsBatchSize = 5) { const ranges = [] @@ -185,6 +198,96 @@ export class SearchService { return response } + public async searchCounter( + sequelize: Sequelize, + query: SearchCounterQuery, + parameters: { + startDate: string + endDate: string + windowSize: number + q: string + uid?: number + }, + ): Promise { + const whereParts: string[] = ['pc.house IN (:houses)', 'si.date >= :startDate', 'si.date <= :endDate'] + const repl: SqlBindings = { + houses: [HOUSE_CODE.DEWAN_RAKYAT, HOUSE_CODE.DEWAN_NEGARA, HOUSE_CODE.KAMAR_KHAS], + startDate: parameters.startDate, + endDate: parameters.endDate, + } + + if (query.party) { + whereParts.push('ah.party = :party') + repl.party = query.party + } + + if (query.sex) { + whereParts.push('a.sex = :sex') + repl.sex = query.sex + } + + if (query.ethnicity) { + whereParts.push('a.ethnicity = :ethnicity') + repl.ethnicity = query.ethnicity + } + + if (query.age_group) { + const grp = query.age_group + const currentYear = new Date().getFullYear() + const trans = translateAgeGroupToBirthYearBounds(grp, currentYear) + if (trans) { + whereParts.push(trans.clause) + Object.assign(repl, trans.params) + } + } + + if (parameters.uid) { + whereParts.push('a.new_author_id = :uid') + repl.uid = parameters.uid + } + + const headlineFragment = parameters.q ? buildHeadlineFragment(parameters.q, parameters.windowSize) : null + if (headlineFragment) { + Object.assign(repl, headlineFragment.params) + whereParts.push(headlineFragment.condition) + } + + const baseFrom = ` + FROM api_speech s + JOIN api_sitting si ON s.sitting_id = si.sitting_id + JOIN api_parliamentary_cycle pc ON si.cycle_id = pc.cycle_id + LEFT JOIN api_author_history ah ON s.speaker_id = ah.record_id + LEFT JOIN api_author a ON ah.author_id = a.new_author_id + ` + const whereSql = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '' + const countSql = ` + SELECT pc.house as house, count(*) as count + ${baseFrom} + ${whereSql} + GROUP BY pc.house + ` + + const countRows = await sequelize.query(countSql, { + replacements: repl, + type: QueryTypes.SELECT, + }) + + const houseCounts: SearchCounterResponse['house_counts'] = { + dewan_rakyat: 0, + dewan_negara: 0, + kamar_khas: 0, + } + for (const row of countRows) { + const house = Number(row.house) + const count = Number(row.count ?? 0) + if (house === HOUSE_CODE.DEWAN_RAKYAT) houseCounts.dewan_rakyat = count + if (house === HOUSE_CODE.DEWAN_NEGARA) houseCounts.dewan_negara = count + if (house === HOUSE_CODE.KAMAR_KHAS) houseCounts.kamar_khas = count + } + + return { success: { house_counts: houseCounts } } + } + public async searchMPDoc( sequelize: Sequelize, query: SearchQuery, diff --git a/src/types/schema-derived/requests.ts b/src/types/schema-derived/requests.ts index dda1577..535c9d0 100644 --- a/src/types/schema-derived/requests.ts +++ b/src/types/schema-derived/requests.ts @@ -3,12 +3,13 @@ import { z } from 'zod' import { attendanceQuerySchema } from '@/schema/attendance/request.schema' import { autocompleteQuerySchema } from '@/schema/autocomplete/request.schema' import { catalogueQuerySchema, createCycleBodySchema } from '@/schema/parliamentary/request.schema' -import { searchPlotQuerySchema, searchQuerySchema } from '@/schema/search/request.schema' +import { searchCounterQuerySchema, searchPlotQuerySchema, searchQuerySchema } from '@/schema/search/request.schema' import { getSittingListQuerySchema, getSittingQuerySchema, upsertSittingBodySchema } from '@/schema/sitting/request.schema' import { speechBulkBodySchema } from '@/schema/speech/request.schema' export type AttendanceQuery = z.infer export type SearchQuery = z.infer +export type SearchCounterQuery = z.infer export type SearchPlotQuery = z.infer export type AutocompleteQuery = z.infer export type GetSittingQuery = z.infer From bdc2f1cc609e3f16ef3110fffa929f44242df3f9 Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Thu, 26 Mar 2026 15:40:05 +0800 Subject: [PATCH 3/7] SSD-1156 new keyword search counter api --- src/schema/search/request.schema.ts | 12 ++++++++++++ src/schema/search/response.schema.ts | 8 ++++++++ src/types/schema-derived/responses.ts | 8 +++++++- src/types/sql-rows.ts | 5 +++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/schema/search/request.schema.ts b/src/schema/search/request.schema.ts index 80416e5..bc924d3 100644 --- a/src/schema/search/request.schema.ts +++ b/src/schema/search/request.schema.ts @@ -17,4 +17,16 @@ export const searchQuerySchema = z.object({ page_size: z.coerce.number().optional(), }) +export const searchCounterQuerySchema = z.object({ + start_date: z.string().optional(), + end_date: z.string().optional(), + window_size: z.coerce.number().optional(), + party: z.string().optional(), + age_group: z.string().optional(), + sex: z.enum(['m', 'f']).optional(), + ethnicity: z.string().optional(), + q: z.string().optional(), + uid: z.coerce.number().optional(), +}) + export const searchPlotQuerySchema = searchQuerySchema diff --git a/src/schema/search/response.schema.ts b/src/schema/search/response.schema.ts index 00d1865..020f78c 100644 --- a/src/schema/search/response.schema.ts +++ b/src/schema/search/response.schema.ts @@ -40,6 +40,14 @@ export const searchMPDocResultsResponseSchema = z.object({ previous: z.number().nullable(), }) +export const searchCounterResponseSchema = z.object({ + house_counts: z.object({ + dewan_rakyat: z.number(), + dewan_negara: z.number(), + kamar_khas: z.number(), + }), +}) + // Raw payload for GET /search-plot export const searchPlotResponseSchema = z.object({ chart_data: z.object({ date: z.array(z.string()), freq: z.array(z.number()) }), diff --git a/src/types/schema-derived/responses.ts b/src/types/schema-derived/responses.ts index e7c73e2..d55b0e3 100644 --- a/src/types/schema-derived/responses.ts +++ b/src/types/schema-derived/responses.ts @@ -3,12 +3,18 @@ import { z } from 'zod' import { attendanceResponseSchema } from '@/schema/attendance/response.schema' import { autocompleteResponseSchema } from '@/schema/autocomplete/response.schema' import { getCatalogueResponseSchema } from '@/schema/catalogue/response.schema' -import { searchMPDocResultsResponseSchema, searchPlotResponseSchema, searchResultsResponseSchema } from '@/schema/search/response.schema' +import { + searchCounterResponseSchema, + searchMPDocResultsResponseSchema, + searchPlotResponseSchema, + searchResultsResponseSchema, +} from '@/schema/search/response.schema' import { getSittingResponseSchema, upsertSittingResponseSchema } from '@/schema/sitting/response.schema' export type AttendanceResponse = z.infer export type SearchResultsResponse = z.infer export type SearchMPDocResultsResponse = z.infer +export type SearchCounterResponse = z.infer export type SearchPlotResponse = z.infer export type GetSittingResponse = z.infer export type UpsertSittingResponse = z.infer diff --git a/src/types/sql-rows.ts b/src/types/sql-rows.ts index cb19376..11e6280 100644 --- a/src/types/sql-rows.ts +++ b/src/types/sql-rows.ts @@ -21,6 +21,11 @@ export interface SearchCountRow { count: number | string | null } +export interface SearchHouseCountRow { + house: number | string | null + count: number | string | null +} + export interface SearchSpeechRow { index: number | string speaker_name: string | null From 4cda326c3907ed94ed4c039b36b5e0c12eb359ba Mon Sep 17 00:00:00 2001 From: Wan Aqim <136215265+PyConqueror@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:38:15 +0800 Subject: [PATCH 4/7] Update src/schema/search/request.schema.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/schema/search/request.schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/schema/search/request.schema.ts b/src/schema/search/request.schema.ts index bc924d3..8ca1d20 100644 --- a/src/schema/search/request.schema.ts +++ b/src/schema/search/request.schema.ts @@ -20,7 +20,6 @@ export const searchQuerySchema = z.object({ export const searchCounterQuerySchema = z.object({ start_date: z.string().optional(), end_date: z.string().optional(), - window_size: z.coerce.number().optional(), party: z.string().optional(), age_group: z.string().optional(), sex: z.enum(['m', 'f']).optional(), From efbc46b28fa350ad9d9f43196463830b3cc21a25 Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Fri, 27 Mar 2026 07:52:25 +0800 Subject: [PATCH 5/7] hotfix : remove unused types --- src/controllers/search.controller.ts | 2 -- src/services/search.svc.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 81a1084..84fceb3 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -122,7 +122,6 @@ export async function getSearchCounter(request: FastifyRequest<{ Querystring: Se const { sequelize } = request.server const startDate = request.query.start_date ?? (await deriveDefaultStartDateDR(request.server.models)) const endDate = request.query.end_date ?? new Date().toISOString().slice(0, 10) - const windowSize = Number(request.query.window_size ?? 120) const q = (request.query.q ?? '').toString().trim() const uid = request.query.uid ? Number(request.query.uid) : undefined @@ -130,7 +129,6 @@ export async function getSearchCounter(request: FastifyRequest<{ Querystring: Se const serviceResponse = await searchSvc.searchCounter(sequelize, request.query, { startDate, endDate, - windowSize, q, uid, }) diff --git a/src/services/search.svc.ts b/src/services/search.svc.ts index 3b15aff..8c69348 100644 --- a/src/services/search.svc.ts +++ b/src/services/search.svc.ts @@ -204,7 +204,6 @@ export class SearchService { parameters: { startDate: string endDate: string - windowSize: number q: string uid?: number }, @@ -246,7 +245,7 @@ export class SearchService { repl.uid = parameters.uid } - const headlineFragment = parameters.q ? buildHeadlineFragment(parameters.q, parameters.windowSize) : null + const headlineFragment = parameters.q ? buildHeadlineFragment(parameters.q, 120) : null if (headlineFragment) { Object.assign(repl, headlineFragment.params) whereParts.push(headlineFragment.condition) From 5d94af2c2e274c6635611ec526ca2f65ec33285e Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Sat, 28 Mar 2026 09:20:36 +0800 Subject: [PATCH 6/7] SSD-1161 add keyword search by MPs --- src/controllers/search.controller.ts | 40 +++++++++++++++++++++++++++- src/services/search.svc.ts | 35 ++++++++++++++++++++++++ src/types/sql-rows.ts | 5 ++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 84fceb3..3e8f2cc 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -36,11 +36,48 @@ export async function getSearchResults(request: FastifyRequest<{ Querystring: Se const startDate = await startDatePromise const endDate = request.query.end_date ?? new Date().toISOString().slice(0, 10) const windowSize = Number(request.query.window_size ?? 120) - const q = (request.query.q ?? '').toString().trim() + const rawQ = (request.query.q ?? '').toString().trim() const uid = request.query.uid ? Number(request.query.uid) : undefined const pageSize = Number(request.query.page_size ?? DEFAULT_PAGE_SIZE) const pageInput = Math.max(1, Number(request.query.page ?? 1)) const searchSvc = new SearchService() + let q = rawQ + let authorIds: number[] | undefined + + const separatorIndex = rawQ.indexOf(':') + if (separatorIndex > -1) { + const keywordQuery = rawQ.slice(0, separatorIndex).trim() + const mpNameQuery = rawQ.slice(separatorIndex + 1).trim() + console.log('[search:query-parse] detected colon query', { + rawQ, + keywordQuery, + keywordTerms: keywordQuery.split(/\s+/).filter(Boolean), + mpNameQuery, + }) + + if (keywordQuery && mpNameQuery) { + const matchedMPs = await searchSvc.findMatchingMPsByName(sequelize, mpNameQuery) + console.log('[search:query-parse] mp match result', { + mpNameQuery, + matchedMPCount: matchedMPs.length, + matchedAuthorIds: matchedMPs.map(mp => mp.author_id), + }) + + if (matchedMPs.length > 0) { + q = keywordQuery + authorIds = matchedMPs.map(mp => mp.author_id) + console.log('[search:query-parse] applying mp-aware search', { + effectiveKeywordQuery: q, + authorIds, + }) + } else { + console.log('[search:query-parse] no mp matched, fallback to normal search', { effectiveQuery: rawQ }) + } + } else { + console.log('[search:query-parse] invalid colon query, fallback to normal search', { effectiveQuery: rawQ }) + } + } + const serviceResponse = await searchSvc.search(sequelize, request.query, { startDate, endDate, @@ -50,6 +87,7 @@ export async function getSearchResults(request: FastifyRequest<{ Querystring: Se uid, pageSize, pageInput, + authorIds, }) if (serviceResponse.error || !serviceResponse.success) { diff --git a/src/services/search.svc.ts b/src/services/search.svc.ts index 8c69348..029acd1 100644 --- a/src/services/search.svc.ts +++ b/src/services/search.svc.ts @@ -1,6 +1,7 @@ import { QueryTypes, type Sequelize } from 'sequelize' import type { + SearchAuthorRow, SearchCounterQuery, SearchCounterResponse, SearchCountRow, @@ -18,6 +19,10 @@ import type { import { HOUSE_CODE } from '@/types/enum' import { buildHeadlineFragment, paginate, resampleSeries, translateAgeGroupToBirthYearBounds } from '@/utils' +function escapeLikePattern(value: string): string { + return value.replace(/[\\%_]/g, '\\$&') +} + export interface SearchServiceResponse { success?: SearchResultsResponse error?: { @@ -72,6 +77,30 @@ export class SearchService { return ranges } + public async findMatchingMPsByName(sequelize: Sequelize, mpName: string): Promise> { + const normalizedName = mpName.trim().toLowerCase() + if (!normalizedName) return [] + + const sql = ` + SELECT a.new_author_id as author_id, a.name + FROM api_author a + WHERE LOWER(a.name) LIKE :namePattern ESCAPE '\\' + ORDER BY a.new_author_id ASC + ` + const rows = await sequelize.query(sql, { + replacements: { namePattern: `%${escapeLikePattern(normalizedName)}%` }, + type: QueryTypes.SELECT, + }) + + return rows + .map(row => { + const authorId = Number(row.author_id) + if (!Number.isFinite(authorId)) return null + return { author_id: authorId, name: row.name ?? '' } + }) + .filter((row): row is { author_id: number; name: string } => row != null) + } + public async search( sequelize: Sequelize, query: SearchQuery, @@ -82,6 +111,7 @@ export class SearchService { windowSize: number q: string uid?: number + authorIds?: number[] pageSize: number pageInput: number }, @@ -125,6 +155,11 @@ export class SearchService { repl.uid = parameters.uid } + if (parameters.authorIds && parameters.authorIds.length > 0) { + whereParts.push('a.new_author_id IN (:authorIds)') + repl.authorIds = parameters.authorIds + } + let selectHeadline = '' let selectRank = '' let orderBy = 'si.date DESC' diff --git a/src/types/sql-rows.ts b/src/types/sql-rows.ts index 11e6280..cb877fa 100644 --- a/src/types/sql-rows.ts +++ b/src/types/sql-rows.ts @@ -21,6 +21,11 @@ export interface SearchCountRow { count: number | string | null } +export interface SearchAuthorRow { + author_id: number | string | null + name: string | null +} + export interface SearchHouseCountRow { house: number | string | null count: number | string | null From 285cf69e67657538c246f75926b714f8cf6203c1 Mon Sep 17 00:00:00 2001 From: PyConqueror Date: Mon, 30 Mar 2026 08:01:32 +0800 Subject: [PATCH 7/7] chore : remove console.log --- src/controllers/search.controller.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/controllers/search.controller.ts b/src/controllers/search.controller.ts index 3e8f2cc..a9af28a 100644 --- a/src/controllers/search.controller.ts +++ b/src/controllers/search.controller.ts @@ -48,33 +48,14 @@ export async function getSearchResults(request: FastifyRequest<{ Querystring: Se if (separatorIndex > -1) { const keywordQuery = rawQ.slice(0, separatorIndex).trim() const mpNameQuery = rawQ.slice(separatorIndex + 1).trim() - console.log('[search:query-parse] detected colon query', { - rawQ, - keywordQuery, - keywordTerms: keywordQuery.split(/\s+/).filter(Boolean), - mpNameQuery, - }) if (keywordQuery && mpNameQuery) { const matchedMPs = await searchSvc.findMatchingMPsByName(sequelize, mpNameQuery) - console.log('[search:query-parse] mp match result', { - mpNameQuery, - matchedMPCount: matchedMPs.length, - matchedAuthorIds: matchedMPs.map(mp => mp.author_id), - }) if (matchedMPs.length > 0) { q = keywordQuery authorIds = matchedMPs.map(mp => mp.author_id) - console.log('[search:query-parse] applying mp-aware search', { - effectiveKeywordQuery: q, - authorIds, - }) - } else { - console.log('[search:query-parse] no mp matched, fallback to normal search', { effectiveQuery: rawQ }) } - } else { - console.log('[search:query-parse] invalid colon query, fallback to normal search', { effectiveQuery: rawQ }) } }