@@ -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+
110119function 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. */
237248export 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 ( ', ' ) ;
0 commit comments