盒子
盒子
Posts List
  1. 一.前言
  2. 二.初步的思路
  3. 三.动手吧,在动手过程中改进。
    1. 1.集成 sqlite
    2. 2.设计fts表的结构
    3. 3.android 实现搜索控件
    4. 4.sqlite数据填充
    5. 5.检查sqlite 数据是否正确
    6. 6.分词问题
      1. 1.重写分词器,编译生成so库
      2. 2.使用模糊匹配
      3. 3.仍然存在的问题

android app实现全文检索

一.前言

项目需要实现一个搜索框,输入关键字,能够以列表的形式展示搜索结果,这是原始需求。

在google,百度上搜索了一些现有实现,都是一些零碎的只言片语。有些重点介绍了sqlite如何使用fst3,有些介绍了怎样实现一个漂亮的搜索框,悲剧的是,真没有搜到完整介绍的。自己动手,丰衣足食。

这篇文章可能较长,我在尝试边实现,边写感受,充分还原自己的心路历程。

二.初步的思路

根据从google和百度上得来的一些信息,以下信息是有用的:

  1. 开启了fts的表搜索速度是未开启的表搜索速度的N倍,因此决定利用sqlite嵌入式数据库的fts优势

  2. 我需要一个搜索框,能够监听输入变化,在变化的时候,搜索并返回结果,动态更新展示列表,那种特别low的点击搜索展示结果的,我忍受不了。

如图所示:

image

三.动手吧,在动手过程中改进。

1.集成 sqlite

项目中目前使用的是realm作为嵌入式数据库,经过多方查找,realm暂不支持fts,并且,realm的so库实在太大了。整个包大了大约5M,果断弃用realm,改用sqlite。

参考官方文档:

https://developer.android.com/training/basics/data-storage/databases.html#DefineContract

关于sqlite初步设计:
OXSqliteTableHelper 继承自 SQLiteOpenHelper:负责处理sqlite中建表,查询,将cursor转化为bean等公共操作。

根据每个表,建立 XXXTableHelper 继承自OXSqliteTableHelper : 负责具体的FTS数据插入,操作。

FTSRepo 负责发起从服务端获取FTS的版本号,详细数据等操作。


在建表时开启FTS ,SQL语句为

CREATE VIRTUAL TABLE USING FTS4 XXXX()

核心代码如下:

private void createTable(SQLiteDatabase db ,String tableName , String[] columnSqls){
if (columnSqls.length == 0){
mLogger.e("table column is empty !!!!");
return;
}
String columnDesc = "";
for (String desc:columnSqls) {
columnDesc = columnDesc.concat(desc).concat(COMMA_SEP);
}
String sql = "";
if (!mUseFTS){
sql = "CREATE TABLE IF NOT EXISTS " + tableName + " ("+columnDesc+ " ) ENGINE=InnoDB DEFAULT CHARSET=utf8";
}else{
sql = "CREATE VIRTUAL TABLE IF NOT EXISTS " + tableName + " USING FTS4("+columnDesc+ " )";
}
mLogger.d("sql create table ===> " + sql);
db.execSQL(sql);
}

FTSRepo : 集成RxJava,从服务端获取数据。

核心代码如下:

public Observable<BaseEntity<List<SymbotomEntity>>> load(){
return transform(
!MockUtil.OPEN_MOCK
?
mService.load(Const.SERVICE_FTS_LOAD , Const.AUTH_TOKEN)
:
mService.loadTest(Const.SERVICE_FTS_LOAD , Const.AUTH_TOKEN)
);
}

2.设计fts表的结构

这部分需要根据各自的需求来定制,这个项目仅需要根据特定的tag,来搜索出内容和内容相关的id,展示出列表,点击列表里面的每一项,展示id的实际内容。因此表结构如下:

"_id INTEGER PRIMARY KEY",
"type INTEGER",
"tag VARCHAR(300)",
"name VARCHAR(100)",
"id bigint(20)",

3.android 实现搜索控件

搜索控件由一个搜索框和一个RecycleView 组成,当输入搜索内容时,RecycleView中能实时的查询并展示相关联内容。

EditText 监听搜索变化,查询表数据并展示:

searchEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String tag = s.toString();
if (tag.equals("") || tag == null){
return ;
}
mLogger.d("search tag :" + tag);
mHelper.searchSymbotom(tag).subscribe(mSubscriber);
}
});

RecyclerView 展示列表:

searchResultView.setLayoutManager(LayoutUtil.getLinearLayout(mContext));
mAdapter = new RecyclerAdapter<SymbotomEntity>(mContext , R.layout.layout_search_item) {
@Override
protected void convert(RecyclerAdapterHelper helper, SymbotomEntity item) {
helper.setText(R.id.searchContent , item.getName());
}
};
searchResultView.setAdapter(mAdapter);
searchResultView.addItemDecoration(LayoutUtil.getHorizontalDivider_3(mContext));

4.sqlite数据填充

由于进入app之前,有大约3秒的闪屏时间,因此我选择在进入app的时候,获取数据,并填充到sqlite中。

大体思路是:

  1. 获取FTS数据版本号
  2. 检查是否需要更新,如需更新,进入3,否则进入6
  3. 加载FTS数据
  4. 删除本地fts数据,插入新的FTS数据。
  5. 更新FTS数据版本号
  6. 进入下一步

核心代码如下:

public void checkReload(final IOXCallback<Boolean> callback){
mRepo.version().subscribe(new DefaultSubscriber<BaseEntity<Integer>>() {
@Override
protected void _onNext(BaseEntity<Integer> entity) {
server_table_version = entity.data;
if (needReload(server_table_version) ){
mLogger.d("===>FTS need update !");
loadFTS(callback);
}else {
mLogger.d("===>FTS is up to date !");
callback.call(true);
}
}
});
}

注意:在批量插入数据的时候,一定要开启事务。插入性能提高N倍。

5.检查sqlite 数据是否正确

这里使用facebook的包 stetho 来检查数据是否正确。

具体使用方式:

  1. 在gradle中添加

    compile 'com.facebook.stetho:stetho:1.4.1'
  2. 修改Application的onCreate

    @Override
    public void onCreate() {
    super.onCreate();
    mContext = getApplicationContext();
    Stetho.initializeWithDefaults(this);
    Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler());
    }
  3. 用chrome打开网址 chrome://inspect/#devices,切换到resources 如图:

image

如需修改数据,点击数据库的名字,则进入到cmd命令行模式,如图所示:
image

6.分词问题

测试的时候发现一个问题,如果插入一条记录
里面包含中国,查询语句为:

select * from XXXX where content match '中'

这是查不到的。为什么呢?

经过验证是sqlite默认使用的分词器无法准确分词造成。

sqlite fts 默认支持三种分词 分别是 :

tokenize=simple

tokenize=porter

tokenize=icu zh_cn

如果建表时,不配置分词器,默认使用的是simple 分词器,这个分词器对于中文是相当不友好的。

如果需要换分词器为porter,语句是这样的:

CREATE VIRTUAL TABLE USING FTS4 XXXX(tokenize=porter)

有两种办法解决这个问题

1.重写分词器,编译生成so库

这种方法的好处在于可以完全定制成自己想要的分词和搜索,缺点就是需要自己重新编译生成so库。

2.使用模糊匹配

天无绝人之路,还有一种简单的方式

在查询的时候,对所有的match都默认加上模糊匹配符*,这样就可以查询单个词了。

比如 中* 可以查询中国,中 ,中国人 ,中XXXX等

具体实现,查询语句改为:

"SELECT * FROM " + TABLE_NAME + " WHERE XXX MATCH '?*'";

然后将XXX替换为自己的表名,?替换为自己的key,在10万条记录的情况下测试,几乎没影响,查询速率为毫秒级的。

3.仍然存在的问题

?* 只能向后匹配,不能向前匹配。比如
好好学习,天天向上。

‘好‘ 能查到好好学习,
但是’
习’无法查询到好好学习。

支持一下
扫一扫,支持牛头码农