mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 17:05:16 +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,
|
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
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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({
|
||||||
|
Loading…
Reference in New Issue
Block a user