Skip to content

Commit

Permalink
feat: Improve search performances
Browse files Browse the repository at this point in the history
Since #6,
we noticed a severe performance degradation when performing search.

We measured the following:
- Flexsearch search: <ms
- Query cozy-client store to get file paths: ~120ms for 60 files.
- Results normalization: ~3ms

We used to make multiple `getById` queries to get file path, each of
them taking few ms. So we tried to use a `getByIds` instead, and got
~70ms per query, which is better, but still slow.
Then, we tried the following:
```
  const allFiles = client.getCollectionFromState(FILES_DOCTYPE) as IOCozyFile[]
  const dirs = allFiles.filter(file => file.type === TYPE_DIRECTORY)
  return dirs.filter(dir => dirIds.includes(dir._id))
```

This takes 2ms.

We should eventually investigate why the store queries are so slow, but
for now let's use this trick.
  • Loading branch information
paultranvan committed Oct 30, 2024
1 parent 05534bd commit 5d619aa
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 49 deletions.
6 changes: 5 additions & 1 deletion src/@types/cozy-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,14 @@ declare module 'cozy-client' {
queryDefinition: QueryDefinition,
options?: QueryOptions
) => Promise<QueryResult>
queryAll: <T>(queryDefinition: QueryDefinition) => Promise<T>
queryAll: <T>(
queryDefinition: QueryDefinition,
options?: QueryOptions
) => Promise<T>
links: CozyLink[]
capabilities: ClientCapabilities
registerPlugin: (Plugin: Function, options: unknown) => void
getCollectionFromState: (doctype: string) => unknown
}

export const createMockClient = (options?: ClientOptions): CozyClient =>
Expand Down
2 changes: 1 addition & 1 deletion src/dataproxy/common/DataProxyInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ClientCapabilities } from 'cozy-client/types/types'
export type { SearchIndexes } from '@/search/types'

export interface DataProxyWorker {
search: (query: string) => Promise<unknown>
search: (query: string) => unknown
setClient: (clientData: ClientData) => Promise<void>
}

Expand Down
9 changes: 6 additions & 3 deletions src/dataproxy/worker/shared-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const dataProxy: DataProxyWorker = {
updateState()
},

search: async (query: string) => {
search: (query: string) => {
if (!client) {
throw new Error(
'Client is required to execute a search, please initialize CozyClient'
Expand All @@ -75,8 +75,11 @@ const dataProxy: DataProxyWorker = {
if (!searchEngine) {
throw new Error('SearchEngine is not initialized')
}

return searchEngine.search(query)
const startSearchTime = performance.now()
const results = searchEngine.search(query)
const endSearchTime = performance.now()
log.debug(`Search took ${endSearchTime - startSearchTime} ms`)
return results
}
}

Expand Down
13 changes: 7 additions & 6 deletions src/search/SearchEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
isSearchedDoctype
} from '@/search/types'

import { shouldKeepFile } from './helpers/normalizeFile'
import { addFilePaths, shouldKeepFile } from './helpers/normalizeFile'

const log = Minilog('🗂️ [Indexing]')

Expand Down Expand Up @@ -254,9 +254,7 @@ class SearchEngine {
return this.searchIndexes
}

async search(query: string): Promise<SearchResult[]> {
log.debug('[SEARCH] indexes : ', this.searchIndexes)

search(query: string): SearchResult[] {
if (!this.searchIndexes) {
// TODO: What if the indexing is running but not finished yet?
log.warn('[SEARCH] No search index available')
Expand All @@ -269,8 +267,9 @@ class SearchEngine {
const results = this.limitSearchResults(sortedResults)

const normResults: SearchResult[] = []
for (const res of results) {
const normalizedRes = await normalizeSearchResult(this.client, res, query)
const completedResults = addFilePaths(this.client, results)
for (const res of completedResults) {
const normalizedRes = normalizeSearchResult(this.client, res, query)
normResults.push(normalizedRes)
}
return normResults.filter(res => res.title)
Expand Down Expand Up @@ -301,12 +300,14 @@ class SearchEngine {
limit: FLEXSEARCH_LIMIT,
enrich: true
})

const newResults = indexResults.map(res => ({
...res,
doctype: doctype
}))
searchResults = searchResults.concat(newResults)
}

return searchResults
}

Expand Down
1 change: 1 addition & 0 deletions src/search/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const CONTACTS_DOCTYPE = 'io.cozy.contacts'
export const APPS_DOCTYPE = 'io.cozy.apps'

export const TYPE_DIRECTORY = 'directory'
export const TYPE_FILE = 'file'
export const ROOT_DIR_ID = 'io.cozy.files.root-dir'
export const TRASH_DIR_ID = 'io.cozy.files.trash-dir'
export const SHARED_DRIVES_DIR_ID = 'io.cozy.files.shared-drives-dir'
Expand Down
57 changes: 34 additions & 23 deletions src/search/helpers/normalizeFile.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import CozyClient, { Q } from 'cozy-client'
import CozyClient from 'cozy-client'
import { IOCozyFile } from 'cozy-client/types/types'

import {
FILES_DOCTYPE,
TYPE_DIRECTORY,
TYPE_FILE,
ROOT_DIR_ID,
SHARED_DRIVES_DIR_ID
} from '@/search/consts'
import { CozyDoc } from '@/search/types'

interface FileQueryResult {
data: IOCozyFile
}
import { CozyDoc, isIOCozyFile, RawSearchResult } from '@/search/types'

/**
* Normalize file for Front usage in <AutoSuggestion> component inside <BarSearchAutosuggest>
Expand Down Expand Up @@ -39,25 +36,39 @@ export const normalizeFileWithFolders = (
return { ...file, _type: 'io.cozy.files', path }
}

export const normalizeFileWithStore = async (
export const addFilePaths = (
client: CozyClient,
file: IOCozyFile
): Promise<IOCozyFile> => {
const isDir = file.type === TYPE_DIRECTORY
let path = ''
if (isDir) {
path = file.path ?? ''
} else {
const query = Q(FILES_DOCTYPE).getById(file.dir_id).limitBy(1)
// XXX - Take advantage of cozy-client store to avoid querying database
const { data: parentDir } = (await client.query(query, {
executeFromStore: true,
singleDocData: true
})) as FileQueryResult
const parentPath = parentDir?.path ?? ''
path = `${parentPath}/${file.name}`
results: RawSearchResult[]
): RawSearchResult[] => {
const normResults = [...results]
const filesResults = normResults
.map(res => res.doc)
.filter(doc => isIOCozyFile(doc))
const files = filesResults.filter(file => file.type === TYPE_FILE)

if (files.length > 0) {
const dirIds = files.map(file => file.dir_id)
const parentDirs = getDirsFromStore(client, dirIds)
for (const file of files) {
const dir = parentDirs.find(dir => dir._id === file.dir_id)
if (dir) {
const idx = normResults.findIndex(res => res.doc._id === file._id)
normResults[idx].doc.path = dir.path
}
}
}
return { ...file, _type: 'io.cozy.files', path }
return normResults
}

const getDirsFromStore = (
client: CozyClient,
dirIds: string[]
): IOCozyFile[] => {
// XXX querying from store is surprisingly slow: 100+ ms for 50 docs, while
// this approach takes 2-3ms... It should be investigated in cozy-client
const allFiles = client.getCollectionFromState(FILES_DOCTYPE) as IOCozyFile[]
const dirs = allFiles.filter(file => file.type === TYPE_DIRECTORY)
return dirs.filter(dir => dirIds.includes(dir._id))
}

export const shouldKeepFile = (file: IOCozyFile): boolean => {
Expand Down
18 changes: 3 additions & 15 deletions src/search/helpers/normalizeSearchResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ import {
SearchResult
} from '@/search/types'

import { normalizeFileWithStore } from './normalizeFile'

export const normalizeSearchResult = async (
export const normalizeSearchResult = (
client: CozyClient,
searchResults: RawSearchResult,
query: string
): Promise<SearchResult> => {
const doc = await normalizeDoc(client, searchResults.doc)
): SearchResult => {
const doc = searchResults.doc
const url = buildOpenURL(client, doc)
const type = getSearchResultSlug(doc)
const title = getSearchResultTitle(doc)
Expand All @@ -32,16 +30,6 @@ export const normalizeSearchResult = async (
return normalizedRes
}

const normalizeDoc = async (
client: CozyClient,
doc: CozyDoc
): Promise<CozyDoc> => {
if (isIOCozyFile(doc)) {
return normalizeFileWithStore(client, doc)
}
return doc
}

const getSearchResultTitle = (doc: CozyDoc): string | null => {
if (isIOCozyFile(doc)) {
return doc.name
Expand Down

0 comments on commit 5d619aa

Please sign in to comment.