Tuning the search ranker

Search is one of the few things on this site that runs on every request. No DB, no Elastic — just a slice of `SearchDoc` and a scorer that walks it.

The current scoring weights

FieldWeight
title50
tags30
summary20
body10

Title hits dominate on purpose. If you search norg and a post has "Norg" in the title, that's almost certainly the result you want — outranking five other posts that mention the word in passing.

The actual scorer

func scoreDoc(doc content.SearchDoc, tokens []string) int {
    score := 0
    for _, token := range tokens {
        if strings.Contains(strings.ToLower(doc.Title), token) {
            score += 50
        }
        if strings.Contains(strings.ToLower(strings.Join(doc.Tags, " ")), token) {
            score += 30
        }
        if strings.Contains(strings.ToLower(doc.Summary), token) {
            score += 20
        }
        if strings.Contains(strings.ToLower(doc.Body), token) {
            score += 10
        }
    }
    return score
}

Where it falls apart

  • type filter prefix (`blog ...`, `projects ...`)
  • ParentSlug for subpost URLs in results
  • phrase matching ("project subposts" as one token, not two)
  • stemming so rebuild matches rebuilt
  • tag boost cap so a post tagged five times with the same word does not run away

Phrase matching is the next thing to land. Right now the indexer splits on whitespace and treats each token independently, so a search for `bento grid` ranks anything containing bento or grid even if they never appear together.

Lesson: simple scorers are easy to read and easy to be wrong about. Always sanity-check against real queries before trusting the numbers.