From f0a2d38471c9e52c23ff336d80253d8eb157a7c2 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:47:16 +0200 Subject: [PATCH] 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. --- .../graphql-query-search-resolver.service.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts index 378dfff97..ac77cb9d7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -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(