django-neural-feed 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_neural_feed-1.0.0/LICENSE +21 -0
- django_neural_feed-1.0.0/PKG-INFO +323 -0
- django_neural_feed-1.0.0/README.md +284 -0
- django_neural_feed-1.0.0/pyproject.toml +60 -0
- django_neural_feed-1.0.0/setup.cfg +4 -0
- django_neural_feed-1.0.0/src/django_neural_feed/__init__.py +1 -0
- django_neural_feed-1.0.0/src/django_neural_feed/apps.py +27 -0
- django_neural_feed-1.0.0/src/django_neural_feed/conf.py +122 -0
- django_neural_feed-1.0.0/src/django_neural_feed/encoders.py +75 -0
- django_neural_feed-1.0.0/src/django_neural_feed/feeds.py +153 -0
- django_neural_feed-1.0.0/src/django_neural_feed/migrations/0001_initial.py +48 -0
- django_neural_feed-1.0.0/src/django_neural_feed/migrations/__init__.py +0 -0
- django_neural_feed-1.0.0/src/django_neural_feed/mixins.py +30 -0
- django_neural_feed-1.0.0/src/django_neural_feed/models.py +23 -0
- django_neural_feed-1.0.0/src/django_neural_feed/signals.py +253 -0
- django_neural_feed-1.0.0/src/django_neural_feed/tasks.py +99 -0
- django_neural_feed-1.0.0/src/django_neural_feed.egg-info/PKG-INFO +323 -0
- django_neural_feed-1.0.0/src/django_neural_feed.egg-info/SOURCES.txt +22 -0
- django_neural_feed-1.0.0/src/django_neural_feed.egg-info/dependency_links.txt +1 -0
- django_neural_feed-1.0.0/src/django_neural_feed.egg-info/requires.txt +17 -0
- django_neural_feed-1.0.0/src/django_neural_feed.egg-info/top_level.txt +1 -0
- django_neural_feed-1.0.0/tests/test_feeds_coverage.py +176 -0
- django_neural_feed-1.0.0/tests/test_neural_architecture.py +774 -0
- django_neural_feed-1.0.0/tests/test_signals_coverage.py +657 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dersty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-neural-feed
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Personalized content feeds using vector embeddings, pgvector, and hybrid database-level scoring.
|
|
5
|
+
Author: itsDersty
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Django :: 4.2
|
|
11
|
+
Classifier: Framework :: Django :: 5.0
|
|
12
|
+
Classifier: Framework :: Django :: 6.0
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: Django>=4.2
|
|
24
|
+
Requires-Dist: numpy>=2.0.0
|
|
25
|
+
Requires-Dist: pgvector>=0.4.0
|
|
26
|
+
Requires-Dist: sentence-transformers>=3.0.0
|
|
27
|
+
Provides-Extra: celery
|
|
28
|
+
Requires-Dist: celery>=5.3.0; extra == "celery"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: flake8>=7.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-django>=4.8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "dev"
|
|
37
|
+
Requires-Dist: celery>=5.3.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Django Neural Feed (DNF)
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://github.com/ItsDersty/django-neural-feed/actions/workflows/main.yml">
|
|
44
|
+
<img src="https://img.shields.io/github/actions/workflow/status/ItsDersty/django-neural-feed/main.yml?branch=feature/oop-architecture&style=flat-square&label=tests" alt="Build Status">
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
47
|
+
<img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square" alt="Coverage">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://pypi.org/project/django-neural-feed/">
|
|
50
|
+
<img src="https://img.shields.io/pypi/v/django-neural-feed?style=flat-square&color=blue" alt="PyPI Version">
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://github.com/ItsDersty/django-neural-feed/blob/main/LICENSE">
|
|
53
|
+
<img src="https://img.shields.io/github/license/ItsDersty/django-neural-feed?style=flat-square&color=green" alt="License">
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
56
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue?style=flat-square" alt="Python Version">
|
|
57
|
+
</a>
|
|
58
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
59
|
+
<img src="https://img.shields.io/badge/django-4.2%2B-darkgreen?style=flat-square" alt="Django Version">
|
|
60
|
+
</a>
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
## Overview
|
|
64
|
+
|
|
65
|
+
**Django Neural Feed (DNF)** is a production-ready Django application designed to build intelligent, personalized content feeds powered by semantic vector embeddings. It leverages PostgreSQL's `pgvector` extension to compute vector similarity at the database level, combined with customizable content freshness and popularity metrics—all evaluated in a single optimized SQL query.
|
|
66
|
+
|
|
67
|
+
With its object-oriented architecture, DNF decouples your configuration logic into dedicated Feed classes. It tracks user interactions non-intrusively via Django signals and supports flexible deployment execution blocks, easily falling back from Celery asynchronous queues to synchronous background threads if the broker is offline.
|
|
68
|
+
|
|
69
|
+
## Core Features
|
|
70
|
+
|
|
71
|
+
- **🧠 Object-Oriented Feed Configuration**: Define isolated, multi-tenant recommendation feeds by subclassing a unified `BaseNeuralFeed` class.
|
|
72
|
+
- **⚡ Bulletproof Asynchronous Pipeline**: Offload embedding generation and vector aggregation to Celery. Features an automated synchronous thread fallback system.
|
|
73
|
+
- **📊 Dedicated Multi-Feed User Profiles**: Stores vector profiles in an isolated `UserFeedProfile` model partitioned by `feed_id`, keeping your core Auth User table clean.
|
|
74
|
+
- **🎯 Hybrid Multi-Criteria Scoring**: Merges semantic similarity (pgvector cosine distance), content recency, and custom popularity expressions into a single database-level annotation.
|
|
75
|
+
- **🚀 Non-Invasive Integration**: Attach recommendation behavior to existing content models with minimal migrations, leaving your interaction tables (Likes/Dislikes) completely untouched.
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- **Python**: 3.10+
|
|
80
|
+
- **Django**: 4.2, 5.0, 6.0+
|
|
81
|
+
- **PostgreSQL**: 12+ (with `pgvector` extension installed)
|
|
82
|
+
- **NumPy**: 2.0.0+
|
|
83
|
+
- **pgvector**: 0.4.0+
|
|
84
|
+
- **SentenceTransformers**: 3.0.0+
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
### 1. Install the Package
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install django-neural-feed
|
|
92
|
+
```
|
|
93
|
+
### **2. Add to Django Settings**
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
INSTALLED_APPS = [
|
|
97
|
+
# ... other apps
|
|
98
|
+
'django_neural_feed',
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
### **3. Initialize PostgreSQL Extension**
|
|
102
|
+
|
|
103
|
+
Ensure pgvector is enabled in your database instance:
|
|
104
|
+
|
|
105
|
+
```sql
|
|
106
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## **Quick Start**
|
|
110
|
+
|
|
111
|
+
### **Step 1: Configure Your Content Model**
|
|
112
|
+
|
|
113
|
+
Inherit from NeuralRecommendMixin to inject a vector embedding column into your target content table.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from django.conf import settings
|
|
117
|
+
from django.db import models
|
|
118
|
+
from django_neural_feed.mixins import NeuralRecommendMixin
|
|
119
|
+
|
|
120
|
+
class Post(NeuralRecommendMixin, models.Model): # NOTE: NeuralRecommendMixin must be BEFORE models.Model!
|
|
121
|
+
title = models.CharField(max_length=255)
|
|
122
|
+
content = models.TextField()
|
|
123
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
124
|
+
likes = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="liked_posts")
|
|
125
|
+
|
|
126
|
+
def get_ready_text(self) -> str:
|
|
127
|
+
return f"{self.title} {self.content}"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Prepare and apply your migrations:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
python manage.py makemigrations
|
|
134
|
+
python manage.py migrate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### **Step 2: Define a Custom Feed Class**
|
|
138
|
+
|
|
139
|
+
Create a dedicated `feeds.py` configuration to encapsulate tracking thresholds, model fields, math scoring expressions, and hybrid weights.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from django.db.models import Count, F, FloatField, ExpressionWrapper, Value
|
|
143
|
+
from django.db.models.functions import Cast, Ln, Extract, Now
|
|
144
|
+
from django_neural_feed.feeds import BaseNeuralFeed
|
|
145
|
+
from your_app.models import Post
|
|
146
|
+
|
|
147
|
+
class PostFeed(BaseNeuralFeed):
|
|
148
|
+
# 1. Core Feed Identity
|
|
149
|
+
feed_id = "posts_main"
|
|
150
|
+
parent_feed = None # Optional: Reference to a parent feed class for inheritance hierarchy
|
|
151
|
+
|
|
152
|
+
# 2. Target Django Models Configuration
|
|
153
|
+
content_django_model = Post
|
|
154
|
+
interaction_django_model = Post.likes.through
|
|
155
|
+
|
|
156
|
+
# 3. Interaction Tracking Pipelines
|
|
157
|
+
mode = "m2m" # Use "m2m" for ManyToMany fields, or "model" for explicit through models
|
|
158
|
+
user_field_name = "user" # Field pointing to User model (not needed if mode is "m2m")
|
|
159
|
+
content_field_name = "post" # Field pointing to Content model (not needed if mode is "m2m")
|
|
160
|
+
|
|
161
|
+
# 4. Model & Pipeline Thresholds
|
|
162
|
+
embedding_model_name = "paraphrase-multilingual-MiniLM-L12-v2" # Overrides global setting
|
|
163
|
+
user_likes_limit = 20 # Max target sample size slice for vector profile aggregation
|
|
164
|
+
|
|
165
|
+
# 5. Hybrid Scoring Global Weights (Should ideally sum up to 1.0)
|
|
166
|
+
weight_similarity = 0.6
|
|
167
|
+
weight_freshness = 0.2
|
|
168
|
+
weight_popularity = 0.2
|
|
169
|
+
|
|
170
|
+
# 6. Popularity: Logarithmic scaling using natural logarithm to keep viral jumps balanced
|
|
171
|
+
# Ln(Value(1000.0)) scales the metric dynamically, hitting a 1.0 score modifier at 1000 likes.
|
|
172
|
+
popularity_expression = ExpressionWrapper(
|
|
173
|
+
Ln(Cast(Count("likes"), FloatField()) + Value(1.0)) / Ln(Value(1000.0)),
|
|
174
|
+
output_field=FloatField()
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# 7. Freshness: Time-decay function based on post age in hours
|
|
178
|
+
# Safely subtracts timestamps inside the database, converting the interval to hours.
|
|
179
|
+
freshness_expression = ExpressionWrapper(
|
|
180
|
+
Value(1.0) / (
|
|
181
|
+
Value(1.0) + (
|
|
182
|
+
Extract(Now() - F("created_at"), "epoch") / 3600.0
|
|
183
|
+
)
|
|
184
|
+
),
|
|
185
|
+
output_field=FloatField()
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### **Step 3: Register Feed in Settings**
|
|
190
|
+
|
|
191
|
+
Register the string path to your custom feed configuration within the `DJANGO_NEURAL_FEED["FEEDS"]` list inside your `settings.py`.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
DJANGO_NEURAL_FEED = {
|
|
195
|
+
"FEEDS": [
|
|
196
|
+
"your_app.feeds.PostFeed", # DNF hooks up all model and M2M signals automatically
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### **Step 4: Fetch Personalized Feed Results**
|
|
202
|
+
|
|
203
|
+
Use your feed's `.get_feed()` function to obtain optimized querysets sorted by hybrid weights.
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from your_app.feeds import PostFeed
|
|
207
|
+
from your_app.models import Post
|
|
208
|
+
|
|
209
|
+
def user_feed_view(request):
|
|
210
|
+
# Gather IDs of posts the user has already liked to exclude them from the feed
|
|
211
|
+
excluded_ids = Post.objects.filter(
|
|
212
|
+
likes=request.user
|
|
213
|
+
).values_list('id', flat=True)
|
|
214
|
+
|
|
215
|
+
# Generate personalized recommendations directly via your Feed class
|
|
216
|
+
feed_queryset = PostFeed.get_feed(
|
|
217
|
+
user=request.user,
|
|
218
|
+
queryset=Post.objects.all(),
|
|
219
|
+
excluded_ids=excluded_ids,
|
|
220
|
+
limit=20
|
|
221
|
+
)
|
|
222
|
+
return feed_queryset
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### **Configuration Reference**
|
|
226
|
+
|
|
227
|
+
You can pass default global limits and model engine backends via standard DJANGO_NEURAL_FEED dictionary keys in your settings.py:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
DJANGO_NEURAL_FEED = {
|
|
231
|
+
"MODEL_NAME": "paraphrase-multilingual-MiniLM-L12-v2",
|
|
232
|
+
"VECTOR_DIMENSION": 384,
|
|
233
|
+
"CELERY_ENABLED": True,
|
|
234
|
+
"WEIGHT_SIMILARITY": 0.6,
|
|
235
|
+
"WEIGHT_FRESHNESS": 0.2,
|
|
236
|
+
"WEIGHT_POPULARITY": 0.2,
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
| Global Config Key | Type | Default | Purpose |
|
|
241
|
+
| :---- | :---- | :---- | :---- |
|
|
242
|
+
| MODEL_NAME | str | paraphrase-multilingual-MiniLM-L12-v2 | Target HuggingFace SentenceTransformer engine. |
|
|
243
|
+
| VECTOR_DIMENSION | int | 384 | Embedding dense matrix array dimension sizes. |
|
|
244
|
+
| ENCODER_CLASS | str/type | django_neural_feed.encoders.DefaultVectorEncoder | Path to the vectorization engine class interface. |
|
|
245
|
+
| WEIGHT_SIMILARITY | float | 0.6 | Default proportional weight of cosine similarity scoring. |
|
|
246
|
+
| WEIGHT_FRESHNESS | float | 0.2 | Default proportional weight of item creation recency. |
|
|
247
|
+
| WEIGHT_POPULARITY | float | 0.2 | Default proportional weight of user interaction counts. |
|
|
248
|
+
| USER_LIKES_LIMIT | int | 20 | Max target sample size slice for vector aggregation. |
|
|
249
|
+
| CELERY_ENABLED | bool | False | Toggles routing tasks to background Celery workers. |
|
|
250
|
+
|
|
251
|
+
### **Advanced Settings Overriding**
|
|
252
|
+
|
|
253
|
+
Every specific attribute can be declared dynamically within your custom BaseNeuralFeed class implementation to build separate configurations for multiple models (e.g., separate metrics weights for ArticlesFeed vs VideoFeed).
|
|
254
|
+
|
|
255
|
+
| Feed Class Attribute | Type | Default Value / Fallback | Purpose |
|
|
256
|
+
| :---- | :---- | :---- | :---- |
|
|
257
|
+
| feed_id | str | "default_feed" | Unique identifier for partitioning user vector profiles. |
|
|
258
|
+
| mode | str | *Required* ("m2m" | "model") | Toggles the internal signal tracking pipeline architecture. |
|
|
259
|
+
| embedding_model_name | str | settings.MODEL_NAME | Overrides the text-embedding engine for this specific feed. |
|
|
260
|
+
| user_likes_limit | int | settings.USER_LIKES_LIMIT | Overrides the history interaction slice size for this feed. |
|
|
261
|
+
| weight_similarity | float | settings.WEIGHT_SIMILARITY | Fine-tunes semantic similarity importance for this feed. |
|
|
262
|
+
| weight_freshness | float | settings.WEIGHT_FRESHNESS | Fine-tunes time-decay metric importance for this feed. |
|
|
263
|
+
| weight_popularity | float | settings.WEIGHT_POPULARITY | Fine-tunes interaction count importance for this feed. |
|
|
264
|
+
| popularity_expression | Expression | Value(1.0) | Custom Django ORM expression for parsing popularity scoring. |
|
|
265
|
+
| freshness_expression | Expression | Value(1.0) | Custom Django ORM expression for parsing time-decay scoring. |
|
|
266
|
+
|
|
267
|
+
## **Architecture Mechanics**
|
|
268
|
+
|
|
269
|
+
1. **Content Structuring**: When an entity subclassing NeuralRecommendMixin fires a post_save execution block, DNF reads get_ready_text() to calculate a dense float vector.
|
|
270
|
+
2. **Preference Profiling**: On target connection updates, an isolated worker fetches the latest interaction history rows, calculates an averaged, L2-normalized mean representation vector, and updates UserFeedProfile.
|
|
271
|
+
3. **Query Engine Generation**: Invoking Feed.get_feed() applies pgvector operations combined with standard math normalization, avoiding redundant lookups.
|
|
272
|
+
|
|
273
|
+
## **Custom Vector Encoders (Advanced)**
|
|
274
|
+
|
|
275
|
+
By default, DNF uses local `sentence-transformers` via `DefaultVectorEncoder`. If you prefer to use external cloud APIs (like OpenAI, Cohere, or custom microservices) for generating embeddings, you can implement a custom encoder.
|
|
276
|
+
|
|
277
|
+
### 1. Subclass BaseVectorEncoder
|
|
278
|
+
|
|
279
|
+
Create a custom encoder class anywhere in your Django project and implement the text_to_vector method. You can optionally override average_vectors if you want to replace NumPy-based processing.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
import requests
|
|
283
|
+
from django_neural_feed.encoders import BaseVectorEncoder
|
|
284
|
+
|
|
285
|
+
class OpenAIAppEncoder(BaseVectorEncoder):
|
|
286
|
+
@classmethod
|
|
287
|
+
def text_to_vector(cls, text: str, model_name: str) -> list[float]:
|
|
288
|
+
"""Fetch embeddings via custom third-party cloud API endpoint."""
|
|
289
|
+
if not text.strip():
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
# Example API execution blueprint
|
|
293
|
+
response = requests.post(
|
|
294
|
+
"https://api.openai.com/v1/embeddings",
|
|
295
|
+
headers={"Authorization": "Bearer YOUR_API_KEY"},
|
|
296
|
+
json={"input": text, "model": model_name}
|
|
297
|
+
)
|
|
298
|
+
return response.json()["data"][0]["embedding"]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 2. Register Custom Encoder in Settings
|
|
302
|
+
|
|
303
|
+
Pass the absolute string path pointing to your class implementation inside the global dictionary configuration:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
DJANGO_NEURAL_FEED = {
|
|
307
|
+
"ENCODER_CLASS": "your_app.encoders.OpenAIAppEncoder",
|
|
308
|
+
"MODEL_NAME": "text-embedding-3-small", # Passed down as model_name argument
|
|
309
|
+
"VECTOR_DIMENSION": 1536, # Adjust to match your custom API provider
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## **Testing**
|
|
314
|
+
|
|
315
|
+
DNF maintains full code coverage execution metrics. Run the suite natively using:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
pytest --cov=src/django_neural_feed
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## **License**
|
|
322
|
+
|
|
323
|
+
Distributed under the terms of the MIT License.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Django Neural Feed (DNF)
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://github.com/ItsDersty/django-neural-feed/actions/workflows/main.yml">
|
|
5
|
+
<img src="https://img.shields.io/github/actions/workflow/status/ItsDersty/django-neural-feed/main.yml?branch=feature/oop-architecture&style=flat-square&label=tests" alt="Build Status">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
8
|
+
<img src="https://img.shields.io/badge/coverage-100%25-brightgreen?style=flat-square" alt="Coverage">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://pypi.org/project/django-neural-feed/">
|
|
11
|
+
<img src="https://img.shields.io/pypi/v/django-neural-feed?style=flat-square&color=blue" alt="PyPI Version">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://github.com/ItsDersty/django-neural-feed/blob/main/LICENSE">
|
|
14
|
+
<img src="https://img.shields.io/github/license/ItsDersty/django-neural-feed?style=flat-square&color=green" alt="License">
|
|
15
|
+
</a>
|
|
16
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
17
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue?style=flat-square" alt="Python Version">
|
|
18
|
+
</a>
|
|
19
|
+
<a href="https://github.com/ItsDersty/django-neural-feed">
|
|
20
|
+
<img src="https://img.shields.io/badge/django-4.2%2B-darkgreen?style=flat-square" alt="Django Version">
|
|
21
|
+
</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
**Django Neural Feed (DNF)** is a production-ready Django application designed to build intelligent, personalized content feeds powered by semantic vector embeddings. It leverages PostgreSQL's `pgvector` extension to compute vector similarity at the database level, combined with customizable content freshness and popularity metrics—all evaluated in a single optimized SQL query.
|
|
27
|
+
|
|
28
|
+
With its object-oriented architecture, DNF decouples your configuration logic into dedicated Feed classes. It tracks user interactions non-intrusively via Django signals and supports flexible deployment execution blocks, easily falling back from Celery asynchronous queues to synchronous background threads if the broker is offline.
|
|
29
|
+
|
|
30
|
+
## Core Features
|
|
31
|
+
|
|
32
|
+
- **🧠 Object-Oriented Feed Configuration**: Define isolated, multi-tenant recommendation feeds by subclassing a unified `BaseNeuralFeed` class.
|
|
33
|
+
- **⚡ Bulletproof Asynchronous Pipeline**: Offload embedding generation and vector aggregation to Celery. Features an automated synchronous thread fallback system.
|
|
34
|
+
- **📊 Dedicated Multi-Feed User Profiles**: Stores vector profiles in an isolated `UserFeedProfile` model partitioned by `feed_id`, keeping your core Auth User table clean.
|
|
35
|
+
- **🎯 Hybrid Multi-Criteria Scoring**: Merges semantic similarity (pgvector cosine distance), content recency, and custom popularity expressions into a single database-level annotation.
|
|
36
|
+
- **🚀 Non-Invasive Integration**: Attach recommendation behavior to existing content models with minimal migrations, leaving your interaction tables (Likes/Dislikes) completely untouched.
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- **Python**: 3.10+
|
|
41
|
+
- **Django**: 4.2, 5.0, 6.0+
|
|
42
|
+
- **PostgreSQL**: 12+ (with `pgvector` extension installed)
|
|
43
|
+
- **NumPy**: 2.0.0+
|
|
44
|
+
- **pgvector**: 0.4.0+
|
|
45
|
+
- **SentenceTransformers**: 3.0.0+
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### 1. Install the Package
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install django-neural-feed
|
|
53
|
+
```
|
|
54
|
+
### **2. Add to Django Settings**
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
INSTALLED_APPS = [
|
|
58
|
+
# ... other apps
|
|
59
|
+
'django_neural_feed',
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
### **3. Initialize PostgreSQL Extension**
|
|
63
|
+
|
|
64
|
+
Ensure pgvector is enabled in your database instance:
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
CREATE EXTENSION IF NOT EXISTS vector;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## **Quick Start**
|
|
71
|
+
|
|
72
|
+
### **Step 1: Configure Your Content Model**
|
|
73
|
+
|
|
74
|
+
Inherit from NeuralRecommendMixin to inject a vector embedding column into your target content table.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from django.conf import settings
|
|
78
|
+
from django.db import models
|
|
79
|
+
from django_neural_feed.mixins import NeuralRecommendMixin
|
|
80
|
+
|
|
81
|
+
class Post(NeuralRecommendMixin, models.Model): # NOTE: NeuralRecommendMixin must be BEFORE models.Model!
|
|
82
|
+
title = models.CharField(max_length=255)
|
|
83
|
+
content = models.TextField()
|
|
84
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
85
|
+
likes = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="liked_posts")
|
|
86
|
+
|
|
87
|
+
def get_ready_text(self) -> str:
|
|
88
|
+
return f"{self.title} {self.content}"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Prepare and apply your migrations:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python manage.py makemigrations
|
|
95
|
+
python manage.py migrate
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### **Step 2: Define a Custom Feed Class**
|
|
99
|
+
|
|
100
|
+
Create a dedicated `feeds.py` configuration to encapsulate tracking thresholds, model fields, math scoring expressions, and hybrid weights.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from django.db.models import Count, F, FloatField, ExpressionWrapper, Value
|
|
104
|
+
from django.db.models.functions import Cast, Ln, Extract, Now
|
|
105
|
+
from django_neural_feed.feeds import BaseNeuralFeed
|
|
106
|
+
from your_app.models import Post
|
|
107
|
+
|
|
108
|
+
class PostFeed(BaseNeuralFeed):
|
|
109
|
+
# 1. Core Feed Identity
|
|
110
|
+
feed_id = "posts_main"
|
|
111
|
+
parent_feed = None # Optional: Reference to a parent feed class for inheritance hierarchy
|
|
112
|
+
|
|
113
|
+
# 2. Target Django Models Configuration
|
|
114
|
+
content_django_model = Post
|
|
115
|
+
interaction_django_model = Post.likes.through
|
|
116
|
+
|
|
117
|
+
# 3. Interaction Tracking Pipelines
|
|
118
|
+
mode = "m2m" # Use "m2m" for ManyToMany fields, or "model" for explicit through models
|
|
119
|
+
user_field_name = "user" # Field pointing to User model (not needed if mode is "m2m")
|
|
120
|
+
content_field_name = "post" # Field pointing to Content model (not needed if mode is "m2m")
|
|
121
|
+
|
|
122
|
+
# 4. Model & Pipeline Thresholds
|
|
123
|
+
embedding_model_name = "paraphrase-multilingual-MiniLM-L12-v2" # Overrides global setting
|
|
124
|
+
user_likes_limit = 20 # Max target sample size slice for vector profile aggregation
|
|
125
|
+
|
|
126
|
+
# 5. Hybrid Scoring Global Weights (Should ideally sum up to 1.0)
|
|
127
|
+
weight_similarity = 0.6
|
|
128
|
+
weight_freshness = 0.2
|
|
129
|
+
weight_popularity = 0.2
|
|
130
|
+
|
|
131
|
+
# 6. Popularity: Logarithmic scaling using natural logarithm to keep viral jumps balanced
|
|
132
|
+
# Ln(Value(1000.0)) scales the metric dynamically, hitting a 1.0 score modifier at 1000 likes.
|
|
133
|
+
popularity_expression = ExpressionWrapper(
|
|
134
|
+
Ln(Cast(Count("likes"), FloatField()) + Value(1.0)) / Ln(Value(1000.0)),
|
|
135
|
+
output_field=FloatField()
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 7. Freshness: Time-decay function based on post age in hours
|
|
139
|
+
# Safely subtracts timestamps inside the database, converting the interval to hours.
|
|
140
|
+
freshness_expression = ExpressionWrapper(
|
|
141
|
+
Value(1.0) / (
|
|
142
|
+
Value(1.0) + (
|
|
143
|
+
Extract(Now() - F("created_at"), "epoch") / 3600.0
|
|
144
|
+
)
|
|
145
|
+
),
|
|
146
|
+
output_field=FloatField()
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### **Step 3: Register Feed in Settings**
|
|
151
|
+
|
|
152
|
+
Register the string path to your custom feed configuration within the `DJANGO_NEURAL_FEED["FEEDS"]` list inside your `settings.py`.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
DJANGO_NEURAL_FEED = {
|
|
156
|
+
"FEEDS": [
|
|
157
|
+
"your_app.feeds.PostFeed", # DNF hooks up all model and M2M signals automatically
|
|
158
|
+
],
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### **Step 4: Fetch Personalized Feed Results**
|
|
163
|
+
|
|
164
|
+
Use your feed's `.get_feed()` function to obtain optimized querysets sorted by hybrid weights.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from your_app.feeds import PostFeed
|
|
168
|
+
from your_app.models import Post
|
|
169
|
+
|
|
170
|
+
def user_feed_view(request):
|
|
171
|
+
# Gather IDs of posts the user has already liked to exclude them from the feed
|
|
172
|
+
excluded_ids = Post.objects.filter(
|
|
173
|
+
likes=request.user
|
|
174
|
+
).values_list('id', flat=True)
|
|
175
|
+
|
|
176
|
+
# Generate personalized recommendations directly via your Feed class
|
|
177
|
+
feed_queryset = PostFeed.get_feed(
|
|
178
|
+
user=request.user,
|
|
179
|
+
queryset=Post.objects.all(),
|
|
180
|
+
excluded_ids=excluded_ids,
|
|
181
|
+
limit=20
|
|
182
|
+
)
|
|
183
|
+
return feed_queryset
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### **Configuration Reference**
|
|
187
|
+
|
|
188
|
+
You can pass default global limits and model engine backends via standard DJANGO_NEURAL_FEED dictionary keys in your settings.py:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
DJANGO_NEURAL_FEED = {
|
|
192
|
+
"MODEL_NAME": "paraphrase-multilingual-MiniLM-L12-v2",
|
|
193
|
+
"VECTOR_DIMENSION": 384,
|
|
194
|
+
"CELERY_ENABLED": True,
|
|
195
|
+
"WEIGHT_SIMILARITY": 0.6,
|
|
196
|
+
"WEIGHT_FRESHNESS": 0.2,
|
|
197
|
+
"WEIGHT_POPULARITY": 0.2,
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
| Global Config Key | Type | Default | Purpose |
|
|
202
|
+
| :---- | :---- | :---- | :---- |
|
|
203
|
+
| MODEL_NAME | str | paraphrase-multilingual-MiniLM-L12-v2 | Target HuggingFace SentenceTransformer engine. |
|
|
204
|
+
| VECTOR_DIMENSION | int | 384 | Embedding dense matrix array dimension sizes. |
|
|
205
|
+
| ENCODER_CLASS | str/type | django_neural_feed.encoders.DefaultVectorEncoder | Path to the vectorization engine class interface. |
|
|
206
|
+
| WEIGHT_SIMILARITY | float | 0.6 | Default proportional weight of cosine similarity scoring. |
|
|
207
|
+
| WEIGHT_FRESHNESS | float | 0.2 | Default proportional weight of item creation recency. |
|
|
208
|
+
| WEIGHT_POPULARITY | float | 0.2 | Default proportional weight of user interaction counts. |
|
|
209
|
+
| USER_LIKES_LIMIT | int | 20 | Max target sample size slice for vector aggregation. |
|
|
210
|
+
| CELERY_ENABLED | bool | False | Toggles routing tasks to background Celery workers. |
|
|
211
|
+
|
|
212
|
+
### **Advanced Settings Overriding**
|
|
213
|
+
|
|
214
|
+
Every specific attribute can be declared dynamically within your custom BaseNeuralFeed class implementation to build separate configurations for multiple models (e.g., separate metrics weights for ArticlesFeed vs VideoFeed).
|
|
215
|
+
|
|
216
|
+
| Feed Class Attribute | Type | Default Value / Fallback | Purpose |
|
|
217
|
+
| :---- | :---- | :---- | :---- |
|
|
218
|
+
| feed_id | str | "default_feed" | Unique identifier for partitioning user vector profiles. |
|
|
219
|
+
| mode | str | *Required* ("m2m" | "model") | Toggles the internal signal tracking pipeline architecture. |
|
|
220
|
+
| embedding_model_name | str | settings.MODEL_NAME | Overrides the text-embedding engine for this specific feed. |
|
|
221
|
+
| user_likes_limit | int | settings.USER_LIKES_LIMIT | Overrides the history interaction slice size for this feed. |
|
|
222
|
+
| weight_similarity | float | settings.WEIGHT_SIMILARITY | Fine-tunes semantic similarity importance for this feed. |
|
|
223
|
+
| weight_freshness | float | settings.WEIGHT_FRESHNESS | Fine-tunes time-decay metric importance for this feed. |
|
|
224
|
+
| weight_popularity | float | settings.WEIGHT_POPULARITY | Fine-tunes interaction count importance for this feed. |
|
|
225
|
+
| popularity_expression | Expression | Value(1.0) | Custom Django ORM expression for parsing popularity scoring. |
|
|
226
|
+
| freshness_expression | Expression | Value(1.0) | Custom Django ORM expression for parsing time-decay scoring. |
|
|
227
|
+
|
|
228
|
+
## **Architecture Mechanics**
|
|
229
|
+
|
|
230
|
+
1. **Content Structuring**: When an entity subclassing NeuralRecommendMixin fires a post_save execution block, DNF reads get_ready_text() to calculate a dense float vector.
|
|
231
|
+
2. **Preference Profiling**: On target connection updates, an isolated worker fetches the latest interaction history rows, calculates an averaged, L2-normalized mean representation vector, and updates UserFeedProfile.
|
|
232
|
+
3. **Query Engine Generation**: Invoking Feed.get_feed() applies pgvector operations combined with standard math normalization, avoiding redundant lookups.
|
|
233
|
+
|
|
234
|
+
## **Custom Vector Encoders (Advanced)**
|
|
235
|
+
|
|
236
|
+
By default, DNF uses local `sentence-transformers` via `DefaultVectorEncoder`. If you prefer to use external cloud APIs (like OpenAI, Cohere, or custom microservices) for generating embeddings, you can implement a custom encoder.
|
|
237
|
+
|
|
238
|
+
### 1. Subclass BaseVectorEncoder
|
|
239
|
+
|
|
240
|
+
Create a custom encoder class anywhere in your Django project and implement the text_to_vector method. You can optionally override average_vectors if you want to replace NumPy-based processing.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import requests
|
|
244
|
+
from django_neural_feed.encoders import BaseVectorEncoder
|
|
245
|
+
|
|
246
|
+
class OpenAIAppEncoder(BaseVectorEncoder):
|
|
247
|
+
@classmethod
|
|
248
|
+
def text_to_vector(cls, text: str, model_name: str) -> list[float]:
|
|
249
|
+
"""Fetch embeddings via custom third-party cloud API endpoint."""
|
|
250
|
+
if not text.strip():
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
# Example API execution blueprint
|
|
254
|
+
response = requests.post(
|
|
255
|
+
"https://api.openai.com/v1/embeddings",
|
|
256
|
+
headers={"Authorization": "Bearer YOUR_API_KEY"},
|
|
257
|
+
json={"input": text, "model": model_name}
|
|
258
|
+
)
|
|
259
|
+
return response.json()["data"][0]["embedding"]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 2. Register Custom Encoder in Settings
|
|
263
|
+
|
|
264
|
+
Pass the absolute string path pointing to your class implementation inside the global dictionary configuration:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
DJANGO_NEURAL_FEED = {
|
|
268
|
+
"ENCODER_CLASS": "your_app.encoders.OpenAIAppEncoder",
|
|
269
|
+
"MODEL_NAME": "text-embedding-3-small", # Passed down as model_name argument
|
|
270
|
+
"VECTOR_DIMENSION": 1536, # Adjust to match your custom API provider
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## **Testing**
|
|
275
|
+
|
|
276
|
+
DNF maintains full code coverage execution metrics. Run the suite natively using:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
pytest --cov=src/django_neural_feed
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## **License**
|
|
283
|
+
|
|
284
|
+
Distributed under the terms of the MIT License.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-neural-feed"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "itsDersty" }
|
|
10
|
+
]
|
|
11
|
+
description = "Personalized content feeds using vector embeddings, pgvector, and hybrid database-level scoring."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Web Environment",
|
|
18
|
+
"Framework :: Django",
|
|
19
|
+
"Framework :: Django :: 4.2",
|
|
20
|
+
"Framework :: Django :: 5.0",
|
|
21
|
+
"Framework :: Django :: 6.0",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = [
|
|
32
|
+
"Django>=4.2",
|
|
33
|
+
"numpy>=2.0.0",
|
|
34
|
+
"pgvector>=0.4.0",
|
|
35
|
+
"sentence-transformers>=3.0.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
celery = [
|
|
40
|
+
"celery>=5.3.0",
|
|
41
|
+
]
|
|
42
|
+
dev = [
|
|
43
|
+
"black>=24.0.0",
|
|
44
|
+
"flake8>=7.0.0",
|
|
45
|
+
"pytest>=8.0.0",
|
|
46
|
+
"pytest-django>=4.8.0",
|
|
47
|
+
"pytest-mock>=3.14.0",
|
|
48
|
+
"pytest-cov>=5.0.0",
|
|
49
|
+
"psycopg2-binary>=2.9.0",
|
|
50
|
+
"celery>=5.3.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
include = ["django_neural_feed*"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.run]
|
|
58
|
+
omit = [
|
|
59
|
+
"*/migrations/*",
|
|
60
|
+
]
|