כאשר משתמש פותח עדכון חדשות המצטבר ממאות מקורות, אפליקציית המובייל חייבת להציג מיד נתונים מהמטמון תוך כדי שליפת עדכונים חדשים. במכשיר עם 50,000 מאמרים במטמון, שאילתת LIKE ב-SQLite אורכת 800ms — בלתי מקובל. אנו מחליפים את LIKE ב-FTS5, ומצמצמים את הזמן ל-50ms. האתגר הטכני המרכזי הוא הבטחת היענות ממשק המשתמש עם מאות מקורות, מטמון לא מקוון, ועדכון מותאם אישית. הפתרונות שלנו מיושמים בחברות מדיה עם ניסיון של למעלה מ-5 שנים — יותר מ-20 פרויקטים.
משתמשים מצפים שהעדכון יתרענן כל 5 דקות, התראות חדשות חמות בתוך 2 דקות, וגישה לא מקוונת ל-100 המאמרים האחרונים. עמידה בדרישות אלו דורשת ארכיטקטורה מחושבת היטב גם בצד הלקוח וגם בצד השרת.
בעיות שאנו פותרים
תוכן כפול
אותה ידיעה חדשותית יכולה להופיע בחמישה מקורות שונים עם כתובות URL שונות וטקסט קטוע. ללא הסרת כפילויות, משתמשים רואים כרטיסים חוזרים, מה שפוגע בחוויית השימוש. אנו מיישמים MinHash להשוואת דמיון טקסטואלי, ומסננים עד 95% מהכפילויות ומפחיתים את עומס האחסון ב-70%.
חיפוש מקומי איטי
חיפוש ב-50,000 מאמרים במטמון עם class NewsRepository( private val newsApi: NewsApi, private val newsDao: NewsDao ) { fun getFeed(userId: String): Flow<Resource<List<Article>>> = networkBoundResource( query = { newsDao.getArticles(userId) }, fetch = { newsApi.getFeed(userId, page = 1) }, saveFetchResult = { articles -> newsDao.deleteOldArticles(olderThan = System.currentTimeMillis() - 7.days) newsDao.insertArticles(articles) }, shouldFetch = { cached -> cached.isEmpty() || cached.first().isStale() } ) } הוא איטי. אנו משתמשים ב-SQLite FTS5 לחיפוש טקסט מלא, ומחזירים תוצאות במילישניות.
גישה לא מקוונת
משתמשים צריכים לקרוא מאמרים ללא חיבור לאינטרנט. אנו מיישמים גם מטמון אוטומטי וגם שמירה ידנית לקריאה לא מקוונת, תוך אחסון תוכן HTML מלא מקומית.
התראות בזמן אמת
חדשות חמות חייבות להידחף בתוך דקות. הסורק שלנו בצד השרת מזהה סיפורים בעדיפות גבוהה ושולח התראות ממוקדות דרך FCM/APNs עם קישור עמוק.
איך אנחנו עושים זאת
בחירת ארכיטקטורה
שלוש גישות לאיסוף תוכן:
- סורק מותאם אישית בצד השרת — מנתח עדכוני RSS/Atom בלוח זמנים, ומנרמל מאמרים במסד הנתונים. לקוח המובייל מדבר רק עם ה-API שלך. יתרונות: שליטה מלאה בפורמט, מטמון והסרת כפילויות. חסרונות: דורש תשתית שרת.
- NewsAPI / GNews / Currents API — אגרגטורים מוכנים עם REST API. התחלה מהירה, אך בתשלום לשימוש מסחרי וקבוצת מקורות מוגבלת.
- היברידי — סורק מותאם אישית למקורות עדיפות + API של צד שלישי כגיבוי. אנו ממליצים על כך לאפליקציות ייצור מכיוון שזה מביא מוצר לשוק 40% מהר יותר מפתרון מותאם לחלוטין.
| גישה | שליטה | זמן עד השקה | עלות תחזוקה |
|---|---|---|---|
| סורק מותאם אישית | מלאה | ארוך (4–8 שבועות) | גבוהה |
| API מוכן | מוגבלת | מהיר (1–2 שבועות) | בינונית (מנוי) |
| היברידי | מאוזנת | בינוני (3–6 שבועות) | אופטימלית |
הסרת כפילויות עם MinHash
הסרת כפילויות היא אחד האתגרים הגדולים ביותר. סיפור בודד יכול להופיע בחמישה מקורות עם כתובות URL שונות. אנו משתמשים ב-MinHash בצד השרת כדי להשוות דמיון טקסטואלי. זה מסנן כפילויות בדיוק של 95% ומפחית את הנתונים המאוחסנים בעד 70%. הלקוח מקבל עדכון נקי.
פרטי אלגוריתם MinHash
אנו משתמשים ב-3-grams (shingles) ומבצעים hash שלהם דרך 200 פונקציות hash. הדמיון מוערך לפי חלק ה-minimum hashes התואמים. סף של 0.8 קובע כפילויות.עדכון מותאם אישית ומטמון לא מקוון
העדכון נבנה על בסיס מינויים של המשתמש (מקורות, תגים, קטגוריות) בתוספת אלגוריתם דירוג. בצד הלקוח, רשימה מפולחת עם מטמון באמצעות Room (אנדרואיד) או Core Data (iOS). אסטרטגיה: בפתיחת האפליקציה, הצג נתונים מהמטמון מיד תוך שליפת נתונים חדשים במקביל.
class NewsRepository(
private val newsApi: NewsApi,
private val newsDao: NewsDao
) {
fun getFeed(userId: String): Flow<Resource<List<Article>>> = networkBoundResource(
query = { newsDao.getArticles(userId) },
fetch = { newsApi.getFeed(userId, page = 1) },
saveFetchResult = { articles ->
newsDao.deleteOldArticles(olderThan = System.currentTimeMillis() - 7.days)
newsDao.insertArticles(articles)
},
shouldFetch = { cached -> cached.isEmpty() || cached.first().isStale() }
)
}פילוח — Paging 3 באנדרואיד, פילוח מותאם אישית מבוסס cursor ב-iOS. פילוח מבוסס offset (page=2&per_page=20) נשבר כאשר מאמרים חדשים מוכנסים בראש העדכון — משתמשים רואים כפילויות. פילוח מבוסס cursor (after_id=article_12345) מונע זאת.
קריאה לא מקוונת
העבודה הלא מקוונת מתבצעת באמצעות שני מנגנונים:
- מטמון אוטומטי של העדכון ב-Room/Core Data (N המאמרים האחרונים).
- שמירה ידנית — המשתמש מוסיף במפורש מאמר ל"קרא מאוחר יותר".
לקריאה לא מקוונת מלאה, אנו מאחסנים לא רק מטא-דאטה אלא גם את תוכן ה-HTML של המאמר. או במסד הנתונים (blob) או במערכת הקבצים. ה-HTML מנותח ומוצג דרך WKWebView (iOS) או WebView עם רשת מושבתת (אנדרואיד).
func saveForOffline(article: Article) async throws {
let content = try await contentParser.fetchFullText(url: article.url)
let sanitizedHTML = HTMLSanitizer.sanitize(content, baseURL: article.url)
let offlineArticle = OfflineArticle(
id: article.id,
title: article.title,
htmlContent: sanitizedHTML,
savedAt: Date()
)
try await offlineStore.save(offlineArticle)
} התראות Push לחדשות חמות
חדשות חמות — ההתראה חייבת להגיע בתוך דקות מהפרסום. תהליך:
- הסורק בצד השרת מזהה מאמר עם תג
func saveForOffline(article: Article) async throws { let content = try await contentParser.fetchFullText(url: article.url) let sanitizedHTML = HTMLSanitizer.sanitize(content, baseURL: article.url) let offlineArticle = OfflineArticle( id: article.id, title: article.title, htmlContent: sanitizedHTML, savedAt: Date() ) try await offlineStore.save(offlineArticle) }או עם קצב מעורבות גבוה. - קובע משתמשים רלוונטיים (לפי מינויים למקור/נושא).
- שולח push דרך FCM/APNs עם
breaking.
בצד הלקוח, קישור עמוק ב-push פותח את המאמר הספציפי:
override fun onMessageReceived(message: RemoteMessage) {
val articleId = message.data["article_id"] ?: return
val intent = Intent(this, ArticleActivity::class.java).apply {
putExtra("article_id", articleId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
} חיפוש מקומי מהיר
חיפוש מיידי במטמון המקומי דרך Room FTS:
@Fts4(contentEntity = ArticleEntity::class)
@Entity(tableName = "articles_fts")
data class ArticleFts(
@PrimaryKey
@ColumnInfo(name = "rowid")
val rowid: Int = 0,
val title: String,
val description: String
)
@Query("SELECT * FROM articles INNER JOIN articles_fts ON articles.rowid = articles_fts.rowid WHERE articles_fts MATCH :query")
fun searchArticles(query: String): Flow<List<ArticleEntity>>
FTS4/FTS5 ב-SQLite מספק חיפוש ב-50,000 מאמרים במילישניות.
הערכת תהליך ושלבי עבודה
- איסוף נתונים — איסוף דרישות, הגדרת מקורות, תרחישי משתמש.
- ביקורת/ניתוח — הערכת תשתית קיימת, ציפיות תעבורה.
- עיצוב — תוכנית ארכיטקטונית, החלטות על טכנולוגיות.
- הערכה — היקף ולוח זמנים.
- פיתוח — סורק בצד השרת, API, לקוחות מובייל (iOS & Android).
- בדיקות — בדיקות עומס (10,000 משתמשים במקביל), בדיקות UI/UX.
- השקה — פריסה ל-App Store ו-Google Play, ניטור.
בעיות וטעויות נפוצות
- הסרת כפילויות לא מטופלת מוקדם — מובילה לכרטיסים כפולים וחוויית משתמש ירודה. אנו משתמשים ב-MinHash בצד השרת.
-
טעינה עצלה של תמונות — 50 תמונות במהלך גלילה מהירה גורמות ל-50 בקשות במקביל. אנו מיישמים prefetch עם עדיפות:
priority: high+ GlidePrefetcher באנדרואיד,override fun onMessageReceived(message: RemoteMessage) { val articleId = message.data["article_id"] ?: return val intent = Intent(this, ArticleActivity::class.java).apply { putExtra("article_id", articleId) flags = Intent.FLAG_ACTIVITY_NEW_TASK } }ב-iOS. - זמן קריאה — הצג "קריאה של 5 דקות" על ידי ספירת מילים בצד השרת, ואחסן במטא-דאטה של המאמר.
מה כלול בעבודה
- עיצוב ארכיטקטורה לסקלאביליות עד 100,000 משתמשים
- יישום לקוח ב-Swift (iOS) ו-Kotlin (אנדרואיד) עם Room/Core Data
- הקמת שרת (Node.js/Python) לסריקת RSS/Atom ו-API מסוג REST/GraphQL
- שילוב התראות push דרך FCM/APNs עם קישור עמוק
- בדיקות עומס (10,000 קוראים במקביל)
- פריסה ל-App Store ו-Google Play
- תיעוד והדרכה לצוות הלקוח
לוחות זמנים אופייניים
| רכיב | מסגרת זמן |
|---|---|
| MVP (עדכון, מטמון, חיפוש, push) | 6–10 שבועות |
| סורק שרת מלא | 2–4 שבועות |
| שילוב התראות push | 1–2 שבועות |
| אופטימיזציית ביצועים | 1–2 שבועות |
עלות פיתוח ה-MVP תלויה במורכבות ובדרישות הספציפיות שלך. אנו מייעלים עלויות על ידי ניצול מודולים מוכנים — חיסכון של עד 30% לעומת בנייה מאפס. האפליקציות שלנו נבדקות בעומסי שיא של 10,000 משתמשים במקביל, מה שמבטיח פעולה יציבה. צור קשר כדי לדון בפרויקט שלך ולקבל ייעוץ מהנדסי לבחירת הארכיטקטורה האופטימלית לתקציב שלך.







