fix: match Turkish search accents

This commit is contained in:
2026-08-23 06:12:49 +03:00
parent 705d94037d
commit 931036fe62
3 changed files with 33 additions and 9 deletions
+8 -8
View File
@@ -8,7 +8,7 @@ import { Card } from "@/components/ui/card";
import { AdCreativeMedia } from "@/components/ad-creative-media";
import { ApifyEmptySearch } from "@/components/ads/apify-empty-search";
import { AD_COUNTRIES } from "@/lib/countries";
import { AdSearchMatchMode, adSearchRelevance, adSearchTokens } from "@/lib/ad-search";
import { AdSearchMatchMode, adSearchDatabaseTerms, adSearchRelevance } from "@/lib/ad-search";
export default async function AdsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
const user = await requireUser();
@@ -26,13 +26,13 @@ export default async function AdsPage({ searchParams }: { searchParams: Promise<
const displayableCreativeWhere: Prisma.AdCreativeWhereInput = { url: { not: "" } };
const where: Prisma.AdWhereInput = { creatives: { some: displayableCreativeWhere } };
if (q) {
const terms = matchMode === "EXACT_PHRASE" ? [q] : adSearchTokens(q);
where.AND = terms.map((term) => ({ OR: [
{ primaryText: { contains: term, mode: "insensitive" } },
{ headline: { contains: term, mode: "insensitive" } },
{ description: { contains: term, mode: "insensitive" } },
{ brandPage: { name: { contains: term, mode: "insensitive" } } }
] }));
const terms = adSearchDatabaseTerms(q, matchMode);
where.AND = terms.map((variants) => ({ OR: variants.flatMap((term) => [
{ primaryText: { contains: term, mode: "insensitive" as const } },
{ headline: { contains: term, mode: "insensitive" as const } },
{ description: { contains: term, mode: "insensitive" as const } },
{ brandPage: { name: { contains: term, mode: "insensitive" as const } } }
]) }));
}
if (niche) where.niche = { contains: niche, mode: "insensitive" };
if (mediaType) where.mediaType = mediaType as any;
+19
View File
@@ -22,6 +22,25 @@ export function adSearchTokens(query: string) {
return [...new Set(normalizeAdSearchText(query).split(" ").filter(Boolean))];
}
function preserveAdSearchText(value: string | null | undefined) {
return (value || "")
.normalize("NFKC")
.toLocaleLowerCase("tr-TR")
.replace(/[^\p{L}\p{N}]+/gu, " ")
.trim()
.replace(/\s+/g, " ");
}
export function adSearchDatabaseTerms(query: string, mode: AdSearchMatchMode) {
const preserved = preserveAdSearchText(query);
const ascii = normalizeAdSearchText(query);
if (mode === "EXACT_PHRASE") return [[...new Set([preserved, ascii].filter(Boolean))]];
const preservedTokens = preserved.split(" ").filter(Boolean);
const asciiTokens = ascii.split(" ").filter(Boolean);
return asciiTokens.map((token, index) => [...new Set([preservedTokens[index], token].filter(Boolean))]);
}
function containsPhrase(text: string, phrase: string) {
return Boolean(text && phrase && ` ${text} `.includes(` ${phrase} `));
}
+6 -1
View File
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import { MediaType } from "@prisma/client";
import { actorInput, normalizeApifyAd, selectMediaRecordsForSearch } from "../src/lib/apify";
import { adSearchRelevance, filterRelevantAds } from "../src/lib/ad-search";
import { adSearchDatabaseTerms, adSearchRelevance, filterRelevantAds } from "../src/lib/ad-search";
import { getFeatureLimit } from "../src/lib/plans";
import { istemciIp, hizSiniriAsimi } from "../src/lib/rate-limit";
import { stripePriceFor } from "../src/lib/stripe";
@@ -155,6 +155,11 @@ test("reklam araması tam kelimeyi eşleştirir ve alakasız alt dizeleri dışa
assert.equal(adSearchRelevance({ primaryText: "Unrelated summer sale" }, "translate", "ALL_WORDS"), 0);
});
test("veritabanı araması Türkçe karakterli ve ASCII yazımları birlikte tarar", () => {
assert.deepEqual(adSearchDatabaseTerms("çeviri", "ALL_WORDS"), [["çeviri", "ceviri"]]);
assert.deepEqual(adSearchDatabaseTerms("hızlı çeviri", "EXACT_PHRASE"), [["hızlı çeviri", "hızlı ceviri"]]);
});
test("arama sonuçları ilgililiğe göre sıralanıp istenen sayıda kesilir", () => {
const ads = [
{ headline: null, primaryText: "Use translate today", brandName: "Other" },