separate mention category for employees (#5095)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2024-03-29 18:37:36 +05:00 committed by GitHub
parent 16204edaac
commit 72a93ad388
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 56 additions and 52 deletions

View File

@ -836,7 +836,8 @@ export function createModel (builder: Builder): void {
label: contact.string.SearchEmployee,
title: contact.string.Employees,
query: contact.completion.EmployeeQuery,
context: ['search']
context: ['search', 'mention'],
classToSearch: contact.mixin.Employee
},
contact.completion.EmployeeCategory
)
@ -849,7 +850,7 @@ export function createModel (builder: Builder): void {
label: contact.string.SearchPerson,
title: contact.string.People,
query: contact.completion.PersonQuery,
context: ['search', 'mention', 'spotlight'],
context: ['search', 'spotlight'],
classToSearch: contact.class.Person
},
contact.completion.PersonCategory

View File

@ -521,7 +521,7 @@ export interface DocIndexState extends Doc {
// Indexable attributes, including child ones.
attributes: Record<string, any>
mixins?: Ref<Class<Doc>>[]
// Full Summary
fullSummary?: Markup | null
shortSummary?: Markup | null

View File

@ -141,6 +141,21 @@ export class Hierarchy {
return result
}
findAllMixins<D extends Doc, M extends D>(doc: Doc): Ref<Class<M>>[] {
const _doc = _toDoc(doc)
const resultSet = new Set<Ref<Class<M>>>()
for (const [k, v] of Object.entries(_doc)) {
if (typeof v === 'object' && this.classifiers.has(k as Ref<Classifier>)) {
if (this.isMixin(k as Ref<Classifier>)) {
if (!resultSet.has(k as Ref<Classifier>)) {
resultSet.add(k as Ref<Classifier>)
}
}
}
}
return Array.from(resultSet)
}
isMixin (_class: Ref<Class<Doc>>): boolean {
const data = this.classifiers.get(_class)
return data !== undefined && this._isMixin(data)

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Class, Ref, Doc, SearchResultDoc, TxOperations, SearchResult } from '@hcengineering/core'
import type { Class, Ref, Doc, SearchResultDoc, TxOperations } from '@hcengineering/core'
import { type ObjectSearchCategory } from './types'
import plugin from './plugin'
import { getClient } from './utils'
@ -67,7 +67,7 @@ async function doFulltextSearch (
query: string,
categories: ObjectSearchCategory[]
): Promise<SearchSection[]> {
let result: SearchResult | undefined
const sections: SearchSection[] = []
for (const cl of classes) {
const r = await client.searchFulltext(
{
@ -78,31 +78,10 @@ async function doFulltextSearch (
limit: 5
}
)
if (result === undefined) {
result = r
} else {
result.docs.push(...r.docs)
result.total = (result?.total ?? 0) + (r.total ?? 0)
}
const category = findCategoryByClass(categories, cl)
if (category !== undefined) sections.push({ category, items: r.docs })
}
const itemsByClass = new Map<Ref<Class<Doc>>, SearchResultDoc[]>()
for (const item of result?.docs ?? []) {
const list = itemsByClass.get(item.doc._class)
if (list === undefined) {
itemsByClass.set(item.doc._class, [item])
} else {
list.push(item)
}
}
const sections: SearchSection[] = []
for (const [_class, items] of itemsByClass.entries()) {
const category = findCategoryByClass(categories, _class)
if (category !== undefined) {
sections.push({ category, items })
}
}
return sections.sort((a, b) => {
const maxScoreA = Math.max(...(a?.items ?? []).map((obj) => obj?.score ?? 0))
const maxScoreB = Math.max(...(b?.items ?? []).map((obj) => obj?.score ?? 0))
@ -112,7 +91,10 @@ async function doFulltextSearch (
const categoriesByContext = new Map<string, ObjectSearchCategory[]>()
export async function searchFor (context: 'mention' | 'spotlight', query: string): Promise<SearchItem[]> {
export async function searchFor (
context: 'mention' | 'spotlight',
query: string
): Promise<{ items: SearchItem[], query: string }> {
const client = getClient()
let categories = categoriesByContext.get(context)
if (categories === undefined) {
@ -121,7 +103,7 @@ export async function searchFor (context: 'mention' | 'spotlight', query: string
}
if (categories === undefined) {
return []
return { items: [], query }
}
const classesToSearch: Array<Ref<Class<Doc>>> = []
@ -132,5 +114,5 @@ export async function searchFor (context: 'mention' | 'spotlight', query: string
}
const sections = await doFulltextSearch(client, classesToSearch, query, categories)
return packSearchResultsForListView(sections)
return { items: packSearchResultsForListView(sections), query }
}

View File

@ -67,8 +67,11 @@
return false
}
async function updateItems (query: string): Promise<void> {
items = await searchFor('mention', query)
async function updateItems (localQuery: string): Promise<void> {
const r = await searchFor('mention', localQuery)
if (r.query === query) {
items = r.items
}
}
$: void updateItems(query)
</script>

View File

@ -244,7 +244,7 @@
async function updateItems (query: string, filteredActions: Array<WithLookup<Action>>): Promise<void> {
let searchItems: SearchItem[] = []
if (query !== '' && query.indexOf('/') !== 0) {
searchItems = await searchFor('spotlight', query)
searchItems = (await searchFor('spotlight', query)).items
}
items = packSearchAndActions(searchItems, filteredActions)
}

View File

@ -114,16 +114,19 @@ export class FullTextIndex implements WithFind {
const old = stDocs.get(cud.objectId as Ref<DocIndexState>)
if (cud._class === core.class.TxRemoveDoc && old?.create !== undefined) {
// Object created and deleted, skip index
stDocs.delete(cud.objectId as Ref<DocIndexState>)
continue
} else if (old !== undefined) {
// Create and update
// Skip update
continue
if (old.removed) continue
else {
stDocs.set(cud.objectId as Ref<DocIndexState>, {
...old,
updated: cud._class !== core.class.TxRemoveDoc,
removed: cud._class === core.class.TxRemoveDoc
})
}
}
stDocs.set(cud.objectId as Ref<DocIndexState>, {
updated: cud._class !== core.class.TxRemoveDoc,
removed: cud._class === core.class.TxRemoveDoc
})
}
}
}
@ -207,7 +210,7 @@ export class FullTextIndex implements WithFind {
const indexedDocMap = new Map<Ref<Doc>, IndexedDoc>()
for (const doc of docs) {
if (this.hierarchy.isDerived(doc._class, baseClass)) {
if (doc._class.some((cl) => this.hierarchy.isDerived(cl, baseClass))) {
ids.add(doc.id)
indexedDocMap.set(doc.id, doc)
}

View File

@ -152,7 +152,7 @@ export class IndexedFieldStage implements FullTextPipelineStage {
const docUpdate: DocumentUpdate<DocIndexState> = {}
let changes = 0
docUpdate.mixins = pipeline.hierarchy.findAllMixins(doc as Doc)
// Convert previous child fields to just
for (const [k] of Object.entries(docState.attributes)) {
const { attr, docId, _class } = extractDocKey(k)

View File

@ -242,7 +242,7 @@ export class FullTextPushStage implements FullTextPipelineStage {
})
)
const allAttributes = pipeline.hierarchy.getAllAttributes(elasticDoc._class)
const allAttributes = pipeline.hierarchy.getAllAttributes(doc.objectClass)
// Include child ref attributes
await this.indexRefAttributes(allAttributes, doc, elasticDoc, ctx)
@ -290,7 +290,7 @@ export class FullTextPushStage implements FullTextPipelineStage {
export function createElasticDoc (upd: DocIndexState): IndexedDoc {
const doc = {
id: upd._id,
_class: upd.objectClass,
_class: [upd.objectClass, ...(upd.mixins ?? [])],
modifiedBy: upd.modifiedBy,
modifiedOn: upd.modifiedOn,
space: upd.space,

View File

@ -102,9 +102,9 @@ export const contentStageId = 'cnt-v2b'
/**
* @public
*/
export const fieldStateId = 'fld-v12'
export const fieldStateId = 'fld-v13a'
/**
* @public
*/
export const fullTextPushStageId = 'fts-v10b'
export const fullTextPushStageId = 'fts-v11a'

View File

@ -112,12 +112,12 @@ export async function updateDocWithPresenter (
spaceDoc: DocIndexState | undefined
}
): Promise<void> {
const searchPresenter = findSearchPresenter(hierarchy, elasticDoc._class)
const searchPresenter = findSearchPresenter(hierarchy, doc.objectClass)
if (searchPresenter === undefined) {
return
}
const reader = createIndexedReader(elasticDoc._class, hierarchy, doc, {
const reader = createIndexedReader(doc.objectClass, hierarchy, doc, {
space: refDocs.spaceDoc,
attachedTo: refDocs.parentDoc
})
@ -156,7 +156,7 @@ export async function updateDocWithPresenter (
} else if (prop.provider !== undefined) {
const func = await getResource(Object.values(prop.provider)[0] as Resource<any>)
const renderProps = await readAndMapProps(reader, prop.config.props)
value = func(hierarchy, { _class: elasticDoc._class, ...renderProps })
value = func(hierarchy, { _class: doc.objectClass, ...renderProps })
} else if (prop.name === 'searchIcon') {
value = await readAndMapProps(reader, prop.config.props)
}
@ -186,7 +186,7 @@ export function mapSearchResultDoc (hierarchy: Hierarchy, raw: IndexedDoc): Sear
iconProps: raw.searchIcon,
doc: {
_id: raw.id,
_class: raw._class
_class: raw._class[0]
},
score: raw._score
}

View File

@ -183,7 +183,7 @@ export interface EmbeddingSearchOption {
*/
export interface IndexedDoc {
id: Ref<Doc>
_class: Ref<Class<Doc>>
_class: Ref<Class<Doc>>[]
space: Ref<Space>
modifiedOn: Timestamp
modifiedBy: Ref<Account>

View File

@ -41,7 +41,7 @@ describe('Elastic Adapter', () => {
it('should create document', async () => {
const doc: IndexedDoc = {
id: 'doc1' as Ref<Doc>,
_class: 'class1' as Ref<Class<Doc>>,
_class: ['class1' as Ref<Class<Doc>>],
modifiedBy: 'andrey' as Ref<Account>,
modifiedOn: 0,
space: 'space1' as Ref<Space>,