Sota 404: Vanhan tavaran tekeminen taas toimivaksi (Suomi (Finnish))

Sota 404: Vanhan tavaran tekeminen taas toimivaksi

Sunday, 23 November 2025

//

16 minute read

Johdanto

Kun olet bloggannut vuodesta 2004 lähtien (kyllä, oikeasti), sinulla on paljon digitaalista detriittiä. Tuotin hiljattain vanhoja viestejäni vuosilta 2004–2009 ( https://www.mostlylucid.net/blog/category/Imported) ja huomasin, että noin kaikki Ulkoiset linkit viittaavat sivustoihin, jotka katosivat vuosikymmen sitten, vanhoihin URL-järjestelmiin, jotka eivät enää vastaa nykyistä rakennetta, koko joukkoa.

Ongelma jakautuu kolmeen osaan:

  1. Sisäiset linkit - Kiinteä itse tuontiprosessin aikana käyttäen ArkistoOrg-tuojatyökaluVanhat viestini viittasivat toisiinsa vanhan URL-järjestelmän avulla, joten kirjoitin ne uudelleen osana muuttoa.

  2. Ulkoiset linkit (ulkoinen) - Tämä on iso juttu. Linkkejä ulkoisiin resursseihin, jotka ovat sittemmin hävinneet, muuttaneet tai muuttuneet täysin eri sivustoiksi. Linkki dokumentteihin vuodelta 2006? Poissa. Viittaus blogikirjoitukseen henkilöltä, joka on kauan sitten poistanut sivunsa? Kuolleena. Nämä tarvitsevat ajallista käsittelyä.

  3. Saapuvat pyynnöt - Ihmiset (ja hakukoneet) yrittävät yhä käyttää vanhoja URL-osoitteita, kuten /archive/2006/05/15/123.aspxSemanttinen etsintäjärjestelmä voi usein selvittää, mitä he etsivät, jopa ilman tarkkaa osumaa.

Nyt minä voisi ovat leiponeet archive.org-katselun tuontiprosessiin. Mutta asia on näin: linkit katkeavat ajan myötä. Tänään toimiva sivusto voi olla poissa ensi kuussa. Käsittelemällä tätä käynnissä määräaikaisella uudelleentarkastuksella järjestelmä saa automaattisesti kiinni tulevaisuus repeämiä, ei vain niitä, jotka olivat olemassa tuontihetkellä.

Tässä artikkelissa käsitellään lähestymistapaani:

  • Lähtevät linkit: BrokenLinkArchiveMiddleware - korvaa kuolleet ulkoiset linkit archive.org-kuvakuvilla
  • Saapuvat linkit: Semanttisella haulla varustettu 404-käsittelijä löytää oikean sisällön jopa vanhoista URL-järjestelmistä
  • Oppimisjärjestelmä: Miten sivusto tulee ajan myötä älykkäämmäksi käyttäjän klikkauksilla
  • Taustan käsittely: Linkkien tarkastaminen ilman pyyntöjen estämistä

Vanhan sisällön ongelma

Internet ei ole pysyvä. Se erinomainen blogikirjoitus, johon linkitit vuonna 2006? Poissa. Tuo dokumenttisivusto? Uudelleenrakennettu kolme kertaa. Oma URL-ohjelma ennen kuin päätit kunnon rökitystilaisuudesta? Noloa.

graph TD
    A[User requests old post] --> B{Link valid?}
    B -->|Yes| C[Happy user]
    B -->|No| D[404 Error]
    D --> E[Frustrated user]
    E --> F[User leaves]

    style D stroke:#ef4444,stroke-width:3px
    style F stroke:#ef4444,stroke-width:3px
    style C stroke:#10b981,stroke-width:3px

Naiivi lähestymistapa on korjata linkit manuaalisesti, mutta kun on satoja tuhansia linkkejä, se ei ole päällä. Tarvitsemme automaatiota.

Arkkitehtuurin yleiskatsaus

Järjestelmässä on kaksi pääkomponenttia, jotka toimivat rinnakkain:

flowchart TB
    subgraph Incoming["Incoming Requests"]
        A[User Request] --> B{Page exists?}
        B -->|Yes| C[Render Page]
        B -->|No| D[404 Handler]
        D --> E{Learned redirect?}
        E -->|Yes| F[301 Permanent Redirect]
        E -->|No| G{High-confidence match?}
        G -->|Yes| H[302 Temporary Redirect]
        G -->|No| I[Show suggestions]
        I --> J[User clicks suggestion]
        J --> K[Learn redirect]
    end

    subgraph Outgoing["Outgoing Links"]
        C --> L[BrokenLinkArchiveMiddleware]
        L --> M[Extract all links]
        M --> N[Register for checking]
        L --> O[Replace broken links]
        O --> P[Archive.org URLs]
        O --> Q[Semantic search results]
        O --> R[Remove dead links]
    end

    style F stroke:#10b981,stroke-width:3px
    style H stroke:#f59e0b,stroke-width:3px
    style P stroke:#3b82f6,stroke-width:3px
    style Q stroke:#8b5cf6,stroke-width:3px

Osa 1: Lähtevien linkkien käsittely

BrokenLinkArchiveMiddle -ohjelmisto

Tämä keskiohjelmisto sieppaa HTML-vastaukset ja tekee kolme asiaa:

  1. Poistaa kaikki linkit taustojen tarkistamista varten
  2. Korvaa tunnetut rikkinäiset ulkoiset linkit archive.org-versioilla
  3. Käyttää semanttista hakua löytääkseen korvauksia rikkoutuneille sisäisille linkeille

Avainymmärrys tässä on se, että haluamme löytää archive.org-kuvakuvia samoihin aikoihin postia kirjoitettiin. Kuva vuodelta 2024 vuodelta 2006 voi viitata täysin erilaiseen sisältöön. Joten etsimme blogikirjoituksen julkaisupäivän ja kysymme archive.org-sivustolta lähintä tilannekuvaa.

Tässä on ydinrakenne:

public partial class BrokenLinkArchiveMiddleware(
    RequestDelegate next,
    ILogger<BrokenLinkArchiveMiddleware> logger,
    IServiceScopeFactory serviceScopeFactory)
{
    public async Task InvokeAsync(
        HttpContext context,
        IBrokenLinkService? brokenLinkService,
        ISemanticSearchService? semanticSearchService)
    {
        // Only process HTML responses for blog pages
        if (!ShouldProcessRequest(context))
        {
            await next(context);
            return;
        }

        // Capture the response so we can modify it
        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;

        await next(context);

        // Process the HTML response
        if (IsSuccessfulHtmlResponse(context, responseBody))
        {
            var html = await ReadResponseAsync(responseBody);
            html = await ProcessLinksAsync(html, context, brokenLinkService, semanticSearchService);
            await WriteModifiedResponseAsync(originalBodyStream, html, context);
        }
        else
        {
            await CopyOriginalResponseAsync(responseBody, originalBodyStream);
        }
    }
}

Eritellään linkkejä

Käytämme luotua regexiä vetääksemme pois kaikki href Ominaisuudet. [GeneratedRegex] Attribuutti .NETissä antaa meille kooste-ajan regex-sukupolven, joka on sekä nopeampi että jakovapaa:

[GeneratedRegex(@"<a[^>]*\shref\s*=\s*[""']([^""']+)[""'][^>]*>",
    RegexOptions.IgnoreCase | RegexOptions.Compiled)]
private static partial Regex HrefRegex();

private List<string> ExtractAllLinks(string html, HttpRequest request)
{
    var links = new List<string>();
    var matches = HrefRegex().Matches(html);

    foreach (Match match in matches)
    {
        var href = match.Groups[1].Value;

        // Skip special links (anchors, mailto, etc.)
        if (SkipPatterns.Any(p => href.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
            continue;

        if (Uri.TryCreate(href, UriKind.Absolute, out var uri))
        {
            if (uri.Scheme == "http" || uri.Scheme == "https")
                links.Add(href);
        }
        else if (href.StartsWith("/"))
        {
            // Convert relative URLs to absolute for tracking
            var baseUri = new UriBuilder(request.Scheme, request.Host.Host,
                request.Host.Port ?? (request.Scheme == "https" ? 443 : 80));
            links.Add(new Uri(baseUri.Uri, href).ToString());
        }
    }

    return links.Distinct().ToList();
}

Nyt voit aivan kohtuullisesti käyttää HtmlAgilityPack / https://github.com/AngleSharp/AngleSharp tai vastaavat HTML (ja muut) parserit täällä monimutkaisempia skenaarioita varten, mutta se oli yliampuva tähän tarkoitukseen - meidän täytyy vain poimia hrefit nopeasti.

Linkkien rekisteröiminen taustatarkistusta varten

Emme halua estää vastausta tarkistamalla linkkejä, vaan laukaisemme taustatehtävän:

var allLinks = ExtractAllLinks(html, context.Request);
var sourcePageUrl = context.Request.Path.Value;

if (allLinks.Count > 0)
{
    // Fire and forget - don't block the response
    _ = Task.Run(async () =>
    {
        using var scope = serviceScopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider.GetRequiredService<IBrokenLinkService>();
        await scopedService.RegisterUrlsAsync(allLinks, sourcePageUrl);
    });
}

Huomaa, että IServiceScopeFactory - tarvitsemme uuden soveltamisalan, koska alkuperäisen pyynnön laaja-alaiset palvelut hävitetään ennen kuin taustatehtävämme valmistuu.

Tärkeää: Tämä on mahdollinen yhdenmukaisuus malli. Kun mitä tahansa Linkki löytyy ensin, se jonotetaan vahvistukseen - emme tiedä, onko se vielä rikki. Taustapalvelu tarkistaa sen (HEAD-pyyntö), ja jos se on rikki, se hakee archive.org-korvaajan. seuraava Sivun kävijä näkee kiinteän linkin. Säännöllisen liikenteen blogissa tämä tarkoittaa tyypillisesti sitä, että katkenneita linkkejä korjataan tuntien sisällä ensimmäisestä löydöstä. Kauneinta on, ettei tarvitse arvailla, mitkä linkit saattavat rikkoutua - me vain vahvistamme ne kaikki automaattisesti.

Rikkinäisten linkkien korvaaminen

Kun tiedämme, että linkki on rikki ja siinä on arkisto.org-korvaus (aiemmasta taustatarkistuksesta), vaihdamme sen hyödylliseen työkaluvihjeeseen:

foreach (var (originalUrl, archiveUrl) in archiveMappings)
{
    if (html.Contains(originalUrl))
    {
        var tooltipText = $"Original link ({originalUrl}) is dead - archive.org version used";
        var originalPattern = $"href=\"{originalUrl}\"";
        var archivePattern = $"href=\"{archiveUrl}\" class=\"tooltip tooltip-warning\" " +
                            $"data-tip=\"{tooltipText}\" data-original-url=\"{originalUrl}\"";

        html = html.Replace(originalPattern, archivePattern);
    }
}

Sisäisten rikkinäisten linkkien osalta kokeilemme ensin semanttista hakua:

if (isInternal && semanticSearchService != null)
{
    var replacement = await TryFindSemanticReplacementAsync(
        brokenUrl, semanticSearchService, request, cancellationToken);

    if (replacement != null)
    {
        html = ReplaceHref(html, brokenUrl, replacement);
        continue;
    }
}

// No replacement found - convert to plain text
html = RemoveHref(html, brokenUrl);

Osa 2: Taustalinkin tarkistus

Väliohjelmisto ei tarkista linkkejä pyynnön aikana - se olisi aivan liian hidasta. Sen sijaan se jonottaa löytämiään linkkejä taustakäsittelyyn tietokantataulukon kautta, ja BrokenLinkCheckerBackgroundService Hoitaa varsinaisen tarkastuksen.

Palvelu toimii tuntitolkulla ja tekee kaksi asiaa:

  1. Tarkistuslinkin voimassaolo - Pää haluaa tietää, ovatko yhteydet yhä elossa
  2. Hae arkisto.org URL-osoitteet - Rikkinäisille linkeille löytyy lähin historiallinen kuva

Olemme hyviä kansalaisia tässä asiassa - tarkistaja käyttää käyttäjä-agenttia, joka tunnistaa itsensä ja linkittää takaisin sivustolle:

request.Headers.UserAgent.ParseAdd(
    "Mozilla/5.0 (compatible; MostlylucidBot/1.0; +https://www.mostlylucid.net)");

Näin sivuston ylläpitäjät näkevät, mikä heidän palvelimeensa iskee, ja he voivat etsiä meidät, jos he ovat uteliaita. Me myös kaasutamme pyyntöjä (2 sekunnin välein) välttääksemme kenenkään palvelimen moukaroimisen.

Yhteyksillä on ratkaiseva merkitys Tarkistetaan säännöllisesti uudelleen. Linkki, joka toimi viime viikolla, saattaa olla tänään kuollut. Palvelu poimii linkit, joita ei ole tarkistettu viimeisen 24 tunnin aikana, ja tarkistaa ne uudelleen. Mutta kun olemme löytäneet arkisto.org-korvaajan rikkinäiselle linkille, emme tarkista alkuperäistä - se on jo kuollut ja meillä on toimiva korvaaja.

sequenceDiagram
    participant BG as Background Service
    participant DB as Database
    participant Web as External Sites
    participant Archive as Archive.org CDX API

    loop Every Hour
        BG->>DB: Get links not checked in 24h (batch of 20)
        loop For each link
            BG->>Web: HEAD request
            Web-->>BG: Status code
            BG->>DB: Update link status + LastCheckedAt
        end

        BG->>DB: Get broken links needing archive lookup
        loop For each broken link
            BG->>DB: Look up source post publish date
            BG->>Archive: CDX API query (filtered by date)
            Archive-->>BG: Closest snapshot
            BG->>DB: Store archive URL (permanent)
        end
    end

Arkisto.org CDX API

Archive.org tarjoaa CDX:n (Capture Index) API:n, jonka avulla voimme tiedustella tilannekuvia. Näppärä bitti suodattuu päivämäärään mennessä:

private async Task<string?> GetArchiveUrlAsync(
    string originalUrl,
    DateTime? beforeDate,
    CancellationToken cancellationToken)
{
    var queryParams = new List<string>
    {
        $"url={Uri.EscapeDataString(originalUrl)}",
        "output=json",
        "fl=timestamp,original,statuscode",
        "filter=statuscode:200",  // Only successful responses
        "limit=1"
    };

    // Find snapshot closest to the blog post's publish date
    if (beforeDate.HasValue)
    {
        queryParams.Add($"to={beforeDate.Value:yyyyMMdd}");
        queryParams.Add("sort=closest");
        queryParams.Add($"closest={beforeDate.Value:yyyyMMdd}");
    }

    var apiUrl = $"https://web.archive.org/cdx/search/cdx?{string.Join("&", queryParams)}";

    // ... fetch and parse response

    return $"https://web.archive.org/web/{timestamp}/{original}";
}

Tämä tarkoittaa, että jos kirjoitin vuonna 2008 viestin, joka yhdistää johonkin resurssiin, saan arkisto.org-kuvan noin vuodelta 2008, en modernia versiota, joka voisi olla täysin erilainen.

Linkin tilan seuranta

Seuraamme yhteyksiä oikeaan kokonaisuuteen:

[Table("broken_links", Schema = "mostlylucid")]
public class BrokenLinkEntity
{
    public int Id { get; set; }
    public string OriginalUrl { get; set; } = string.Empty;
    public string? ArchiveUrl { get; set; }
    public bool IsBroken { get; set; } = false;
    public int? LastStatusCode { get; set; }
    public DateTimeOffset? LastCheckedAt { get; set; }
    public int ConsecutiveFailures { get; set; } = 0;
    public string? SourcePageUrl { get; set; }  // For publish date lookup
}

Säännöllinen uudelleentarkastus

Avain tulevien katkosten kiinnisaamiseen on uudelleentarkistus. Viime aikoina vahvistamattomien linkkien palvelukyselyt:

public async Task<List<BrokenLinkEntity>> GetLinksToCheckAsync(int batchSize, CancellationToken cancellationToken)
{
    var cutoff = DateTimeOffset.UtcNow.AddHours(-24);

    return await _dbContext.BrokenLinks
        .Where(x => x.LastCheckedAt == null || x.LastCheckedAt < cutoff)
        .OrderBy(x => x.LastCheckedAt ?? DateTimeOffset.MinValue)  // Oldest first
        .Take(batchSize)
        .ToListAsync(cancellationToken);
}

Tämä tarkoittaa, että jokaisen linkin voimassaoloa jatketaan vähintään kerran päivässä. Jos aiemmin toimiva linkki alkaa palauttaa 404:ää, otamme sen kiinni ja alamme etsiä archive.org:n korvaajaa. ConsecutiveFailures Kenttä antaa meille hieman anteeksi - emme merkitse linkkiä katkenneeksi yhden ohimenevän virheen jälkeen.

Osa 3: Tulevien pyyntöjen käsittely

Nyt kolikon toiselle puolelle: ihmiset (ja hakukoneet) pyytävät URL-osoitteita, joita ei ole olemassa.

  • Vanhat URL- järjestelmät Vanha blogini käytti polkuja kuten /archive/2006/05/15/123.aspx. Osa näistä on edelleen indeksoitu, kirjanmerkitty tai linkitetty muista sivustoista.
  • Typos - Joku läski sormettaa URL-osoitteen tai kopioi sen väärin.
  • Osittaiset etanat - Etsintäkoneissa on joskus outoja sirpaleita.

Semanttinen hakujärjestelmä on tässä erityisen hyödyllinen. Ilman suoraa osumaakin voimme poimia merkityksellisiä termejä haetusta URL-osoitteesta ja löytää sisältöä, joka semanttisesti täsmää. Järjestelmä tuntee sekä luodin että on upottanut dataa jokaiseen viestiin, jotta se voi löytää "riittävän lähelle" vastaavuuksia jopa täysin eri URL-rakenteisiin.

Miksei Middlewarea?

Ensimmäinen vaistoni oli käsitellä uudelleenohjauksia väliohjelmistoissa - siepata pyynnöt aikaisin, tarkistaa tunnetut uudelleenohjaukset ja suunnata uudelleen ennen kuin reititys edes käynnistyy. voisi toimi, ja tältä se näyttäisi:

// DON'T DO THIS - runs on EVERY request
public class SlugRedirectMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context, ISlugSuggestionService? service)
    {
        if (context.Request.Path.StartsWithSegments("/blog"))
        {
            var targetSlug = await service.GetAutoRedirectSlugAsync(slug, language);
            if (!string.IsNullOrWhiteSpace(targetSlug))
            {
                context.Response.Redirect($"/blog/{targetSlug}", permanent: true);
                return;
            }
        }
        await next(context);
    }
}

Ongelmana on, että tämä jatkuu. joka ikinen pyyntö Kohtiin /blog/*. Tämä on tietokantakysely joka sivulla, jopa täysin kelvollisten URL-osoitteiden osalta. Blogissa, jossa on kunnollinen liikenne, haaroitat tietokantaa ilman hyvää syytä 99 prosenttia ajasta.

Paljon parempi lähestymistapa: kytke 404-käsittimeen. Jos ASP.NET on jo todennut, että sivua ei ole olemassa, sitten Tarkistamme uudelleenohjauksia. Ei tuhlattuja kyselyitä voimassa olevilla sivuilla.

Hauska fakta: ASP.NET MVC:n alkuaikoina teimme ystävällisiä URL-osoitteita. Scott Guthrien konedemo, joka aloitti MVC:n, käytti tätä lähestymistapaa, kytke IIS 404 -käsittelyjärjestelmään, lue URL ja tarjoile oikeaa sisältöä. Se oli myös se, mitä käytin WebForms-järjestelmään rakentamassani järjestelmässä 3 vuotta pervious ... ja miten sain ASP.NET-tiimin PM-keikan!

Oppimisjärjestelmä

Kun joku osuu 404:een, näytämme hänelle ehdotuksia sumealla merkkijonolla ja (valinnainen) semanttisella haulla. Jos he napsauttavat ehdotusta, tallennamme sen. Kun klikkauksia on tarpeeksi, aloitamme automaattisen uudelleenohjauksen.

stateDiagram-v2
    [*] --> RequestReceived
    RequestReceived --> RoutingCheck

    RoutingCheck --> PageFound: Exists
    RoutingCheck --> NotFound: 404

    PageFound --> [*]

    NotFound --> ErrorController
    ErrorController --> CheckLearnedRedirect
    CheckLearnedRedirect --> Redirect301: Has learned redirect
    CheckLearnedRedirect --> CheckHighConfidence: No learned redirect

    CheckHighConfidence --> Redirect302: Score >= 0.85 & gap >= 0.15
    CheckHighConfidence --> ShowSuggestions: Lower confidence

    ShowSuggestions --> UserClicks
    UserClicks --> RecordClick
    RecordClick --> UpdateWeight
    UpdateWeight --> CheckThreshold

    CheckThreshold --> EnableAutoRedirect: Weight >= 5 & confidence >= 70%
    CheckThreshold --> [*]: Below threshold

    EnableAutoRedirect --> [*]

404 Handler

Kaikki uudelleenohjauslogiikka elää ErrorController. ASP.NET's UseStatusCodePagesWithReExecute Middleware toteuttaa pyynnön uudelleen virhekäsittelijämme kautta, kun 404 tapahtuu:

public class ErrorController(
    BaseControllerService baseControllerService,
    ILogger<ErrorController> logger,
    ISlugSuggestionService? slugSuggestionService = null) : BaseController(baseControllerService, logger)
{
    [Route("/error/{statusCode}")]
    [HttpGet]
    public async Task<IActionResult> HandleError(int statusCode, CancellationToken cancellationToken = default)
    {
        var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        switch (statusCode)
        {
            case 404:
                // Check for auto-redirects before showing 404 page
                var autoRedirectResult = await TryAutoRedirectAsync(statusCodeReExecuteFeature, cancellationToken);
                if (autoRedirectResult != null)
                    return autoRedirectResult;

                var model = await CreateNotFoundModel(statusCodeReExecuteFeature, cancellationToken);
                return View("NotFound", model);

            case 500:
                return View("ServerError");

            default:
                return View("Error");
        }
    }
}

Erytropoietiini TryAutoRedirectAsync Metodi käsittelee sekä opittuja uudelleenohjauksia että korkeaa itseluottamusta ensikertalaisotteluissa:

private async Task<IActionResult?> TryAutoRedirectAsync(
    IStatusCodeReExecuteFeature? statusCodeReExecuteFeature,
    CancellationToken cancellationToken)
{
    if (slugSuggestionService == null || statusCodeReExecuteFeature == null)
        return null;

    var originalPath = statusCodeReExecuteFeature.OriginalPath ?? string.Empty;
    var (slug, language) = ExtractSlugAndLanguage(originalPath);

    // First: check for learned redirects (user previously clicked a suggestion)
    // These get 301 Permanent Redirect - confirmed patterns
    var learnedTargetSlug = await slugSuggestionService.GetAutoRedirectSlugAsync(
        slug, language, cancellationToken);

    if (!string.IsNullOrWhiteSpace(learnedTargetSlug))
    {
        var redirectUrl = BuildRedirectUrl(learnedTargetSlug, language);
        logger.LogInformation("Learned auto-redirect (301): {Original} -> {Target}", originalPath, redirectUrl);
        return RedirectPermanent(redirectUrl);
    }

    // Second: check for high-confidence first-time matches
    // These get 302 Temporary Redirect until confirmed by user clicks
    var firstTimeTargetSlug = await slugSuggestionService.GetFirstTimeAutoRedirectSlugAsync(
        slug, language, cancellationToken);

    if (!string.IsNullOrWhiteSpace(firstTimeTargetSlug))
    {
        var redirectUrl = BuildRedirectUrl(firstTimeTargetSlug, language);
        logger.LogInformation("First-time auto-redirect (302): {Original} -> {Target}", originalPath, redirectUrl);
        return Redirect(redirectUrl);
    }

    return null;  // No redirect - show suggestions
}

Samankaltaisuus Scoring

Ehdotuspalvelussa käytetään Levenshtein-etäisyyttä (edit-etäisyyttä) sekä heuristiikkaa:

private double CalculateSimilarity(string source, string target)
{
    if (string.Equals(source, target, StringComparison.OrdinalIgnoreCase))
        return 1.0;

    source = source.ToLowerInvariant();
    target = target.ToLowerInvariant();

    // Levenshtein distance converted to similarity (0-1)
    var distance = CalculateLevenshteinDistance(source, target);
    var maxLength = Math.Max(source.Length, target.Length);
    var levenshteinSimilarity = 1.0 - (double)distance / maxLength;

    // Bonus if one string contains the other
    var substringBonus = (source.Contains(target) || target.Contains(source)) ? 0.2 : 0.0;

    // Bonus for common prefix (catches typos at the end)
    var prefixLength = GetCommonPrefixLength(source, target);
    var prefixBonus = (double)prefixLength / Math.Min(source.Length, target.Length) * 0.1;

    return Math.Min(1.0, levenshteinSimilarity + substringBonus + prefixBonus);
}

Opettelua käyttäjän käytöksestä

Kun käyttäjä napsauttaa ehdotusta, tallennamme sen ja päivitämme luottamustuloksia:

public async Task RecordSuggestionClickAsync(
    string requestedSlug,
    string clickedSlug,
    string language,
    int suggestionPosition,
    double originalScore,
    CancellationToken cancellationToken = default)
{
    var redirect = await _context.SlugRedirects
        .FirstOrDefaultAsync(r =>
            r.FromSlug == normalizedRequestedSlug &&
            r.ToSlug == normalizedClickedSlug &&
            r.Language == language,
            cancellationToken);

    if (redirect == null)
    {
        redirect = new SlugRedirectEntity
        {
            FromSlug = normalizedRequestedSlug,
            ToSlug = normalizedClickedSlug,
            Language = language,
            Weight = 1
        };
        _context.SlugRedirects.Add(redirect);
    }
    else
    {
        redirect.Weight++;
        redirect.LastClickedAt = DateTimeOffset.UtcNow;
    }

    redirect.UpdateConfidenceScore();
    await _context.SaveChangesAsync(cancellationToken);
}

Luottamuslaskennassa otetaan huomioon sekä klikkaukset että vaikutelmat:

public void UpdateConfidenceScore()
{
    var total = Weight + ShownCount;
    ConfidenceScore = total > 0 ? (double)Weight / total : 0.0;

    // Enable auto-redirect after 5+ clicks with 70%+ confidence
    if (Weight >= AutoRedirectWeightThreshold &&
        ConfidenceScore >= AutoRedirectConfidenceThreshold)
    {
        AutoRedirect = true;
    }
}

Kaksikerroksinen automaattiohjaus

Meillä on kaksi tasoista automaattiohjausta:

  1. Ensikertalainen suuri itseluottamus (302): Jos samankaltaisuuspistemäärä on > = 0,85 ja toiseksi parhaassa ottelussa on merkittävä ero, suuntaamme heti 302:lla. Tämä on selvää kirjoitusvirhettä.

  2. **Opitut uudelleenohjaukset (301)**5+ klikkauksen jälkeen 70 prosentin itseluottamuksella käytämme pysyvää 301 suuntaa. Tämä on "ihmiset ovat puhuneet" -tapaus.

public async Task<string?> GetFirstTimeAutoRedirectSlugAsync(
    string requestedSlug,
    string language,
    CancellationToken cancellationToken = default)
{
    var suggestions = await GetSuggestionsWithScoreAsync(requestedSlug, language, 2, cancellationToken);

    if (suggestions.Count == 0) return null;

    var topMatch = suggestions[0];

    // Need high confidence
    if (topMatch.Score < 0.85) return null;

    // If there's only one suggestion with high score, redirect
    if (suggestions.Count == 1) return topMatch.Post.Slug;

    // Multiple suggestions - only redirect if there's a clear winner
    var scoreGap = topMatch.Score - suggestions[1].Score;
    if (scoreGap >= 0.15)
        return topMatch.Post.Slug;

    return null;  // Too close to call - show suggestions instead
}

Kaiken kokoaminen yhteen

Kun Middleware tekee järkeä (ja kun ei)

FINREP:n puolesta lähteviä linkkejä, Middleware on oikea valinta. BrokenLinkArchiveMiddleware täytyy siepata HTML-vastaus jälkeen Se on renderoitu, mutta ennen Se on lähetetty asiakkaalle. Tähän ei ole muuta järkevää paikkaa - meidän on muokattava vastausvirtaa, ja väliohjelmisto on suunniteltu juuri siihen.

FINREP:n puolesta Tulossa olevat uudelleenohjaukset, Middleware on väärä valinta. Käymme läpi tietokantakyselyitä jokaisesta /blog/* Pyydä vain tarkistamaan, tarvitseeko tämä URL mahdollisesti uudelleenohjausta. 404 käsittelijän lähestymistapa toimii näin vain, kun olemme jo todenneet, että sivua ei ole olemassa - paljon tehokkaammin.

// In Program.cs
app.UseStatusCodePagesWithReExecute("/error/{0}");  // Handles 404s through ErrorController
app.UseStaticFiles();
app.UseRouting();
// ... other middleware
app.UseBrokenLinkArchive();  // Process outgoing links in responses (ONLY for middleware)

Virta näyttää tältä:

flowchart LR
    subgraph Request["Request Processing"]
        direction TB
        A[Request] --> B[Static Files]
        B --> C[Routing]
        C --> D{Page exists?}
        D -->|Yes| E[MVC/Endpoints]
        D -->|No| F[ErrorController 404]
        F --> G{Auto-redirect?}
        G -->|Yes| H[Redirect]
        G -->|No| I[Show suggestions]
    end

    subgraph Response["Response Processing"]
        direction TB
        E --> J[BrokenLinkArchiveMiddleware]
        J --> K[Link Extraction]
        K --> L[Link Replacement]
        L --> M[Response]
    end

    subgraph Background["Background Processing"]
        direction TB
        N[BrokenLinkCheckerService] --> O[Check URLs]
        O --> P[Fetch Archive.org]
        P --> Q[Update Database]
    end

    K -.->|Register links| N

    style F stroke:#ef4444,stroke-width:3px
    style J stroke:#8b5cf6,stroke-width:3px
    style N stroke:#f59e0b,stroke-width:3px

Päätelmät

Tämä järjestelmä on toiminut jo jonkin aikaa, ja se on melko tyydyttävää katsoessaan sen oppivan. Tietokanta täyttyy vähitellen uudelleenohjauskartoituksista, rikkinäisistä linkeistä saa archive.org-korvauksia, ja käyttäjät näkevät enää harvoin alastonta 404:tä.

Tärkeimmät periaatteet:

  • Älä estä pyyntöjä - käytä taustakäsittelyä kalliisiin toimiin
  • Opettele käyttäjiltä - heidän klikkauksensa ovat arvokkaita signaaleja
  • Ole konservatiivinen automaattiohjauksen kanssa - Suuntaa vain, kun olet itsevarma
  • Aikatietoinen arkistointi - löydä arkisto.org-kuvakuvia sisällön kirjoittamishetkestä
  • Armollinen rappeutuminen - jos palveluita ei ole saatavilla, jatka normaalisti

Se ei ole täydellinen - osa lenkeistä on lopullisesti poissa, eikä semanttinen etsintä aina löydä oikeaa korvaajaa, mutta se on pirun parempi näky kuin tuhansien katkenneiden lenkkien jättäminen lojumaan.

Tulossa pian: Semanttinen etsintä syväsukellus

Tällä viikolla (w/c 24. marraskuuta 2025) julkaisen semanttisen hakusarjani, jossa kerrotaan paljon yksityiskohtaisemmin siitä, miten vektoripohjainen haku toimii konepellin alla. Sarja kattaa upotettavan sukupolven, Qdrantin vektorivaraston ja sen, miten se kaikki liittyy tähän 404-käsittelyjärjestelmään. ISemanticSearchService.SearchAsync Itse asiassa löytää "samanlaista" sisältöä, sieltä kannattaa etsiä.

Koko lähde on arkistossa, jos haluat nipistää siitä jotain omiin projekteihisi.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.