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,
props: ['avatar', 'name']
},
title: { props: ['name'] }
title: { props: ['name'] },
scoring: [
{
attr: 'space',
value: contact.space.Employee as string,
boost: 2
}
]
},
getSearchTitle: serverContact.function.ContactNameProvider
})

View File

@ -44,7 +44,7 @@ import core, {
import { MinioService } from '@hcengineering/minio'
import { FullTextIndexPipeline } from './indexer'
import { createStateDoc, isClassIndexable } from './indexer/utils'
import { mapSearchResultDoc } from './mapper'
import { mapSearchResultDoc, getScoringConfig } from './mapper'
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> {
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 = {
...resultRaw,

View File

@ -2,7 +2,7 @@ import { Hierarchy, Ref, RefTo, Class, Doc, SearchResultDoc, docKey } from '@hce
import { getResource } from '@hcengineering/platform'
import plugin from './plugin'
import { IndexedDoc, SearchPresenter, ClassSearchConfigProps } from './types'
import { IndexedDoc, SearchPresenter, ClassSearchConfigProps, SearchScoring } from './types'
interface IndexedReader {
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
*/

View File

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

View File

@ -27,7 +27,13 @@ import {
SearchQuery,
SearchOptions
} 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 { Domain } from 'node:domain'
@ -105,24 +111,50 @@ class ElasticAdapter implements FullTextAdapter {
return this._metrics
}
async searchString (query: SearchQuery, options: SearchOptions): Promise<SearchStringResult> {
async searchString (
query: SearchQuery,
options: SearchOptions & { scoring?: SearchScoring[] }
): Promise<SearchStringResult> {
try {
const elasticQuery: any = {
query: {
bool: {
must: {
simple_query_string: {
query: query.query,
analyze_wildcard: true,
flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE',
default_operator: 'and',
fields: [
'searchTitle^15', // boost
'searchShortTitle^15',
'*' // Search in all other fields without a boost
]
function_score: {
query: {
bool: {
must: {
simple_query_string: {
query: query.query,
analyze_wildcard: true,
flags: 'OR|PREFIX|PHRASE|FUZZY|NOT|ESCAPE',
default_operator: 'and',
fields: [
'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
@ -146,7 +178,21 @@ class ElasticAdapter implements FullTextAdapter {
}
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({