The Complete Computer

Elfeed Adaptive Scoring

LifeTechEmacsArcology

Adaptive Scoring

the populace yearns for a news reader that gently learns its preferences and isn't a fucking weirdo about it. something that lets one step in to the stream of information, the really simply syndicated under-belly of the internet, and filter that stream to a trickle of the most important information.

recommendation systems are always an interesting thing, finding the personalized content that works best for each person was long the goal, but one thing I have noticed over the last five or so years is a shift toward cohort grouping, where you're probably being placed in a group or a number of groups of users that watch similar videos to what you watch and show each other in the cohort everything everyone else watches. So less personalized, and more of a wobbly-edged zeitgeist where everyone "discovers" the same content that their particular subculture recently discovered.

one could naively rub a large language model on this, asking the computer to rank content or preform vector search; perhaps this would even be good. but you can't act like that is affordable.

It was often said that the end-user's devices were too limited to run coherent powerful ranking algorithms locally, but here I present an alternative that works, Adaptive Scoring

It's simple enough:

  • you have a map of words -> score

  • you can up/down score content

  • when you do, it extracts large words from the title, modifies the score for each word by a velocity, then stores the rule

  • when you open a search in elfeed it will evaluate these rules to create a score for each entry, and then sort the entries by the score.

the elfeed adaptive scoring functions

Implementing this is straightforward with roam:elfeed-score :

emacs-lisp source: :tangle ~/org/cce/elfeed-adaptive-scoring.el
(defcustom elfeed-score-min-word-length 5 "Words shorter than this will not be scored.") (defcustom elfeed-score-adaptive-velocity-title 2 "Add or remove VALUE to each word's score to adapt the system.") (defcustom elfeed-score-adaptive-velocity-author 5 "Add or remove VALUE to each author's score to adapt the system.")

These are the entrypoint for the scoring mechanism:

emacs-lisp source: :tangle ~/org/cce/elfeed-adaptive-scoring.el
(defun elfeed-score-adaptive-up () (interactive) (elfeed-score-adaptive-title elfeed-show-entry elfeed-score-adaptive-velocity-title) (elfeed-score-adaptive-author elfeed-show-entry elfeed-score-adaptive-velocity-author) (elfeed-show-next)) (defun elfeed-score-adaptive-down () (interactive) (elfeed-score-adaptive-title elfeed-show-entry (* -1 elfeed-score-adaptive-velocity-title)) (elfeed-score-adaptive-author elfeed-show-entry (* -1 elfeed-score-adaptive-velocity-author)) (elfeed-show-next)) (evil-collection-define-key 'normal 'elfeed-show-mode-map (kbd "C-u") #'elfeed-score-adaptive-up (kbd "C-m") #'elfeed-score-adaptive-down) (elfeed-score-enable)

Those keybindings are probably going to infuriate someone but Personal Software Can Be Shitty .

I want to rewrite these, i hate them they were thrown together in ielm and scratch in a handful of hours. They do work though!

emacs-lisp source: :tangle ~/org/cce/elfeed-adaptive-scoring.el
(defun elfeed-score-adaptive-author (entry val) (let ((authors (seq-map (lambda (pl) (plist-get pl :name)) (or (plist-get (elfeed-entry-meta entry) :authors) (elfeed-feed-author (elfeed-entry-feed entry)) )))) (when (and authors (car authors)) (thread-last authors (seq-uniq) (seq-map (lambda (author) ;; find one or make a new rule... (or (seq-find (lambda (rule) (equal (elfeed-score-authors-rule-comment rule) (format "[ADAPTIVE:%s]" author))) elfeed-score-serde-authors-rules) (let ((rule (elfeed-score-authors-rule--create :text author :value 0 :type 'W :comment (format "[ADAPTIVE:%s]" author)))) (elfeed-score-serde-add-rule rule) rule)))) (seq-map (lambda (rule) (let ((score (elfeed-score-authors-rule-value rule))) (setf (elfeed-score-authors-rule-value rule) (+ val score)) rule))))))) (defun elfeed-score-adaptive-title (entry val) (thread-last entry (elfeed-entry-title) (s-split-words) (seq-filter (lambda (it) (< elfeed-score-min-word-length (length it)))) (seq-uniq) (seq-map (lambda (word) ;; find one or make a new rule... (or (seq-find (lambda (rule) (equal (elfeed-score-title-rule-comment rule) (format "[ADAPTIVE:%s]" word))) elfeed-score-serde-title-rules) (let ((rule (elfeed-score-title-rule--create :text word :value 0 :type 'W :comment (format "[ADAPTIVE:%s]" word)))) (elfeed-score-serde-add-rule rule) rule)))) (seq-map (lambda (rule) (let ((score (elfeed-score-title-rule-value rule))) (setf (elfeed-score-title-rule-value rule) (+ val score)) rule))))) (provide 'elfeed-adaptive-scoring)

it's crazy how easy it is to bodge features in to Emacs software, I wish any other system was this powerfully Malleable , especially something which runs more easily on my phone than in Termux/=nix-on-droid.nix= ...

NEXT push scores to ttrss in elfeed-sync plugin

these are definitely going to be nice to have on elfeed but these rules can go to tt-rss as filters, too, so that scoring is adaptive when on the go even if it will not adapt without changes to the android app...

the PHP plugin needs an API endpoint that clears all the adaptive filters, adds new ones based on the current elfeed scores... might be a pain in the ass with timeouts. maybe we can just search for updates instead of blowing the bad ones away

need to think about how to translate from the one data schema to the other; how to handle future updates, since it'll be expected to call often-ish

NEXT tt-rss plugin can add the up/down buttons in web view, too

WAITING android app would need an API and UI changes, I think...