X-KGRank: When Small Language Models Lie (And How Knowledge Graphs Fix It)
~3 min readKnowledge Graphs for AIRecommendation SystemsModern recommender systems produce predictions that users cannot interrogate. Collaborative filtering captures behavioural signals but offers no reasoning. Large Language Models generate fluent explanations but hallucinate and are poorly grounded in user history.
X-KGRank (Explainable Knowledge Graph RAG) unifies structural collaborative filtering with LLM-based explanation. From the MovieLens-1M dataset, it constructs a heterogeneous knowledge graph of 9,762 nodes and 999,264 edges. It trains a LightGCN ranker with content-aware SBERT initialization and applies popularity selective routing that grounds long-tail items in knowledge-graph paths while serving popular items from pre-trained knowledge.
The result: NDCG@10 = 0.2956, +17.1% over popularity baseline, and a critical finding β smaller models match larger models on explanation quality but fabricate facts more often.
The Explainability Problem
Traditional recommender systems:
User β Collaborative Filtering β Recommendation
(matrix factorization, LightGCN, etc.)
Output: "You might like Movie X"
Explanation: None (or "because you liked Y")
LLM-based recommenders:
User β LLM β Recommendation + Explanation
Output: "You might like Movie X because..."
Explanation: "Based on your love of sci-fi and strong female leads..."
Problem: Hallucinated, not grounded in actual history
The gap: Collaborative filtering is accurate but unexplainable. LLMs are explainable but hallucinate.
X-KGRank bridges this gap by grounding explanations in knowledge graph structure.
X-KGRank Architecture
1. Heterogeneous Knowledge Graph Construction
# From MovieLens-1M (6,040 users, 3,704 items, 988,129 interactions)
class X_KGRankKG:
def __init__(self, ratings_data):
self.graph = Neo4jGraph()
# Create user nodes
for user_id in ratings_data.users:
self.graph.create_node('User', user_id, metadata={
'rating_count': len(ratings_data.get_user_ratings(user_id))
})
# Create item nodes
for item_id in ratings_data.items:
self.graph.create_node('Item', item_id, metadata={
'title': ratings_data.get_item_title(item_id),
'genres': ratings_data.get_item_genres(item_id)
})
# Create rating edges
for user_id, item_id, rating in ratings_data.interactions:
self.graph.create_edge('User', user_id, 'RATED', 'Item', item_id, {
'rating': rating,
'timestamp': ratings_data.get_timestamp(user_id, item_id)
})
# Create genre edges
for item_id, genres in ratings_data.item_genres.items():
for genre in genres:
self.graph.create_edge('Item', item_id, 'HAS_GENRE', 'Genre', genre)
# Create co-rating edges (popularity signal)
item_counts = ratings_data.get_item_popularity()
popularity_threshold = 100 # Items with >100 ratings are "popular"
for item_id, count in item_counts.items():
if count > popularity_threshold:
self.graph.create_edge('Item', item_id, 'CO_RATED', 'Item', item_id, {
'co_rating_count': count
})