Skip to content

Commit 701edf5

Browse files
authored
Reduce D1 reads for article pages (#45)
1 parent d7cc961 commit 701edf5

4 files changed

Lines changed: 117 additions & 44 deletions

File tree

‎src/data/equinoxHaberD1.ts‎

Lines changed: 102 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ const ARTICLE_META_COLUMNS = `slug, title, description, category, author,
107107
'' AS body_md, '' AS body_html,
108108
hero_image, hero_alt, pub_date, updated_date, is_draft, breaking, editor_pick`;
109109

110+
/* Bunlar yayın hattı/metaveri etiketleri; haberler arası anlamlı konu bağı
111+
* değildir. İlgili haber skoruna katılırlarsa yüzlerce kaydı yapay biçimde
112+
* birbirine bağlayıp hem öneri kalitesini hem D1 rows_read maliyetini bozar. */
113+
const RELATED_IGNORED_TAGS = new Set(['pipeline', 'haber']);
114+
115+
function isRelatedTag(tag: string) {
116+
return !RELATED_IGNORED_TAGS.has(tag.trim().toLowerCase());
117+
}
118+
110119
function positiveInteger(value?: number) {
111120
if (value === undefined) return undefined;
112121
if (!Number.isFinite(value)) return undefined;
@@ -227,13 +236,15 @@ export interface D1ArticlePageData {
227236
relatedEntries: D1NewsEntry[];
228237
}
229238

230-
/** Haber detay sayfası için bütün arşivi çekmeden gereken küçük çalışma seti.
239+
/** Haber detay sayfası için gereken küçük çalışma seti.
231240
*
232-
* Eski yol her tekil haber isteğinde ~4-7 bin D1 satırı okuyordu çünkü ilgili
233-
* haberleri JS'te hesaplamak için tüm haberler + tüm etiketler + tüm kaynaklar
234-
* belleğe alınıyordu. Burada yalnız mevcut haber, komşuları ve üç ilgili aday
235-
* okunur. Botların eski haber arşivini taraması bu yüzden günlük kotayı artık
236-
* doğrusal biçimde eritmez. */
241+
* D1 kotası döndürülen değil TARANAN satırı sayar. Bu yüzden `LIMIT 3` tek
242+
* başına ucuzluk garantisi değildir: korelasyonlu etiket sorgusu birkaç sonuç
243+
* için binlerce satır tarayabiliyordu. Buradaki yol, mevcut indeksleri açıkça
244+
* kullanır: komşular iki basit tarih/slug sorgusuyla; ilgili haber adaylarıysa
245+
* etiket indeksinden küçük bir slug kümesi çıkarılıp uygulamada puanlanarak
246+
* bulunur. Böylece crawler trafiği arşiv büyüdükçe doğrusal D1 taramasına
247+
* dönüşmez. */
237248
export async function getArticlePageFromD1(
238249
db: D1Database,
239250
slug: string,
@@ -246,58 +257,111 @@ export async function getArticlePageFromD1(
246257
).bind(slug).first<ArticleRow>();
247258
if (!current) return null;
248259

249-
const [tagsResult, sourcesResult, newer, older] = await Promise.all([
260+
/* `(pub_date > ? OR (pub_date = ? ...))` biçimi SQLite planner'ında arşivin
261+
* büyük bölümünü tarayabiliyor. Aynı tarih ve başka tarih durumlarını ayırıp
262+
* en yakın iki adayı uygulamada seçmek indeksin doğrudan kullanılmasını
263+
* sağlıyor. */
264+
const [tagsResult, sourcesResult, newerSameDate, newerDate, olderSameDate, olderDate] = await Promise.all([
250265
db.prepare('SELECT slug, tag FROM article_tags WHERE slug = ? ORDER BY position')
251266
.bind(slug).all<{ slug: string; tag: string }>(),
252267
db.prepare('SELECT slug, name, url FROM article_sources WHERE slug = ? ORDER BY position')
253268
.bind(slug).all<{ slug: string; name: string; url: string }>(),
254269
db.prepare(
255270
`SELECT ${ARTICLE_META_COLUMNS}
256271
FROM articles
257-
WHERE is_draft = 0
258-
AND (pub_date > ? OR (pub_date = ? AND slug < ?))
272+
WHERE is_draft = 0 AND pub_date = ? AND slug < ?
273+
ORDER BY slug DESC
274+
LIMIT 1`,
275+
).bind(current.pub_date, current.slug).first<ArticleRow>(),
276+
db.prepare(
277+
`SELECT ${ARTICLE_META_COLUMNS}
278+
FROM articles
279+
WHERE is_draft = 0 AND pub_date > ?
259280
ORDER BY pub_date ASC, slug DESC
260281
LIMIT 1`,
261-
).bind(current.pub_date, current.pub_date, current.slug).first<ArticleRow>(),
282+
).bind(current.pub_date).first<ArticleRow>(),
262283
db.prepare(
263284
`SELECT ${ARTICLE_META_COLUMNS}
264285
FROM articles
265-
WHERE is_draft = 0
266-
AND (pub_date < ? OR (pub_date = ? AND slug > ?))
286+
WHERE is_draft = 0 AND pub_date = ? AND slug > ?
287+
ORDER BY slug ASC
288+
LIMIT 1`,
289+
).bind(current.pub_date, current.slug).first<ArticleRow>(),
290+
db.prepare(
291+
`SELECT ${ARTICLE_META_COLUMNS}
292+
FROM articles
293+
WHERE is_draft = 0 AND pub_date < ?
267294
ORDER BY pub_date DESC, slug ASC
268295
LIMIT 1`,
269-
).bind(current.pub_date, current.pub_date, current.slug).first<ArticleRow>(),
296+
).bind(current.pub_date).first<ArticleRow>(),
270297
]);
271298

299+
const newer = newerSameDate ?? newerDate;
300+
const older = olderSameDate ?? olderDate;
272301
const currentTags = (tagsResult.results ?? []).map((row) => row.tag);
273-
let relatedWhere = 'a.category = ?';
274-
let scoreExpression = '6';
275-
let relatedBindings: unknown[] = [slug, current.category];
276-
277-
if (currentTags.length > 0) {
278-
const placeholders = currentTags.map(() => '?').join(', ');
279-
relatedWhere = `(a.category = ? OR a.slug IN (
280-
SELECT slug FROM article_tags WHERE tag IN (${placeholders})
281-
))`;
282-
scoreExpression = `(CASE WHEN a.category = ? THEN 6 ELSE 0 END) + 3 * (
283-
SELECT COUNT(*) FROM article_tags matched
284-
WHERE matched.slug = a.slug AND matched.tag IN (${placeholders})
285-
)`;
286-
/* SELECT içindeki yer tutucular WHERE'dekilerden önce bağlanır. */
287-
relatedBindings = [current.category, ...currentTags, slug, current.category, ...currentTags];
288-
}
302+
const relatedTags = currentTags.filter(isRelatedTag);
289303

290-
const related = await db.prepare(
291-
`SELECT ${ARTICLE_META_COLUMNS}, ${scoreExpression} AS relation_score
292-
FROM articles a
293-
WHERE a.is_draft = 0
294-
AND a.slug <> ?
295-
AND ${relatedWhere}
296-
ORDER BY relation_score DESC, a.pub_date DESC, a.slug ASC
304+
/* Kategori bonusu olan ama ortak etiketi olmayan adaylardan yalnız en yeni
305+
* üçü gerekebilir: hepsinin skoru 6 ve eşitlikte tarih kazanır. Ortak etiketli
306+
* tüm adaylar ayrıca aşağıdaki indeksli etiket sorgusundan gelir. */
307+
const categoryPromise = db.prepare(
308+
`SELECT ${ARTICLE_META_COLUMNS}
309+
FROM articles
310+
WHERE is_draft = 0 AND category = ? AND slug <> ?
311+
ORDER BY pub_date DESC, slug ASC
297312
LIMIT 3`,
298-
).bind(...relatedBindings).all<ArticleRow>();
313+
).bind(current.category, slug).all<ArticleRow>();
314+
315+
const tagMatchesPromise = relatedTags.length > 0
316+
? (() => {
317+
const placeholders = relatedTags.map(() => '?').join(', ');
318+
return db.prepare(
319+
`SELECT slug, COUNT(*) AS shared_tags
320+
FROM article_tags
321+
WHERE tag IN (${placeholders}) AND slug <> ?
322+
GROUP BY slug`,
323+
).bind(...relatedTags, slug).all<{ slug: string; shared_tags: number }>();
324+
})()
325+
: Promise.resolve({ results: [] } as Pick<D1Result<{ slug: string; shared_tags: number }>, 'results'>);
326+
327+
const [categoryResult, tagMatchesResult] = await Promise.all([categoryPromise, tagMatchesPromise]);
328+
const sharedBySlug = new Map(
329+
(tagMatchesResult.results ?? []).map((row) => [row.slug, Number(row.shared_tags)] as const),
330+
);
331+
const tagSlugs = [...sharedBySlug.keys()];
332+
const taggedRows = tagSlugs.length > 0
333+
? await readRelations<ArticleRow>(
334+
db,
335+
tagSlugs,
336+
/* `is_draft = 0 AND slug IN (...)` planner'ı tarih indeksine çekip
337+
* bütün yayımlanmış tabloyu taratabiliyor. PK lookup yap, taslağı
338+
* aşağıda bellekte ele. */
339+
(placeholders) => `SELECT ${ARTICLE_META_COLUMNS}
340+
FROM articles
341+
WHERE slug IN (${placeholders})`,
342+
)
343+
: [];
344+
345+
const candidates = new Map<string, ArticleRow>();
346+
for (const row of categoryResult.results ?? []) candidates.set(row.slug, row);
347+
for (const row of taggedRows) {
348+
if (row.is_draft === 0) candidates.set(row.slug, row);
349+
}
350+
351+
let relatedRows = [...candidates.values()]
352+
.map((row) => ({
353+
row,
354+
score: (row.category === current.category ? 6 : 0) + 3 * (sharedBySlug.get(row.slug) ?? 0),
355+
}))
356+
.sort((a, b) => {
357+
const scoreOrder = b.score - a.score;
358+
if (scoreOrder) return scoreOrder;
359+
const dateOrder = b.row.pub_date.localeCompare(a.row.pub_date);
360+
return dateOrder || a.row.slug.localeCompare(b.row.slug);
361+
})
362+
.slice(0, 3)
363+
.map(({ row }) => row);
299364

300-
let relatedRows = related.results ?? [];
301365
if (relatedRows.length < 3) {
302366
const excluded = [slug, ...relatedRows.map((row) => row.slug)];
303367
const placeholders = excluded.map(() => '?').join(', ');

‎src/data/newsSource.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { getCollection, render, type CollectionEntry } from 'astro:content';
1313

1414
export type NewsEntry = CollectionEntry<'equinoxHaber'>;
1515

16+
const RELATED_IGNORED_TAGS = new Set(['pipeline', 'haber']);
17+
const isRelatedTag = (tag: string) => !RELATED_IGNORED_TAGS.has(tag.trim().toLowerCase());
18+
1619
export interface PublishedOptions {
1720
limit?: number;
1821
offset?: number;
@@ -55,7 +58,7 @@ export async function getArticlePage(slug: string) {
5558
if (currentIndex < 0) return null;
5659

5760
const entry = entries[currentIndex]!;
58-
const currentTags = new Set(entry.data.tags ?? []);
61+
const currentTags = new Set((entry.data.tags ?? []).filter((tag: string) => isRelatedTag(tag)));
5962
const relatedEntries = entries
6063
.filter((item) => item.id !== entry.id)
6164
.map((item, index) => {

‎src/middleware.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,11 @@ export const onRequest: MiddlewareHandler = async (context, next) => {
120120

121121
const response = await next();
122122

123-
/* Yalnız 200 önbelleklenir. 404 ve 500'ü tutmak, bir hatayı bir dakika
124-
boyunca herkese servis etmek demek. */
125-
if (response.status !== 200) return response;
123+
/* 404 de sürümlü anahtarla güvenle önbelleklenebilir: daha sonra aynı slug
124+
* yayımlanırsa content_version değişir ve eski 404 anahtarı erişilemez olur.
125+
* Scanner'ların aynı olmayan yolu tekrar tekrar D1'e sordurmasını önler.
126+
* Sunucu hataları ise asla önbelleğe girmez. */
127+
if (response.status !== 200 && response.status !== 404) return response;
126128

127129
const cacheable = new Response(response.body, response);
128130
cacheable.headers.set(

‎src/pages/[...slug].astro‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ export async function getStaticPaths() {
1414
}
1515
1616
const routeSlug = Astro.params.slug;
17+
const articleSlug = routeSlug && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeSlug) ? routeSlug : null;
1718
1819
/* Sunucu modunda tekil haber yolu bütün arşivi okumaz. D1 kaynağı mevcut
1920
haber + komşular + ilgili üçlü için hedefli sorgular kullanır; statik kaynak
2021
aynı şekli içerik koleksiyonundan kurar. */
21-
const articlePage = routeSlug ? await getArticlePage(routeSlug) : null;
22+
const articlePage = articleSlug ? await getArticlePage(articleSlug) : null;
2223
const entry = articlePage?.entry;
2324
2425
if (!entry) {
26+
/* Dosya/klasör ve exploit-probe yollarını haber slug'ı sanıp D1'e sorma. */
27+
if (!articleSlug) return new Response(null, { status: 404 });
28+
2529
/* Yayından kaldırılmış bir haber mi?
2630
*
2731
* Adres bir süre yayında kaldı; paylaşımlarda ve arama motorlarında
@@ -36,7 +40,7 @@ if (!entry) {
3640
* Gerekçe SAYFADA GÖSTERİLMİYOR: `article_withdrawals.reason` operatöre
3741
* yazılmış bir iç nottur ("kaynak yanlış, teyit bekliyoruz") ve
3842
* kendiliğinden yayımlanması sürpriz olurdu. */
39-
const kaldirilan = await getWithdrawal(routeSlug!);
43+
const kaldirilan = await getWithdrawal(articleSlug);
4044
if (kaldirilan) {
4145
return new Response(kaldirilanSayfasi(kaldirilan.tarih), {
4246
status: 410,

0 commit comments

Comments
 (0)