In part one of this blog series , I explored the motivation behind developing a personal recommendation system. The main goals are to learn how recommendation systems work and to build a tool that helps me find interesting blog posts and articles from feeds where only 1 in 20 posts might match my content interests.

If you are interested in the technical implementation, the complete codebase is available in this github repository .

Creating an Articles Dataset

Step 1: Initial List of Liked Articles

Daily browsing of RSS feeds involves scanning through many articles, where sometimes engaging titles lead me to read their opening paragraphs. Through my established content workflow , I save interesting items to Pocket using Inoreader’s built-in feature.

Initially, I planned to use a list of RSS items that I had clicked on. However, Inoreader doesn’t provide an API to access this reading history, which led me to explore other options.

My RSS-based Content Consumption Workflow

Looking further into my workflow, I found a better solution: my archived items in Pocket. While I regularly read through my Pocket items, I never delete them - just archive them. This gave me a valuable collection of reading history.

Using the Pocket API , I retrieved around 4,000 URLs, dating back to December 2022. Though some archived items might be less relevant now, the dataset reflects my reading interests well. Later, I could add features like upvoting and downvoting to refine the content selection, but that’s beyond the current scope.

Each item in the dataset includes this basic information:

{
  "pocket_item_id": 5598506,
  "given_url": "https://nat.org/",
  "resolved_url": "http://nat.org/",
  "title": "Nat Friedman",
  "time_added": 1736940058,
  "word_count": 451,
  "domain": "nat.org"
}

Step 2: Text Content of Articles

To build an effective recommendation model, we need the actual content of each URL, not just its metadata, but unfortunately Pocket doesn’t provide that. While I love writing scrapers, for this project I want to focus on developing the recommendation model itself.

The Jina Reader API offers a straightforward solution, converting webpage content into markdown format. Here’s an example of what we get (shortened version, full content available here ):

T U M I S S i R a ' o o t L r m m m l k e e e S d a G O W S C L W T : o o n t r n e t E i o e t u w h e n a O v r s h N r n i i w t t r e k t i a c n n h t o i e n t e C v g u e t e f i n d g : o e s p o d n g s F n s I [ 3 r h t t a i n M [ G C o 0 I i t e o b n t I t i a n 0 e t n r o e T w t l b d p t , u C r o H i r B e m s : t h n b ] u f e a l a : e a e e ( b o a y i n / n m r t c h ] r d e / t e l a t ( n i A v n r : o s u t h i n r e a e t i s p t a g e : t p t n e s t a . r e c : p t o e s e I / s h f r n v / : e o g e i 1 l e / o / u l 9 o n [ d r l 9 v . g H s , e 1 e w i e , , d i t r f d k h c o e V w t i u u r v A h h p b l e i e e . a [ l c d c n p o h R i o e l p i a m u a e i c . m s r s h t . a r P i m r g f a c y d / r p w o y c a F i m r h c e k i e t y i 2 ] m u n / 0 ( i a m X 1 h c l a i 8 t a n m t l " i t p s h [ a h s ] o a n r : ( m u ) o / h e t u / t o [ g s t t b c h c p o i o r s w o m 2 o : n g p 0 l / " r a 2 l / a n 1 p p p i r l h e i a i s z s e ] e t s ( . i ] h o c ( t r l h t g i t p / s t s ) t p : . s / o : r e g / n / w . ) w w w i . k a i m p a e z d o i n a . . c o o r m g / / R w i i c k h i a / r X d a - m P a - r F i e n y ) n m a n / e / B 0 0 0 A Q 4 7 U 8 / r e f = d p _ b y l i n e _ c o n t _ p o p _ b o o k _ 1 )

The converted content is excellent, but it has two main challenges:

  1. Markdown formatting adds unnecessary complexity for our use case. Plain text would work better.
  2. Articles vary greatly in length - from short paragraphs to long essays - which could make comparing them via semantic similarity difficult later.

Distribution of Documents Lengths (in Tokens)

Step 3: Summarizing the Markdown Content

To solve both issues at once, I used the gemini-2.0-flash model to create consistent-length summaries of each document. Here’s an example summary for https://nat.org/:

I F I ' u m n a d l a a s n m o e i n c n t h v a a e l l s l l t y e o , n r g , I e e b t n e h t l e r i e e e p v f r e f e i n w c e e i u e r h n , a t v a e m n a d a r k d r e e i t v g e h h l t y o , p p o e p t r e h r e w h s h a i o p s ' s , s a v b i e d e e u w n t i y n o , g n l t i i o t n e s a h s s a i p a n e c f e t l h a 1 e w 9 e 9 u d 1 n , i h v e c e u o r r n s i s e s i t d t i e o c r , i o n u a g r n d i l t i e k m m i p y n h g a t , s r i u a z e n e d h t o t h m e e e c t h i o n m w o p n l o . o r g t I y a , n w c e b e n e t i o n f t g o r a e M p c I p o T l g i n i e i n d z s i p k n i n g r o e w t d l h e e b d y g l e i R , m i i c e t h n s a a r b o d l f e F s o e u y t r n h m i k a s n n . o ' w s I l e a t d u h g t i e o n . b k i I o w n g e r m a s y p h h o v i u i e l e s d w , a f n o t d c h u e t s h c e o u n n l t w r u e a r n i a t s l i o n a n g v e t t r o h s e i s o t c n a e r i t t l o i t n m w g i o , c r c n o o o m m t a p n a t a n h g i e e e m s f e l n a o t n o d r h , i s n e a d r n e v d r e s t a h p s a o t t C e E e n O n t t i o h a f u l s , G i i a a t s s H m u e b i m s p f o r c w o r e m u r c i 2 i n 0 a g 1 l 8 g b r t e e o c a a t 2 u 0 s i 2 e n 1 d . i i t v C i u f d r u u r e a e l l n s s t l p i y r s , o g e I r s e s l s e i s n v . t e i S a i p l n e e f C d o a r l i i s a f c o a h r l i n s e i o v a i i n a m g n p d o e r x a t c m a e n p d t t e , i d o i f n c a a a c l t i i l r n i e g t s a u t t l i i t m n s e g . t f I o a s a t t d h e v e r o c H l a e e t r a e c r u n f l i o a n r n g e s u a m m n a d l P l a f e p o r y r r c t i i e n a p g m r s o f j o f e c o c u r t s f a o a n n s d t w e i h r n a v t d e e s t c t r i i u s g l i a y o t n i m s n a g t a t n p e d l r a s m s . o t r i e c e c n h j e o m y i a c b a l l e s w i o n r k B , a y b e A l r i e e a v i f n o g o d t s h . a t m a n y t e c h c o m p a n i e s a r e s i g n i f i c a n t l y v e r s t a f f e d . F i n a l l y , u n d e r s t a n d i n g w h e r e y o u d e r i v e y o u r d o p a m i n e i s v i t a l f o r u n d e r s t a n d i n g y o u r b e h a v i o r , a n d I b e l i e v e w e ' r e a l l c a p a b l e o f f a r m o r e t h a n w e r e a l i z e , b o u n d o n l y b y t h e l a w s o f p h y s i c s a n d i n v i s i b l e o r t h o d o x y .

By having the model to wrap summaries in XML tags (<summary> ... </summary>), I achieved two things:

  • Clean extraction of just the summary text, in case LLM generates other texts before or after the summary (e.g. if it starts with Sure, I can help you with summarizing the content ...).
  • Easy identification of broken links and error pages. This helped remove 141 invalid entries, leaving me with 3,642 quality documents.

The summaries created a more balanced distribution of text lengths:

Distribution of Summary Lengths (in Tokens)

Next Steps

The final dataset now contains well-organized entries with clean metadata and text summaries:

{
  "pocket_item_id": 5598506,
  "given_url": "https://nat.org/",
  "resolved_url": "http://nat.org/",
  "title": "Nat Friedman",
  "time_added": 1736940058,
  "word_count": 451,
  "domain": "nat.org",
  "text": "Title: Nat Friedman\n\nURL Source: https://nat.org/\n\nMarkdown Content:\n\nI'm an investor, ...",
  "summary": "I'm an investor, entrepreneur, and developer who's been online since 1991, considering it my true hometown. I went to ..."
}

Next week, I’ll work on creating a user profile by concatenating these text summaries and metadata and converting them into vectors using Embedding models . I’m also researching how modern recommendation systems work with transformers and LLMs, which will help guide this project. After all, learning is the main goal here.

I welcome any thoughts or questions about this series - feel free to reach out!

Comment? Reply via Email, Bluesky or Twitter.