UBERF-4493: Mentions. When there is a lot of Applicants it's really difficult to mention employee (#4119)

Signed-off-by: Maxim Karmatskikh <mkarmatskih@gmail.com>
This commit is contained in:
Maksim Karmatskikh 2023-12-01 08:30:25 +01:00 committed by GitHub
parent e361325a73
commit 84d23c2e86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 21 deletions

View File

@ -46,7 +46,14 @@ export function createModel (builder: Builder): void {
component: contact.component.Avatar, component: contact.component.Avatar,
props: ['avatar', 'name'] props: ['avatar', 'name']
}, },
title: { props: ['name'] } title: { props: ['name'] },
scoring: [
{
attr: 'space',
value: contact.space.Employee as string,
boost: 2
}
]
}, },
getSearchTitle: serverContact.function.ContactNameProvider getSearchTitle: serverContact.function.ContactNameProvider
}) })

View File

@ -44,7 +44,7 @@ import core, {
import { MinioService } from '@hcengineering/minio' import { MinioService } from '@hcengineering/minio'
import { FullTextIndexPipeline } from './indexer' import { FullTextIndexPipeline } from './indexer'
import { createStateDoc, isClassIndexable } from './indexer/utils' import { createStateDoc, isClassIndexable } from './indexer/utils'
import { mapSearchResultDoc } from './mapper' import { mapSearchResultDoc, getScoringConfig } from './mapper'
import type { FullTextAdapter, WithFind, IndexedDoc } from './types' import type { FullTextAdapter, WithFind, IndexedDoc } from './types'
/** /**
@ -246,7 +246,10 @@ export class FullTextIndex implements WithFind {
} }
async searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> { async searchFulltext (ctx: MeasureContext, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
const resultRaw = await this.adapter.searchString(query, options) const resultRaw = await this.adapter.searchString(query, {
...options,
scoring: getScoringConfig(this.hierarchy, query.classes ?? [])
})
const result: SearchResult = { const result: SearchResult = {
...resultRaw, ...resultRaw,

View File

@ -2,7 +2,7 @@ import { Hierarchy, Ref, RefTo, Class, Doc, SearchResultDoc, docKey } from '@hce
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import plugin from './plugin' import plugin from './plugin'
import { IndexedDoc, SearchPresenter, ClassSearchConfigProps } from './types' import { IndexedDoc, SearchPresenter, ClassSearchConfigProps, SearchScoring } from './types'
interface IndexedReader { interface IndexedReader {
get: (attribute: string) => any get: (attribute: string) => any
@ -106,6 +106,17 @@ export async function updateDocWithPresenter (hierarchy: Hierarchy, doc: Indexed
} }
} }
export function getScoringConfig (hierarchy: Hierarchy, classes: Ref<Class<Doc>>[]): SearchScoring[] {
let results: SearchScoring[] = []
for (const _class of classes) {
const searchPresenter = findSearchPresenter(hierarchy, _class)
if (searchPresenter?.searchConfig.scoring !== undefined) {
results = results.concat(searchPresenter?.searchConfig.scoring)
}
}
return results
}
/** /**
* @public * @public
*/ */

View File

@ -189,7 +189,10 @@ export interface FullTextAdapter {
remove: (id: Ref<Doc>[]) => Promise<void> remove: (id: Ref<Doc>[]) => Promise<void>
updateMany: (docs: IndexedDoc[]) => Promise<TxResult[]> updateMany: (docs: IndexedDoc[]) => Promise<TxResult[]>
searchString: (query: SearchQuery, options: SearchOptions) => Promise<SearchStringResult> searchString: (
query: SearchQuery,
options: SearchOptions & { scoring?: SearchScoring[] }
) => Promise<SearchStringResult>
search: ( search: (
_classes: Ref<Class<Doc>>[], _classes: Ref<Class<Doc>>[],
@ -350,6 +353,15 @@ export type ClassSearchConfigProps = string | Record<string, string[]>
*/ */
export type ClassSearchConfigProperty = string | { tmpl?: string, props: ClassSearchConfigProps[] } export type ClassSearchConfigProperty = string | { tmpl?: string, props: ClassSearchConfigProps[] }
/**
* @public
*/
export interface SearchScoring {
attr: string
value: string
boost: number
}
/** /**
* @public * @public
*/ */
@ -358,6 +370,7 @@ export interface ClassSearchConfig {
iconConfig?: { component: any, props: ClassSearchConfigProps[] } iconConfig?: { component: any, props: ClassSearchConfigProps[] }
title: ClassSearchConfigProperty title: ClassSearchConfigProperty
shortTitle?: ClassSearchConfigProperty shortTitle?: ClassSearchConfigProperty
scoring?: SearchScoring[]
} }
/** /**

View File

@ -27,7 +27,13 @@ import {
SearchQuery, SearchQuery,
SearchOptions SearchOptions
} from '@hcengineering/core' } from '@hcengineering/core'
import type { EmbeddingSearchOption, FullTextAdapter, SearchStringResult, IndexedDoc } from '@hcengineering/server-core' import type {
EmbeddingSearchOption,
FullTextAdapter,
SearchStringResult,
SearchScoring,
IndexedDoc
} from '@hcengineering/server-core'
import { Client, errors as esErr } from '@elastic/elasticsearch' import { Client, errors as esErr } from '@elastic/elasticsearch'
import { Domain } from 'node:domain' import { Domain } from 'node:domain'
@ -105,24 +111,50 @@ class ElasticAdapter implements FullTextAdapter {
return this._metrics return this._metrics
} }
async searchString (query: SearchQuery, options: SearchOptions): Promise<SearchStringResult> { async searchString (
query: SearchQuery,
options: SearchOptions & { scoring?: SearchScoring[] }
): Promise<SearchStringResult> {
try { try {
const elasticQuery: any = { const elasticQuery: any = {
query: { query: {
bool: { function_score: {
must: { query: {
simple_query_string: { bool: {
query: query.query, must: {
analyze_wildcard: true, simple_query_string: {
flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE', query: query.query,
default_operator: 'and', analyze_wildcard: true,
fields: [ flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE',
'searchTitle^15', // boost default_operator: 'and',
'searchShortTitle^15', fields: [
'*' // Search in all other fields without a boost 'searchTitle^50', // boost
] 'searchShortTitle^50',
'*' // Search in all other fields without a boost
]
}
}
} }
} },
functions: [
{
script_score: {
script: {
source: "Math.max(0, ((doc['modifiedOn'].value / 1000 - 1672531200) / 2592000))"
/*
Give more score for more recent objects. 1672531200 is the start of 2023
2592000 is a month. The idea is go give 1 point for each month. For objects
older than Jan 2023 it will give just zero.
Better approach is to use gauss function, need to investigate futher how be
map modifiedOn, need to tell elastic that this is a date.
But linear function is perfect to conduct an experiment
*/
}
}
}
],
boost_mode: 'sum'
} }
}, },
size: options.limit ?? DEFAULT_LIMIT size: options.limit ?? DEFAULT_LIMIT
@ -146,7 +178,21 @@ class ElasticAdapter implements FullTextAdapter {
} }
if (filter.length > 0) { if (filter.length > 0) {
elasticQuery.query.bool.filter = filter elasticQuery.query.function_score.query.bool.filter = filter
}
if (options.scoring !== undefined) {
const scoringTerms: any[] = options.scoring.map((scoringOption): any => {
return {
term: {
[`${scoringOption.attr}.keyword`]: {
value: scoringOption.value,
boost: scoringOption.boost
}
}
}
})
elasticQuery.query.function_score.query.bool.should = scoringTerms
} }
const result = await this.client.search({ const result = await this.client.search({