Skip to content

Commit 5c6556d

Browse files
author
ndu
committed
feat: add algolia index
1 parent d142959 commit 5c6556d

File tree

5 files changed

+181
-3
lines changed

5 files changed

+181
-3
lines changed

server/apps/research/apps.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
from django.apps import AppConfig
2-
2+
import algoliasearch_django as algoliasearch
3+
from algoliasearch_django.registration import RegistrationError
34

45
class ResearchConfig(AppConfig):
56
default_auto_field = 'django.db.models.BigAutoField'
67
name = 'apps.research'
8+
9+
def ready(self):
10+
from .models.article import Article
11+
from .models.author import Author
12+
from .models.category import Category
13+
from .models.algolia_index import ArticleIndex, AuthorIndex, CategoryIndex
14+
15+
try:
16+
algoliasearch.register(Article, ArticleIndex)
17+
algoliasearch.register(Author, AuthorIndex)
18+
algoliasearch.register(Category, CategoryIndex)
19+
except RegistrationError:
20+
pass
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from algoliasearch_django import AlgoliaIndex
2+
from algoliasearch_django.decorators import register
3+
from .article import Article
4+
from .author import Author
5+
from .category import Category
6+
from bs4 import BeautifulSoup
7+
8+
def truncate_text(text, max_chars=8000):
9+
"""Truncate text to stay within Algolia's size limits"""
10+
if not text:
11+
return ""
12+
if len(text) <= max_chars:
13+
return text
14+
return text[:max_chars] + "..."
15+
16+
@register(Article)
17+
class ArticleIndex(AlgoliaIndex):
18+
index_queryset = Article.objects.filter(status='ready')
19+
20+
fields = [
21+
'title',
22+
'slug',
23+
'status',
24+
'views',
25+
'is_sponsored',
26+
]
27+
28+
settings = {
29+
'searchableAttributes': [
30+
'title',
31+
'content_excerpt',
32+
'summary',
33+
],
34+
'attributesToSnippet': [
35+
'content_excerpt:50',
36+
'summary:30',
37+
],
38+
'snippetEllipsisText': '...',
39+
'attributesForFaceting': [
40+
'status',
41+
'is_sponsored',
42+
'categories',
43+
'authors',
44+
]
45+
}
46+
47+
def get_raw_record(self, instance):
48+
record = super().get_raw_record(instance)
49+
50+
# Convert UUID to string
51+
record['objectID'] = str(instance.id)
52+
53+
# Clean and truncate HTML content
54+
if instance.content:
55+
soup = BeautifulSoup(instance.content, 'html.parser')
56+
clean_content = soup.get_text(separator=' ', strip=True)
57+
record['content_excerpt'] = truncate_text(clean_content, 8000)
58+
59+
if instance.summary:
60+
record['summary'] = truncate_text(instance.summary, 1000)
61+
62+
# Add thumbnail URL
63+
if instance.thumb:
64+
record['thumb_url'] = instance.thumb.url
65+
66+
# Handle datetime fields
67+
if instance.scheduled_publish_time:
68+
record['scheduled_publish_time'] = instance.scheduled_publish_time.isoformat()
69+
70+
if instance.created_at:
71+
record['created_at'] = instance.created_at.isoformat()
72+
73+
if instance.updated_at:
74+
record['updated_at'] = instance.updated_at.isoformat()
75+
76+
# Handle relationships
77+
if instance.categories.exists():
78+
record['categories'] = [
79+
{
80+
'name': category.name,
81+
'slug': category.slug,
82+
'id': str(category.id)
83+
}
84+
for category in instance.categories.all()
85+
]
86+
87+
if instance.authors.exists():
88+
record['authors'] = [
89+
{
90+
'name': author.full_name or author.user.get_full_name(),
91+
'username': author.user.username,
92+
'id': str(author.id)
93+
}
94+
for author in instance.authors.all()
95+
]
96+
97+
return record
98+
99+
@register(Category)
100+
class CategoryIndex(AlgoliaIndex):
101+
fields = [
102+
'name',
103+
'slug',
104+
'is_primary',
105+
]
106+
107+
def get_raw_record(self, instance):
108+
record = super().get_raw_record(instance)
109+
record['objectID'] = str(instance.id)
110+
111+
if instance.created_at:
112+
record['created_at'] = instance.created_at.isoformat()
113+
114+
if instance.updated_at:
115+
record['updated_at'] = instance.updated_at.isoformat()
116+
117+
return record
118+
119+
settings = {
120+
'searchableAttributes': ['name'],
121+
'attributesForFaceting': ['is_primary']
122+
}
123+
124+
@register(Author)
125+
class AuthorIndex(AlgoliaIndex):
126+
fields = [
127+
'full_name',
128+
'bio',
129+
'twitter_username',
130+
]
131+
132+
def get_raw_record(self, instance):
133+
record = super().get_raw_record(instance)
134+
record['objectID'] = str(instance.id)
135+
136+
if instance.bio:
137+
record['bio'] = truncate_text(instance.bio, 1000)
138+
139+
if instance.created_at:
140+
record['created_at'] = instance.created_at.isoformat()
141+
142+
if instance.updated_at:
143+
record['updated_at'] = instance.updated_at.isoformat()
144+
145+
return record
146+
147+
settings = {
148+
'searchableAttributes': [
149+
'full_name',
150+
'bio'
151+
]
152+
}

server/core/config/algolia.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from decouple import config
2+
3+
ALGOLIA = {
4+
'APPLICATION_ID': config('ALGOLIA_APPLICATION_ID'),
5+
'API_KEY': config('ALGOLIA_API_KEY'),
6+
}

server/core/config/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from decouple import config
2727
from .cloudinary import CLOUDINARY_DOMAIN, CLOUDINARY_STORAGE
2828
from .beehiiv import BEEHIIV_CONFIG
29+
from .algolia import ALGOLIA
2930

3031
# Build paths inside the project like this: BASE_DIR / 'subdir'.
3132
BASE_DIR = Path(__file__).resolve().parent.parent.parent
@@ -67,7 +68,8 @@
6768
'django_celery_beat',
6869
'tinymce',
6970
'cloudinary_storage',
70-
'cloudinary',
71+
'cloudinary',
72+
'algoliasearch_django',
7173
]
7274

7375
INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS + THIRD_PARTY_APPS
@@ -211,4 +213,6 @@
211213

212214
# Beehiiv Settings
213215
BEEHIIV_API_KEY = BEEHIIV_CONFIG['API_KEY']
214-
BEEHIIV_PUBLICATION_ID = BEEHIIV_CONFIG['PUBLICATION_ID']
216+
BEEHIIV_PUBLICATION_ID = BEEHIIV_CONFIG['PUBLICATION_ID']
217+
218+
ALGOLIA = ALGOLIA

server/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
aiodns==3.0.0
22
aiohttp==3.10.11
33
aiosignal==1.3.2
4+
algoliasearch==4.13.1
5+
algoliasearch-django==4.0.0
46
amqp==5.2.0
57
annotated-types==0.7.0
68
anyio==4.4.0

0 commit comments

Comments
 (0)