diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 177465955e..2017e80df7 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -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 diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index f92c8d9801..fb57d3ce95 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -521,7 +521,7 @@ export interface DocIndexState extends Doc { // Indexable attributes, including child ones. attributes: Record - + mixins?: Ref>[] // Full Summary fullSummary?: Markup | null shortSummary?: Markup | null diff --git a/packages/core/src/hierarchy.ts b/packages/core/src/hierarchy.ts index b667458f88..9496b74d37 100644 --- a/packages/core/src/hierarchy.ts +++ b/packages/core/src/hierarchy.ts @@ -141,6 +141,21 @@ export class Hierarchy { return result } + findAllMixins(doc: Doc): Ref>[] { + const _doc = _toDoc(doc) + const resultSet = new Set>>() + for (const [k, v] of Object.entries(_doc)) { + if (typeof v === 'object' && this.classifiers.has(k as Ref)) { + if (this.isMixin(k as Ref)) { + if (!resultSet.has(k as Ref)) { + resultSet.add(k as Ref) + } + } + } + } + return Array.from(resultSet) + } + isMixin (_class: Ref>): boolean { const data = this.classifiers.get(_class) return data !== undefined && this._isMixin(data) diff --git a/packages/presentation/src/search.ts b/packages/presentation/src/search.ts index fe62e690ee..cc85e512e6 100644 --- a/packages/presentation/src/search.ts +++ b/packages/presentation/src/search.ts @@ -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 { - 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>, 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() -export async function searchFor (context: 'mention' | 'spotlight', query: string): Promise { +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>> = [] @@ -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 } } diff --git a/packages/text-editor/src/components/MentionPopup.svelte b/packages/text-editor/src/components/MentionPopup.svelte index a353e36744..37670cc2e9 100644 --- a/packages/text-editor/src/components/MentionPopup.svelte +++ b/packages/text-editor/src/components/MentionPopup.svelte @@ -67,8 +67,11 @@ return false } - async function updateItems (query: string): Promise { - items = await searchFor('mention', query) + async function updateItems (localQuery: string): Promise { + const r = await searchFor('mention', localQuery) + if (r.query === query) { + items = r.items + } } $: void updateItems(query) diff --git a/plugins/view-resources/src/components/ActionsPopup.svelte b/plugins/view-resources/src/components/ActionsPopup.svelte index 4bb77f7b2e..51c673292d 100644 --- a/plugins/view-resources/src/components/ActionsPopup.svelte +++ b/plugins/view-resources/src/components/ActionsPopup.svelte @@ -244,7 +244,7 @@ async function updateItems (query: string, filteredActions: Array>): Promise { let searchItems: SearchItem[] = [] if (query !== '' && query.indexOf('/') !== 0) { - searchItems = await searchFor('spotlight', query) + searchItems = (await searchFor('spotlight', query)).items } items = packSearchAndActions(searchItems, filteredActions) } diff --git a/server/core/src/fulltext.ts b/server/core/src/fulltext.ts index 71515afc89..f64b197a35 100644 --- a/server/core/src/fulltext.ts +++ b/server/core/src/fulltext.ts @@ -114,16 +114,19 @@ export class FullTextIndex implements WithFind { const old = stDocs.get(cud.objectId as Ref) if (cud._class === core.class.TxRemoveDoc && old?.create !== undefined) { // Object created and deleted, skip index + stDocs.delete(cud.objectId as Ref) continue } else if (old !== undefined) { // Create and update - // Skip update - continue + if (old.removed) continue + else { + stDocs.set(cud.objectId as Ref, { + ...old, + updated: cud._class !== core.class.TxRemoveDoc, + removed: cud._class === core.class.TxRemoveDoc + }) + } } - stDocs.set(cud.objectId as Ref, { - 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, 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) } diff --git a/server/core/src/indexer/field.ts b/server/core/src/indexer/field.ts index dd615eb41e..7b5886dcb8 100644 --- a/server/core/src/indexer/field.ts +++ b/server/core/src/indexer/field.ts @@ -152,7 +152,7 @@ export class IndexedFieldStage implements FullTextPipelineStage { const docUpdate: DocumentUpdate = {} 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) diff --git a/server/core/src/indexer/fulltextPush.ts b/server/core/src/indexer/fulltextPush.ts index 7c03dea952..5e32b4bc90 100644 --- a/server/core/src/indexer/fulltextPush.ts +++ b/server/core/src/indexer/fulltextPush.ts @@ -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, diff --git a/server/core/src/indexer/types.ts b/server/core/src/indexer/types.ts index 9bb6709958..f8d2f6de20 100644 --- a/server/core/src/indexer/types.ts +++ b/server/core/src/indexer/types.ts @@ -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' diff --git a/server/core/src/mapper.ts b/server/core/src/mapper.ts index b7f377a238..20580298cc 100644 --- a/server/core/src/mapper.ts +++ b/server/core/src/mapper.ts @@ -112,12 +112,12 @@ export async function updateDocWithPresenter ( spaceDoc: DocIndexState | undefined } ): Promise { - 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) 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 } diff --git a/server/core/src/types.ts b/server/core/src/types.ts index fbb4917617..53a1433731 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -183,7 +183,7 @@ export interface EmbeddingSearchOption { */ export interface IndexedDoc { id: Ref - _class: Ref> + _class: Ref>[] space: Ref modifiedOn: Timestamp modifiedBy: Ref diff --git a/server/elastic/src/__tests__/adapter.test.ts b/server/elastic/src/__tests__/adapter.test.ts index 3d0ae56e58..8e6b20d4fc 100644 --- a/server/elastic/src/__tests__/adapter.test.ts +++ b/server/elastic/src/__tests__/adapter.test.ts @@ -41,7 +41,7 @@ describe('Elastic Adapter', () => { it('should create document', async () => { const doc: IndexedDoc = { id: 'doc1' as Ref, - _class: 'class1' as Ref>, + _class: ['class1' as Ref>], modifiedBy: 'andrey' as Ref, modifiedOn: 0, space: 'space1' as Ref,