Browse Source

fix(server): search and explore part 2 (#2031)

* explore logging

* chore: regenerate open api

* fix: explore page
Jason Rasmussen 2 years ago
parent
commit
25a10784eb

+ 1 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
 
-- API version: 1.51.0
+- API version: 1.51.1
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 
 ## Requirements
 ## Requirements

+ 58 - 36
server/libs/infra/src/search/typesense.repository.ts

@@ -8,7 +8,7 @@ import {
 } from '@app/domain';
 } from '@app/domain';
 import { Injectable, Logger } from '@nestjs/common';
 import { Injectable, Logger } from '@nestjs/common';
 import _, { Dictionary } from 'lodash';
 import _, { Dictionary } from 'lodash';
-import { filter, firstValueFrom, from, map, mergeMap, toArray } from 'rxjs';
+import { catchError, filter, firstValueFrom, from, map, mergeMap, of, toArray } from 'rxjs';
 import { Client } from 'typesense';
 import { Client } from 'typesense';
 import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
 import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
@@ -148,7 +148,7 @@ export class TypesenseRepository implements ISearchRepository {
 
 
     const common = {
     const common = {
       q: '*',
       q: '*',
-      filter_by: `ownerId:${userId}`,
+      filter_by: this.buildFilterBy('ownerId', userId, true),
       per_page: 100,
       per_page: 100,
     };
     };
 
 
@@ -157,8 +157,8 @@ export class TypesenseRepository implements ISearchRepository {
     const { facet_counts: facets } = await asset$.search({
     const { facet_counts: facets } = await asset$.search({
       ...common,
       ...common,
       query_by: 'exifInfo.imageName',
       query_by: 'exifInfo.imageName',
-      facet_by: this.getFacetFieldNames(SearchCollection.ASSETS),
-      max_facet_values: 50,
+      facet_by: 'exifInfo.city,smartInfo.objects',
+      max_facet_values: 12,
     });
     });
 
 
     return firstValueFrom(
     return firstValueFrom(
@@ -166,23 +166,31 @@ export class TypesenseRepository implements ISearchRepository {
         mergeMap(
         mergeMap(
           (facet) =>
           (facet) =>
             from(facet.counts).pipe(
             from(facet.counts).pipe(
-              mergeMap(
-                (count) =>
-                  from(
-                    asset$.search({
-                      ...common,
-                      query_by: 'exifInfo.imageName',
-                      filter_by: `${facet.field_name}:${count.value}`,
-                    }),
-                  ).pipe(
-                    map((result) => ({
-                      value: count.value,
-                      data: result.hits?.[0]?.document as AssetEntity,
-                    })),
-                    filter((item) => !!item.data),
-                  ),
-                5,
-              ),
+              mergeMap((count) => {
+                const config = {
+                  ...common,
+                  query_by: 'exifInfo.imageName',
+                  filter_by: [
+                    this.buildFilterBy('ownerId', userId, true),
+                    this.buildFilterBy(facet.field_name, count.value, true),
+                  ].join(' && '),
+                  per_page: 1,
+                };
+
+                this.logger.verbose(`Explore subquery: "filter_by:${config.filter_by}" (count:${count.count})`);
+
+                return from(asset$.search(config)).pipe(
+                  catchError((error: any) => {
+                    this.logger.warn(`Explore subquery error: ${error}`, error?.stack);
+                    return of({ hits: [] });
+                  }),
+                  map((result) => ({
+                    value: count.value,
+                    data: result.hits?.[0]?.document as AssetEntity,
+                  })),
+                  filter((item) => !!item.data),
+                );
+              }, 5),
               toArray(),
               toArray(),
               map((items) => ({
               map((items) => ({
                 fieldName: facet.field_name as string,
                 fieldName: facet.field_name as string,
@@ -208,7 +216,7 @@ export class TypesenseRepository implements ISearchRepository {
     await this.client
     await this.client
       .collections(schemaMap[collection].name)
       .collections(schemaMap[collection].name)
       .documents()
       .documents()
-      .delete({ filter_by: `id: [${ids.join(',')}]` });
+      .delete({ filter_by: this.buildFilterBy('id', ids, true) });
   }
   }
 
 
   async searchAlbums(query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>> {
   async searchAlbums(query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>> {
@@ -350,18 +358,17 @@ export class TypesenseRepository implements ISearchRepository {
 
 
   private getAlbumFilters(filters: SearchFilter) {
   private getAlbumFilters(filters: SearchFilter) {
     const { userId } = filters;
     const { userId } = filters;
-    const _filters = [`ownerId:${userId}`];
+
+    const _filters = [this.buildFilterBy('ownerId', userId, true)];
+
     if (filters.id) {
     if (filters.id) {
-      _filters.push(`id:=${filters.id}`);
+      _filters.push(this.buildFilterBy('id', filters.id, true));
     }
     }
 
 
     for (const item of albumSchema.fields || []) {
     for (const item of albumSchema.fields || []) {
-      let value = filters[item.name as keyof SearchFilter];
-      if (Array.isArray(value)) {
-        value = `[${value.join(',')}]`;
-      }
+      const value = filters[item.name as keyof SearchFilter];
       if (item.facet && value !== undefined) {
       if (item.facet && value !== undefined) {
-        _filters.push(`${item.name}:${value}`);
+        _filters.push(this.buildFilterBy(item.name, value));
       }
       }
     }
     }
 
 
@@ -373,17 +380,17 @@ export class TypesenseRepository implements ISearchRepository {
   }
   }
 
 
   private getAssetFilters(filters: SearchFilter) {
   private getAssetFilters(filters: SearchFilter) {
-    const _filters = [`ownerId:${filters.userId}`];
+    const { userId } = filters;
+    const _filters = [this.buildFilterBy('ownerId', userId, true)];
+
     if (filters.id) {
     if (filters.id) {
-      _filters.push(`id:=${filters.id}`);
+      _filters.push(this.buildFilterBy('id', filters.id, true));
     }
     }
+
     for (const item of assetSchema.fields || []) {
     for (const item of assetSchema.fields || []) {
-      let value = filters[item.name as keyof SearchFilter];
-      if (Array.isArray(value)) {
-        value = `[${value.join(',')}]`;
-      }
+      const value = filters[item.name as keyof SearchFilter];
       if (item.facet && value !== undefined) {
       if (item.facet && value !== undefined) {
-        _filters.push(`${item.name}:${value}`);
+        _filters.push(this.buildFilterBy(item.name, value));
       }
       }
     }
     }
 
 
@@ -393,4 +400,19 @@ export class TypesenseRepository implements ISearchRepository {
 
 
     return result;
     return result;
   }
   }
+
+  private buildFilterBy(key: string, values: boolean | string | string[], exact?: boolean) {
+    const token = exact ? ':=' : ':';
+
+    const _values = (Array.isArray(values) ? values : [values]).map((value) => {
+      if (typeof value === 'boolean' || value === 'true' || value === 'false') {
+        return value;
+      }
+      return '`' + value + '`';
+    });
+
+    const value = _values.length > 1 ? `[${_values.join(',')}]` : _values[0];
+
+    return `${key}${token}${value}`;
+  }
 }
 }

+ 1 - 1
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich
  * Immich API
  * Immich API
  *
  *
- * The version of the OpenAPI document: 1.51.0
+ * The version of the OpenAPI document: 1.51.1
  * 
  * 
  *
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich
  * Immich API
  * Immich API
  *
  *
- * The version of the OpenAPI document: 1.51.0
+ * The version of the OpenAPI document: 1.51.1
  * 
  * 
  *
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich
  * Immich API
  * Immich API
  *
  *
- * The version of the OpenAPI document: 1.51.0
+ * The version of the OpenAPI document: 1.51.1
  * 
  * 
  *
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich
  * Immich API
  * Immich API
  *
  *
- * The version of the OpenAPI document: 1.51.0
+ * The version of the OpenAPI document: 1.51.1
  * 
  * 
  *
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich
  * Immich API
  * Immich API
  *
  *
- * The version of the OpenAPI document: 1.51.0
+ * The version of the OpenAPI document: 1.51.1
  * 
  * 
  *
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).