Improve search algorithm (#7955)

We were previously checking for matching with each search term
independently. Ex searching for "felix malfait" we were searching for
correspondances with "felix" and "malfait".
As a result record A with name "Marie-Claude Mala" and email
"ma.lala@email.com" had a biggest search score than record B "Felix
Malfait" with email felix@email.com for search "felix ma":
for record A we had 0 match with felix and 3 matches with "ma" ("marie",
"mala", "ma")
for record B we had 1 match with felix and 1 match with "ma" (with
"malfait").

So we want to give more weight to a row that would combine matches with
both terms, considering "felix malfait" altogether.
This commit is contained in:
Marie 2024-10-22 15:47:16 +02:00 committed by GitHub
parent e767f16dbe
commit f0a2d38471
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -13,7 +13,6 @@ import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-bu
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { isDefined } from 'src/utils/is-defined';
@ -24,7 +23,6 @@ export class GraphqlQuerySearchResolverService
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly featureFlagService: FeatureFlagService,
) {}
async resolve<
@ -61,7 +59,8 @@ export class GraphqlQuerySearchResolverService
hasPreviousPage: false,
});
}
const searchTerms = this.formatSearchTerms(args.searchInput);
const searchTerms = this.formatSearchTerms(args.searchInput, 'and');
const searchTermsOr = this.formatSearchTerms(args.searchInput, 'or');
const limit = args?.limit ?? QUERY_MAX_RECORDS;
@ -86,11 +85,22 @@ export class GraphqlQuerySearchResolverService
: `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`,
searchTerms === '' ? {} : { searchTerms },
)
.orWhere(
searchTermsOr === ''
? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL`
: `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTermsOr)`,
searchTermsOr === '' ? {} : { searchTermsOr },
)
.orderBy(
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
`ts_rank_cd("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
'DESC',
)
.addOrderBy(
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTermsOr))`,
'DESC',
)
.setParameter('searchTerms', searchTerms)
.setParameter('searchTermsOr', searchTermsOr)
.take(limit)
.getMany()) as ObjectRecord[];
@ -110,7 +120,10 @@ export class GraphqlQuerySearchResolverService
});
}
private formatSearchTerms(searchTerm: string) {
private formatSearchTerms(
searchTerm: string,
operator: 'and' | 'or' = 'and',
) {
if (searchTerm === '') {
return '';
}
@ -121,7 +134,7 @@ export class GraphqlQuerySearchResolverService
return `${escapedWord}:*`;
});
return formattedWords.join(' | ');
return formattedWords.join(` ${operator === 'and' ? '&' : '|'} `);
}
async validate(