Jetpack 系列之AppSearch

简介: Jetpack 系列之AppSearch

 前言

       在今年的Google I/O 大会上,Jetpack库新增了三个组件(刚发布Alpha版本),分别是MarcrobenChmarkAppSearchGoogle Shortcuts,MarcrobenChmark组件是用来衡量代码性能一个库,Google Shortcuts听起来像是一种快捷方式,本文我们将着重带领大家领略一下AppSearch的使用。那么什么是AppSearch呢?

什么是AppSearch

       按照官方描述,AppSearch 是一个搜索库,用于管理本地存储的结构化数据,其中包含用于将数据编入索引和通过全文内搜索来检索数据的 API。您可以使用此库来为用户构建自定义的应用内搜索功能。看到应用内搜索,我首先想到了Android设置中的搜索页面,比如我们搜索显示两个字,这里将显示出所有包含“显示”字样的功能入口,如图1所示:

                                         image.gif

                                                          图1 设置内搜索

接下来我们来详细看如何使用AppSearch以及我踩过的那些坑。

引入相关库

首先我们在build.gradle中引入AppSearch组件的相关库,代码如下所示:

def appsearch_version = "1.0.0-alpha01"
implementation("androidx.appsearch:appsearch:$appsearch_version")
kapt("androidx.appsearch:appsearch-compiler:$appsearch_version")
implementation("androidx.appsearch:appsearch-local-storage:$appsearch_version")

image.gif

在 AppSearch 中,一个数据单元被表示为一个文档。 AppSearch 数据库中的每个文档都由其命名空间和 ID 唯一标识。 命名空间用于将来自不同来源的数据分开,这一点相当于sql中的表。所以接下来我们来创建一个数据单元。

创建一个数据单元

我们以新闻类为例,创建的数据类如下所示:

@Document
data class News(
    @Document.Namespace
    val namespace: String,
    @Document.Id
    val id: String,
    @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    val newsTitle: String,
    @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    val newsContent: String
)

image.gif

首先在AppSearch中所有的数据单元都要使用@Document注解,namespace和id在上面说了是数据类型的必须字段,newsTitle和newsContent是我们自己定义的新闻标题和新闻内容字段,这里提一下

@Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)

image.gif

这个注解,@Document.StringProperty 就是要将字符串类型的变量配置成AppSearch的属性,如果是整型那就是

@Document.Int64Property

image.gif

布尔类型就是

@Document.BooleanProperty

image.gif

等等等等,indexingType 属性值可以理解为匹配方式,这里设置为INDEXING_TYPE_PREFIXES,如当匹配条件是Huang的时候 可以匹配到HuangLinqing,其他属性感兴趣的可以看下源码androidx.appsearch.app.AppSearchSchema类。创建完数据类之后,同其他数据库操作一样,接下来来创建一个数据库。

创建数据库

创建数据库就会返回给我们一个ListenableFuture,用于整个数据库的操作,代码如下所示:

val context: Context = applicationContext
val sessionFuture = LocalStorage.createSearchSession(
    LocalStorage.SearchContext.Builder(context, /*databaseName=*/"news")
        .build()
)

image.gif

此时我们可以看到这行代码报了一个错误,错误如下所示:

image.gif

大致意思是说还需要依赖一个库,说实话,其实AppSearch库完全可以自己依赖一下,这样对开发者方便很多,但是毕竟AppSearch刚出测试版,要求不能太高。

我们在build.gradle中引入guava库,代码如下所示:

 

implementation("com.google.guava:guava:30.1.1-android")

image.gif

依赖之后,上述代码就可以正常运行了,不过运行的话这里还不行,我们设置java1.8的环境才可以,否则后面运行会出现java.lang.NoSuchMethodError: No static method metafactory的错误

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
    jvmTarget = "1.8"
}

image.gif

原问题我提给了Google,可参看https://issuetracker.google.com/issues/191389033

设置数据模式

AppSearch中有Schema 和schema types的概念,意思是模式和模式类型,模式由表示独特数据类型的模式类型组成这里指的就是News类,模式类型由包含名称、数据类型和基数的属性组成。这里设置数据模式,其实就是指定我们可以在这个名称为“news”的数据空中可以添加什么样类型的数据。

val setChemaRequest = SetSchemaRequest
    .Builder()
    .addDocumentClasses(News::class.java).build()
var setSchemaFuture = Futures.transformAsync(
    sessionFuture,
    AsyncFunction<AppSearchSession?, SetSchemaResponse?> {
        it?.setSchema(setChemaRequest)
    },
    mainExecutor
)

image.gif

首先我们创建了一个数据类型为News类的模式类,然后通过AppSearchSession的setSchema方法为数据文档设置了数据模式,这里大家疑惑的地方可能是Futures.transformAsync这个方法,其实很简单,Future就是一个Java中的异步线程框架,可以类比为协程,所以说如果AppSearch的设计可以不依赖Future在使用上可能会简单许多。

不过令我差异的是,我咨询了若干做Java的朋友,他们都表示,这个东西很少用。所以这里咱们只专注AppSearch的使用,Futures相关类的使用,感兴趣的可以深入学习下。

设置好数据模式后,我们就可以写入数据了。

写入数据

我们首先定义一个要插入的数据类,如下所示:

val new1 = News(
    namespace = "new1",
    id = "new_id_2",
    newsTitle = "who is a boy",
    newsContent = "Everyone, guess who is the handsome boy"
)

image.gif

构建PutDocumentsRequest对象并执行

val putRequest = PutDocumentsRequest.Builder().addDocuments(new1).build()
val putFuture = Futures.transformAsync(
    sessionFuture,
    AsyncFunction<AppSearchSession?, AppSearchBatchResult<String, Void>?> {
        it?.put(putRequest)
    },
    mainExecutor
)

image.gif

执行结果我们可以通过Futures.addCallback来监听,方法如下所示:

Futures.addCallback(
    putFuture,
    object : FutureCallback<AppSearchBatchResult<String, Void>?> {
        override fun onSuccess(result: AppSearchBatchResult<String, Void>?) {
            // Gets map of successful results from Id to Void
            val successfulResults = result?.successes
            // Gets map of failed results from Id to AppSearchResult
            val failedResults = result?.failures
            Log.d(TAG, "成功:" + successfulResults.toString())
            Log.d(TAG, "失败:" + failedResults.toString())
        }
        override fun onFailure(t: Throwable) {
            Log.d(TAG, t.message.toString())
        }
    },
    mainExecutor
)

image.gif

运行,程序打印如下所示:

com.lonbon.appsearchdemo D/MainActivity: 成功:{new_id_1=null}

com.lonbon.appsearchdemo D/MainActivity: 失败:{}

说明存储成功了,接下来我们再插入一条数据,插入代码一致,就不重复展示了,数据如下所示:

val news2 = News(
    namespace = "new1",
    id = "new_id_1",
    newsTitle = "Huang Linqing is handsome a boy",
    newsContent = "Huang Linqing is an Android development engineer working in Hefei"
)

image.gif

查询数据

查询数据首先我们要指定查询的范围要就是namespace,相当于指定数据表,毕竟不同表中可能存在相同符合条件的数据。

val searchSpec = SearchSpec.Builder()
    .addFilterNamespaces("new1")
    .build()

image.gif

然后执行查询操作,我们这里查询的关键字是”handsome“

val searchFuture = Futures.transform(
    sessionFuture,
    Function<AppSearchSession?, SearchResults> {
        it?.search("handsome", searchSpec)
    },
    mainExecutor
)

image.gif

同样的我们使用addCallback方法来检测查询结果,代码如下所示:

Futures.addCallback(
    searchFuture,
    object : FutureCallback<SearchResults> {
        override fun onSuccess(result: SearchResults?) {
            iterateSearchResults(result)
        }
        override fun onFailure(t: Throwable) {
            Log.d(
                TAG, "查询失败:" + t
                    .message
            )
        }
    },
    mainExecutor
)

image.gif

查询成功会返回SearchResults类,我们需要遍历这个实例取出所有数据打印出来,即iterateSearchResults方法,代码如下所示:

private fun iterateSearchResults(searchResults: SearchResults?) {
    Futures.transform(searchResults?.nextPage, Function<List<SearchResult>, Any> {
        it?.let {
            it.forEach { searchResult ->
                val genericDocument: GenericDocument = searchResult.genericDocument
                val schemaType = genericDocument.schemaType
                if (schemaType == "News") {
                    try {
                        var note = genericDocument.toDocumentClass(News::class.java)
                        Log.d(
                            TAG,
                            "查询结果:新闻标题-" + note.newsTitle
                        )
                        Log.d(
                            TAG,
                            "查询结果:新闻内容-" + note.newsContent
                        )
                    } catch (e: AppSearchException) {
                        Log.e(
                            TAG,
                            "Failed to convert GenericDocument to Note",
                            e
                        )
                    }
                }
            }
        }
    }, mainExecutor)
}

image.gif

查询出来的结果是一个集合,所以我们需要遍历集合,并且数据类型需要是News类才可以继续下一步,这里我们将符合条件查询的新闻标题打印出来,结果如下所示:

D/MainActivity: 查询结果:新闻标题-who is a boy

.appsearchdemo D/MainActivity: 查询结果:新闻内容-Everyone, guess who is the handsome boy

.appsearchdemo D/MainActivity: 查询结果:新闻标题-Huang Linqing is a handsome boy

.appsearchdemo D/MainActivity: 查询结果:新闻内容-Huang Linqing is an Android development engineer working

这里我们可以看到我们查询的关键字是handsome的时候将两个结果都打印出来了,而第一条结果是新闻标题包含handsome关键字,第二条结果是新闻内容包含关键字,如果我们使用普通的sql,大概需要这么做

select * from table where newsTitle like %key% or newsContent like %key%

而使用AppSearch 不需要关心具体匹配的是哪个字段,只要任一字段包含相关内容,就将结果显示出来,有点像百度搜索时,我们可以看到有些关键字是在标题中有些关键字是在内容中而这些内容都可以很快的查询出来。

我为什么夸自己

这里我们搜索的关键字是handsome,新闻标题是 Huang Linqing is a handsome boy,黄林晴是个帅气的男孩,这里我并不是故意夸我自己的,而是在学习AppSearch的使用时,我发现了一个bug,那就是上面的代码如果插入的是中文,在搜索时将不会得到任何结果,昨天晚上发现这个问题后我将此问题提给了Google

image.gif

Google 也很快给了答复

image.gif

不支持中文搜索,这是一个已知问题,并且Google将在新版本中修复,会尽快发布版本,所以我们在新版本发布前知道有这个问题就行了,避免无效检查自己的代码问题。

删除数据

删除数据时我们需指定命名空间和数据id,构建一个删除数据的请求,代码如下所示:

val deleteRequest = RemoveByDocumentIdRequest.Builder("new1")
    .addIds("new_id_2")
    .build()

image.gif

val removeFuture = Futures.transformAsync(
    sessionFuture, AsyncFunction {
        it?.remove(deleteRequest)
    },
    mainExecutor
)

image.gif

到这里,我们也看出来了,其实Appsearch的使用,对数据的操作都是先构建一个请求,然后使用Futures去执行,如果需要检测结果的话,就通过Futures.addCallback添加一个回调即可,这里执行删除操作后,我们再次通过关键字”handsome“去查询,会发现就只有一条数据显示出来了,这里执行结果就不在展示了。

关闭会话

在开始使用的使用,我们创建了一个

ListenableFuture<AppSearchSession>,后续所有的数据操作都通过这个会话去建立的,在使用结束后我们需要关闭这个会话,代码如下所示:

val closeFuture = Futures.transform<AppSearchSession, Unit>(
    sessionFuture,
    Function {
        it?.close()
    }, mainExecutor
)

image.gif

小结

AppSearch是Jetpack最新推出的组件,AppSearch 是一个搜索库,可以很方便的来实现应用内的搜索功能,AppSearch的 I/O 使用很低,与 SQLite 相比,AppSearch 可能会更高效。但目前个人还是认为针对的问题不同和解决问题的角度不同,和其他数据库没有可比性,选择合适的方案最重要。


目录
相关文章
|
数据采集 安全 Go
Go并发优化的9大技巧,效果立竿见影
Go并发优化的9大技巧,效果立竿见影
911 0
|
Android开发
【Android App】蓝牙的设备配对、音频传输、点对点通信的讲解及实战(附源码和演示 超详细)
【Android App】蓝牙的设备配对、音频传输、点对点通信的讲解及实战(附源码和演示 超详细)
3088 1
|
10月前
|
弹性计算 运维 自然语言处理
操作系统智能助手OS Copilot新功能测评
一文带你了解操作系统智能助手OS Copilot的三大新功能
446 10
|
安全 前端开发 API
ThinkPHP5 API模块开发规范与示例
【7月更文挑战第6天】本技术文档旨在指导开发者如何完全遵循ThinkPHP5框架的开发规范来构建RESTful API模块。ThinkPHP5(简称TP5)是一款基于PHP的轻量级MVC框架,其简洁、高效的特点非常适合快速开发Web应用及API接口。以下是创建API模块的基本步骤、最佳实践以及代码示例。
676 0
|
XML Android开发 数据格式
Android 中如何设置activity的启动动画,让它像dialog一样从底部往上出来
在 Android 中实现 Activity 的对话框式过渡动画:从底部滑入与从顶部滑出。需定义两个 XML 动画文件 `activity_slide_in.xml` 和 `activity_slide_out.xml`,分别控制 Activity 的进入与退出动画。使用 `overridePendingTransition` 方法在启动 (`startActivity`) 或结束 (`finish`) Activity 时应用这些动画。为了使前 Activity 保持静止,可定义 `no_animation.xml` 并在启动新 Activity 时仅设置新 Activity 的进入动画。
625 12
|
存储 Oracle 关系型数据库
Oracle索引知识看这一篇就足够
Oracle索引知识看这一篇就足够
|
人工智能 自然语言处理 API
动手实践:高效构建企业级AI搜索
本文介绍了基于阿里云 Elasticsearch的AI搜索产品能力、业务价值、场景应用,以及搭建演示等。
11797 5
|
缓存 UED 网络架构
网站404该怎么解决
网站404错误通常表示用户尝试访问的网页不存在或无法找到
1740 0
|
存储 关系型数据库 MySQL
MySQL索引的限制
【6月更文挑战第15天】MySQL索引的限制
619 3
|
存储 C++
C++底层原理
C++底层原理
507 0