404年战争:让旧东西再次发挥作用 (中文 (Chinese Simplified))

404年战争:让旧东西再次发挥作用

Sunday, 23 November 2025

//

11 minute read

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

我最近从2004-2009年进口了我的旧文章(https://www. mostlylulucid.net/blog/blog/crime/imported), 一切一切 外部链接指向十年前消失的网站 旧的URL计划 不再符合目前的结构 整个项目

问题分为三部分:

  1. 内部链接 - 在进口过程中用我的 归档导入工具。我的旧文章使用旧的 URL 计划互相参照,所以我重写了这些文章,作为移民的一部分。

  2. 外部链接(持续) 与外部资源的联系,这些外部资源已经消失、移动或变得完全不同。 链接到2006年的一些文件。 消失。 引用早已关闭网站的人的博客文章? 死亡。 这些需要时间处理 。

  3. 收到的请求 - 人们(和搜索引擎)仍然试图访问旧的 URL,例如 /archive/2006/05/15/123.aspx。语义搜索系统往往可以找出他们想要的是什么,即使没有精确的子弹匹配。

现在,我 能够 org 已烤烤了归档, org 查看了导入过程 。 但问题是: 链接会随着时间的流逝而断断 。 今天工作的站点下个月可能会消失 。 如果在运行时通过定期重新检查处理, 系统会自动抓取 。 未来 断裂,不只是进口时存在的断裂。

本条涵盖我的做法:

  • 流出链接: BrokenLinkArchiveMiddleware - 以档案.org快照取代死外部链接
  • 接入链接:404 处理语义搜索的404个处理器 - 即使从旧的 URL 方案中也能找到正确的内容
  • 学习系统:网站如何随着时间的推移从用户点击中变得更加聪明
  • A. 背景处理:检查链接而不封锁请求

旧内容问题

这是关于互联网的一点: 这不是永久的。 您在2006年链接到的极好的博客文章? 消失。 该文件站点? 重组了三次。 您自己的 URL 计划在您决定正式的打击大会之前就已经解决了? 丢脸了 。

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

幼稚的方法是手动修补链接。 但是当您有上百个有上千个链接的邮箱时, 还没有打开。 我们需要自动化 。

建筑结构概览

该系统有两个主要部分同时运作:

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

第1部分:处理流出链接

破断的连环工程中继器Name

中间软件拦截 HTML 反应, 做了三件事:

  1. 为背景检查提取所有链接
  2. 将已知断开的外部链接替换为归档.org版本
  3. 使用语义搜索以寻找内部断断断链接的替换符

我们想要找到档案. org的快照 文章写作时的前后图片来自2024年的一篇文章, 可能引用的内容完全不同。所以我们查查博客文章的出版日期,

以下是核心结构:

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);
        }
    }
}

提取链接

我们用生成的正gex 来拉出全部 href 属性。 [GeneratedRegex] 在.NET 的属性中, NET 给我们提供编译时间 regex 生成, 该生成速度更快, 且没有分配 :

[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();
}

现在你可以相当合理地使用 Html 灵活性纸包 https://github.com/AngleSharp/AngleSharp/ AngleSharp 或类似的 HTML (和其他) 解析器, 用于更复杂的场景,

用于背景检查的注册链接

我们不想在检查链接时阻断响应。 相反,我们取消了一个背景任务:

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);
    });
}

注注: IServiceScopeFactory - 我们需要一个新的范围,因为原始请求的范围服务将在我们完成背景任务之前处理完毕。

重要:这是 最终最终一致性 模型模型。 任何 首次发现链接, 它会排队等待验证 - 我们还不知道它是否坏了。 背景服务会检查它( HEAD 请求) , 如果它坏了, 将获取一个归档. org 替换 。 下一个 访问该页面的人会看到固定的链接。 对于一个有定期流量的博客来说,这通常意味着断开的链接会在第一次发现后数小时内被固定下来。 美丽的是,我们不必猜测哪些链接会被打破 — — 我们只是自动验证这些链接。

替换断断断的链接

一旦我们知道一个链接被打破, 并有一个归档. org 替换(从先前的背景调查中得出), 我们用一个有用的工具提示交换它们:

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);
    }
}

对于内部断裂的链接, 我们首先尝试语义搜索 :

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);

第2部分:背景链接检查

在请求中, 中间软件在请求期间不检查链接 - 这将太慢 。 相反, 它的队列会通过数据库表格发现背景处理的链接, 并且 BrokenLinkCheckerBackgroundService 处理实际检查。

服务每小时运行,做两件事:

  1. 检查链接有效性 - 总部总部要求检查链接是否仍然有效
  2. Fetch 归档. org URLs - 对于断断的链接, 找到最接近历史的快照

检查员使用自定义用户代理, 识别自己, 并连接到网站:

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

这让站点管理员看到他们的服务器受到什么打击, 如果他们好奇的话,他们可以找我们。 我们也要求加速速度, 以避免敲敲敲任何人的服务器。

关键的是,链接是 定期重新检查。 上周使用的链接今天可能已绝迹。 服务会接收过去24小时内没有被检查过的链接, 并再次验证这些链接。 但是, 一旦我们找到一个归档. org 替换断层链接, 我们就不会重新检查原始链接 - 它已经死了, 我们有一个工作上的替换 。

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

档案馆.org CDX API

org 提供了一个 CDX (Capture Invice) API, 用于让我们查询快照。 智能位元按日期过滤 :

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}";
}

这意味着如果我在2008年写了一篇文章, 连结到某些资源, 我会得到2008年左右的档案. org快照, 而不是一个可能完全不同的现代版本。

情况追踪链接

我们追踪与适当实体的联系:

[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
}

定期重新检查

获取未来断裂点的关键是重新检查。 最近尚未验证的链接服务查询 :

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);
}

这意味着每个链接每天至少重新验证一次。 如果一个先前的工作链接开始返回404s, 我们将抓住它, 并开始寻找一个归档. org 替换 。 ConsecutiveFailures 字段让我们略加原谅 - 我们不会将链接标记为在一个瞬时错误后断开的链接 。

第3部分:处理收到的请求

现在换到硬币的另一面:人们(和搜索引擎)要求没有的 URL。这包括:

  • 旧的URL 办法 - 我的古老博客 使用路径像 /archive/2006/05/15/123.aspx。其中一些网站仍然有索引、书签或从其他网站链接。
  • Typos 调频站 - 某个胖胖的用户的 URL 或复制错误的。
  • 部分子弹 - 搜索引擎有时会把怪异的碎片指数化

语义搜索系统在此特别有用 。 即使没有直接的子弹匹配, 我们也可以从请求的 URL 中提取有意义的术语, 并找到符合语义的内容 。 该系统既了解子弹, 也嵌入了每个文章的数据, 这样它就可以找到“ 足够接近” 的匹配, 甚至对于完全不同的 URL 结构 。

为什么不是Midleware?

我的第一个本能是处理中间软件的改变方向 早期拦截请求 检查已知的改变方向 能够 工作,这就是它看起来的样子:

// 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);
    }
}

问题? 每项要求/blog/*。这是每个页面视图的数据库查询,即使对完全有效的 URL 来说也是如此。对于一个交通顺畅的博客来说,你毫无理由地在99%的时间里锁定数据库。

更好的办法:连接到404个处理器中。如果 ASP.NET 已经确定页面不存在, 时当时 我们检查重定向。在有效页面上不要浪费查询。

有趣的是:在ASP.NET MVC的早期,我们就是这样建立友好的 URL的。启动MVC的Scott Guthrie的平面演示使用了这个方法;连接到 IIS 404 操作系统,读读了 URL 并服务了正确的内容。这也是我在WebForms 3 年中建立的一个系统中所使用的方法...以及我是如何在 ASP.NET 团队中找到一个PM的工作!

学习系统

当有人点击404时,我们使用模糊的字符串匹配和(可选的)语义搜索向他们显示建议。如果他们点击一个建议,我们就记录下来。在足够自信地点击足够多的点击后,我们就开始自动调整方向。

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个处理器

所有改变逻辑的逻辑 生命在 ErrorControllerASP. NET UseStatusCodePagesWithReExecute 当发生404时, 中件重新通过错误处理器执行请求 :

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");
        }
    }
}

缩略 TryAutoRedirectAsync 方法既处理学习的重定向,又处理高度自信的首次匹配:

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
}

相似位置分布

建议服务使用Levenshtein距离(编辑距离)加上一些超自然学:

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);
}

从用户行为中学习

当用户点击一个建议时, 我们记录并更新信任分数:

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);
}

信心评分计算既考虑点击,也考虑印象:

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;
    }
}

双双双双自动中转

我们有两个层次的自动调整方向:

  1. 首次高自信心(302):如果相似得分为 0.85, 且第二最佳得分差距很大, 我们立即重新定位 302 。 这有明显的打字错误 。

  2. 重置(301):在以 70 + 表示信任的 5+ 点击后,我们使用永久的 301 调整方向。这是“人类已经谈到” 的案例。

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
}

团结在一起

当Midleware 发出感知(和当它不发出感知)

用于 退出链接中间软件是正确的选择。 BrokenLinkArchiveMiddleware 需要来拦截 HTML 响应 之后 已经写好了,但是 之前 没有别的地方可以这样做了 我们需要修改反应流 中间软件正是为此设计的

用于 输入的重定向,中间软件是错误的选择。 /blog/* 请求仅检查这个 URL 是否可能需要重定向 。 404 处理器的方法只有在我们已经确定页面不存在时才运行这个逻辑, 效率要高得多 。

// 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)

水流看起来是这样的:

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

结论 结论 结论 结论 结论

这个系统已经运行了一段时间,它相当满意地看着它学习。数据库逐渐填满了重新定位的地图,断断的链接得到了归档.org的替换,用户也很少看到一个裸体的404人了。

关键原则:

  • 不要阻止请求 - 昂贵业务使用背景处理
  • 向用户学习 - 他们的点击是有价值的信号
  • 与自动中转程序保持保守 - 只有当你有信心时才会改变方向
  • 时间意识存档 - 查找内容撰写时的档案.org快照
  • 优减降解 - 如果服务不可用,请保持正常状态

它不是完美的 - 有些链接真的永远消失了, 语义搜索并不总是能找到正确的替代。 但是这比留下数千条断断的链接更好。

即将到来:语义搜索深海底

本周(w/c 2025年11月24日)我将出版我的语义搜索系列(w/c 2025年11月24日),该系列将更详细地介绍基于矢量的搜索如何在引擎盖下运作。该系列将涵盖嵌入的生成、 Qdrant 矢量存储以及它与这404个操作系统是如何连接的。如果您想知道如何 ISemanticSearchService.SearchAsync 实际上找到“相似”的内容, 那就是你想找的地方。

完整的源在存储库中, 如果您想为您自己的工程窃取其中的任何内容 。

Finding related posts...
logo

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