diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 87d576441b..c523d7591d 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/server/libs/infra/src/search/typesense.repository.ts b/server/libs/infra/src/search/typesense.repository.ts index 91c801c54f..c0c676de6d 100644 --- a/server/libs/infra/src/search/typesense.repository.ts +++ b/server/libs/infra/src/search/typesense.repository.ts @@ -8,7 +8,7 @@ import { } from '@app/domain'; import { Injectable, Logger } from '@nestjs/common'; 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 { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections'; import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents'; @@ -148,7 +148,7 @@ export class TypesenseRepository implements ISearchRepository { const common = { q: '*', - filter_by: `ownerId:${userId}`, + filter_by: this.buildFilterBy('ownerId', userId, true), per_page: 100, }; @@ -157,8 +157,8 @@ export class TypesenseRepository implements ISearchRepository { const { facet_counts: facets } = await asset$.search({ ...common, 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( @@ -166,23 +166,31 @@ export class TypesenseRepository implements ISearchRepository { mergeMap( (facet) => 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(), map((items) => ({ fieldName: facet.field_name as string, @@ -208,7 +216,7 @@ export class TypesenseRepository implements ISearchRepository { await this.client .collections(schemaMap[collection].name) .documents() - .delete({ filter_by: `id: [${ids.join(',')}]` }); + .delete({ filter_by: this.buildFilterBy('id', ids, true) }); } async searchAlbums(query: string, filters: SearchFilter): Promise> { @@ -350,18 +358,17 @@ export class TypesenseRepository implements ISearchRepository { private getAlbumFilters(filters: SearchFilter) { const { userId } = filters; - const _filters = [`ownerId:${userId}`]; + + const _filters = [this.buildFilterBy('ownerId', userId, true)]; + if (filters.id) { - _filters.push(`id:=${filters.id}`); + _filters.push(this.buildFilterBy('id', filters.id, true)); } 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) { - _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) { - const _filters = [`ownerId:${filters.userId}`]; + const { userId } = filters; + const _filters = [this.buildFilterBy('ownerId', userId, true)]; + if (filters.id) { - _filters.push(`id:=${filters.id}`); + _filters.push(this.buildFilterBy('id', filters.id, true)); } + 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) { - _filters.push(`${item.name}:${value}`); + _filters.push(this.buildFilterBy(item.name, value)); } } @@ -393,4 +400,19 @@ export class TypesenseRepository implements ISearchRepository { 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}`; + } } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index f66c191d9a..b28456fe20 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * 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). diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index c05ea9590e..7a5d265267 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * 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). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 0afaffa577..5b565e9787 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * 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). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index d04f16ff3a..e341f61156 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * 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). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 5ac63f889e..5e68b5ae47 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * 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).