सामग्री पर जाएं
सभी पोस्ट

Room में फुल-टेक्स्ट सर्च: 2026 में एक लोकल-फर्स्ट Android ऐप में इंस्टेंट सर्च जोड़ना

Android पर Room के FTS4 सपोर्ट की एक व्यावहारिक गाइड — एक वर्चुअल सर्च टेबल बनाना, उसे ट्रिगर्स से सिंक रखना, और FTS5 को मैनुअल माइग्रेशन की ज़रूरत क्यों पड़ती है।

MFKAPPS 6 मिनट पढ़ना

सर्च वह फीचर है जिसे तब तक कोई नोटिस नहीं करता जब तक वह धीमा न हो जाए। तीन सौ आइटम वाली पैंट्री में “दही” टाइप करें और उम्मीद करें कि टाइप करते ही लिस्ट फ़िल्टर हो जाए — अगर जवाब देने में 200 मिलीसेकंड भी लगें, तो ऐप टूटा हुआ महसूस होता है। ज़्यादातर Room-आधारित ऐप इसे LIKE '%query%' क्लॉज़ से हैंडल करते हैं, और यह छोटी टेबल्स पर ठीक काम करता है। जैसे ही टेबल बढ़ती है, क्वेरी में एक से ज़्यादा शब्द होते हैं, या आपको टाइपो-सहनशीलता चाहिए होती है, यह काम करना बंद कर देता है। SQLite के पास इसका एक असली जवाब हमेशा से रहा है: फुल-टेक्स्ट सर्च, जो Room में @Fts4 एनोटेशन के रूप में सामने आता है। यहाँ बताया गया है कि यह वास्तव में कैसे काम करता है, कहाँ टूटता है, और ज़्यादातर ट्यूटोरियल जिस FTS5 को मान लेते हैं वह Room आपको मुफ़्त में क्यों नहीं देता।

LIKE स्केल क्यों नहीं करता

SELECT * FROM pantry_items WHERE name LIKE '%yogurt%' एक इंडेक्स इस्तेमाल नहीं कर सकता। SQLite को हर पंक्ति स्कैन करनी होती है और हर एक पर सबस्ट्रिंग मैच चलाना होता है। कुछ सौ पंक्तियों पर यह अदृश्य होता है। कुछ हज़ार पर — जहाँ बारकोड स्कैनिंग और रसीद इतिहास वाला एक पैंट्री ऐप सोचे-से जल्दी पहुँच जाता है — यह हर कीस्ट्रोक पर एक दिखने वाली अटकन बन जाता है, खासकर अगर क्वेरी को कई कॉलम (नाम, ब्रांड, कैटेगरी) पर OR भी चलाना पड़े।

फुल-टेक्स्ट सर्च इसे उलट देता है। पंक्तियाँ स्कैन करने के बजाय, SQLite लिखते समय एक इनवर्टेड इंडेक्स बनाता है: हर शब्द उन पंक्तियों से मैप होता है जिनमें वह मौजूद है। एक सर्च स्कैन नहीं, एक इंडेक्स लुकअप बन जाता है, इसलिए टेबल बढ़ने पर भी परफ़ॉर्मेंस स्थिर रहता है।

Room वास्तव में क्या देता है: FTS4, FTS5 नहीं

यही वह हिस्सा है जो ज़्यादातर लोगों को उलझा देता है, क्योंकि वेब पर ज़्यादातर FTS कंटेंट SQLite के नए FTS5 मॉड्यूल को मान लेता है। Room का @Fts4 एनोटेशन एक FTS4 वर्चुअल टेबल जोड़ता है — इसका कोई @Fts5 समकक्ष नहीं है। FTS4 और FTS5 इतने अलग हैं कि यह मायने रखता है: FTS5 में एक ज़्यादा समझदार क्वेरी सिंटैक्स, बिल्ट-इन bm25() रैंकिंग, और प्रीफ़िक्स क्वेरी की बेहतर हैंडलिंग है — इनमें से कुछ भी Room आपको FTS4 के साथ अपने आप नहीं देता।

अगर आपको FTS5 चाहिए, तो भी आप इसे पा सकते हैं — बस इसे मैन्युअली करना होगा: एनोटेट की गई एंटिटी के बजाय एक Migration के अंदर रॉ SQL (CREATE VIRTUAL TABLE ... USING fts5(...)) से वर्चुअल टेबल खुद बनाकर, और रीड्स के लिए इसे एक सादे @DatabaseView या रॉ क्वेरी से मैप करके। ज़्यादातर लोकल सर्च यूज़ केस के लिए — पैंट्री आइटम नाम, सब्सक्रिप्शन नाम, नोट टाइटल — FTS4 काफ़ी है, और इसकी कीमत एक एनोटेशन से ज़्यादा कुछ नहीं। वहीं से शुरू करें; मैन्युअल FTS5 रास्ता तभी अपनाएँ जब आपको वाकई फ़्रेज़ क्वेरीज़ या बिल्ट-इन रैंकिंग चाहिए।

एक FTS4 टेबल को Room से जोड़ना

एक FTS टेबल को एक साथी “कंटेंट” एंटिटी की ज़रूरत होती है — वही सामान्य टेबल जिसे आप पहले से बाकी सब चीज़ों के लिए क्वेरी करते हैं — साथ ही एक वर्चुअल टेबल जो सर्च करने योग्य कॉलम्स को इंडेक्स करती है:

@Entity(tableName = "pantry_items")
data class PantryItem(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val brand: String,
    val category: String,
)

@Fts4(contentEntity = PantryItem::class)
@Entity(tableName = "pantry_items_fts")
data class PantryItemFts(
    val name: String,
    val brand: String,
    val category: String,
)

contentEntity, Room से कहता है कि pantry_items को सच का स्रोत माने और FTS टेबल में हर पंक्ति की कॉपी नहीं, बल्कि सिर्फ़ इंडेक्स स्टोर करे। DAO क्वेरी लगभग एक सामान्य क्वेरी जैसी ही दिखती है, सिवाय इसके कि यह FTS टेबल के rowid से जॉइन करती है:

@Query("""
    SELECT pantry_items.* FROM pantry_items
    JOIN pantry_items_fts ON pantry_items.id = pantry_items_fts.rowid
    WHERE pantry_items_fts MATCH :query
""")
fun search(query: String): Flow<List<PantryItem>>

MATCH FTS ऑपरेटर है — यही वह चीज़ है जो क्वेरी को स्कैन के बजाय इंडेक्स लुकअप में बदल देती है। "yog" की जगह "yog*" पास करने से आपको प्रीफ़िक्स मैचिंग मिलती है, जो टाइप करते ही सर्च के लिए वही है जो आप चाहते हैं।

इंडेक्स को सिंक में रखना

contentEntity के साथ, Room वे ट्रिगर्स जनरेट करता है जो इंसर्ट, अपडेट और डिलीट पर pantry_items_fts को pantry_items के साथ सिंक रखते हैं — इन्हें आप मैन्युअली नहीं लिखते। ध्यान रखने वाली एक बात: यह सिंक सिर्फ़ उन राइट्स को कवर करता है जो Room के जनरेट किए गए insert/update/delete मेथड्स से होकर जाती हैं। Room की DAO लेयर के बाहर चलाया गया एक रॉ SQL UPDATE, या execSQL से किया गया बल्क इम्पोर्ट, ट्रिगर-समर्थित कंटेंट एंटिटी मैपिंग को ऐसे सूक्ष्म तरीकों से बायपास कर देता है जिन्हें गलत करना आसान है। पैंट्री में होने वाले बदलावों — बारकोड-स्कैन इंसर्ट पाथ सहित — को “तेज़” इम्पोर्ट के लिए एक अलग रॉ-SQL शॉर्टकट के बजाय, उन्हीं DAO मेथड्स से गुज़ारें जिन्हें Room का स्कीमा वैलिडेशन पहले से ध्यान में रखता है।

रैंकिंग: FTS4 की असली सीमा

FTS4 आपको सही मैच तो देता है लेकिन मैच ऑर्डर से आगे कोई रेलेवेंस रैंकिंग नहीं देता। अगर कोई “दूध” सर्च करे और “फुल क्रीम दूध” और “मिल्क चॉकलेट बार” दोनों मैच हों, तो FTS4 आपको नहीं बताएगा कि यूज़र शायद किसका मतलब कह रहा था — आपको पंक्तियाँ टेबल के क्रम में मिलती हैं, रेलेवेंस के क्रम में नहीं। कुछ सौ पैंट्री आइटम्स के लिए यह व्यावहारिक रूप से शायद ही मायने रखता है; लिस्ट देखने भर के लिए काफ़ी छोटी होती है। अगर यह मायने रखने लगे — एक बड़ा कैटलॉग, या फ़ील्ड्स के व्यापक सेट में सर्च — तो FTS5 पर छलांग लगाए बिना समाधान है एक साधारण क्लाइंट-साइड री-ऑर्डर: मैच निकालें, फिर इस आधार पर सॉर्ट करें कि क्वेरी बाकी सब चीज़ों से पहले नाम की शुरुआत से मैच करती है या नहीं। यह कुछ लाइन Kotlin है, डेटाबेस माइग्रेशन नहीं, और यह उस केस को ठीक करता है जो यूज़र्स को वाकई परेशान करता है: एग्ज़ैक्ट और प्रीफ़िक्स मैच अनरिलेटेड पार्शियल मैचेस के नीचे दबे होना।

निष्कर्ष

Room का FTS4 सपोर्ट एक एनोटेशन और थोड़ी अलग DAO क्वेरी की कीमत पर एक लीनियर स्कैन को इंडेक्स लुकअप में बदल देता है — कोई सर्वर नहीं, कोई थर्ड-पार्टी सर्च SDK नहीं, किसी ऐसी चीज़ के लिए नेटवर्क राउंड ट्रिप नहीं जो इंस्टेंट महसूस होनी चाहिए। किसी भी लोकल-फर्स्ट Android ऐप के अंदर सर्च के लिए यही सही डिफ़ॉल्ट है। याद रखने लायक दो बातें: Room FTS4 बोलता है, FTS5 नहीं, इसलिए उन रैंकिंग फ़ीचर्स के इर्द-गिर्द डिज़ाइन न करें जो सिर्फ़ नए मॉड्यूल में मौजूद हैं; और हर वह राइट पाथ जिसे सर्च करने योग्य होना चाहिए, Room की DAO लेयर से गुज़रना ही होगा, वरना इंडेक्स चुपचाप उस टेबल से सिंक से बाहर हो जाता है जिसे उसे बताना है। ये दोनों बातें सही करें, और सर्च एक ऐसा फ़ीचर नहीं रह जाता जिसकी चिंता करनी पड़े।

अगर आप इस पैटर्न को एक शिप्ड ऐप में देखना चाहते हैं, तो Stocky इसका इस्तेमाल एक ऐसी पैंट्री में सर्च के लिए करता है जो सैकड़ों आइटम्स तक बढ़ सकती है — बारकोड स्कैन, रसीद इम्पोर्ट, और मैन्युअल एंट्री सभी उसी सर्च करने योग्य टेबल में उतरते हैं।

// संबंधित पठन

जर्नल से और भी

MFKAPPS 6 मिनट पढ़ना

2026 में Room TypeConverters: अपने स्कीमा को खराब किए बिना एनम, डेट, और लिस्ट स्टोर करना

Android पर Room TypeConverters के लिए एक व्यावहारिक गाइड — एनम, Instant/LocalDate, और लिस्ट — साथ ही वे ग़लतियाँ जो एक कनवर्टर को एक चुपचाप डेटा-करप्शन बग में बदल देती हैं।

#android #engineering #room
MFKAPPS 5 मिनट पढ़ना

2026 में Room डेटाबेस इंडेक्स: वाकई धीमी क्वेरी को ढूँढना और ठीक करना

Android पर Room/SQLite डेटाबेस को इंडेक्स करने की व्यावहारिक गाइड — EXPLAIN QUERY PLAN पढ़ना, बिना अंदाज़े के @Index जोड़ना, और वे गलतियाँ जो चुपचाप इंडेक्स को बेअसर कर देती हैं।

#android #engineering #room
MFKAPPS 5 मिनट पढ़ना

Room का @Relation: Android पर N+1 क्वेरी के बिना वन-टू-मेनी डेटा क्वेरी करना

Room के @Relation एनोटेशन की एक व्यावहारिक गाइड — categories और entries जैसे वन-टू-मेनी डेटा को N+1 क्वेरी या मैनुअल join के बिना मॉडल करना।

#android #engineering #room