Files
goodgo-platform/apps/api/src/modules/search/infrastructure/services/search-result-mapper.ts
Ho Ngoc Hai 7a854373b3 feat(search): configure Typesense for Vietnamese diacritic search
Add normalized (ASCII-only) fields to Typesense schema and indexer so
users can search without diacritics (e.g. "can ho" finds "căn hộ").
Create synonym collection for HCMC district abbreviations and common
property-type aliases. Enable num_typos:2 for fuzzy matching.

- Add 7 normalized fields (title, description, address, ward, district,
  city, projectName) using Address.normalize() at index time
- Search queries both original Vietnamese and normalized field sets
- Upsert 28 Vietnamese synonym rules on collection init
- Normalize user query to ASCII alongside original for dual matching
- Update tests for new fields and synonym upsert behavior

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 00:41:14 +07:00

79 lines
2.6 KiB
TypeScript

import { type ListingDocument } from '../../domain/repositories/search.repository';
import { Address } from '@modules/listings/domain/value-objects/address.vo';
export interface RawListingRow {
listingId: string;
propertyId: string;
title: string;
description: string;
propertyType: string;
transactionType: string;
priceVND: bigint;
pricePerM2: number | null;
areaM2: number;
bedrooms: number | null;
bathrooms: number | null;
floors: number | null;
direction: string | null;
address: string;
ward: string;
district: string;
city: string;
lat: number | null;
lng: number | null;
agentId: string | null;
sellerId: string;
status: string;
publishedAt: Date | string | null;
viewCount: number;
saveCount: number;
projectName: string | null;
legalStatus?: string | null;
amenities: unknown;
featuredUntil?: Date | string | null;
}
/** Map a raw SQL row to the domain ListingDocument shape. */
export function mapRowToListingDocument(row: RawListingRow): ListingDocument {
return {
id: row.listingId,
listingId: row.listingId,
propertyId: row.propertyId,
title: row.title,
description: row.description,
propertyType: row.propertyType,
transactionType: row.transactionType,
priceVND: Number(row.priceVND),
pricePerM2: row.pricePerM2 ? Number(row.pricePerM2) : null,
areaM2: Number(row.areaM2),
bedrooms: row.bedrooms,
bathrooms: row.bathrooms,
floors: row.floors,
direction: row.direction,
address: row.address,
ward: row.ward,
district: row.district,
city: row.city,
location: [row.lat ?? 0, row.lng ?? 0] as [number, number],
agentId: row.agentId,
sellerId: row.sellerId,
status: row.status,
publishedAt: row.publishedAt ? Math.floor(new Date(row.publishedAt).getTime() / 1000) : 0,
viewCount: row.viewCount ?? 0,
saveCount: row.saveCount ?? 0,
projectName: row.projectName,
legalStatus: row.legalStatus ?? null,
amenities: Array.isArray(row.amenities) ? (row.amenities as string[]) : [],
isFeatured: row.featuredUntil && new Date(row.featuredUntil) > new Date() ? 1 : 0,
// Vietnamese diacritic-normalized fields
titleNormalized: Address.normalize(row.title),
descriptionNormalized: Address.normalize(row.description),
addressNormalized: Address.normalize(row.address),
wardNormalized: Address.normalize(row.ward),
districtNormalized: Address.normalize(row.district),
cityNormalized: Address.normalize(row.city),
projectNameNormalized: row.projectName ? Address.normalize(row.projectName) : null,
};
}