AppSearch of Jetpack Series
Memory Distribution of JVM Runtime Introduction:
Foreword
Memory Distribution of JVM Runtime.At this year's Google I/O conference, the Jetpack library added three new components ( the Alpha version has just been released ), namely MarcrobenChmark , AppSearch and Google Shortcuts. The MarcrobenChmark component is a library used to measure code performance. Google Shortcuts sounds like It is a shortcut. In this article, we will focus on leading you to appreciate the use of AppSearch. So what is AppSearch?
According to the official description , AppSearch is a search library for managing locally stored structured data, which contains APIs for indexing and retrieving data through full-text search. You can use this library to build custom in-app search functionality for users. When I saw the in-app search, I first thought of the search page in the Android settings. For example, when we search for two words, all function entries containing the word "display" will be displayed here, as shown in Figure 1:
Next, let's take a detailed look at how to use AppSearch and the pits I have stepped on.
Import related libraries
First, we introduce the relevant library of the AppSearch component in build.gradle, the code is as follows:
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")
In AppSearch, a unit of data is represented as a document. Each document in the AppSearch database is uniquely identified by its namespace and ID. Namespaces are used to separate data from different sources, which is equivalent to tables in SQL. So let's create a data unit next.
Create a data unit
Let's take the news class as an example, and the created data class is as follows :
@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
)
Memory Distribution of JVM Runtime.First of all, all data units in AppSearch must be annotated with @Document. Namespace and id are required fields of the data type as mentioned above. newsTitle and newsContent are the news title and news content fields that we define by ourselves, which are mentioned here.
@Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES )
This annotation, @Document.StringProperty is to configure the variable of type string as the property of AppSearch, if it is an integer, it is
@Document.Int64Property
boolean type is
@Document.BooleanProperty
Wait, etc. , the indexingType attribute value can be understood as the matching method, which is set to INDEXING_TYPE_PREFIXES here. For example, when the matching condition is Huang, HuangLinqing can be matched. If you are interested in other attributes, you can see the source code androidx.appsearch.app.AppSearchSchema class. After creating the data class, as with other database operations, the next step is to create a database.
create database
Creating a database will return us a ListenableFuture for the operation of the entire database. The code is as follows:
val context: Context = applicationContext
val sessionFuture = LocalStorage.createSearchSession(
LocalStorage.SearchContext.Builder (context, /*databaseName=*/"news")
.build ()
)
At this point we can see that this line of code reported an error, the error is as follows:
It roughly means that you still need to rely on a library. To be honest, the AppSearch library can be completely depended on by yourself, which is much more convenient for developers, but after all, AppSearch has just released the beta version, so the requirements cannot be too high.
We introduce the guava library in build.gradle, the code is as follows:
implementation(" com.google.guava :guava:30.1.1-android")
After the dependency, the above code can run normally, but if it runs, it will not work here. We can set the environment of java1.8, otherwise the error of java.lang.NoSuchMethodError: No static method metafactory will appear in the subsequent operation.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
I mentioned the original issue to Google, see https://issuetracker.google.com/issues/191389033
set data mode
AppSearch has the concept of Schema and schema types, which means schema and schema type. Schema consists of schema types that represent unique data types. This refers to the News class. Schema types consist of attributes including name, data type and cardinality. Setting the data mode here is actually specifying what type of data we can add to the data space named "news".
val setChemaRequest = SetSchemaRequest
.Builder ()
.addDocumentClasses (News::class.java).build()
var setSchemaFuture = Futures.transformAsync(
sessionFuture,
AsyncFunction< AppSearchSession?, SetSchemaResponse?> {
it? .setSchema(setChemaRequest)
},
mainExecutor
)
First, we created a schema class with the data type News class, and then set the data schema for the data document through the setSchema method of AppSearchSession. The place where everyone is confused may be the method Futures.transformAsync, which is actually very simple. Future is a Java The asynchronous thread framework can be compared to coroutines, so it may be much simpler to use if AppSearch can be designed without relying on Future.
But what makes me different is that I consulted a few friends who do Java, and they all said that this thing is rarely used. So here we only focus on the use of AppSearch and the use of Futures-related classes. Those who are interested can learn more.
Once the data schema is set, we can write data.
data input
We start by defining a data class to insert as follows:
val new1 = News(
namespace = "new1",
id = "new_id_2",
newsTitle = "who is a boy",
newsContent = "Everyone, guess who is the handsome boy"
)
Build the PutDocumentsRequest object and execute
val putRequest = PutDocumentsRequest.Builder( ).addDocuments (new1).build()
val putFuture = Futures.transformAsync(
sessionFuture,
AsyncFunction< AppSearchSession?, AppSearchBatchResult?> {
it? .put(putRequest)
},
mainExecutor
)
We can monitor the execution result through Futures.addCallback, the method is as follows:
Futures.addCallback(
putFuture,
object : FutureCallback?> {
override fun onSuccess(result: AppSearchBatchResult?) {
// 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
)
When run, the program prints as follows:
com.lonbon.appsearchdemo D/MainActivity: Success: {new_id_1=null}
com.lonbon.appsearchdemo D/MainActivity: failed: {}
It means that the storage is successful. Next, we insert another piece of data. The inserted code is the same, so it will not be displayed repeatedly. The data is as follows:
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"
)
Query data
To query data, first of all, we need to specify the scope of the query to be namespace, which is equivalent to specifying a data table. After all, there may be the same qualified data in different tables.
val searchSpec = SearchSpec.Builder()
.addFilterNamespaces ("new1")
.build ()
Then execute the query operation, the keyword we query here is " handsome"
val searchFuture = Futures.transform(
sessionFuture,
Function< AppSearchSession?, SearchResults> {
it? .search("handsome", searchSpec)
},
mainExecutor
)
Similarly, we use the addCallback method to detect the query result, the code is as follows:
Futures.addCallback(
searchFuture,
object : FutureCallback {
override fun onSuccess( result: SearchResults?) {
iterateSearchResults(result)
}
override fun onFailure( t: Throwable) {
Log.d(
TAG, "Query failed:" + t
.message
)
}
},
mainExecutor
)
If the query is successful, it will return the SearchResults class. We need to traverse this instance to take out all the data and print it out, that is, the iterateSearchResults method. The code is as follows:
private fun iterateSearchResults( searchResults: SearchResults?) {
Futures.transform( searchResults? .nextPage , Function
AppSearch of Jetpack Series
Foreword
Memory Distribution of JVM Runtime.At this year's Google I/O conference, the Jetpack library added three new components ( the Alpha version has just been released ), namely MarcrobenChmark , AppSearch and Google Shortcuts. The MarcrobenChmark component is a library used to measure code performance. Google Shortcuts sounds like It is a shortcut. In this article, we will focus on leading you to appreciate the use of AppSearch. So what is AppSearch?
Memory Distribution of JVM Runtime.What is AppSearch
According to the official description , AppSearch is a search library for managing locally stored structured data, which contains APIs for indexing and retrieving data through full-text search. You can use this library to build custom in-app search functionality for users. When I saw the in-app search, I first thought of the search page in the Android settings. For example, when we search for two words, all function entries containing the word "display" will be displayed here, as shown in Figure 1:
Memory Distribution of JVM Runtime.Search within settings
Next, let's take a detailed look at how to use AppSearch and the pits I have stepped on.
Import related libraries
First, we introduce the relevant library of the AppSearch component in build.gradle, the code is as follows:
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")
In AppSearch, a unit of data is represented as a document. Each document in the AppSearch database is uniquely identified by its namespace and ID. Namespaces are used to separate data from different sources, which is equivalent to tables in SQL. So let's create a data unit next.
Create a data unit
Let's take the news class as an example, and the created data class is as follows :
@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
)
Memory Distribution of JVM Runtime.First of all, all data units in AppSearch must be annotated with @Document. Namespace and id are required fields of the data type as mentioned above. newsTitle and newsContent are the news title and news content fields that we define by ourselves, which are mentioned here.
@Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES )
This annotation, @Document.StringProperty is to configure the variable of type string as the property of AppSearch, if it is an integer, it is
@Document.Int64Property
boolean type is
@Document.BooleanProperty
Wait, etc. , the indexingType attribute value can be understood as the matching method, which is set to INDEXING_TYPE_PREFIXES here. For example, when the matching condition is Huang, HuangLinqing can be matched. If you are interested in other attributes, you can see the source code androidx.appsearch.app.AppSearchSchema class. After creating the data class, as with other database operations, the next step is to create a database.
create database
Creating a database will return us a ListenableFuture for the operation of the entire database. The code is as follows:
val context: Context = applicationContext
val sessionFuture = LocalStorage.createSearchSession(
LocalStorage.SearchContext.Builder (context, /*databaseName=*/"news")
.build ()
)
At this point we can see that this line of code reported an error, the error is as follows:
It roughly means that you still need to rely on a library. To be honest, the AppSearch library can be completely depended on by yourself, which is much more convenient for developers, but after all, AppSearch has just released the beta version, so the requirements cannot be too high.
We introduce the guava library in build.gradle, the code is as follows:
implementation(" com.google.guava :guava:30.1.1-android")
After the dependency, the above code can run normally, but if it runs, it will not work here. We can set the environment of java1.8, otherwise the error of java.lang.NoSuchMethodError: No static method metafactory will appear in the subsequent operation.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Kotlin projects
kotlinOptions {
jvmTarget = "1.8"
}
I mentioned the original issue to Google, see https://issuetracker.google.com/issues/191389033
set data mode
AppSearch has the concept of Schema and schema types, which means schema and schema type. Schema consists of schema types that represent unique data types. This refers to the News class. Schema types consist of attributes including name, data type and cardinality. Setting the data mode here is actually specifying what type of data we can add to the data space named "news".
val setChemaRequest = SetSchemaRequest
.Builder ()
.addDocumentClasses (News::class.java).build()
var setSchemaFuture = Futures.transformAsync(
sessionFuture,
AsyncFunction< AppSearchSession?, SetSchemaResponse?> {
it? .setSchema(setChemaRequest)
},
mainExecutor
)
First, we created a schema class with the data type News class, and then set the data schema for the data document through the setSchema method of AppSearchSession. The place where everyone is confused may be the method Futures.transformAsync, which is actually very simple. Future is a Java The asynchronous thread framework can be compared to coroutines, so it may be much simpler to use if AppSearch can be designed without relying on Future.
But what makes me different is that I consulted a few friends who do Java, and they all said that this thing is rarely used. So here we only focus on the use of AppSearch and the use of Futures-related classes. Those who are interested can learn more.
Once the data schema is set, we can write data.
data input
We start by defining a data class to insert as follows:
val new1 = News(
namespace = "new1",
id = "new_id_2",
newsTitle = "who is a boy",
newsContent = "Everyone, guess who is the handsome boy"
)
Build the PutDocumentsRequest object and execute
val putRequest = PutDocumentsRequest.Builder( ).addDocuments (new1).build()
val putFuture = Futures.transformAsync(
sessionFuture,
AsyncFunction< AppSearchSession?, AppSearchBatchResult
it? .put(putRequest)
},
mainExecutor
)
We can monitor the execution result through Futures.addCallback, the method is as follows:
Futures.addCallback(
putFuture,
object : FutureCallback
override fun onSuccess(result: AppSearchBatchResult
// 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
)
When run, the program prints as follows:
com.lonbon.appsearchdemo D/MainActivity: Success: {new_id_1=null}
com.lonbon.appsearchdemo D/MainActivity: failed: {}
It means that the storage is successful. Next, we insert another piece of data. The inserted code is the same, so it will not be displayed repeatedly. The data is as follows:
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"
)
Query data
To query data, first of all, we need to specify the scope of the query to be namespace, which is equivalent to specifying a data table. After all, there may be the same qualified data in different tables.
val searchSpec = SearchSpec.Builder()
.addFilterNamespaces ("new1")
.build ()
Then execute the query operation, the keyword we query here is " handsome"
val searchFuture = Futures.transform(
sessionFuture,
Function< AppSearchSession?, SearchResults> {
it? .search("handsome", searchSpec)
},
mainExecutor
)
Similarly, we use the addCallback method to detect the query result, the code is as follows:
Futures.addCallback(
searchFuture,
object : FutureCallback
override fun onSuccess( result: SearchResults?) {
iterateSearchResults(result)
}
override fun onFailure( t: Throwable) {
Log.d(
TAG, "Query failed:" + t
.message
)
}
},
mainExecutor
)
If the query is successful, it will return the SearchResults class. We need to traverse this instance to take out all the data and print it out, that is, the iterateSearchResults method. The code is as follows:
private fun iterateSearchResults( searchResults: SearchResults?) {
Futures.transform( searchResults? .nextPage , Function
- , 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,
"Query result: news title-" + note.newsTitle
)
Log.d(
TAG,
"Query result: news content-" + note.newsContent
)
} catch (e: AppSearchException) {
Log.e(
TAG,
"Failed to convert GenericDocument to Note",
e
)
}
}
}
}
}, mainExecutor)
}
The result of the query is a set, so we need to traverse the set, and the data type needs to be the News class before we can continue to the next step, here we print out the news titles that meet the conditional query, and the results are as follows:
D/MainActivity: Query result: news title-who is a boy
.appsearchdemo D/MainActivity: Query result: news content-Everyone, guess who is the handsome boy
.appsearchdemo D/MainActivity: Query result: News title-Huang Linqing is a handsome boy
.appsearchdemo D/MainActivity: Query result: news content-Huang Linqing is an Android development engineer working
Here we can see that when the keyword we query is handsome, both results are printed, and the first result is that the news title contains the keyword handsome, and the second result is that the news content contains the keyword. If we use Ordinary sql, probably need to do this
select * from table where newsTitle like %key% or newsContent like %key%
When using AppSearch, you don't need to care about which field is matched. As long as any field contains relevant content, the result will be displayed. It's a bit like Baidu search. We can see that some keywords are in the title and some keywords are in the content. And these contents can be quickly queried out.
why do i brag about myself
Here the keyword we are searching for is handsome, the news title is Huang Linqing is a handsome boy, Huang Linqing is a handsome boy, here I am not trying to brag about myself, but when I was learning how to use AppSearch, I found a bug, That is, if the above code is inserted in Chinese, it will not get any results when searching. After I found this problem last night, I raised this problem to Google
Chinese search is not supported, this is a known problem, and Google will fix it in the new version and release the version as soon as possible, so we only need to know this problem before the new version is released, so as to avoid invalid checking our own code problems.
delete data
When deleting data, we need to specify the namespace and data id, and construct a request to delete data. The code is as follows:
val deleteRequest = RemoveByDocumentIdRequest.Builder("new1")
.addIds ("new_id_2")
.build ()
val removeFuture = Futures.transformAsync(
sessionFuture, AsyncFunction {
it? .remove(deleteRequest)
},
mainExecutor
)
At this point, we can also see that, in fact, the use of Appsearch, the operation of data is to construct a request first, and then use Futures to execute it. If you need to detect the result, you can add a callback through Futures.addCallback, which is executed here. After the delete operation, we use the keyword " handsome" to query again, and we will find that only one piece of data is displayed, and the execution result is not displayed here.
close session
To get started, we created a
ListenableFuture
val closeFuture = Futures.transform
sessionFuture,
Function {
it? .close()
}, mainExecutor
)
summary
Memory Distribution of JVM Runtime.AppSearch is the latest component launched by Jetpack. AppSearch is a search library that can easily implement in-app search functions. The I/O usage of AppSearch is very low. Compared with SQLite, AppSearch may be more efficient. But at present, I still think that the problem is different and the angle of solving the problem is different, and there is no comparability with other databases. It is the most important to choose the appropriate solution.
Related Articles
-
A detailed explanation of Hadoop core architecture HDFS
Knowledge Base Team
-
What Does IOT Mean
Knowledge Base Team
-
6 Optional Technologies for Data Storage
Knowledge Base Team
-
What Is Blockchain Technology
Knowledge Base Team
Explore More Special Offers
-
Short Message Service(SMS) & Mail Service
50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00