Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/@types/cozy-client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,6 @@ declare module 'cozy-client' {
}[]
}

interface MissingFileDocumentAttributes {
md5sum: string
}

type IOCozyFile = {
attributes: MissingFileDocumentAttributes & FileDocument
} & CozyClientDocument

interface Collection {
findReferencedBy: (
params: object
Expand Down
19 changes: 15 additions & 4 deletions src/search/SearchEngine.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,30 @@ describe('sortSearchResults', () => {
{
doctype: FILES_DOCTYPE,
doc: {
name: 'DirName',
name: 'DirName1',
path: 'test1/path',
type: 'directory',
_type: FILES_DOCTYPE
},
fields: ['path']
},
{
doctype: FILES_DOCTYPE,
doc: {
name: 'DirName2',
path: 'test1/path',
type: 'directory',
_type: FILES_DOCTYPE
},
fields: ['name']
}
]

const sortedResults = searchEngine.sortSearchResults(searchResults)

expect(sortedResults[0].doc.name).toBe('test1') // File match on name
expect(sortedResults[1].doc.name).toBe('test11') // File match on name
expect(sortedResults[2].doc.name).toBe('DirName') // Directory
expect(sortedResults[0].doc.name).toBe('DirName2') // Dir match on name
expect(sortedResults[1].doc.name).toBe('test1') // File match on name
expect(sortedResults[2].doc.name).toBe('test11') // File match on name
expect(sortedResults[3].doc.name).toBe('DirName1') // Directory
})
})
39 changes: 25 additions & 14 deletions src/search/SearchEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
DOCTYPE_ORDER,
LIMIT_DOCTYPE_SEARCH,
REPLICATION_DEBOUNCE,
ROOT_DIR_ID,
SHARED_DRIVES_DIR_ID,
SearchedDoctype
} from '@/search/consts'
import { getPouchLink } from '@/search/helpers/client'
Expand All @@ -37,6 +35,8 @@ import {
isSearchedDoctype
} from '@/search/types'

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

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

interface FlexSearchResultWithDoctype
Expand Down Expand Up @@ -83,7 +83,7 @@ class SearchEngine {

subscribeDoctype(client: CozyClient, doctype: string): void {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
const realtime = this.client.plugins.realtime
const realtime = client.plugins.realtime
realtime.subscribe('created', doctype, this.handleUpdatedOrCreatedDoc)
realtime.subscribe('updated', doctype, this.handleUpdatedOrCreatedDoc)
realtime.subscribe('deleted', doctype, this.handleDeletedDoc)
Expand Down Expand Up @@ -158,13 +158,7 @@ class SearchEngine {

shouldIndexDoc(doc: CozyDoc): boolean {
if (isIOCozyFile(doc)) {
const notInTrash = !doc.trashed && !/^\/\.cozy_trash/.test(doc.path ?? '')
const notRootDir = doc._id !== ROOT_DIR_ID
// Shared drives folder to be hidden in search.
// The files inside it though must appear. Thus only the file with the folder ID is filtered out.
const notSharedDrivesDir = doc._id !== SHARED_DRIVES_DIR_ID

return notInTrash && notRootDir && notSharedDrivesDir
return shouldKeepFile(doc)
}
return true
}
Expand All @@ -181,10 +175,21 @@ class SearchEngine {

if (!searchIndex) {
// First creation of search index
const startTimeQ = performance.now()
const docs = await this.client.queryAll<CozyDoc[]>(
Q(doctype).limitBy(null)
)
const endTimeQ = performance.now()
log.debug(
`Query ${docs.length} docs doctype ${doctype} took ${(endTimeQ - startTimeQ).toFixed(2)} ms`
)

const startTimeIndex = performance.now()
const index = this.buildSearchIndex(doctype, docs)
const endTimeIndex = performance.now()
log.debug(
`Create ${doctype} index took ${(endTimeIndex - startTimeIndex).toFixed(2)} ms`
)
const info = await pouchLink.getDbInfo(doctype)

this.searchIndexes[doctype] = {
Expand Down Expand Up @@ -249,7 +254,7 @@ class SearchEngine {
return this.searchIndexes
}

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

if (!this.searchIndexes) {
Expand All @@ -262,9 +267,12 @@ class SearchEngine {
const results = this.deduplicateAndFlatten(allResults)
const sortedResults = this.sortSearchResults(results)

return sortedResults
.map(res => normalizeSearchResult(this.client, res, query))
.filter(res => res.title)
const normResults: SearchResult[] = []
for (const res of sortedResults) {
const normalizedRes = await normalizeSearchResult(this.client, res, query)
normResults.push(normalizedRes)
}
return normResults.filter(res => res.title)
}

searchOnIndexes(query: string): FlexSearchResultWithDoctype[] {
Expand Down Expand Up @@ -355,11 +363,14 @@ class SearchEngine {
return 0
}
if (!aRes.fields.includes('name') || !bRes.fields.includes('name')) {
// First, sort docs with a match on the name field
return aRes.fields.includes('name') ? -1 : 1
}
if (aRes.doc.type !== bRes.doc.type) {
// Then, directories
return aRes.doc.type === 'directory' ? -1 : 1
}
// Then name
return this.compareStrings(aRes.doc.name, bRes.doc.name)
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/search/helpers/normalizeFile.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IOCozyFile } from 'cozy-client/types/types'

import { normalizeFile } from './normalizeFile'
import { normalizeFileWithFolders } from './normalizeFile'

test('should get path for directories', () => {
const folders = [] as IOCozyFile[]
Expand All @@ -11,7 +11,7 @@ test('should get path for directories', () => {
path: 'SOME/PATH/'
} as IOCozyFile

const result = normalizeFile(folders, file)
const result = normalizeFileWithFolders(folders, file)

expect(result).toStrictEqual({
type: 'directory',
Expand All @@ -35,7 +35,7 @@ test(`should get parent folder's path for files`, () => {
dir_id: 'SOME_DIR_ID'
} as IOCozyFile

const result = normalizeFile(folders, file)
const result = normalizeFileWithFolders(folders, file)

expect(result).toStrictEqual({
type: 'file',
Expand Down
45 changes: 43 additions & 2 deletions src/search/helpers/normalizeFile.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import CozyClient, { Q } from 'cozy-client'
import { IOCozyFile } from 'cozy-client/types/types'

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

interface FileQueryResult {
data: IOCozyFile
}

/**
* Normalize file for Front usage in <AutoSuggestion> component inside <BarSearchAutosuggest>
*
Expand All @@ -14,7 +24,7 @@ import { CozyDoc } from '@/search/types'
* @param {IOCozyFile} file - file to normalize
* @returns file with normalized field to be used in AutoSuggestion
*/
export const normalizeFile = (
export const normalizeFileWithFolders = (
folders: IOCozyFile[],
file: IOCozyFile
): CozyDoc => {
Expand All @@ -28,3 +38,34 @@ export const normalizeFile = (
}
return { ...file, _type: 'io.cozy.files', path }
}

export const normalizeFileWithStore = async (
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}`
}
return { ...file, _type: 'io.cozy.files', path }
}

export const shouldKeepFile = (file: IOCozyFile): boolean => {
const notInTrash = !file.trashed && !/^\/\.cozy_trash/.test(file.path ?? '')
const notRootDir = file._id !== ROOT_DIR_ID
// Shared drives folder to be hidden in search.
// The files inside it though must appear. Thus only the file with the folder ID is filtered out.
const notSharedDrivesDir = file._id !== SHARED_DRIVES_DIR_ID

return notInTrash && notRootDir && notSharedDrivesDir
}
59 changes: 38 additions & 21 deletions src/search/helpers/normalizeSearchResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,35 @@ import {
SearchResult
} from '@/search/types'

export const normalizeSearchResult = (
import { normalizeFileWithStore } from './normalizeFile'

export const normalizeSearchResult = async (
client: CozyClient,
searchResults: RawSearchResult,
query: string
): SearchResult => {
const url = buildOpenURL(client, searchResults.doc)
const type = getSearchResultSlug(searchResults.doc)
const title = getSearchResultTitle(searchResults.doc)
const name = getSearchResultSubTitle(client, searchResults, query)
const normalizedDoc = { doc: searchResults.doc, type, title, name, url }

return normalizedDoc
): Promise<SearchResult> => {
const doc = await normalizeDoc(client, searchResults.doc)
const url = buildOpenURL(client, doc)
const type = getSearchResultSlug(doc)
const title = getSearchResultTitle(doc)
const name = getSearchResultSubTitle(client, {
fields: searchResults.fields,
doc,
query
})
const normalizedRes = { doc, type, title, name, url }

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 => {
Expand All @@ -43,18 +60,18 @@ const getSearchResultTitle = (doc: CozyDoc): string | null => {

const getSearchResultSubTitle = (
client: CozyClient,
searchResult: RawSearchResult,
query: string
params: { fields: string[]; doc: CozyDoc; query: string }
): string | null => {
if (isIOCozyFile(searchResult.doc)) {
return searchResult.doc.path ?? null
const { fields, doc, query } = params
if (isIOCozyFile(doc)) {
return doc.path ?? null
}

if (isIOCozyContact(searchResult.doc)) {
if (isIOCozyContact(doc)) {
let matchingValue

// Several document fields might match a search query. Let's take the first one different from name, assuming a relevance order
const matchingField = searchResult.fields.find(
const matchingField = fields.find(
field => field !== 'displayName' && field !== 'fullname'
)

Expand All @@ -70,7 +87,7 @@ const getSearchResultSubTitle = (
const arrayAttributeName = tokens[0] as keyof IOCozyContact
const valueAttribute = tokens[1]

const array = searchResult.doc[arrayAttributeName]
const array = doc[arrayAttributeName]
const matchingArrayItem =
Array.isArray(array) &&
array.find(item => {
Expand All @@ -89,21 +106,21 @@ const getSearchResultSubTitle = (
matchingValue =
matchingArrayItem[valueAttribute as keyof typeof matchingArrayItem]
} else {
matchingValue = searchResult.doc[matchingField as keyof IOCozyContact]
matchingValue = doc[matchingField as keyof IOCozyContact]
}

return matchingValue?.toString() ?? null
}

if (searchResult.doc._type === APPS_DOCTYPE) {
if (doc._type === APPS_DOCTYPE) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const locale: string = client.getInstanceOptions().locale || 'en'
if (searchResult.doc.locales[locale]) {
return searchResult.doc.locales[locale].short_description
if (doc.locales[locale]) {
return doc.locales[locale].short_description
}
} catch {
return searchResult.doc.name
return doc.name
}
}
return null
Expand Down
Loading