From e18ac0a46810b9d33e87eaca57468b3fd50ab7fe Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Thu, 12 Sep 2024 09:54:37 +0200
Subject: [PATCH] Improve local search relevancy

---
 server/core/initializers/database.ts            | 17 ++++++-----------
 server/core/models/shared/abstract-run-query.ts |  8 +++++++-
 .../sql/video/videos-id-list-query-builder.ts   | 13 ++++++++++---
 .../video/videos-model-list-query-builder.ts    |  3 ++-
 4 files changed, 25 insertions(+), 16 deletions(-)

diff --git a/server/core/initializers/database.ts b/server/core/initializers/database.ts
index 14ba74bdc..bf1eea67d 100644
--- a/server/core/initializers/database.ts
+++ b/server/core/initializers/database.ts
@@ -1,8 +1,9 @@
 import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
 import { ActorCustomPageModel } from '@server/models/account/actor-custom-page.js'
+import { AccountAutomaticTagPolicyModel } from '@server/models/automatic-tag/account-automatic-tag-policy.js'
 import { AutomaticTagModel } from '@server/models/automatic-tag/automatic-tag.js'
-import { VideoAutomaticTagModel } from '@server/models/automatic-tag/video-automatic-tag.js'
 import { CommentAutomaticTagModel } from '@server/models/automatic-tag/comment-automatic-tag.js'
+import { VideoAutomaticTagModel } from '@server/models/automatic-tag/video-automatic-tag.js'
 import { RunnerJobModel } from '@server/models/runner/runner-job.js'
 import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
 import { RunnerModel } from '@server/models/runner/runner.js'
@@ -66,7 +67,6 @@ import { VideoTagModel } from '../models/video/video-tag.js'
 import { VideoModel } from '../models/video/video.js'
 import { VideoViewModel } from '../models/view/video-view.js'
 import { CONFIG } from './config.js'
-import { AccountAutomaticTagPolicyModel } from '@server/models/automatic-tag/account-automatic-tag-policy.js'
 
 pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
 
@@ -87,7 +87,7 @@ if (CONFIG.DATABASE.SSL) {
   }
 }
 
-const sequelizeTypescript = new SequelizeTypescript({
+export const sequelizeTypescript = new SequelizeTypescript({
   database: dbname,
   dialect: 'postgres',
   dialectOptions,
@@ -112,7 +112,7 @@ const sequelizeTypescript = new SequelizeTypescript({
   }
 })
 
-function checkDatabaseConnectionOrDie () {
+export function checkDatabaseConnectionOrDie () {
   sequelizeTypescript.authenticate()
     .then(() => logger.debug('Connection to PostgreSQL has been established successfully.'))
     .catch(err => {
@@ -122,7 +122,7 @@ function checkDatabaseConnectionOrDie () {
     })
 }
 
-async function initDatabaseModels (silent: boolean) {
+export async function initDatabaseModels (silent: boolean) {
   sequelizeTypescript.addModels([
     ApplicationModel,
     ActorModel,
@@ -192,18 +192,13 @@ async function initDatabaseModels (silent: boolean) {
   // Check extensions exist in the database
   await checkPostgresExtensions()
 
-  // Create custom PostgreSQL functions
   await createFunctions()
 
   if (!silent) logger.info('Database %s is ready.', dbname)
 }
 
 // ---------------------------------------------------------------------------
-
-export {
-  checkDatabaseConnectionOrDie, initDatabaseModels, sequelizeTypescript
-}
-
+// Private
 // ---------------------------------------------------------------------------
 
 async function checkPostgresExtensions () {
diff --git a/server/core/models/shared/abstract-run-query.ts b/server/core/models/shared/abstract-run-query.ts
index 7f27a0c4b..e856cc523 100644
--- a/server/core/models/shared/abstract-run-query.ts
+++ b/server/core/models/shared/abstract-run-query.ts
@@ -10,11 +10,13 @@ export class AbstractRunQuery {
   protected query: string
   protected replacements: any = {}
 
+  protected queryConfig = ''
+
   constructor (protected readonly sequelize: Sequelize) {
 
   }
 
-  protected runQuery (options: { nest?: boolean, transaction?: Transaction, logging?: boolean } = {}) {
+  protected async runQuery (options: { nest?: boolean, transaction?: Transaction, logging?: boolean } = {}) {
     const queryOptions = {
       transaction: options.transaction,
       logging: options.logging,
@@ -23,6 +25,10 @@ export class AbstractRunQuery {
       nest: options.nest ?? false
     }
 
+    if (this.queryConfig) {
+      await this.sequelize.query(this.queryConfig, queryOptions)
+    }
+
     return this.sequelize.query<any>(this.query, queryOptions)
   }
 
diff --git a/server/core/models/video/sql/video/videos-id-list-query-builder.ts b/server/core/models/video/sql/video/videos-id-list-query-builder.ts
index f6e7be911..f4c9a7c66 100644
--- a/server/core/models/video/sql/video/videos-id-list-query-builder.ts
+++ b/server/core/models/video/sql/video/videos-id-list-query-builder.ts
@@ -127,7 +127,12 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
   getQuery (options: BuildVideosListQueryOptions) {
     this.buildIdsListQuery(options)
 
-    return { query: this.query, sort: this.sort, replacements: this.replacements }
+    return {
+      query: this.query,
+      sort: this.sort,
+      replacements: this.replacements,
+      queryConfig: this.queryConfig
+    }
   }
 
   private buildIdsListQuery (options: BuildVideosListQueryOptions) {
@@ -574,12 +579,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
     const escapedSearch = this.sequelize.escape(search)
     const escapedLikeSearch = this.sequelize.escape('%' + search + '%')
 
+    this.queryConfig = 'SET pg_trgm.word_similarity_threshold = 0.40;'
+
     this.cte.push(
       '"trigramSearch" AS (' +
       '  SELECT "video"."id", ' +
-      `  word_similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` +
+      `  word_similarity(lower(immutable_unaccent(${escapedSearch})), lower(immutable_unaccent("video"."name"))) as similarity ` +
       '  FROM "video" ' +
-      '  WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
+      '  WHERE lower(immutable_unaccent(' + escapedSearch + ')) <% lower(immutable_unaccent("video"."name")) OR ' +
       '        lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
       ')'
     )
diff --git a/server/core/models/video/sql/video/videos-model-list-query-builder.ts b/server/core/models/video/sql/video/videos-model-list-query-builder.ts
index dc5df090a..1cbde1b76 100644
--- a/server/core/models/video/sql/video/videos-model-list-query-builder.ts
+++ b/server/core/models/video/sql/video/videos-model-list-query-builder.ts
@@ -66,11 +66,12 @@ export class VideosModelListQueryBuilder extends AbstractVideoQueryBuilder {
 
   private buildInnerQuery (options: BuildVideosListQueryOptions) {
     const idsQueryBuilder = new VideosIdListQueryBuilder(this.sequelize)
-    const { query, sort, replacements } = idsQueryBuilder.getQuery(options)
+    const { query, sort, replacements, queryConfig } = idsQueryBuilder.getQuery(options)
 
     this.replacements = replacements
     this.innerQuery = query
     this.innerSort = sort
+    this.queryConfig = queryConfig
   }
 
   private buildMainQuery (options: BuildVideosListQueryOptions, serverActor: MActorAccount) {