mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
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:
parent
e361325a73
commit
84d23c2e86
@ -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
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user