在数字化办公场景中,快速定位文件、应用和系统设置是提升效率的关键。HarmonyOS 提供了融合搜索能力,支持应用内搜索和系统全局搜索,这为开发者打造跨设备、多模态的搜索工具奠定了基础。本文基于三层架构,使用 ArkUI 和 ArkTS 实现一个鸿蒙全局搜索工具,涵盖搜索界面、搜索引擎核心、索引管理及性能优化策略。
### 系统架构设计
全局搜索工具采用数据层、业务逻辑层和 UI 层的三层架构。数据层负责索引管理、搜索引擎和结果排序;业务逻辑层包含全局搜索调度器、跨设备协同引擎和意图识别模块;UI 层提供简洁直观的搜索界面,支持文本、语音、图像多模态输入。这种分层设计充分利用鸿蒙分布式特性,保证高性能和可扩展性。
### 核心功能实现
#### 搜索主界面
搜索界面使用 ArkUI 声明式组件构建,核心在于 `GlobalSearch` 结构体。它通过 `@State` 管理搜索关键词、历史记录和结果列表,采用 `@Builder` 分离搜索栏、历史记录和结果列表的布局。搜索栏使用 `Search` 组件,设置圆角样式和占位提示;历史记录区域渲染最近10条搜索记录,每条记录支持点击搜索和删除操作;结果列表采用 `List` 组件展示,每个结果项显示类型图标、标题和路径,点击后执行打开操作。- // GlobalSearch.ets
- @Entry
- @Component
- struct GlobalSearch {
- @State searchKeyword: string = ''
- @State searchHistory: string[] = ['鸿蒙系统开发', '性能优化', '分布式架构']
- @State searchResults: SearchResult[] = []
- @State isSearching: boolean = false
- build() {
- Column() {
- this.SearchBar()
- if (this.searchKeyword === '') { this.SearchHistory() }
- if (this.searchResults.length > 0) { this.SearchResults() }
- }
- .width('100%').height('100%').backgroundColor('#F5F5F5')
- }
- @Builder SearchBar() {
- Search({ placeholder: '搜索文件、应用、设置...', value: this.searchKeyword })
- .width('90%').height(50).margin({ top: 20, bottom: 10 })
- .backgroundColor(Color.White).borderRadius(25)
- .onChange((value: string) => {
- this.searchKeyword = value
- if (value.length > 0) { this.performSearch(value) }
- })
- .onSubmit(() => { this.addToHistory(this.searchKeyword) })
- }
- @Builder SearchHistory() {
- Column() {
- Text('搜索历史').fontSize(16).fontWeight(FontWeight.Bold).margin({ left: 20, top: 10 })
- ForEach(this.searchHistory, (item: string) => {
- Row() {
- Text(item).fontSize(14).margin({ left: 20 })
- Spacer()
- Image($r('app.media.ic_delete')).width(20).height(20).margin({ right: 20 })
- .onClick(() => { this.removeHistory(item) })
- }
- .width('100%').height(45).backgroundColor(Color.White).margin({ top: 5 })
- .onClick(() => {
- this.searchKeyword = item
- this.performSearch(item)
- })
- })
- }
- }
- @Builder SearchResults() {
- List() {
- ForEach(this.searchResults, (item: SearchResult) => {
- Row() {
- Image(this.getIconByType(item.type)).width(40).height(40).margin({ left: 15 })
- Column() {
- Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)
- Text(item.path).fontSize(12).fontColor('#999999').maxLines(1)
- }.margin({ left: 15 }).alignItems(HorizontalAlign.Start)
- Spacer()
- }
- .width('100%').height(70).backgroundColor(Color.White).margin({ top: 2 })
- .onClick(() => { this.openItem(item) })
- })
- }
- .width('100%').layoutWeight(1)
- }
- private performSearch(keyword: string) {
- this.isSearching = true
- setTimeout(() => {
- this.searchResults = [
- { title: '鸿蒙开发指南.pdf', path: '/文档/技术资料/鸿蒙开发指南.pdf', type: 'document', size: '2.3 MB' },
- { title: '系统设置', path: '设置/系统/显示', type: 'setting', size: '' },
- { title: 'DevEco Studio', path: '/应用/开发工具/DevEco Studio', type: 'application', size: '1.2 GB' }
- ]
- this.isSearching = false
- }, 300)
- }
- private addToHistory(keyword: string) {
- if (!this.searchHistory.includes(keyword)) {
- this.searchHistory.unshift(keyword)
- if (this.searchHistory.length > 10) { this.searchHistory.pop() }
- }
- }
- private removeHistory(item: string) {
- this.searchHistory = this.searchHistory.filter(h => h !== item)
- }
- private getIconByType(type: string): Resource {
- switch(type) {
- case 'document': return $r('app.media.ic_document')
- case 'application': return $r('app.media.ic_app')
- case 'setting': return $r('app.media.ic_setting')
- default: return $r('app.media.ic_file')
- }
- }
- private openItem(item: SearchResult) {
- console.info(`Opening: ${item.title}`)
- }
- }
- class SearchResult {
- title: string = ''
- path: string = ''
- type: string = ''
- size: string = ''
- }
复制代码
#### 搜索引擎核心模块
搜索引擎类 `SearchEngine` 封装了文件、应用、设置三类内容的并行搜索逻辑。它使用 `IndexManager` 管理索引,通过 `KeywordAnalyzer` 进行分词。搜索时根据 `SearchOptions` 分别调用 `searchFiles`、`searchApplications`、`searchSettings` 方法,最后对结果排序去重。- // SearchEngine.ets
- export class SearchEngine {
- private indexManager: IndexManager
- private keywordAnalyzer: KeywordAnalyzer
- constructor() {
- this.indexManager = new IndexManager()
- this.keywordAnalyzer = new KeywordAnalyzer()
- }
- async search(keyword: string, options: SearchOptions): Promise<SearchResult[]> {
- const keywords = this.keywordAnalyzer.analyze(keyword)
- const results: SearchResult[] = []
- if (options.searchFiles) {
- const fileResults = await this.searchFiles(keywords)
- results.push(...fileResults)
- }
- if (options.searchApps) {
- const appResults = await this.searchApplications(keywords)
- results.push(...appResults)
- }
- if (options.searchSettings) {
- const settingResults = await this.searchSettings(keywords)
- results.push(...settingResults)
- }
- return this.rankAndDeduplicate(results)
- }
- private async searchFiles(keywords: string[]): Promise<SearchResult[]> {
- try {
- const files = await fileio.getDirEntries('/data')
- const results: SearchResult[] = []
- for (const file of files) {
- if (this.matchKeywords(file.name, keywords)) {
- results.push({ title: file.name, path: file.path, type: 'document', size: this.formatSize(file.size) })
- }
- }
- return results
- } catch (error) {
- console.error('File search error:', error)
- return []
- }
- }
- private async searchApplications(keywords: string[]): Promise<SearchResult[]> {
- const bundleManager = bundle.getBundleManager()
- const bundles = await bundleManager.getInstalledBundles()
- return bundles
- .filter(bundle => this.matchKeywords(bundle.appName, keywords))
- .map(bundle => ({
- title: bundle.appName,
- path: bundle.bundleName,
- type: 'application',
- size: this.formatSize(bundle.size)
- }))
- }
- private async searchSettings(keywords: string[]): Promise<SearchResult[]> {
- const settings = [
- { name: '显示设置', path: '设置/系统/显示' },
- { name: '网络设置', path: '设置/连接/网络' },
- { name: '声音设置', path: '设置/声音/音量' }
- ]
- return settings
- .filter(setting => this.matchKeywords(setting.name, keywords))
- .map(setting => ({ title: setting.name, path: setting.path, type: 'setting', size: '' }))
- }
- private matchKeywords(text: string, keywords: string[]): boolean {
- const lowerText = text.toLowerCase()
- return keywords.some(keyword => lowerText.includes(keyword.toLowerCase()))
- }
- private rankAndDeduplicate(results: SearchResult[]): SearchResult[] {
- const uniqueResults = Array.from(new Map(results.map(item => [item.path, item])).values())
- return uniqueResults.sort((a, b) => a.title.localeCompare(b.title))
- }
- private formatSize(bytes: number): string {
- if (bytes < 1024) return bytes + ' B'
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'
- return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
- }
- }
- class SearchOptions {
- searchFiles: boolean = true
- searchApps: boolean = true
- searchSettings: boolean = true
- }
复制代码
#### 索引管理模块
索引管理器 `IndexManager` 在启动时构建全量索引,覆盖文件系统、已安装应用和系统设置。它使用 Map 存储索引条目,支持增删改操作。索引构建采用异步方式,分别调用 `indexFileSystem`、`indexApplications`、`indexSystemSettings`。文件路径包括 `/data`、`/storage`、`/cache`;应用通过 `bundle.getBundleManager()` 获取已安装包;设置项预定义了常用系统设置名称。- // IndexManager.ets
- export class IndexManager {
- private indexDB: Map<string, IndexEntry>
- private isIndexing: boolean = false
- constructor() {
- this.indexDB = new Map()
- this.startIndexing()
- }
- async startIndexing() {
- if (this.isIndexing) return
- this.isIndexing = true
- try {
- await this.indexFileSystem()
- await this.indexApplications()
- await this.indexSystemSettings()
- console.info('Indexing completed')
- } catch (error) {
- console.error('Indexing error:', error)
- } finally {
- this.isIndexing = false
- }
- }
- private async indexFileSystem() {
- const directories = ['/data', '/storage', '/cache']
- for (const dir of directories) {
- try {
- const entries = await fileio.getDirEntries(dir)
- for (const entry of entries) {
- this.indexDB.set(entry.path, {
- name: entry.name,
- path: entry.path,
- type: this.getFileType(entry.name),
- lastModified: entry.mtime,
- size: entry.size
- })
- }
- } catch (error) {
- console.error(`Failed to index ${dir}:`, error)
- }
- }
- }
- private async indexApplications() {
- const bundleManager = bundle.getBundleManager()
- const bundles = await bundleManager.getInstalledBundles()
- for (const bundle of bundles) {
- this.indexDB.set(bundle.bundleName, {
- name: bundle.appName,
- path: bundle.bundleName,
- type: 'application',
- lastModified: bundle.installTime,
- size: bundle.size
- })
- }
- }
- private indexSystemSettings() {
- const settings = [
- { name: '显示设置', path: 'settings/display' },
- { name: '网络设置', path: 'settings/network' },
- { name: '声音设置', path: 'settings/sound' }
- ]
- for (const setting of settings) {
- this.indexDB.set(setting.path, {
- name: setting.name,
- path: setting.path,
- type: 'setting',
- lastModified: Date.now(),
- size: 0
- })
- }
- }
- private getFileType(filename: string): string {
- const ext = filename.split('.').pop()?.toLowerCase() || ''
- const imageExts = ['jpg', 'png', 'gif', 'webp']
- const docExts = ['pdf', 'doc', 'docx', 'txt']
- const videoExts = ['mp4', 'avi', 'mkv']
- if (imageExts.includes(ext)) return 'image'
- if (docExts.includes(ext)) return 'document'
- if (videoExts.includes(ext)) return 'video'
- return 'file'
- }
- async updateIndex(path: string, operation: 'add' | 'update' | 'delete') {
- if (operation === 'delete') {
- this.indexDB.delete(path)
- } else {
- try {
- const info = await fileio.getFileInfo(path)
- this.indexDB.set(path, {
- name: info.name,
- path: path,
- type: this.getFileType(info.name),
- lastModified: info.mtime,
- size: info.size
- })
- } catch (error) {
- console.error('Update index error:', error)
- }
- }
- }
- }
- class IndexEntry {
- name: string = ''
- path: string = ''
- type: string = ''
- lastModified: number = 0
- size: number = 0
- }
复制代码
### 性能优化策略
#### 增量索引更新
鸿蒙系统支持实时监听文件系统变化(如 `FileWatcher`),我们采用增量索引策略,仅在文件创建、修改或删除时更新对应索引,避免全量重建导致的大量 I/O 和 CPU 开销。
#### 异步搜索与缓存
搜索结果使用 `SearchCache` 类缓存,默认缓存5分钟,减少重复搜索时的延迟。缓存以关键词为 key,存储结果和时间戳,在缓存有效期内直接返回。- class SearchCache {
- private cache: Map<string, { results: SearchResult[], timestamp: number }>
- private readonly CACHE_TTL = 5 * 60 * 1000 // 5分钟
- get(keyword: string): SearchResult[] | null {
- const cached = this.cache.get(keyword)
- if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
- return cached.results
- }
- return null
- }
- set(keyword: string, results: SearchResult[]) {
- this.cache.set(keyword, {
- results,
- timestamp: Date.now()
- })
- }
- }
复制代码
#### 多线程搜索
利用鸿蒙的并发能力(如 `TaskPool` 或 `Worker`),将文件搜索、应用搜索、设置搜索分配给不同线程并行执行,减少总搜索时间。上述 `SearchEngine.search` 中已使用 `async/await` 实现逻辑上的并发,实际部署时可创建独立线程以提升响应速度。
### 功能扩展与优化
#### 智能排序算法
排序时综合考虑搜索频率、最后访问时间、关键词匹配度(如标题优先、路径匹配次要),可参考 Lucene 的 TF-IDF 思想,在 `rankAndDeduplicate` 中实现加权评分。
#### 跨设备搜索
借助鸿蒙分布式能力(如 `DistributedDataManager` 或 `RemoteObject`),将索引和搜索请求同步到其他设备,实现手机、平板、PC 间无缝搜索。数据层需要支持跨设备索引同步。
#### 语音搜索集成
通过鸿蒙语音引擎 `@ohos.voiceEngine` 接收语音输入,转换为文本后再调用搜索接口。实现时需申请麦克风权限,并处理语音识别的回调结果。- import voiceEngine from '@ohos.voiceEngine'
- async function startVoiceSearch(): Promise<string> {
- return new Promise((resolve, reject) => {
- voiceEngine.startListening({
- onResult: (text: string) => resolve(text),
- onError: (err: Error) => reject(err)
- })
- })
- }
复制代码
### 总结
本文基于 HarmonyOS 的三层架构和 ArkUI/ArkTS 技术,实现了一个支持文件、应用、设置搜索的全局搜索工具。通过索引管理、并行搜索、缓存和增量更新等策略保证了性能,并可扩展语音和跨设备搜索能力。开发者可根据实际需求调整搜索范围、排序算法和 UI 样式,打造个性化的桌面搜索体验。 |