aboutsummaryrefslogtreecommitdiff
path: root/core/scoring_text.go
diff options
context:
space:
mode:
authorSam Scholten2026-08-30 12:50:24 +1000
committerSam Scholten2026-08-30 12:52:39 +1000
commit96ab2fcb2ff442698465389d75390afa91629165 (patch)
treeb36ef45dccaa791b0fce4dcad99d564635663c10 /core/scoring_text.go
downloadfluxrec-96ab2fcb2ff442698465389d75390afa91629165.tar.gz
fluxrec-96ab2fcb2ff442698465389d75390afa91629165.zip
fluxrec: personal Miniflux article recommender
Diffstat (limited to 'core/scoring_text.go')
-rw-r--r--core/scoring_text.go43
1 files changed, 43 insertions, 0 deletions
diff --git a/core/scoring_text.go b/core/scoring_text.go
new file mode 100644
index 0000000..84cd758
--- /dev/null
+++ b/core/scoring_text.go
@@ -0,0 +1,43 @@
+// Scoring-text construction: the document fed to the vectorizer.
+//
+// fluxrec is title-only by design (no body/excerpt ever enters the scoring
+// text). On top of the raw title we inject one synthetic feed-identity token
+// `feed-<slug>` built from the entry's feed_title, so LR can learn a per-feed
+// prior inside the existing TF-IDF + LR model. MinDF shrinks low-data feeds
+// toward zero for free (a feed with <2 labeled entries never enters the
+// vocabulary), so no separate shrinkage-prior machinery.
+//
+// Hyphens, not underscores: the tokenizer keeps [a-zA-Z0-9-] and splits on
+// everything else, so `feed-marginal-revolution` survives as ONE token while
+// `feed_marginal_revolution` would fragment into three. Identical slug
+// construction at train (from labels.jsonl feed_title) and serve (from the
+// entry's feed.title) keeps the feature consistent across both sides.
+package core
+
+import (
+ "regexp"
+ "strings"
+)
+
+var slugSep = regexp.MustCompile(`[^a-z0-9]+`)
+
+// FeedSlug normalizes a feed title into a stable slug of lowercase
+// letters/digits joined by single hyphens, trimmed. Empty/whitespace input
+// returns "". Idempotent: slug(slug(x)) == slug(x).
+func FeedSlug(feedTitle string) string {
+ s := strings.ToLower(strings.TrimSpace(feedTitle))
+ s = slugSep.ReplaceAllString(s, "-")
+ return strings.Trim(s, "-")
+}
+
+// ScoringText builds the document fed to the vectorizer: the entry title plus
+// one synthetic token "feed-<slug>". If the feed title is empty (no slug), the
+// token is omitted and the title alone is returned. No body/excerpt is ever
+// added — title-only is the design.
+func ScoringText(title, feedTitle string) string {
+ slug := FeedSlug(feedTitle)
+ if slug == "" {
+ return title
+ }
+ return title + " feed-" + slug
+}