<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Adem TONAY</title>
        <link>https://ademtonay.com/</link>
        <description>Adem TONAY' Blog</description>
        <lastBuildDate>Wed, 08 Jul 2026 14:16:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Adem TONAY</title>
            <url>https://ademtonay.com/avatar.png</url>
            <link>https://ademtonay.com/</link>
        </image>
        <copyright>CC BY-NC-SA 4.0 2021 © Adem TONAY</copyright>
        <atom:link href="https://ademtonay.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring Boot ile AI Retrieval Kalitesini Golden Pair'ler Üzerinden Ölçmek]]></title>
            <link>https://ademtonay.com/posts/ai-retrieval-kalitesini-golden-pair-ile-olcmek</link>
            <guid>https://ademtonay.com/posts/ai-retrieval-kalitesini-golden-pair-ile-olcmek</guid>
            <pubDate>Wed, 29 Apr 2026 10:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Embedding ve vektör veritabanı üzerine kurulu bir &quot;benzer kayıtları getir&quot; özelliği geliştirdiğinizde her değişiklik — yeni bir model, farklı bir chunking stratejisi, ayarlanmış bir threshold — aynı soruyu beraberinde getirir: bu değişiklik retrieval'ı gerçekten daha mı iyi yaptı, yoksa sadece <em>farklı</em> mı yaptı? Ölçülebilir bir referansınız yoksa, sonuç sezgilerle ilerlemek olur.</p>
<p>Bu yazıda, Spring Boot uygulamamda <em>golden pair</em> adı verilen, etiketlenmiş örneklerden oluşan küçük ama düzgün kurulmuş bir değerlendirme (evaluation) altyapısının nasıl inşa edildiğini anlatacağım. Bu altyapı, Recall@K ve Mean Reciprocal Rank (MRR) gibi sektörde yaygın metrikleri hesaplıyor ve yetkili kullanıcılara açtığım bir admin endpoint üzerinden, üretim ortamına yeni bir değişiklik geçirilmeden önce her zaman yeniden çalıştırılabiliyor.</p>
<h2>1. Sezgilerle Ayar Yapmanın Sorunu</h2>
<p>Bir destek portalında, agent'lar gelen yeni bir ticket'a benzer geçmiş ticket'lar önerildiğinde çok daha hızlı çözüme ulaşıyorlar. &quot;Benzerini getir&quot; özelliği temelde üç parçadan oluşuyor:</p>
<ul>
<li>ticket'ı düz metne dönüştüren bir <em>content builder</em>,</li>
<li>bu metni vektöre çeviren bir <em>embedding</em> modeli,</li>
<li>en yakın komşuları (top-K) döndüren bir vektör veritabanı.</li>
</ul>
<p>Bu pipeline'daki her parça aslında bir ayar düğmesi. Embedding modelini değiştirin? Vektör boyutu, semantik komşuluk ve benzerlik skorları değişir. Ticket'ın metne dönüştürülme şeklini değiştirin? Modele giren sinyaller değişir. Benzerlik eşiğini değiştirin? Recall/precision dengesi kayar.</p>
<p>Bir değişikliğin gerçekten iyileştirme olup olmadığını anlamanın tek dürüst yolu, &quot;iyi&quot; olanı somut örneklerle tanımlamak ve onunla karşılaştırmaktan geçiyor.</p>
<h2>2. Doğruluğun Kaynağı: Golden Pair'ler</h2>
<p>Bir golden pair, etiketlenmiş bir sorgudur: &quot;Bu girdiye karşılık, sistem şu ticket'ları döndürmeli.&quot; Etiketler, alanı bilen kişiler tarafından üretilir.</p>
<p>Tek bir pair'i Java <code>record</code> ile şu şekilde modelliyorum:</p>
<pre><code class="language-java">@JsonInclude(JsonInclude.Include.NON_NULL)
public record EvalGoldenPair(
        String id,
        String queryText,
        Long queryTicketId,
        List&lt;Long&gt; expectedSimilarTicketIds,
        String note
) {
    public EvalGoldenPair {
        if ((queryText == null || queryText.isBlank()) &amp;&amp; queryTicketId == null) {
            throw new IllegalArgumentException(
                    &quot;EvalGoldenPair &quot; + id + &quot; must define either queryText or queryTicketId&quot;);
        }
        if (expectedSimilarTicketIds == null || expectedSimilarTicketIds.isEmpty()) {
            throw new IllegalArgumentException(
                    &quot;EvalGoldenPair &quot; + id + &quot; must define at least one expectedSimilarTicketId&quot;);
        }
    }
}
</code></pre>
<p>Burada altını çizmek istediğim birkaç tasarım kararı var:</p>
<ul>
<li><strong><code>queryText</code> <em>veya</em> <code>queryTicketId</code>.</strong> Bazı pair'lerde sorgu, yeni bir ticket'ı simüle eden, elle yazılmış bir metindir. Bazılarında ise zaten çözülmüş mevcut bir ticket'ı, kalan korpus üzerinde sorgu olarak kullanırız — bu <em>leave-one-out</em> tarzı bir değerlendirmedir. Compact constructor, bu iki yoldan en az birinin kullanılmasını zorunlu kılıyor.</li>
<li><strong><code>expectedSimilarTicketIds</code> tek bir id değil, bir listedir.</strong> Birden fazla geçmiş ticket aynı anda alakalı olabilir. Listenin ilk elemanı en alakalı olan; sonrakiler de geçerli alakalı sonuçlardır.</li>
</ul>
<blockquote>
<p><strong><code>note</code>.</strong>: Etiketleyenin &quot;bu pair neden burada?&quot; sorusuna yazılı cevap verdiği serbest metin alanı. İlerde bir regression olduğunda, &quot;ileride bu kodu okuyacak olan ben&quot; çok minnettar olacaktır.</p>
</blockquote>
<p>Dataset wrapper'ı ise basit bir liste tutucu:</p>
<pre><code class="language-java">public record EvalDataset(
        String name,
        String description,
        List&lt;EvalGoldenPair&gt; pairs
) {}
</code></pre>
<p>Listeyi doğrudan <code>List&lt;EvalGoldenPair&gt;</code> olarak değil de bir record içinde tutmak, ileride mevcut dosyaları kırmadan <code>datasetVersion</code> veya <code>tags</code> gibi alanlar eklenebilmesini sağlıyor.</p>
<h2>3. Dataset'i Classpath'ten Yükleme</h2>
<p>Dataset'ler classpath'te JSON formatında duruyor. Repo'da <code>golden-pairs.example.json</code> adıyla çalıştırılabilir bir başlangıç dosyası tutuyorum; gerçek dataset (müşteri verisi içerebileceği için hassas olabilir) <code>golden-pairs.json</code> adıyla aynı dizine kopyalanıyor ve <code>.gitignore</code>a ekleniyor.</p>
<pre><code class="language-java">@Slf4j
@Component
@RequiredArgsConstructor
public class EvalDatasetLoader {

    static final String DEFAULT_DATASET_PATH = &quot;classpath:ai-eval/golden-pairs.json&quot;;
    static final String EXAMPLE_DATASET_PATH = &quot;classpath:ai-eval/golden-pairs.example.json&quot;;

    private final ResourceLoader resourceLoader;
    private final ObjectMapper objectMapper;

    public EvalDataset load() {
        return load(DEFAULT_DATASET_PATH);
    }

    public EvalDataset load(String path) {
        Resource resource = resourceLoader.getResource(path);
        if (!resource.exists()) {
            log.warn(&quot;Eval dataset not found at {} — falling back to example dataset at {}&quot;,
                    path, EXAMPLE_DATASET_PATH);
            resource = resourceLoader.getResource(EXAMPLE_DATASET_PATH);
            if (!resource.exists()) {
                throw new IllegalStateException(
                        &quot;No eval dataset found at &quot; + path + &quot; or &quot; + EXAMPLE_DATASET_PATH);
            }
        }
        try (InputStream in = resource.getInputStream()) {
            EvalDataset dataset = objectMapper.readValue(in, EvalDataset.class);
            List&lt;EvalGoldenPair&gt; pairs = dataset.pairs();
            if (pairs == null || pairs.isEmpty()) {
                throw new IllegalStateException(&quot;Eval dataset at &quot; + path + &quot; contains no pairs&quot;);
            }
            log.info(&quot;Loaded eval dataset '{}' with {} pairs from {}&quot;,
                    dataset.name(), pairs.size(), path);
            return dataset;
        } catch (IOException ex) {
            throw new IllegalStateException(&quot;Failed to read eval dataset from &quot; + path, ex);
        }
    }
}
</code></pre>
<p><code>path</code> parametre olarak alınıyor; bu sayede ekip birden fazla dataset (örneğin ürün başına, müşteri başına ya da kalite seviyesine göre) tutup admin endpoint üzerinden hangisinin çalıştırılacağını seçebiliyor.</p>
<h2>4. Metrikler: Recall@K ve MRR</h2>
<p>Değerlendirmeyi iki temel metrik yönlendiriyor:</p>
<ul>
<li><strong>Recall@K</strong> — Her pair için, beklenen id'lerden en az biri top-K sonuçları arasında geldi mi? Tüm pair'ler üzerinden ortalama alınır. Ben Recall@1, Recall@3 ve Recall@5 raporluyorum.</li>
<li><strong>Mean Reciprocal Rank (MRR)</strong> — Her pair için <code>1 / (ilk alakalı sonucun sırası)</code> hesaplanır; sıra 1'den başlar; eğer beklenen id'lerden hiçbiri top-K'ya girmediyse o pair <code>0</code> katkıda bulunur. Tüm pair'ler üzerinden ortalama alınır. MRR, sistemi sadece doğru cevabı listeye sokması için değil, <em>üst sıralara</em> koyması için ödüllendirir.</li>
</ul>
<p>Bunlara ek olarak bir de sanity-check niteliğinde &quot;ortalama top-1 cosine similarity skoru&quot; tutuyorum; ama bu <strong>bir kalite metriği değildir</strong>: kendinden emin bir biçimde <em>yanlış</em> dönen bir sonucun skoru, daha temkinli bir doğru sonucun skorundan yüksek olabilir. Bu metrik daha çok, kötü bir konfigürasyon değişikliğinden sonra skorların topluca çökmesi gibi durumları yakalamak için kullanılıyor.</p>
<h2>5. Değerlendirmenin Çalıştırılması</h2>
<p><code>EmbeddingEvaluator</code>, her pair için canlı retrieval pipeline'ını bir kez sorguluyor; pair bazlı metrikleri hesaplıyor ve sonuçları rapora topluyor:</p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class EmbeddingEvaluator {

    static final int TOP_K = 5;

    private final TicketEmbeddingService embeddingService;
    private final TicketRepository ticketRepository;
    private final TicketContentBuilder contentBuilder;

    public EvalReport evaluate(EvalDataset dataset) {
        long started = System.currentTimeMillis();
        List&lt;EvalReport.PerPairResult&gt; perPair = new ArrayList&lt;&gt;(dataset.pairs().size());
        int hitsAt1 = 0, hitsAt3 = 0, hitsAt5 = 0;
        double mrrSum = 0.0;
        double topScoreSum = 0.0;
        int topScoreCount = 0;

        for (EvalGoldenPair pair : dataset.pairs()) {
            EvalReport.PerPairResult result = evaluatePair(pair);
            perPair.add(result);
            if (result.hitAt1()) hitsAt1++;
            if (result.hitAt3()) hitsAt3++;
            if (result.hitAt5()) hitsAt5++;
            if (result.firstRelevantRank() &gt; 0) {
                mrrSum += 1.0 / result.firstRelevantRank();
            }
            if (!result.retrievedScores().isEmpty()) {
                topScoreSum += result.retrievedScores().getFirst();
                topScoreCount++;
            }
        }

        int total = dataset.pairs().size();
        double averageTopScore = topScoreCount == 0 ? 0.0 : topScoreSum / topScoreCount;
        long duration = System.currentTimeMillis() - started;

        return new EvalReport(
                total,
                (double) hitsAt1 / total,
                (double) hitsAt3 / total,
                (double) hitsAt5 / total,
                mrrSum / total,
                averageTopScore,
                duration,
                perPair
        );
    }

    private EvalReport.PerPairResult evaluatePair(EvalGoldenPair pair) {
        String queryText = resolveQueryText(pair);
        List&lt;TicketSimilarityResult&gt; hits = embeddingService.findSimilar(queryText, TOP_K, 0.0);

        List&lt;Long&gt; retrievedIds = new ArrayList&lt;&gt;(hits.size());
        List&lt;Double&gt; retrievedScores = new ArrayList&lt;&gt;(hits.size());
        for (TicketSimilarityResult hit : hits) {
            // Sorgunun kendi ticket'ını atlıyoruz — leave-one-out tarzı bir
            // değerlendirmede bu ticket her zaman 1. sırada gelirdi ve
            // retrieval kalitesini olduğundan iyi gösterirdi.
            if (pair.queryTicketId() != null &amp;&amp; hit.ticketId() == pair.queryTicketId()) {
                continue;
            }
            retrievedIds.add(hit.ticketId());
            retrievedScores.add(hit.score());
        }

        int firstRelevantRank = firstRelevantRank(retrievedIds, pair.expectedSimilarTicketIds());
        return new EvalReport.PerPairResult(
                pair.id(),
                pair.expectedSimilarTicketIds(),
                retrievedIds,
                retrievedScores,
                firstRelevantRank,
                firstRelevantRank == 1,
                firstRelevantRank &gt; 0 &amp;&amp; firstRelevantRank &lt;= 3,
                firstRelevantRank &gt; 0 &amp;&amp; firstRelevantRank &lt;= 5
        );
    }

    private static int firstRelevantRank(List&lt;Long&gt; retrieved, List&lt;Long&gt; expected) {
        for (int rank = 1; rank &lt;= retrieved.size(); rank++) {
            if (expected.contains(retrieved.get(rank - 1))) {
                return rank;
            }
        }
        return 0;
    }
}
</code></pre>
<p>Burada altını çizmek istediğim üç ince karar var:</p>
<ul>
<li><strong><code>TOP_K</code> config'ten okunmuyor, sabit 5.</strong> Üretimde top-K her ortam için ayrı yapılandırılabiliyor. Ama eval de aynı ayarı kullansaydı, her config değişikliğiyle birlikte raporlanan sayılar sessizce değişirdi — ve modelin gerçekten iyileştiğini mi, yoksa sadece daha çok aday getirdiğinizi mi göremezdiniz. Sabit bir eval penceresi, çalışmaları karşılaştırılabilir kılıyor.</li>
<li><strong><code>findSimilar</code>a verilen minimum benzerlik threshold'u <code>0.0</code>.</strong> Recall zaten &quot;sonuç yeterince iyi miydi?&quot; sorusunu kapsıyor. Skor üzerinden ayrıca filtre uygulamak iki ayrı soruyu birbirine karıştırırdı: &quot;Doğru ticket bulunabilir miydi?&quot; ve &quot;Skor bir eşik üzerinde mi?&quot;.</li>
<li><strong>Leave-one-out atlaması.</strong> Sorgu zaten mevcut bir ticket'sa, vektör veritabanı bu ticket'ı keyifle 1. sıraya yazardı. Sırayı hesaplamadan önce onu listeden çıkarmak, eval'in olduğundan iyi görünmesini engelliyor.</li>
</ul>
<h2>6. Rapor</h2>
<p><code>EvalReport</code> toplu sonuçları taşıyor; ama her pair'in detayı da hata ayıklayabilelim diye saklanıyor:</p>
<pre><code class="language-java">public record EvalReport(
        int totalPairs,
        double recallAt1,
        double recallAt3,
        double recallAt5,
        double meanReciprocalRank,
        double averageTopScore,
        long durationMillis,
        List&lt;PerPairResult&gt; perPair
) {
    public record PerPairResult(
            String id,
            List&lt;Long&gt; expectedSimilarTicketIds,
            List&lt;Long&gt; retrievedTicketIds,
            List&lt;Double&gt; retrievedScores,
            int firstRelevantRank,
            boolean hitAt1,
            boolean hitAt3,
            boolean hitAt5
    ) {}
}
</code></pre>
<p>Üst seviye sayılar, değişikliğin iyi olup olmadığını söyler. Pair bazlı liste ise <em>hangi sorguların</em> gerilediğini söyler — ki bir sonraki iterasyonu mümkün kılan asıl şey budur. &quot;Recall@5, 0,78'den 0,74'e düştü&quot; bir problem ifadesidir; &quot;Recall@5 0,78'den 0,74'e düştü ve şu üç pair durumu değiştirdi&quot; ise üzerinde çalışılabilecek bir ipucudur.</p>
<h2>7. Değerlendirmenin İstendiğinde Çalıştırılması</h2>
<p>Son olarak, eval'i istediğimiz zaman tekrar tetikleyebileceğimiz, sadece admin'e açık bir HTTP endpoint:</p>
<pre><code class="language-java">@RestController
@RequestMapping(API_PREFIX + &quot;/ai/admin/evaluation&quot;)
@RequiredArgsConstructor
public class EvaluationAdminController {

    private final EvalDatasetLoader datasetLoader;
    private final EmbeddingEvaluator evaluator;

    @PostMapping(&quot;/run&quot;)
    @PreAuthorize(Role.HAS_ADMIN)
    public EvalReport run(@RequestParam(required = false) String datasetPath) {
        EvalDataset dataset = datasetPath == null || datasetPath.isBlank()
                ? datasetLoader.load()
                : datasetLoader.load(datasetPath);
        return evaluator.evaluate(dataset);
    }
}
</code></pre>
<p>Beklenen kullanım akışı oldukça basit:</p>
<ol>
<li>Bir retrieval parametresini, modeli veya prompt'u değiştirin.</li>
<li><code>POST /api/ai/admin/evaluation/run</code> endpoint'ine istek atın.</li>
<li>Raporu önceki referansla karşılaştırın.</li>
<li>Sayılar gerçekten iyileştiyse (ya da önemli olan metriklerde sabit kaldıysa) değişikliği üretime alın.</li>
</ol>
<p>Aynı evaluator'ı bir CI işine bağlayıp, Recall@5 belli bir tabanın altına düştüğünde build'i başarısız hale getirmek de mümkün — bu yapı, kod henüz üretime gitmeden devreye giren bir regression koruması haline gelir.</p>
<h2>Sonuç</h2>
<p>Değerlendirme altyapısı olmadan bir AI özelliği geliştirmek, test suite'i olmayan bir refactoring sürecine benziyor: her değişiklik ilerleme gibi hissettiriyor, ama kanıtlamak imkânsız. Bu yazıda yürüdüğüm yapı bilinçli olarak küçük tutuldu — üç record, iki servis, bir controller ve bir JSON dosyası — yine de &quot;bu daha iyi mi?&quot; sorusunu, çalışmalar arasında karşılaştırılabilir bir sayıya çeviriyor.</p>
<p>Bu yatırım, birisi &quot;yeni embedding modelinin gerçekten daha iyi olduğundan emin miyiz?&quot; diye sorduğu ilk anda kendini geri ödüyor: cevap artık &quot;Recall@5, 0,71'den 0,83'e çıktı, durumu değiştiren dört pair de şunlar&quot; oluyor. Sezgilerden sayılara.</p>
<p>Bu minimal versiyonu büyütmek istediğinizde doğal sonraki adımlar şunlar olabilir: dataset bazında metrikleri zaman içinde takip etmek, false-positive'in canınızı yaktığı senaryolarda precision tarzı metrikler eklemek, ya da retrieval yerine doğrudan LLM'in <em>cevabını</em> puanlamak (LLM-as-judge). Ama ilk gün bunların hiçbirine ihtiyacınız yok. Küçük bir dataset ve Recall@K, içgüdüyle değil veriyle yön bulmaya başlamak için yeterli.</p>
<p>Sorularınız için ya da kendi retrieval pipeline'ınız için benzer bir altyapı kurmayı tartışmak isterseniz benimle iletişime geçebilirsiniz.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Evaluating AI Retrieval Quality with Golden Pairs in Spring Boot]]></title>
            <link>https://ademtonay.com/posts/evaluating-ai-retrieval-with-golden-pairs</link>
            <guid>https://ademtonay.com/posts/evaluating-ai-retrieval-with-golden-pairs</guid>
            <pubDate>Wed, 29 Apr 2026 10:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>When you ship an AI-powered &quot;find similar&quot; feature backed by embeddings and a vector store, every change — a new model, a different chunking strategy, a tweaked threshold — raises the same question: did this make retrieval better, or just different? Without a measurable baseline, you end up tuning by vibes.</p>
<p>In this article, I will walk through how I built a small but proper evaluation harness in Spring Boot that measures retrieval quality using a curated set of <em>golden pairs</em>. It computes industry-standard metrics like Recall@K and Mean Reciprocal Rank (MRR), and exposes them via an admin-only endpoint so the team can re-run evaluations any time before promoting a change to production.</p>
<h2>1. The Problem with Tuning by Vibes</h2>
<p>In a support portal, agents resolve incoming tickets faster when the system suggests similar tickets that were already resolved in the past. The &quot;find similar&quot; feature is implemented with three moving parts:</p>
<ul>
<li>a content builder that turns a ticket into a piece of text,</li>
<li>an embedding model that turns that text into a vector,</li>
<li>a vector store that returns the top-K nearest neighbours.</li>
</ul>
<p>Every component in that pipeline is a knob. Change the embedding model? The vector dimensions, semantic neighbourhood, and similarity scores change. Tweak how the ticket is rendered into text? Different signals are encoded. Adjust the similarity threshold? The recall/precision trade-off shifts.</p>
<p>The only honest way to know whether a change is an improvement is to define what &quot;good&quot; looks like — with concrete examples — and measure against it.</p>
<h2>2. Golden Pairs: The Source of Truth</h2>
<p>A golden pair is a labeled query: &quot;for this input, the system should return these tickets&quot;. The labels are produced by domain experts (here, support engineers who know which past resolutions actually applied to a given new ticket).</p>
<p>I model a single pair as a Java <code>record</code>:</p>
<pre><code class="language-java">@JsonInclude(JsonInclude.Include.NON_NULL)
public record EvalGoldenPair(
        String id,
        String queryText,
        Long queryTicketId,
        List&lt;Long&gt; expectedSimilarTicketIds,
        String note
) {
    public EvalGoldenPair {
        if ((queryText == null || queryText.isBlank()) &amp;&amp; queryTicketId == null) {
            throw new IllegalArgumentException(
                    &quot;EvalGoldenPair &quot; + id + &quot; must define either queryText or queryTicketId&quot;);
        }
        if (expectedSimilarTicketIds == null || expectedSimilarTicketIds.isEmpty()) {
            throw new IllegalArgumentException(
                    &quot;EvalGoldenPair &quot; + id + &quot; must define at least one expectedSimilarTicketId&quot;);
        }
    }
}
</code></pre>
<p>A few decisions are worth calling out:</p>
<ul>
<li><strong><code>queryText</code> <em>or</em> <code>queryTicketId</code>.</strong> Sometimes the labeled query is a hand-written description simulating a new ticket. Other times it is an existing resolved ticket used as a query against the rest of the corpus — a <em>leave-one-out</em> style evaluation. The compact constructor enforces that exactly one route is taken.</li>
<li><strong><code>expectedSimilarTicketIds</code> is a list, not a single ID.</strong> Multiple past tickets may legitimately be relevant. The first element is the most relevant; the rest are also acceptable hits.</li>
</ul>
<blockquote>
<p><strong><code>note</code>.</strong> A free-text field where the labeller explains <em>why</em> this is a golden pair. Future-you, debugging a regression, will be grateful.</p>
</blockquote>
<p>The dataset wrapper is a simple list:</p>
<pre><code class="language-java">public record EvalDataset(
        String name,
        String description,
        List&lt;EvalGoldenPair&gt; pairs
) {}
</code></pre>
<p>Wrapping the list in a record (instead of using a bare <code>List&lt;EvalGoldenPair&gt;</code>) lets future versions add fields like <code>datasetVersion</code> or <code>tags</code> without breaking existing files.</p>
<h2>3. Loading the Dataset from the Classpath</h2>
<p>Datasets live in JSON on the classpath. I keep a <code>golden-pairs.example.json</code> checked into the repo so anyone cloning the project gets a runnable starter, while the real dataset (which can include sensitive customer data) lives at <code>golden-pairs.json</code> and is <code>.gitignore</code>d.</p>
<pre><code class="language-java">@Slf4j
@Component
@RequiredArgsConstructor
public class EvalDatasetLoader {

    static final String DEFAULT_DATASET_PATH = &quot;classpath:ai-eval/golden-pairs.json&quot;;
    static final String EXAMPLE_DATASET_PATH = &quot;classpath:ai-eval/golden-pairs.example.json&quot;;

    private final ResourceLoader resourceLoader;
    private final ObjectMapper objectMapper;

    public EvalDataset load() {
        return load(DEFAULT_DATASET_PATH);
    }

    public EvalDataset load(String path) {
        Resource resource = resourceLoader.getResource(path);
        if (!resource.exists()) {
            log.warn(&quot;Eval dataset not found at {} — falling back to example dataset at {}&quot;,
                    path, EXAMPLE_DATASET_PATH);
            resource = resourceLoader.getResource(EXAMPLE_DATASET_PATH);
            if (!resource.exists()) {
                throw new IllegalStateException(
                        &quot;No eval dataset found at &quot; + path + &quot; or &quot; + EXAMPLE_DATASET_PATH);
            }
        }
        try (InputStream in = resource.getInputStream()) {
            EvalDataset dataset = objectMapper.readValue(in, EvalDataset.class);
            List&lt;EvalGoldenPair&gt; pairs = dataset.pairs();
            if (pairs == null || pairs.isEmpty()) {
                throw new IllegalStateException(&quot;Eval dataset at &quot; + path + &quot; contains no pairs&quot;);
            }
            log.info(&quot;Loaded eval dataset '{}' with {} pairs from {}&quot;,
                    dataset.name(), pairs.size(), path);
            return dataset;
        } catch (IOException ex) {
            throw new IllegalStateException(&quot;Failed to read eval dataset from &quot; + path, ex);
        }
    }
}
</code></pre>
<p>The <code>path</code> is parameterised so the team can ship multiple datasets — one per product, customer, or quality tier — and pick which to run from the admin endpoint.</p>
<h2>4. The Metrics: Recall@K and MRR</h2>
<p>Two metrics drive the evaluation:</p>
<ul>
<li><strong>Recall@K</strong> — for each pair, did at least one of the expected IDs show up in the top-K retrieved results? Average over all pairs. I report Recall@1, Recall@3, and Recall@5.</li>
<li><strong>Mean Reciprocal Rank (MRR)</strong> — for each pair, take <code>1 / rank-of-first-relevant-result</code>, where rank is 1-indexed; if none of the expected IDs appears in the top-K, contribute <code>0</code>. Average over all pairs. MRR rewards the system for putting the right answer near the top, not just somewhere in the list.</li>
</ul>
<p>I also keep a sanity-check metric — average top-1 cosine similarity score — but it is <strong>not</strong> a quality signal: a confidently wrong answer can have a higher score than a less confident correct one. It is useful for spotting score collapse after a bad config change, nothing more.</p>
<h2>5. Running the Evaluation</h2>
<p>The <code>EmbeddingEvaluator</code> queries the live retrieval pipeline once per pair, computes per-pair metrics, and aggregates them into a report:</p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class EmbeddingEvaluator {

    static final int TOP_K = 5;

    private final TicketEmbeddingService embeddingService;
    private final TicketRepository ticketRepository;
    private final TicketContentBuilder contentBuilder;

    public EvalReport evaluate(EvalDataset dataset) {
        long started = System.currentTimeMillis();
        List&lt;EvalReport.PerPairResult&gt; perPair = new ArrayList&lt;&gt;(dataset.pairs().size());
        int hitsAt1 = 0, hitsAt3 = 0, hitsAt5 = 0;
        double mrrSum = 0.0;
        double topScoreSum = 0.0;
        int topScoreCount = 0;

        for (EvalGoldenPair pair : dataset.pairs()) {
            EvalReport.PerPairResult result = evaluatePair(pair);
            perPair.add(result);
            if (result.hitAt1()) hitsAt1++;
            if (result.hitAt3()) hitsAt3++;
            if (result.hitAt5()) hitsAt5++;
            if (result.firstRelevantRank() &gt; 0) {
                mrrSum += 1.0 / result.firstRelevantRank();
            }
            if (!result.retrievedScores().isEmpty()) {
                topScoreSum += result.retrievedScores().getFirst();
                topScoreCount++;
            }
        }

        int total = dataset.pairs().size();
        double averageTopScore = topScoreCount == 0 ? 0.0 : topScoreSum / topScoreCount;
        long duration = System.currentTimeMillis() - started;

        return new EvalReport(
                total,
                (double) hitsAt1 / total,
                (double) hitsAt3 / total,
                (double) hitsAt5 / total,
                mrrSum / total,
                averageTopScore,
                duration,
                perPair
        );
    }

    private EvalReport.PerPairResult evaluatePair(EvalGoldenPair pair) {
        String queryText = resolveQueryText(pair);
        List&lt;TicketSimilarityResult&gt; hits = embeddingService.findSimilar(queryText, TOP_K, 0.0);

        List&lt;Long&gt; retrievedIds = new ArrayList&lt;&gt;(hits.size());
        List&lt;Double&gt; retrievedScores = new ArrayList&lt;&gt;(hits.size());
        for (TicketSimilarityResult hit : hits) {
            // Skip the query ticket itself — it would always rank #1 in a
            // leave-one-out eval and would mask retrieval quality.
            if (pair.queryTicketId() != null &amp;&amp; hit.ticketId() == pair.queryTicketId()) {
                continue;
            }
            retrievedIds.add(hit.ticketId());
            retrievedScores.add(hit.score());
        }

        int firstRelevantRank = firstRelevantRank(retrievedIds, pair.expectedSimilarTicketIds());
        return new EvalReport.PerPairResult(
                pair.id(),
                pair.expectedSimilarTicketIds(),
                retrievedIds,
                retrievedScores,
                firstRelevantRank,
                firstRelevantRank == 1,
                firstRelevantRank &gt; 0 &amp;&amp; firstRelevantRank &lt;= 3,
                firstRelevantRank &gt; 0 &amp;&amp; firstRelevantRank &lt;= 5
        );
    }

    private static int firstRelevantRank(List&lt;Long&gt; retrieved, List&lt;Long&gt; expected) {
        for (int rank = 1; rank &lt;= retrieved.size(); rank++) {
            if (expected.contains(retrieved.get(rank - 1))) {
                return rank;
            }
        }
        return 0;
    }
}
</code></pre>
<p>Three subtle decisions are worth pulling out:</p>
<ul>
<li><strong><code>TOP_K</code> is fixed at 5, not read from config.</strong> In production, top-K is configurable per deployment. But if the eval used the same setting, every config tweak would silently change the reported numbers — and you would not know whether the model improved or you simply retrieved more candidates. A fixed eval window keeps runs comparable.</li>
<li><strong>The minimum-similarity threshold passed to <code>findSimilar</code> is <code>0.0</code>.</strong> Recall already encodes whether a result was good enough. Filtering by score would conflate two different questions: &quot;is the right ticket findable?&quot; vs. &quot;is the score above a threshold?&quot;.</li>
<li><strong>Leave-one-out skip.</strong> When the query is itself an existing ticket, the vector store will gleefully return that ticket as the perfect top-1 match. Skipping it before computing rank prevents the eval from looking better than it really is.</li>
</ul>
<h2>6. The Report</h2>
<p><code>EvalReport</code> is the aggregated picture, but each per-pair result is preserved so failures are debuggable:</p>
<pre><code class="language-java">public record EvalReport(
        int totalPairs,
        double recallAt1,
        double recallAt3,
        double recallAt5,
        double meanReciprocalRank,
        double averageTopScore,
        long durationMillis,
        List&lt;PerPairResult&gt; perPair
) {
    public record PerPairResult(
            String id,
            List&lt;Long&gt; expectedSimilarTicketIds,
            List&lt;Long&gt; retrievedTicketIds,
            List&lt;Double&gt; retrievedScores,
            int firstRelevantRank,
            boolean hitAt1,
            boolean hitAt3,
            boolean hitAt5
    ) {}
}
</code></pre>
<p>The top-level numbers tell you whether the change is good. The per-pair list tells you <em>which</em> queries regressed — and that is what makes the next iteration possible. &quot;Recall@5 dropped from 0.78 to 0.74&quot; is a problem statement; &quot;Recall@5 dropped from 0.78 to 0.74 and these three specific pairs flipped&quot; is a debuggable lead.</p>
<h2>7. Triggering Evaluation On-Demand</h2>
<p>Finally, an admin-only HTTP endpoint to re-run the evaluation any time:</p>
<pre><code class="language-java">@RestController
@RequestMapping(API_PREFIX + &quot;/ai/admin/evaluation&quot;)
@RequiredArgsConstructor
public class EvaluationAdminController {

    private final EvalDatasetLoader datasetLoader;
    private final EmbeddingEvaluator evaluator;

    @PostMapping(&quot;/run&quot;)
    @PreAuthorize(Role.HAS_ADMIN)
    public EvalReport run(@RequestParam(required = false) String datasetPath) {
        EvalDataset dataset = datasetPath == null || datasetPath.isBlank()
                ? datasetLoader.load()
                : datasetLoader.load(datasetPath);
        return evaluator.evaluate(dataset);
    }
}
</code></pre>
<p>The intended workflow is simple:</p>
<ol>
<li>Change a retrieval parameter, model, or prompt.</li>
<li>Hit <code>POST /api/ai/admin/evaluation/run</code>.</li>
<li>Compare the report to the previous baseline.</li>
<li>Only promote the change if the numbers improved (or held steady on the metrics that matter).</li>
</ol>
<p>The same evaluator can also be wired into a CI job that fails the build if Recall@5 drops below a floor — turning the harness into a regression gate before code ever reaches production.</p>
<h2>Wrapping Up</h2>
<p>Building an AI feature without an evaluation harness is like refactoring without a test suite: every change feels like progress, but you have no way to prove it. The harness I walked through here is intentionally small — three records, two services, one controller, and a JSON file — yet it converts a fuzzy &quot;is this better?&quot; question into a number you can compare across runs.</p>
<p>The investment pays for itself the first time someone asks &quot;are we sure the new embedding model is actually better?&quot; and the answer is &quot;Recall@5 went from 0.71 to 0.83, and here are the four pairs that flipped&quot;. From vibes to numbers.</p>
<p>Natural next steps, when you outgrow this minimal version, are: tracking metrics over time per dataset, adding precision-style metrics for cases where false positives hurt, or scoring the LLM's <em>answer</em> instead of the retrieval (using LLM-as-judge). But you do not need any of that on day one. A small dataset and Recall@K are enough to start steering with data instead of intuition.</p>
<p>If you have any questions or want to discuss building one of these for your own retrieval pipeline, feel free to reach out.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Authentication with JWT and Spring Boot]]></title>
            <link>https://ademtonay.com/posts/authentication-with-jwt-and-spring-boot</link>
            <guid>https://ademtonay.com/posts/authentication-with-jwt-and-spring-boot</guid>
            <pubDate>Tue, 03 Dec 2024 00:34:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>In web applications, security, especially authentication and authorization processes, plays a critical role. In this article, I will explain in detail how to perform user authentication using JWT (JSON Web Token) in a Spring Boot application.</p>
<h2>1. Adding Required Dependencies</h2>
<p>To perform JWT-based authentication and authorization in a Spring Boot application, you need to add some essential dependencies to your project. These dependencies are necessary for security and JWT operations. Below is how to add these dependencies to your Spring Boot project.</p>
<p><strong>If you are using Maven</strong>:</p>
<p>You can include the required libraries in your <code>pom.xml</code> file by adding the following dependencies.</p>
<pre><code class="language-xml">&lt;dependencies&gt;
    &lt;!-- Spring Security --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- JJWT (Java JWT) --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-api&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-impl&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
        &lt;scope&gt;runtime&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-jackson&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
        &lt;scope&gt;runtime&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</code></pre>
<p><strong>If you are using Gradle</strong>:</p>
<p>If you are using Gradle, you can add the following dependencies to your <code>build.gradle</code> file.</p>
<pre><code>dependencies {
  // Spring Security
  implementation 'org.springframework.boot:spring-boot-starter-security'

  // JJWT (Java JWT) API, Implementation, and Jackson support
  implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
  runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
  runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
</code></pre>
<h2>2. Storing User Information Securely: <code>AuthDto</code></h2>
<p>As the first step, we need to create a DTO (Data Transfer Object) class to store user information. This class will represent the information that will be included in the JWT token. For example, it might include user ID, email address, and roles:</p>
<pre><code class="language-java">@Builder
@Getter
@Setter
public class AuthDto {
    private UUID id;
    private String email;
    private List&lt;GrantedAuthority&gt; roles;
}
</code></pre>
<p>This DTO will carry the user information and will be used securely within the <code>SecurityContext</code>.</p>
<h2>3. JWT Creation and Validation: <code>TokenService</code></h2>
<p>We need to write a service class that will handle the creation and validation of the JWT. This class will manage all operations from generating the token to validation and constructing the authentication context.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class TokenService implements ITokenService {
    private final IUserRepository userRepository;
    private final int MILLISECOND_IN_MINUTE = 1000 * 60; // Milliseconds per minute
    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; // Signing algorithm

    @Value(&quot;${security.token.access.secret}&quot;)
    private String accessTokenSecret; // Secret key for Access token

    @Value(&quot;${security.token.access.expires}&quot;)
    private int accessTokenExpires; // Expiration time for Access token (in minutes)

    // Generating JWT from User object
    @Override
    public String generateAccessToken(User user) {
        Date now = new Date(); // Current time
        int expiresIn = accessTokenExpires * MILLISECOND_IN_MINUTE; // Expiration time in milliseconds

        // Create JWT token
        return Jwts.builder()
                .setSubject(user.getEmail()) // User's email
                .claim(&quot;id&quot;, user.getId()) // User's ID
                .claim(&quot;roles&quot;, rolesToString(user.getRoles())) // User's roles as a string
                .setIssuedAt(now) // Token issued at
                .setExpiration(new Date(now.getTime() + expiresIn)) // Token expiration time
                .signWith(jwtKeyGenerator(accessTokenSecret), SIGNATURE_ALGORITHM) // JWT signing
                .compact(); // Generate the token
    }

    // Generating authentication from JWT token
    @Override
    public Authentication getAuthenticationFromAccessToken(String accessToken) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(jwtKeyGenerator(accessTokenSecret)) // Verify the JWT signature using the secret key
                .build().parseClaimsJws(accessToken).getBody(); // Parse the JWT token

        // Get roles from token and convert to GrantedAuthority
        List&lt;GrantedAuthority&gt; roles = stringToRoles(claims.get(&quot;roles&quot;, String.class));

        // Build Authentication object with user information
        AuthDto authDto = AuthDto.builder()
                .id(UUID.fromString(claims.get(&quot;id&quot;, String.class))) // User's UUID
                .email(claims.getSubject()) // User's email
                .roles(roles) // User's roles
                .build();

        // Return Authentication object
        return new UsernamePasswordAuthenticationToken(authDto, null, roles);
    }

    // Convert roles to a comma-separated string
    private String rolesToString(Collection&lt;Role&gt; roles) {
        return roles.stream()
                .map(Role::name) // Get role names
                .collect(Collectors.joining(&quot;,&quot;)); // Join with commas
    }

    // Convert string roles to GrantedAuthority list
    private List&lt;GrantedAuthority&gt; stringToRoles(String rolesString) {
        if (rolesString == null || rolesString.isEmpty()) {
            return Collections.emptyList(); // Return empty list if no roles
        }

        // Convert string roles to GrantedAuthority
        return Arrays.stream(rolesString.split(&quot;,\\s*&quot;))
                .map(role -&gt; Role.valueOf(role)) // Convert to Role enum
                .filter(Objects::nonNull) // Filter non-null roles
                .map(role -&gt; new SimpleGrantedAuthority(role.name())) // Convert to GrantedAuthority
                .collect(Collectors.toList());
    }

    // Generate secret key for JWT
    private Key jwtKeyGenerator(String secret) {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); // Decode the secret key with BASE64
    }
}
</code></pre>
<h2>4. JWT Validation with <code>AuthTokenFilter</code></h2>
<p>To check the validity of the JWT on every incoming request, we use a filter. This filter verifies the JWT, and if valid, authenticates the user.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
    private final ITokenService tokenService; // TokenService used for JWT verification

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String url = request.getRequestURI(); // Get the URL of the incoming request

        // Skip JWT verification for '/api/auth/*' paths
        if (url.matches(&quot;/api/auth/*&quot;)) {
            filterChain.doFilter(request, response);
            return;
        }

        // Get access token from cookie
        Cookie accessTokenCookie = WebUtils.getCookie(request, &quot;accessToken&quot;);

        // If no token found, attach error and proceed
        if (accessTokenCookie == null) {
            attachError(request, ErrorCode.ACCESS_TOKEN_DOES_NOT_EXIST); // Attach error message
            filterChain.doFilter(request, response);
            return;
        }

        try {
            // Validate the token and get Authentication object
            Authentication authentication = tokenService.getAuthenticationFromAccessToken(accessTokenCookie.getValue());

            // Set the authentication context
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (ExpiredJwtException e) {
            // Handle expired token error
            attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_EXPIRED);
        } catch (MalformedJwtException e) {
            // Handle malformed token error
            attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_MALFORMED);
        } catch (RuntimeException e) {
            // Handle other errors
            attachErrorAndClearAuthContext(request, ErrorCode.INTERNAL_SERVER);
        }

        filterChain.doFilter(request, response); // Continue with the filter chain
    }

    private void attachError(HttpServletRequest request, ErrorCode errorCode) {
        // Attach error information to the request
        request.setAttribute(&quot;exceptionStatus&quot;, errorCode.getHttpStatusCode());
        request.setAttribute(&quot;exceptionCode&quot;, errorCode.getCode());
        request.setAttribute(&quot;exceptionMessage&quot;, errorCode.getMessage());
    }

    private void attachErrorAndClearAuthContext(HttpServletRequest request, ErrorCode errorCode) {
        // Attach error and clear authentication context
        attachError(request, errorCode);
        SecurityContextHolder.clearContext(); // Clear authentication information
    }
}
</code></pre>
<h2>5. Handling JWT Errors with <code>AuthExceptionHandler</code></h2>
<p>The <code>AuthenticationEntryPoint</code> is used to manage errors during JWT verification, sending the correct error messages to users.</p>
<pre><code class="language-java">@Slf4j
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint {
    final ObjectMapper MAPPER = new ObjectMapper(); // ObjectMapper for JSON responses

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error(&quot;Unauthorized error: {}&quot;, authException.getMessage()); // Log error message

        response.setContentType(MediaType.APPLICATION_JSON_VALUE); // Set response type to JSON
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Set HTTP 401 (Unauthorized) status code

        final Map&lt;String, Object&gt; body = new HashMap&lt;&gt;(); // Map to store error details

        // If error details are present in the request, return them in the JSON response
        if (request.getAttribute(&quot;exceptionCode&quot;) != null &amp;&amp;
            request.getAttribute(&quot;exceptionMessage&quot;) != null &amp;&amp;
            request.getAttribute(&quot;exceptionStatus&quot;) != null) {
            body.put(&quot;code&quot;, request.getAttribute(&quot;exceptionCode&quot;).toString());
            body.put(&quot;message&quot;, request.getAttribute(&quot;exceptionMessage&quot;).toString());
            body.put(&quot;status&quot;, Integer.parseInt(request.getAttribute(&quot;exceptionStatus&quot;).toString()));
        } else {
            // If no error details are found, send a general authentication required error
            body.put(&quot;code&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getCode());
            body.put(&quot;message&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getMessage());
            body.put(&quot;status&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getHttpStatusCode());
        }

        // Send JSON response
        MAPPER.writeValue(response.getOutputStream(), body);
    }
}
</code></pre>
<h2>6. Spring Security Configuration: <code>SecurityConfig</code></h2>
<p>Finally, we include the JWT validation filter and error handler in the Spring Security configuration. This configuration will handle security and validation processes across the application.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final AuthTokenFilter authTokenFilter; // Filter for JWT validation
    private final AuthExceptionHandler authExceptionHandler; // Error handler for JWT errors

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .exceptionHandling(customizer -&gt; customizer.authenticationEntryPoint(authExceptionHandler)) // Use the error handler
                .addFilterBefore(authTokenFilter, BasicAuthenticationFilter.class) // Add JWT validation filter
                .csrf(AbstractHttpConfigurer::disable) // Disable CSRF protection
                .sessionManagement(customizer -&gt; customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stateless session management
                .authorizeHttpRequests(requests -&gt; requests
                        .requestMatchers(&quot;/api/auth/*&quot;).permitAll() // Allow unauthenticated access to auth endpoints
                        .anyRequest().authenticated()) // Require authentication for all other requests
                .build(); // Apply configuration
    }
}
</code></pre>
<h2>Wrapping Up</h2>
<p>In this article, we have explored how to implement JWT-based authentication and authorization in Spring Boot applications. We covered key concepts like creating and validating JWT tokens, security filters, and error handling. If you have any questions, feel free to contact me.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[JWT ve Spring Boot ile Kimlik Doğrulama]]></title>
            <link>https://ademtonay.com/posts/jwt-spring-boot-entegrasyonu</link>
            <guid>https://ademtonay.com/posts/jwt-spring-boot-entegrasyonu</guid>
            <pubDate>Tue, 03 Dec 2024 00:34:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Web uygulamalarında güvenlik, özellikle kimlik doğrulama ve yetkilendirme süreçleri son derece kritik bir rol oynamaktadır. Bu yazıda, Spring Boot uygulamanızda JWT (JSON Web Token) kullanarak kullanıcı kimlik doğrulaması nasıl yapılır, detaylı bir şekilde anlatacağım.</p>
<h2>1. Gerekli Bağımlılıkların Eklenmesi</h2>
<p>Spring Boot ile JWT tabanlı kimlik doğrulama ve yetkilendirme işlemleri gerçekleştirebilmek için bazı temel bağımlılıkları projenize dahil etmeniz gerekmektedir. Bu bağımlılıklar, güvenlik ve JWT işlemleri için gereklidir. Aşağıda, Spring Boot projenize bu bağımlılıkların nasıl ekleneceği açıklanmıştır.</p>
<p><strong>Maven Kullanıyorsanız</strong>:</p>
<p><code>pom.xml</code> dosyanıza aşağıdaki bağımlılıkları ekleyerek gerekli kütüphaneleri projenize dahil edebilirsiniz.</p>
<pre><code class="language-xml">&lt;dependencies&gt;
    &lt;!-- Spring Security --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- JJWT (Java JWT) --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-api&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-impl&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
        &lt;scope&gt;runtime&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt-jackson&lt;/artifactId&gt;
        &lt;version&gt;0.11.5&lt;/version&gt;
        &lt;scope&gt;runtime&lt;/scope&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</code></pre>
<p><strong>Gradle Kullanıyorsanız</strong>:</p>
<p>Eğer Gradle kullanıyorsanız, aşağıdaki bağımlılıkları <code>build.gradle</code> dosyanıza ekleyebilirsiniz.</p>
<pre><code>dependencies {
  // Spring Security
  implementation 'org.springframework.boot:spring-boot-starter-security'

  // JJWT (Java JWT) API, Implementation ve Jackson desteği
  implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
  runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
  runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
</code></pre>
<h2>2. Kullanıcı Bilgilerinin Güvenli Bir Şekilde Saklanması: <code>AuthDto</code></h2>
<p>İlk adım olarak, kullanıcı bilgilerini saklayacağımız bir DTO (Data Transfer Object) sınıfı oluşturmalıyız. Bu sınıf, JWT token içinde yer alacak bilgileri temsil edecektir. Örneğin, kullanıcı id'si, email adresi ve rollerini içerecek bir sınıf:</p>
<pre><code class="language-java">@Builder
@Getter
@Setter
public class AuthDto {
    private UUID id;
    private String email;
    private List&lt;GrantedAuthority&gt; roles;
}
</code></pre>
<p>Bu DTO, kullanıcı bilgilerini taşıyacak ve güvenli bir şekilde SecurityContext içinde kullanılacaktır.</p>
<h2>3. JWT Oluşturma ve Doğrulama: <code>TokenService</code></h2>
<p>JWT'yi oluşturup doğrulayacak olan servis sınıfını yazmamız gerekiyor. Bu sınıf, token'ın üretilmesinden, doğrulama ve güvenlik bağlamını (authentication) oluşturma sürecine kadar olan tüm işlemleri yönetecek.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class TokenService implements ITokenService {
    private final IUserRepository userRepository;
    private final int MILLISECOND_IN_MINUTE = 1000 * 60; // Zaman hesaplamalarında kullanmak için milisaniye cinsinden dakika çevirisi
    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; // Kullanılacak imzalama algoritması

    @Value(&quot;${security.token.access.secret}&quot;)
    private String accessTokenSecret; // Access token için gizli anahtar

    @Value(&quot;${security.token.access.expires}&quot;)
    private int accessTokenExpires; // Access token'in geçerlilik süresi (dakika cinsinden)

    // Kullanıcı objesinden JWT'nin oluşturulması
    @Override
    public String generateAccessToken(User user) {
        Date now = new Date(); // Şu anki zaman
        int expiresIn = accessTokenExpires * MILLISECOND_IN_MINUTE; // Token'ın geçerlilik süresi

        // JWT token'ı oluşturuluyor
        return Jwts.builder()
                .setSubject(user.getEmail()) // Kullanıcının email adresi
                .claim(&quot;id&quot;, user.getId()) // Kullanıcının id'si
                .claim(&quot;roles&quot;, rolesToString(user.getRoles())) // Kullanıcının rollerini string olarak ekliyoruz
                .setIssuedAt(now) // Token'ın oluşturulma zamanı
                .setExpiration(new Date(now.getTime() + expiresIn)) // Token'ın son kullanma tarihi
                .signWith(jwtKeyGenerator(accessTokenSecret), SIGNATURE_ALGORITHM) // JWT imzalaması
                .compact(); // Token'ı oluştur
    }

    // JWT token'ından kullanıcı kimlik doğrulaması (Authentication) oluşturulması
    @Override
    public Authentication getAuthenticationFromAccessToken(String accessToken) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(jwtKeyGenerator(accessTokenSecret)) // JWT'nin imzasını doğrulamak için gizli anahtar
                .build().parseClaimsJws(accessToken).getBody(); // JWT token'ını çöz

        // Token'dan roller bilgisini alıyoruz ve GrantedAuthority objelerine dönüştürüyoruz
        List&lt;GrantedAuthority&gt; roles = stringToRoles(claims.get(&quot;roles&quot;, String.class));

        // Authentication objesini oluşturuyoruz
        AuthDto authDto = AuthDto.builder()
                .id(UUID.fromString(claims.get(&quot;id&quot;, String.class))) // Kullanıcının UUID'sini al
                .email(claims.getSubject()) // Kullanıcının e-posta adresini al
                .roles(roles) // Kullanıcının rollerini ekle
                .build();

        // Kullanıcı bilgisiyle birlikte authentication objesini döndür
        return new UsernamePasswordAuthenticationToken(authDto, null, roles);
    }

    // Kullanıcının rollerini string formatına dönüştüren yardımcı metot
    private String rolesToString(Collection&lt;Role&gt; roles) {
        return roles.stream()
                .map(Role::name) // Rollerin isimlerini al
                .collect(Collectors.joining(&quot;,&quot;)); // Virgülle ayırarak tek bir string'e çevir
    }

    // String formatındaki rollerin GrantedAuthority listesine dönüştürülmesi
    private List&lt;GrantedAuthority&gt; stringToRoles(String rolesString) {
        if (rolesString == null || rolesString.isEmpty()) {
            return Collections.emptyList(); // Eğer roller boşsa, boş bir liste döndür
        }

        // String içindeki rollerden her birini GrantedAuthority'ye dönüştür
        return Arrays.stream(rolesString.split(&quot;,\\s*&quot;))
                .map(role -&gt; Role.valueOf(role)) // Her rolü Role enum'ına dönüştür
                .filter(Objects::nonNull) // Null olmayanları filtrele
                .map(role -&gt; new SimpleGrantedAuthority(role.name())) // GrantedAuthority nesnelerine dönüştür
                .collect(Collectors.toList());
    }

    // JWT için gizli anahtarın oluşturulması
    private Key jwtKeyGenerator(String secret) {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); // BASE64 ile decode edilmiş anahtarı kullanıyoruz
    }
}
</code></pre>
<h2>4. JWT Kontrolü İçin <code>AuthTokenFilter</code></h2>
<p>Her gelen istekte, JWT'nin doğruluğunu kontrol etmek için bir filtre kullanıyoruz. Bu filtre, JWT'nin geçerli olup olmadığını kontrol eder ve geçerli ise kullanıcıyı doğrular.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
    private final ITokenService tokenService; // TokenService, JWT doğrulama işlemleri için kullanılır

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String url = request.getRequestURI(); // Gelen isteğin URL'sini al

        // Eğer URL '/api/auth/*' ile başlıyorsa, JWT kontrolü yapma
        if (url.matches(&quot;/api/auth/*&quot;)) {
            filterChain.doFilter(request, response);
            return;
        }

        // Cookie içinde access token'ı alıyoruz
        Cookie accessTokenCookie = WebUtils.getCookie(request, &quot;accessToken&quot;);

        // Eğer token yoksa, hata mesajı ekleyip, filtreyi geçiyoruz
        if (accessTokenCookie == null) {
            attachError(request, ErrorCode.ACCESS_TOKEN_DOES_NOT_EXIST); // Hata mesajı ekle
            filterChain.doFilter(request, response); // Filtreyi geç
            return;
        }

        try {
            // Token'ı doğrulayıp Authentication objesini alıyoruz
            Authentication authentication = tokenService.getAuthenticationFromAccessToken(accessTokenCookie.getValue());

            // SecurityContext'e doğrulama bilgilerini yerleştiriyoruz
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (ExpiredJwtException e) {
            // Token süresi dolmuşsa, hata mesajı ekleyip, SecurityContext'i temizliyoruz
            attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_EXPIRED);
        } catch (MalformedJwtException e) {
            // Token bozulmuşsa, hata mesajı ekleyip, SecurityContext'i temizliyoruz
            attachErrorAndClearAuthContext(request, ErrorCode.ACCESS_TOKEN_MALFORMED);
        } catch (RuntimeException e) {
            // Diğer hata durumlarında, hata mesajı ekleyip, SecurityContext'i temizliyoruz
            attachErrorAndClearAuthContext(request, ErrorCode.INTERNAL_SERVER);
        }

        filterChain.doFilter(request, response); // Filtreyi geç
    }

    private void attachError(HttpServletRequest request, ErrorCode errorCode) {
        // Hata mesajlarını request'e ekliyoruz
        request.setAttribute(&quot;exceptionStatus&quot;, errorCode.getHttpStatusCode());
        request.setAttribute(&quot;exceptionCode&quot;, errorCode.getCode());
        request.setAttribute(&quot;exceptionMessage&quot;, errorCode.getMessage());
    }

    private void attachErrorAndClearAuthContext(HttpServletRequest request, ErrorCode errorCode) {
        // Hata mesajlarını ekleyip, SecurityContext'i temizliyoruz
        attachError(request, errorCode);
        SecurityContextHolder.clearContext(); // Authentication bilgilerini temizle
    }
}
</code></pre>
<h2>5. JWT Hatalarını Yönetmek İçin <code>AuthExceptionHandler</code></h2>
<p>JWT doğrulama sırasında oluşabilecek hataları yönetmek için kullanılan <code>AuthenticationEntryPoint</code>, kullanıcılara doğru hata mesajları göndermek için yapılandırılır.</p>
<pre><code class="language-java">@Slf4j
@Component
public class AuthExceptionHandler implements AuthenticationEntryPoint {
    final ObjectMapper MAPPER = new ObjectMapper(); // JSON yanıtları için ObjectMapper

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error(&quot;Unauthorized error: {}&quot;, authException.getMessage()); // Hata mesajını logluyoruz

        response.setContentType(MediaType.APPLICATION_JSON_VALUE); // Yanıt türünü JSON olarak belirliyoruz
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401 (Unauthorized) durum kodu

        final Map&lt;String, Object&gt; body = new HashMap&lt;&gt;(); // Yanıt için hata bilgilerini içeren bir map

        // Eğer hata bilgileri request içinde varsa, bunları alıp JSON yanıt olarak gönderiyoruz
        if (request.getAttribute(&quot;exceptionCode&quot;) != null &amp;&amp;
            request.getAttribute(&quot;exceptionMessage&quot;) != null &amp;&amp;
            request.getAttribute(&quot;exceptionStatus&quot;) != null) {
            body.put(&quot;code&quot;, request.getAttribute(&quot;exceptionCode&quot;).toString());
            body.put(&quot;message&quot;, request.getAttribute(&quot;exceptionMessage&quot;).toString());
            body.put(&quot;status&quot;, Integer.parseInt(request.getAttribute(&quot;exceptionStatus&quot;).toString()));
        } else {
            // Eğer hata bilgileri yoksa, genel bir yetkilendirme hatası mesajı gönderiyoruz
            body.put(&quot;code&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getCode());
            body.put(&quot;message&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getMessage());
            body.put(&quot;status&quot;, ErrorCode.AUTHENTICATION_REQUIRED.getHttpStatusCode());
        }

        // JSON yanıtı gönder
        MAPPER.writeValue(response.getOutputStream(), body);
    }
}
</code></pre>
<h2>6. Spring Security Yapılandırması: <code>SecurityConfig</code></h2>
<p>Son olarak, JWT doğrulama filtremizi ve hata yönetimi sınıfımızı Spring Security yapılandırmasına dahil ediyoruz. Bu yapılandırma, tüm uygulama genelinde güvenliği sağlayacak şekilde filtre ve doğrulama işlemleri yapar.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final AuthTokenFilter authTokenFilter; // AuthTokenFilter, her gelen istekte JWT doğrulamasını yapacak
    private final AuthExceptionHandler authExceptionHandler; // AuthExceptionHandler, hata mesajlarını yönetir

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .exceptionHandling(customizer -&gt; customizer.authenticationEntryPoint(authExceptionHandler)) // Hata yönetimi sınıfını kullan
                .addFilterBefore(authTokenFilter, BasicAuthenticationFilter.class) // JWT doğrulama filtresini yerleştir
                .csrf(AbstractHttpConfigurer::disable) // CSRF korumasını devre dışı bırak
                .sessionManagement(customizer -&gt; customizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Stateless oturum yönetimi
                .authorizeHttpRequests(requests -&gt; requests
                        .requestMatchers(&quot;/api/auth/*&quot;).permitAll() // Kimlik doğrulaması gerektirmeyen yollar
                        .anyRequest().authenticated()) // Diğer tüm isteklerde kimlik doğrulaması gerektir
                .build(); // Yapılandırmayı uygula
    }
}
</code></pre>
<h2>Sonuç</h2>
<p>Bu yazıda, Spring Boot uygulamalarında JWT tabanlı kimlik doğrulama ve yetkilendirme işlemleri nasıl yapılır, bunu inceledik. JWT'nin oluşturulması, doğrulanması, güvenlik filtreleri ve hata yönetimi gibi temel adımları olabildiğince yalın ve detaylı anlatmaya çalıştım. Sorularınız için benimle iletişime geçebilirsiniz.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[What's Couchbase?]]></title>
            <link>https://ademtonay.com/posts/what-s-couchbase</link>
            <guid>https://ademtonay.com/posts/what-s-couchbase</guid>
            <pubDate>Thu, 22 Feb 2024 01:47:09 GMT</pubDate>
            <content:encoded><![CDATA[<figure>
  <img src="https://ademtonay.com/images/what-s-couchbase_couchbase.svg" class="dark:hidden" />
  <img src="/images/what-s-couchbase_couchbase-dark.png" class="hidden dark:block" />
</figure>
<p>Are you seeking a cutting-edge solution for your data management needs? Look no further than Couchbase, the heavyweight champion in the realm of NoSQL databases. In this article, we will delve into the reasons why Couchbase stands head and shoulders above the rest, and why it should be your top choice for modern data storage and retrieval.</p>
<h2>Lightning-Fast Performance</h2>
<p>Couchbase is renowned for its lightning-fast performance, providing unparalleled speed and efficiency in handling large volumes of data. Its innovative memory-first architecture ensures that data access is swift and seamless, making it the go-to choice for applications requiring real-time responsiveness.</p>
<p><a href="https://docs.couchbase.com/server/current/learn/buckets-memory-and-storage/memory-and-storage.html">Click to see more details on offical document</a></p>
<h2>Scalability at its Finest</h2>
<p>One of the standout features of Couchbase is its exceptional scalability. Whether you're a small startup experiencing rapid growth or a large enterprise handling massive amounts of data, Couchbase can effortlessly scale to meet your evolving needs. Say goodbye to bottlenecks and limitations – with Couchbase, the sky's the limit.</p>
<h2>Built for High Availability</h2>
<p>In today's fast-paced digital landscape, downtime is simply not an option. Couchbase is designed with high availability in mind, ensuring that your data remains accessible and secure around the clock. Its built-in replication and failover capabilities guarantee uninterrupted service, keeping your operations running smoothly without missing a beat.</p>
<h2>Flexible Data Model</h2>
<p>Gone are the days of rigid, inflexible data structures. Couchbase offers a flexible data model that adapts to your evolving requirements, allowing you to store and retrieve data in a way that makes sense for your applications. Whether you're dealing with structured or unstructured data, Couchbase provides the versatility you need to stay agile and responsive in a dynamic environment.</p>
<h2>Robust Querying Capabilities</h2>
<p>Searching for specific data within a vast database can be like finding a needle in a haystack. With Couchbase's powerful querying capabilities, locating the information you need is a breeze. Its support for SQL-like queries and indexing features simplifies the process of data retrieval, empowering you to extract insights and drive informed decision-making with ease.</p>
<p><strong>Example query:</strong></p>
<pre><code class="language-sql">SELECT a.callsign FROM default:`travel-sample`.inventory.airline
a LIMIT 5;
</code></pre>
<p><strong>Result:</strong></p>
<pre><code class="language-json">{
    &quot;requestID&quot;: &quot;cfc095c5-d23b-4a81-a4b5-990ad445559f&quot;,
    &quot;signature&quot;: {
        &quot;callsign&quot;: &quot;json&quot;
    },
    &quot;results&quot;: [
    {
        &quot;callsign&quot;: &quot;MILE-AIR&quot;
    },
    {
        &quot;callsign&quot;: &quot;TXW&quot;
    },
    {
        &quot;callsign&quot;: &quot;atifly&quot;
    },
    {
        &quot;callsign&quot;: null
    },
    {
        &quot;callsign&quot;: &quot;LOCAIR&quot;
    }
    ],
    &quot;status&quot;: &quot;success&quot;,
    &quot;metrics&quot;: {
        &quot;elapsedTime&quot;: &quot;3.197119ms&quot;,
        &quot;executionTime&quot;: &quot;3.086979ms&quot;,
        &quot;resultCount&quot;: 5,
        &quot;resultSize&quot;: 175,
        &quot;serviceLoad&quot;: 3
    }
}
</code></pre>
<h2>Wrapping Up</h2>
<p>In conclusion, Couchbase emerges as the undisputed champion in the world of NoSQL databases, offering unmatched performance, scalability, availability, flexibility, and querying capabilities. By choosing Couchbase as your data management solution, you're not just making a choice – you're making a statement. Elevate your data infrastructure to new heights with Couchbase and experience the power of next-generation database technology.</p>
<p>So why settle for mediocrity when you can have excellence? Make the smart choice. Choose Couchbase.</p>
<p>For more please reach out to Couchbase Türkiye Partner: <a href="https://plainextech.com">Plainex Technology</a></p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Automating Sequelize Model Synchronization]]></title>
            <link>https://ademtonay.com/posts/automating-sequelize-model-synchronization</link>
            <guid>https://ademtonay.com/posts/automating-sequelize-model-synchronization</guid>
            <pubDate>Sun, 18 Feb 2024 12:43:43 GMT</pubDate>
            <content:encoded><![CDATA[<p>Managing database models in a Node.js application often involves keeping them synchronized with the database schema. This process can become cumbersome, especially in larger projects with multiple models. In this article, we'll explore how to automate the synchronization of models using TypeScript, path manipulation, and filesystem operations.</p>
<h2>Understanding the Challenge</h2>
<p>In a typical Node.js application, database models are defined as classes or objects representing database tables. When changes are made to these models, it's essential to reflect these changes in the database schema. Manually synchronizing each model with the database can be time consuming and error-prone, especially as the number of models grows.</p>
<h2>The Solution: Automated Model Synchronization Script</h2>
<p>To address this challenge, we can create a script that automatically synchronizes all models with the database. Let's break down the script step by step:</p>
<pre><code class="language-typescript">import path from 'path'
import config from 'config'
import fs from 'fs'
import { SyncOptions } from 'sequelize'

export default async function syncModels(options?: SyncOptions): Promise&lt;void&gt; {
    // Fetch current environment
    const currentEnv = config.get&lt;string&gt;('server.env')

    // Determine file extension based on environment
    const fileExtension = currentEnv === 'development' ? '.ts' : '.js'

    // Read all files in the current directory
    fs.readdirSync(__dirname)
        .filter((file) =&gt; {
            // Filter out unwanted files and select files with the correct extension
            const returnFile =
                file.indexOf('.') !== 0 &amp;&amp;
                file !== path.basename(__filename) &amp;&amp;
                path.extname(file) === fileExtension

            return returnFile
        })
        .forEach((file) =&gt; {
            // Import the model dynamically
            const model = require(path.join(__dirname, file)).default

            // Synchronize the model with the database
            model.sync(options)
        })
}
</code></pre>
<h2>Handling Model Imports</h2>
<p>In the past, developers commonly used <code>sequelize.import()</code> to load model definitions from separate files. However, this method has been deprecated <a href="https://sequelize.org/docs/v6/moved/models-definition/">see more</a>. Instead, I recommend using native import statements or <code>require()</code>.</p>
<pre><code class="language-typescript">// CommonJS
const ProjectModel = require('./path/to/models/project').default;

// ES Modules
import ProjectModel from './path/to/models/project'
</code></pre>
<h2>Wrapping Up</h2>
<p>By automating the synchronization of database models with the database schema, we can streamline the development process and reduce the risk of inconsistencies between models and the database. This script offers a convenient solution for maintaining data integrity in Node.js applications.</p>
<p>Feel free to integrate this script into your Node.js projects and adapt it to suit your specific requirements. Happy coding!</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Web Accessibility Tips]]></title>
            <link>https://ademtonay.com/posts/web-accessibility-tips</link>
            <guid>https://ademtonay.com/posts/web-accessibility-tips</guid>
            <pubDate>Fri, 16 Feb 2024 09:07:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>When navigating the internet, users who rely on screen readers may encounter certain challenges. Therefore, it's important for web developers to take specific steps to enhance the experience of these users. Here are some key steps web developers should take to ensure a better experience for screen reader users:</p>
<h2>Correct HTML Usage</h2>
<p>When building web pages, use semantic HTML tags correctly. Use <code>&lt;h1&gt;</code> through <code>&lt;h6&gt;</code> for headings, <code>&lt;ul&gt;</code>, <code>&lt;ol&gt;</code>, <code>&lt;li&gt;</code> for lists, and <code>&lt;section&gt;</code>, <code>&lt;article&gt;</code>, <code>&lt;aside&gt;</code> for content sections.</p>
<pre><code class="language-html">&lt;h1&gt;Main Heading&lt;/h1&gt;
&lt;p&gt;This is a paragraph.&lt;/p&gt;
&lt;ul&gt;
    &lt;li&gt;Item 1&lt;/li&gt;
    &lt;li&gt;Item 2&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<h2>Alternative Texts</h2>
<p>When adding images with the <code>&lt;img&gt;</code> tag, include appropriate alternative text (alt attribute) for each image. This is important for screen readers as they cannot view images.</p>
<pre><code class="language-html">&lt;img src=&quot;image.jpg&quot; alt=&quot;A beautiful landscape image&quot;&gt;
</code></pre>
<h2>Labeling Forms</h2>
<p>Add <code>&lt;label&gt;</code> tags to form fields to help users understand what they should input. Additionally, you can group form fields using <code>&lt;fieldset&gt;</code> and <code>&lt;legend&gt;</code>.</p>
<pre><code class="language-html">&lt;label for=&quot;username&quot;&gt;Username:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;&gt;
</code></pre>
<p>Alternatively, you can use the <code>aria-labelledby</code> attribute.</p>
<pre><code class="language-html">&lt;label id=&quot;username-label&quot;&gt;Username:&lt;/label&gt;
&lt;input type=&quot;text&quot; aria-labelledby=&quot;username-label&quot;&gt;
</code></pre>
<h2>Using Aria Labels</h2>
<p>Use <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA">ARIA (Accessible Rich Internet Applications)</a> labels to enhance accessibility for screen reader users, especially for dynamic content and single-page applications.</p>
<pre><code class="language-html">&lt;div role=&quot;navigation&quot;&gt;
    &lt;!-- Navigation menu content --&gt;
&lt;/div&gt;
</code></pre>
<p>For more information on how to use Aria labels, you can refer to the <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Guides">MDN Aria Guides</a>.</p>
<h2>sr-only Class</h2>
<p>The sr-only class is used for content that is visually hidden but accessible to screen readers. It's commonly used to hide text accompanying visual elements while keeping it accessible to screen reader users.</p>
<pre><code class="language-css">.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
}
</code></pre>
<p>This CSS code visually hides the content while keeping it accessible to screen readers.</p>
<p>For example, you can use the <code>sr-only</code> class for the alternative text of a button's icon:</p>
<pre><code class="language-html">&lt;button&gt;
    &lt;span class=&quot;sr-only&quot;&gt;More information&lt;/span&gt;
    &lt;i class=&quot;fas fa-info-circle&quot;&gt;&lt;/i&gt;
&lt;/button&gt;
</code></pre>
<p>Or, for describing a link:</p>
<pre><code class="language-html">&lt;a href=&quot;page.html&quot;&gt;
  &lt;span class=&quot;sr-only&quot;&gt;Click for more information&lt;/span&gt; Click here
&lt;/a&gt;
</code></pre>
<h2>Wrapping Up</h2>
<p>Improving the experience of screen reader users on websites is a crucial responsibility for web developers. By following these guidelines including proper HTML usage, alternative texts, clear and concise texts, link labeling, specifying focus states, utilizing ARIA labels, keyboard accessibility, and continuous testing and feedback, we can create more accessible and user-friendly websites.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Daha Erişilebilir Web Sitesi]]></title>
            <link>https://ademtonay.com/posts/web-erisebilirlik-ipuclari</link>
            <guid>https://ademtonay.com/posts/web-erisebilirlik-ipuclari</guid>
            <pubDate>Fri, 16 Feb 2024 09:06:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p><em>Click <a href="/posts/web-accessibility-tips">here</a> to read this post in english</em></p>
</blockquote>
<p>Ekran okuyucu kullanan kullanıcılar, interneti gezinirken bazı zorluklarla karşılaşabilirler. Bu nedenle, web geliştiricilerin, bu kullanıcıların deneyimini iyileştirmek için belirli adımlar atmaları önemlidir. İşte ekran okuyucu kullanıcılarının daha iyi bir deneyim yaşamasını sağlamak için web geliştiricilerin yapması gereken bazı önemli adımlar:</p>
<h2>Doğru HTML Kullanımı</h2>
<p>Web sayfalarınızı oluştururken, semantik HTML etiketlerini doğru bir şekilde kullanın. Başlıklar için <code>&lt;h1&gt;</code> ile <code>&lt;h6&gt;</code>, listeler için <code>&lt;ul&gt;</code>, <code>&lt;ol&gt;</code>, <code>&lt;li&gt;</code>, ve içerik bölmeleri için <code>&lt;section&gt;</code>, <code>&lt;article&gt;</code>, <code>&lt;aside&gt;</code> gibi etiketler kullanın.</p>
<pre><code class="language-html">&lt;h1&gt;Ana Başlık&lt;/h1&gt;
&lt;p&gt;Bu bir paragraf.&lt;/p&gt;
&lt;ul&gt;
    &lt;li&gt;Madde 1&lt;/li&gt;
    &lt;li&gt;Madde 2&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<h2>Alternatif Metinler</h2>
<p>Resimlerinizi <code>&lt;img&gt;</code> etiketi ile eklerken, her resim için uygun bir alternatif metin (alt attribute) ekleyin. Bu, ekran okuyucular için önemlidir, çünkü resimleri görüntüleyemezler.</p>
<pre><code class="language-html">&lt;img src=&quot;image.jpg&quot; alt=&quot;Güzel bir manzara resmi&quot;&gt;
</code></pre>
<h2>Formları Etiketlemek</h2>
<p>Form alanlarına <code>&lt;label&gt;</code> etiketleri ekleyerek kullanıcıların ne girmeleri gerektiğini anlamalarını sağlayın. Ayrıca, form alanlarını gruplamak için <code>&lt;fieldset&gt;</code> ve <code>&lt;legend&gt;</code> kullanabilirsiniz.</p>
<pre><code class="language-html">&lt;label for=&quot;username&quot;&gt;Kullanıcı Adı:&lt;/label&gt;
&lt;input type=&quot;text&quot; id=&quot;username&quot; name=&quot;username&quot;&gt;
</code></pre>
<p>Farklı bir yaklaşım olarak, <code>aria-labelledby</code> özelliğini de kullanabilirsiniz.</p>
<pre><code class="language-html">&lt;label id=&quot;username-label&quot;&gt;Kullanıcı Adı:&lt;/label&gt;
&lt;input type=&quot;text&quot; aria-labelledby=&quot;username-label&quot;&gt;
</code></pre>
<h2>Aria Etiketlerini Kullanmak</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA">ARIA (Accessible Rich Internet Applications)</a> etiketlerini kullanarak, ekran okuyucular için daha iyi erişilebilirlik sağlayın. Özellikle dinamik içerikler ve SPA'lar (Single Page Applications) için önemlidir.</p>
<pre><code class="language-html">&lt;div role=&quot;navigation&quot;&gt;
    &lt;!-- Navigasyon menüsü içeriği --&gt;
&lt;/div&gt;
</code></pre>
<p>Aria etiketlerinin nasil kullanildigini ogrenmek icin <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Guides">MDN</a> sayfasina bakabilirsiniz.</p>
<h2>sr-only Sınıfı</h2>
<p><code>sr-only</code> sınıfı, içeriği görüntülemeyen, ancak ekran okuyucular tarafından okunabilen içerikler için kullanılır. Bu genellikle görsel öğelerin yanında bulunan metinleri gizlemek için kullanılır, ancak ekran okuyucular tarafından hala erişilebilir olmasını sağlar.</p>
<pre><code class="language-css">.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
}
</code></pre>
<p>Bu CSS, içeriği ekranda görünmez hale getirir, ancak ekran okuyucular için hala erişilebilir kalır.</p>
<p>Örneğin, bir butonun yanında bulunan görsel ikonun alternatif metni için sr-only sınıfını kullanabilirsiniz:</p>
<pre><code class="language-html">&lt;button&gt;
    &lt;span class=&quot;sr-only&quot;&gt;Daha fazla bilgi&lt;/span&gt;
    &lt;i class=&quot;fas fa-info-circle&quot;&gt;&lt;/i&gt;
&lt;/button&gt;
</code></pre>
<p>Ya da, linkin açıklama metni için <strong>sr-only</strong> sınıfını kullanabilirsiniz:</p>
<pre><code class="language-html">&lt;a href=&quot;page.html&quot;&gt;
  &lt;span class=&quot;sr-only&quot;&gt;Daha fazla bilgi almak için&lt;/span&gt; tıklayın
&lt;/a&gt;
</code></pre>
<h2>Toparlamak Gerekirse</h2>
<blockquote>
<p><em><strong>Karanlığa bürüneceğine, sende bir ışık yak!</strong></em></p>
</blockquote>
<p>Ekran okuyucu kullanıcılarının web sitelerindeki deneyimlerini iyileştirmek, web geliştiricilerin önemli bir sorumluluğudur. Doğru HTML kullanımı, alternatif metinler, net ve açık metinler, bağlantı tanımlama, odak durumunu belirtme, ARIA etiketleri, klavye erişilebilirliği ve sürekli test ve geri bildirimler, bu hedefe ulaşmada temel adımlardır. Bu yönergeleri takip ederek, daha erişilebilir ve kullanıcı dostu web siteleri oluşturabiliriz.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[About me]]></title>
            <link>https://ademtonay.com/posts/about-me</link>
            <guid>https://ademtonay.com/posts/about-me</guid>
            <pubDate>Thu, 15 Feb 2024 18:31:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Hello everyone!</p>
<p>I'm Adem, a full-stack developer and architect. I've been working in software development for over 10 years. During this time, I've been involved in various projects and worked with many different technologies. Currently, I'm developing my own projects and creating content to assist other developers.</p>
<figure>
  <img src="https://ademtonay.com/images/about-me.jpg" alt="Me in Brussels, Belgium" />
  <figcaption>Brussels, Belgium</figcaption>
</figure>
<p>Learning new technologies and writing code is a passion of mine. Through this blog, I aim to share content on software development, technology, career, and personal development, helping people who are on the same journey as me.</p>
<figure>
  <img src="/images/about-me-2.jpg" alt="Me in Dinan, France" />
  <figcaption>Dinan, France</figcaption>
</figure>
<p>Apart from coding, I love traveling, cooking, listening to music, and reading books. Additionally, exploring new cultures and meeting new people is very valuable to me. If you share similar interests, please don't hesitate to get in touch with me.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Hakkımda]]></title>
            <link>https://ademtonay.com/posts/hakkimda</link>
            <guid>https://ademtonay.com/posts/hakkimda</guid>
            <pubDate>Thu, 15 Feb 2024 18:31:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p><em>Click <a href="/posts/about-me">here</a> to read this post in english</em></p>
</blockquote>
<p>Herkese merhaba!</p>
<p>Ben Adem full-stack developer ve architect' im. 10 yılı aşkın bir süredir yazılım geliştirme alanında çalışıyorum. Bu süre zarfında birçok farklı projede yer aldım ve birçok farklı teknoloji ile çalıştım. Şu anda ise kendi projelerimi geliştirmek ve diğer geliştiricilere yardımcı olacak içerikler üretiyorum.</p>
<figure>
  <img src="https://ademtonay.com/images/about-me.jpg" alt="Belçika Brüksel de ben" />
  <figcaption>Brüksel, Belçika</figcaption>
</figure>
<p>Yeni teknolojileri öğrenmek ve kod yazmak benim için bir tutku. Bu blogda, yazılım geliştirme, teknoloji, kariyer ve kişisel gelişim konularında içerikler paylaşıp benimle aynı yolda yürüyen insanlara yardımcı olmak istiyorum.</p>
<figure>
  <img src="/images/about-me-2.jpg" alt="Fransa Dinan da ben" />
  <figcaption>Dinan, Fransa</figcaption>
</figure>
<p>Kod yazmak dışında, seyahat etmeyi, yemek yapmayı, müzik dinlemeyi ve kitap okumayı çok severim. Ayrıca, yeni kültürler keşfetmek ve yeni insanlarla tanışmak benim için çok değerli.  Eğer benimle aynı ilgi alanlarına sahipseniz, lütfen benimle iletişime geçmekten çekinmeyin.</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
        <item>
            <title><![CDATA[Coming soon...]]></title>
            <link>https://ademtonay.com/posts/coming-soon</link>
            <guid>https://ademtonay.com/posts/coming-soon</guid>
            <pubDate>Thu, 15 Feb 2024 14:31:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Notes coming soon...</p>
]]></content:encoded>
            <author>ademtonay@gmail.com (Adem TONAY)</author>
        </item>
    </channel>
</rss>