Skip to content

Does Jetpack Room Support FTS5? Full-Text Search in Android Room Database

I was building an Android app that needed search functionality across thousands of articles. The basic LIKE '%query%' approach was too slow. I knew SQLite had FTS5 (Full-Text Search 5) with better performance, but when I tried to use it in Room, I hit a wall.

The Problem: Room Only Supports FTS3 and FTS4

I started by checking Room’s documentation for FTS5 support. What I found:

  • Room has @Fts3 and @Fts4 annotations for full-text search
  • No @Fts5 annotation exists
  • FTS5 offers BM25 ranking, phrase queries, and the NEAR operator - features missing in FTS3/FTS4

This is frustrating. FTS5 has been part of SQLite since 2015, yet Room doesn’t natively support it.

Current FTS Support in Room

Room supports FTS3 and FTS4 through annotations. Here’s how to use FTS4:

Article.kt
@Entity
@Fts4
data class Article(
@PrimaryKey val id: Long,
val title: String,
val content: String
)
ArticleDao.kt
@Dao
interface ArticleDao {
@Query("SELECT * FROM Article WHERE Article MATCH :query")
fun search(query: String): List<Article>
}

This works. The @Fts4 annotation creates a virtual table behind the scenes. But you miss out on FTS5 features like:

  • BM25 ranking - Better relevance sorting
  • Phrase queries - Exact phrase matching
  • NEAR operator - Find words within N tokens of each other
  • Better Unicode support - Improved tokenizer options

Workaround: Using FTS5 with RawQuery

You can still use FTS5 in Room. It just requires more work.

First, create your FTS5 virtual table manually:

Database.kt
@Database(entities = [Article::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
override fun onCreate(db: SupportSQLiteDatabase) {
// Create the FTS5 virtual table manually
db.execSQL("""
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts5 USING fts5(
title,
content,
content='Article',
content_rowid='id'
)
""")
}
}

Then use @RawQuery to search:

ArticleDao.kt
@Dao
interface ArticleDao {
@RawQuery
fun searchFts5(query: SupportSQLiteQuery): List<Article>
@Insert
fun insert(article: Article)
@Query("SELECT * FROM Article WHERE id IN (SELECT rowid FROM articles_fts5 WHERE articles_fts5 MATCH :query ORDER BY bm25(articles_fts5))")
fun searchWithBm25(query: String): List<Article>
}

Using it in your code:

SearchViewModel.kt
val searchTerm = "android development"
val articles = articleDao.searchWithBm25(searchTerm)

Wait, there’s a catch. The second approach with @Query still uses FTS5 table, but the MATCH query syntax needs proper formatting.

Keeping FTS5 Index in Sync

The bigger problem is keeping your FTS5 index synchronized with your main table. FTS5 doesn’t auto-update when you insert into the main table.

You need triggers:

Database.kt
override fun onCreate(db: SupportSQLiteDatabase) {
// Create FTS5 table
db.execSQL("""
CREATE VIRTUAL TABLE IF NOT EXISTS articles_fts5 USING fts5(
title,
content,
content='Article',
content_rowid='id'
)
""")
// Trigger on INSERT
db.execSQL("""
CREATE TRIGGER IF NOT EXISTS article_ai AFTER INSERT ON Article BEGIN
INSERT INTO articles_fts5(rowid, title, content)
VALUES (new.id, new.title, new.content);
END
""")
// Trigger on UPDATE
db.execSQL("""
CREATE TRIGGER IF NOT EXISTS article_au AFTER UPDATE ON Article BEGIN
UPDATE articles_fts5 SET title = new.title, content = new.content
WHERE rowid = new.id;
END
""")
// Trigger on DELETE
db.execSQL("""
CREATE TRIGGER IF NOT EXISTS article_ad AFTER DELETE ON Article BEGIN
DELETE FROM articles_fts5 WHERE rowid = old.id;
END
""")
}

This is error-prone. You’re managing database triggers manually.

What About Room 3.0?

I checked the Room 3.0 alpha01 release notes. There’s no mention of FTS5 support in the changelog.

The Reddit thread where someone asked “Will it support FTS5?” didn’t get an official response from the Android team.

My Recommendation

For simple search needs, stick with @Fts4. It covers most use cases:

SimpleFts4Example.kt
@Entity
@Fts4
data class Note(
@PrimaryKey val id: Long,
val title: String,
val body: String
)
@Dao
interface NoteDao {
@Query("SELECT * FROM Note WHERE Note MATCH :query")
fun search(query: String): List&lt;Note&gt;
}

For advanced search with BM25 ranking, you have two options:

  1. Use raw SQLite with Room - The @RawQuery approach above, but manage triggers yourself
  2. Use a separate search library - SQLite directly, or external search like Meilisearch

The @RawQuery approach works, but adds complexity. You’re responsible for:

  • Creating the FTS5 virtual table
  • Managing triggers for CRUD operations
  • Writing raw SQL queries

Why FTS5 Matters for Your App

Search performance directly affects user experience. Users expect instant results. A well-optimized FTS setup can make the difference between a sluggish app and a responsive one.

FTS5’s BM25 ranking gives more relevant results. Instead of returning results in arbitrary order, you get them sorted by relevance. This matters when users search through large datasets.

Before investing time in FTS5 workarounds, profile your actual search performance. FTS4 might be sufficient for your use case.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments