feat: add diacritics-insensitive name search

Add ho_ten_ascii column with normalized names (no diacritics, lowercase)
so users can search "nguyen van a" to find "NGUYỄN VĂN A".

- ASCII input searches against ho_ten_ascii column
- Vietnamese input searches both ho_ten and ho_ten_ascii
- Indexed for fast lookups
This commit is contained in:
2026-04-14 20:15:20 +07:00
parent 4aa1f5bd2f
commit f885aeb6cc
3 changed files with 48 additions and 6 deletions
+16 -2
View File
@@ -26,6 +26,16 @@ const SCORE_PATTERNS = {
const ALL_SCORE_FIELDS = Object.keys(SCORE_PATTERNS);
// Strip Vietnamese diacritics: "NGUYỄN BŨU LỘC" → "nguyen buu loc"
function toAscii(str) {
return str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d")
.replace(/Đ/g, "D")
.toLowerCase();
}
// Parse score text "Toán: 3.75 Ngữ văn: 5.00 ..." into { toan: 3.75, ... }
function parseScoreString(diemThi) {
const scores = {};
@@ -85,6 +95,7 @@ function processSeparateScoresRow(row) {
return {
so_bao_danh: sbd,
ho_ten: hoTen,
ho_ten_ascii: toAscii(hoTen),
ngay_sinh: null,
ten_cum_thi: null,
gioi_tinh: null,
@@ -126,6 +137,7 @@ function processMappedRow(row, map) {
return {
so_bao_danh: sbd,
ho_ten: hoTen,
ho_ten_ascii: toAscii(hoTen),
ngay_sinh: ngaySinh || null,
ten_cum_thi: tenCumThi || null,
gioi_tinh: gioiTinh || null,
@@ -148,6 +160,7 @@ function main() {
CREATE TABLE student (
so_bao_danh TEXT PRIMARY KEY,
ho_ten TEXT NOT NULL,
ho_ten_ascii TEXT NOT NULL,
ngay_sinh TEXT,
ten_cum_thi TEXT,
gioi_tinh TEXT,
@@ -165,16 +178,17 @@ function main() {
tieng_trung REAL
);
CREATE INDEX idx_ho_ten ON student(ho_ten);
CREATE INDEX idx_ho_ten_ascii ON student(ho_ten_ascii);
CREATE INDEX idx_ten_cum_thi ON student(ten_cum_thi);
`);
const insert = db.prepare(`
INSERT OR REPLACE INTO student
(so_bao_danh, ho_ten, ngay_sinh, ten_cum_thi, gioi_tinh,
(so_bao_danh, ho_ten, ho_ten_ascii, ngay_sinh, ten_cum_thi, gioi_tinh,
toan, ngu_van, vat_ly, hoa_hoc, sinh_hoc, lich_su, dia_ly,
tieng_anh, tieng_phap, tieng_duc, tieng_nhat, tieng_trung)
VALUES
(@so_bao_danh, @ho_ten, @ngay_sinh, @ten_cum_thi, @gioi_tinh,
(@so_bao_danh, @ho_ten, @ho_ten_ascii, @ngay_sinh, @ten_cum_thi, @gioi_tinh,
@toan, @ngu_van, @vat_ly, @hoa_hoc, @sinh_hoc, @lich_su, @dia_ly,
@tieng_anh, @tieng_phap, @tieng_duc, @tieng_nhat, @tieng_trung)
`);
+31 -3
View File
@@ -8,6 +8,20 @@ import "./App.css";
const DB_URL = import.meta.env.BASE_URL + "thptqg2016.db.gz";
const MAX_RESULTS = 100;
// Strip Vietnamese diacritics for search: "nguyễn bữu lộc" → "nguyen buu loc"
function toAscii(str) {
return str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/gi, "d")
.toLowerCase();
}
// Check if string contains only ASCII (no Vietnamese diacritics)
function isAsciiOnly(str) {
return /^[\x00-\x7F]*$/.test(str);
}
function App() {
const { db, loading, error, progress } = useSqlite(DB_URL);
const [results, setResults] = useState(null);
@@ -28,11 +42,25 @@ function App() {
"SELECT * FROM student WHERE so_bao_danh = $q LIMIT $limit",
);
stmt.bind({ $q: query.toUpperCase(), $limit: MAX_RESULTS });
} else {
} else if (isAsciiOnly(query)) {
// ASCII input: search against normalized column (diacritics-insensitive)
stmt = db.prepare(
"SELECT * FROM student WHERE ho_ten LIKE $q LIMIT $limit",
"SELECT * FROM student WHERE ho_ten_ascii LIKE $q LIMIT $limit",
);
stmt.bind({ $q: `%${query}%`, $limit: MAX_RESULTS });
stmt.bind({ $q: `%${toAscii(query)}%`, $limit: MAX_RESULTS });
} else {
// Vietnamese input: search both original and normalized
const normalized = toAscii(query);
stmt = db.prepare(
`SELECT * FROM student
WHERE ho_ten LIKE $q OR ho_ten_ascii LIKE $qn
LIMIT $limit`,
);
stmt.bind({
$q: `%${query}%`,
$qn: `%${normalized}%`,
$limit: MAX_RESULTS,
});
}
const rows = [];
+1 -1
View File
@@ -15,7 +15,7 @@ export function SearchForm({ onSearch, disabled }) {
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Nhập số báo danh hoặc họ tên..."
placeholder="Nhập SBD hoặc họ tên (VD: nguyen van a)..."
disabled={disabled}
autoFocus
/>