一、简介
① 基本概念
- HarmonyOS 融合搜索为开发者提供搜索引擎级的全文搜索能力,可支持应用内搜索和系统全局搜索,为用户提供更加准确、高效的搜索体验。
- 全文索引:记录字或词的位置和次数等属性,建立的倒排索引。
- 全文搜索:通过全文索引进行匹配查找结果的一种搜索引擎技术。
- 全局搜索:可以在系统全局统一的入口进行的搜索行为。
- 全局搜索应用:HarmonyOS 上提供全局搜索入口的应用,一般为桌面下拉框或悬浮搜索框。
- 索引源应用:通过融合搜索索引接口对其数据建立索引的应用。
- 可搜索配置:每个索引源应用应该提供一个包括应用包名、是否支持全局搜索等信息的可搜索实体,以便全局搜索应用发起搜索。
- 群组:经过认证的可信设备圈,可从账号模块获取群组 ID。
- 索引库:一种搜索引擎的倒排索引库,包含多个索引文件的整个目录构成一个索引库。
- 索引域:索引数据的字段名,比如一张图片有文件名、存储路径、大小、拍摄时间等,文件名就是其中的一个索引域。
- 索引属性:描述索引域的信息,包括索引类型、是否为主键、是否存储、是否支持分词等。
② 运作机制
- 索引源应用通过融合搜索接口设置可搜索实体,并为其数据内容构建全文索引。
- 全局搜索应用接收用户发起的搜索请求,遍历支持全局搜索的可搜索实体,解析用户输入并构造查询条件,最后通过融合搜索接口获取各应用搜索结果。
- 融合搜索运作如下图所示:
③ 权限与限制
- 构建索引或者发起搜索前,索引源应用必须先设置索引属性,并且必须有且仅有一个索引域设置为主键,且主键索引域不能分词,索引和搜索都会使用到索引属性。
- 索引源应用的数据发生变动时,开发者应同步通过融合搜索索引接口更新索引,以保证索引和应用原始数据的一致性。
- 批量创建、更新、删除索引时,应控制单次待索引内容大小,建议分批创建索引,防止内存溢出。
- 分页搜索和分组搜索应控制每页返回结果数量,防止内存溢出。
- 构建和搜索本机索引时,应该使用提供的 SearchParameter.DEFAULT_GROUP 作为群组 ID,分布式索引使用通过账号模块获取的群组 ID。
- 搜索时需先创建搜索会话,并务必在搜索结束时关闭搜索会话,释放内存资源。
使用融合搜索服务接口需要在“config.json”配置文件中添加“ohos.permission.ACCESS_SEARCH_SERVICE”权限。
- 搜索时的 SearchParameter.DEVICE_ID_LIST 必须与创建索引时的 deviceId 一致。
④ 应用场景
- 索引源应用,一般为有持久化数据的应用,可以通过融合搜索接口为其应用数据建立索引,并配置全局搜索可搜索实体,帮助用户通过全局搜索应用查找本应用内的数据。
- 应用本身提供搜索框时,也可直接在应用内部通过融合搜索接口实现全文搜索功能。
二、融合搜索 API
类名 |
接口名 |
描述 |
SearchAbility |
public List insert(String groupId, String bundleName, List indexDataList) |
索引插入 |
public List update(String groupId, String bundleName, List indexDataList) |
索引更新 |
public List delete(String groupId, String bundleName, List indexDataList) |
索引删除 |
SearchSession |
public int getSearchHitCount(String queryJsonStr) |
搜索命中结果数量 |
public List search(String queryJsonStr, int start, int limit) |
分页搜索 |
public List groupSearch(String queryJsonStr, int groupLimit) |
分组搜索 |
三、融合搜索流程
- 实例化 SearchAbility,连接融合搜索服务:
SearchAbility searchAbility = new SearchAbility(context);
String bundleName = context.getBundleName();
CountDownLatch lock = new CountDownLatch(1);
searchAbility.connect(new ServiceConnectCallback() {
@Override
public void onConnect() {
lock.countDown();
}
@Override
public void onDisconnect() {
}
});
try {
lock.await(3000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
HiLog.error(LABEL, "await failed, %{public}s", e.getMessage());
}
List<IndexForm> indexFormList = new ArrayList<IndexForm>() { {
add(new IndexForm("id", IndexType.NO_ANALYZED, true, true, false));
add(new IndexForm("title", IndexType.ANALYZED, false, true, true));
add(new IndexForm("tag", IndexType.SORTED, false, true, false));
add(new IndexForm("ocr_text", IndexType.SORTED_NO_ANALYZED, false, true, false));
add(new IndexForm("datetaken", IndexType.LONG, false, true, false));
add(new IndexForm("bucket_id", IndexType.INTEGER, false, true, false));
add(new IndexForm("latitude", IndexType.FLOAT, false, true, false));
add(new IndexForm("longitude", IndexType.DOUBLE, false, true, false));
} };
int result = searchAbility.setIndexForm(bundleName, 1, indexFormList);
List<IndexData> indexDataList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
IndexData indexData = new IndexData();
indexData.put("id", "id" + i);
indexData.put("title", "title" + i);
indexData.put("tag", "tag" + i);
indexData.put("ocr_text", "ocr_text" + i);
indexData.put("datetaken", System.currentTimeMillis());
indexData.put("bucket_id", i);
indexData.put("latitude", i / 5.0 * 180);
indexData.put("longitude", i / 5.0 * 360);
indexDataList.add(indexData);
}
List<IndexData> failedList = searchAbility.insert(SearchParameter.DEFAULT_GROUP, bundleName, indexDataList);
JSONObject jsonObject = new JSONObject();
JSONObject query = new JSONObject();
query.put("天空", new JSONArray(Arrays.asList("title", "tag")));
jsonObject.put(SearchParameter.QUERY, query);
JSONArray filterCondition = new JSONArray();
JSONObject filter1 = new JSONObject();
filter1.put("bucket_id", new JSONArray(Arrays.asList(0, 1, 2)));
filter1.put("id", new JSONArray(Arrays.asList(0, 1)));
filterCondition.put(filter1);
JSONObject filter2 = new JSONObject();
filter2.put("tag", new JSONArray(Arrays.asList("白云")));
filter2.put("ocr_text", new JSONArray(Arrays.asList("白云")));
filterCondition.put(filter2);
jsonObject.put(SearchParameter.FILTER_CONDITION, filterCondition);
JSONObject deviceId = new JSONObject();
deviceId.put("device_id", new JSONArray(Arrays.asList("localDeviceId")));
jsonObject.put(SearchParameter.DEVICE_ID_LIST, deviceId);
JSONObject latitudeObject = new JSONObject();
latitudeObject.put(SearchParameter.LOWER, -40.0f);
latitudeObject.put(SearchParameter.UPPER, 40.0f);
jsonObject.put("latitude", latitudeObject);
JSONObject longitudeObject = new JSONObject();
longitudeObject.put(SearchParameter.LOWER, -90.0);
longitudeObject.put(SearchParameter.UPPER, 90.0);
jsonObject.put("longitude", longitudeObject);
JSONObject order = new JSONObject();
order.put("id", SearchParameter.ASC);
order.put("datetaken", SearchParameter.DESC);
jsonObject.put(SearchParameter.ORDER_BY, order);
jsonObject.put(SearchParameter.GROUP_FIELD_LIST, new JSONArray(Arrays.asList("tag", "ocr_text")));
String queryJsonStr = jsonObject.toString();
SearchSession searchSession = searchAbility.beginSearch(SearchParameter.DEFAULT_GROUP, bundleName);
if (searchSession == null) {
return;
}
try {
int hit = searchSession.getSearchHitCount(queryJsonStr);
int batch = 50;
for (int i = 0; i < hit; i += batch) {
List<IndexData> searchResult = searchSession.search(queryJsonStr, i, batch);
}
int groupLimit = 10;
List<Recommendation> recommendationResult = searchSession.groupSearch(queryJsonStr, groupLimit);
} finally {
searchAbility.endSearch(SearchParameter.DEFAULT_GROUP, bundleName, searchSession);
}