用房间实现搜索

Bry*_*yan 7 android full-text-search android-sqlite android-search android-room

最近我一直在搞乱Android架构组件(更具体地说是Room),但我遇到了一些障碍.

我已经成功构建了一个Room数据库,用于存储部门及其人员的列表.以前,此数据是从服务器中提取的,但未在本地存储.搜索功能也是远程处理的,所以我现在也希望在本地处理搜索功能,但我对SQL的了解有点缺乏.

查看服务器上的SQL代码,搜索语句使用一堆REGEXP函数根据提供的查询搜索两个数据库.这似乎不是处理搜索的最佳方式,但它运作得相当好,并给出了快速响应.所以我尝试在本地模仿这个,但很快发现REGEXPAndroid上不支持(不使用NDK).

至于LIKEGLOB运营商,他们似乎非常有限.例如,我没有看到一种方法可以同时匹配多个关键字; 而REGEXP我可以用一个or(|)运算符替换空格来实现这个功能.

所以,寻找替代方案我遇到了全文搜索(FTS); 这是Android文档中实现搜索的方法.虽然看起来FTS用于搜索完整文档,而不是像我的用例那样简单的数据.

无论如何,Room不支持 FTS .

所以,当然,我试图强制Room创建一个FTS虚拟表而不是一个标准表,通过创建一个实现SupportSQLiteOpenHelper.Factory就是这样.这个实现几乎是默认的直接副本FrameworkSQLiteOpenHelperFactory,以及相关的框架类.必要的代码位于SupportSQLiteDatabase,我覆盖execSQL以在必要时注入虚拟表代码.

class FTSSQLiteDatabase(
    private val delegate: SQLiteDatabase,
    private val ftsOverrides: Array<out String>
) : SupportSQLiteDatabase {

    // Omitted code...

    override fun execSQL(sql: String) {
        delegate.execSQL(injectVirtualTable(sql))
    }

    override fun execSQL(sql: String, bindArgs: Array<out Any>) {
        delegate.execSQL(injectVirtualTable(sql), bindArgs)
    }

    private fun injectVirtualTable(sql: String): String {
        if (!shouldOverride(sql)) return sql

        var newSql = sql

        val tableIndex = sql.indexOf("TABLE")
        if (tableIndex != -1) {
            sql = sql.substring(0..(tableIndex - 1)) + "VIRTUAL " + sql.substring(tableIndex)

            val argumentIndex = sql.indexOf('(')
            if (argumentIndex != -1) {
                sql = sql.substring(0..(argumentIndex - 1) + "USING fts4" + sql.substring(argumentIndex)
            }
        }

        return newSql
    }

    private fun shouldOverride(sql: String): Boolean {
        if (!sql.startsWith("CREATE TABLE")) return false

        val split = sql.split('`')
        if (split.size >= 2) {
            val tableName = split[1]
            return ftsOverrides.contains(tableName)
        } else {
            return false
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

这有点乱,但它有效!好吧,它创建了虚拟表...

但后来我得到以下内容SQLiteException:

04-04 10:54:12.146 20289-20386/com.example.app E/SQLiteLog: (1) cannot create triggers on virtual tables
04-04 10:54:12.148 20289-20386/com.example.app E/ROOM: Cannot run invalidation tracker. Is the db closed?
    android.database.sqlite.SQLiteException: cannot create triggers on virtual tables (code 1): , while compiling: CREATE TEMP TRIGGER IF NOT EXISTS `room_table_modification_trigger_departments_UPDATE` AFTER UPDATE ON `departments` BEGIN INSERT OR REPLACE INTO room_table_modification_log VALUES(null, 0); END
        at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
        at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:890)
        at android.database.sqlite.SQLiteConnection.prepare(SQLiteConnection.java:501)
        at android.database.sqlite.SQLiteSession.prepare(SQLiteSession.java:588)
        at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:58)
        at android.database.sqlite.SQLiteStatement.<init>(SQLiteStatement.java:31)
        at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1752)
        at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1682)
        at com.example.app.data.FTSSQLiteDatabase.execSQL(FTSSQLiteDatabase.kt:164)
        at android.arch.persistence.room.InvalidationTracker.startTrackingTable(InvalidationTracker.java:204)
        at android.arch.persistence.room.InvalidationTracker.access$300(InvalidationTracker.java:62)
        at android.arch.persistence.room.InvalidationTracker$1.run(InvalidationTracker.java:306)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
        at java.lang.Thread.run(Thread.java:764)
Run Code Online (Sandbox Code Playgroud)

Room创建表,但随后尝试在虚拟表上创建一个触发器,这显然是不允许的.如果我试图覆盖触发器(即只是阻止它们执行),我猜这将破坏Room的许多功能.我假设,这是Room首先不支持FTS的原因.

TLDR

所以如果Room不支持FTS(我不能强制它),并且REGEXP不支持(除非我使用NDK); 在使用Room时我还有另一种方法可以实现搜索吗?FTS是否是正确的方法(看起来有点矫枉过正),还是有其他方法更适合我的用例?

Com*_*are 6

我可以确认这有效。它会加重,但它有效。

首先,您需要创建表。对于初始数据库创建,您可以使用 a RoomDatabase.Callback

RoomDatabase.Builder<BookDatabase> b=
  Room.databaseBuilder(ctxt.getApplicationContext(), BookDatabase.class,
    DB_NAME);

b.addCallback(new Callback() {
  @Override
  public void onCreate(@NonNull SupportSQLiteDatabase db) {
    super.onCreate(db);

    db.execSQL("CREATE VIRTUAL TABLE booksearch USING fts4(sequence, prose)");
  }
});

BookDatabase books=b.build();
Run Code Online (Sandbox Code Playgroud)

(另外:如果您需要在迁移中对其进行更改,请记住此表!)

然后,您可以@Dao为此设置一个。所有实际的数据库操作 DAO 方法都需要用 注释@RawQuery,因为其他一切都希望与实体一起使用。而且,由于@RawQuery方法只接受一个SupportSQLiteQuery参数,您可能希望将它们包装在创建SupportSQLiteQuery对象的其他方法中。

因此,例如,要将数据插入到虚拟表中,您可以:

  @RawQuery
  protected abstract long insert(SupportSQLiteQuery queryish);

  void insert(ParagraphEntity entity) {
    insert(new SimpleSQLiteQuery("INSERT INTO booksearch (sequence, prose) VALUES (?, ?)",
      new Object[] {entity.sequence, entity.prose}));
  }
Run Code Online (Sandbox Code Playgroud)

并进行搜索,您可以执行以下操作:

  @RawQuery
  protected abstract List<BookSearchResult> _search(SupportSQLiteQuery query);

  List<BookSearchResult> search(String expr) {
    return _search(query(expr));
  }

  private SimpleSQLiteQuery query(String expr) {
    return new SimpleSQLiteQuery("SELECT sequence, snippet(booksearch) AS snippet FROM booksearch WHERE prose MATCH ? ORDER BY sequence ASC",
      new Object[] {expr});
  }
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,我的@RawQuery方法是protected并使用引导_来强调“这些将是private,但你不能拥有private abstract方法,所以请不要使用它们,好吗?”。

请注意,您的 FTS 搜索表达式需要遵循SQLite FTS 文档


Mic*_*ran 5

我们终于得到了它,从2.1.0-alpha01版本开始Room 支持具有映射 FTS3 或 FTS4 表的实体。有关更多信息和示例用法,您可以转到他们的文档:@Fts3@Fts4