diff --git a/projects/bp-gallery/package.json b/projects/bp-gallery/package.json index 4bbef3148..a3efe5692 100755 --- a/projects/bp-gallery/package.json +++ b/projects/bp-gallery/package.json @@ -29,11 +29,12 @@ "jodit-react": "^1.3.31", "leaflet": "^1.9.3", "lodash": "^4.17.21", + "meilisearch": "^0.33.0", "nanoid": "^4.0.2", "node-sass": "^8.0.0", "puppeteer": "^19.4.0", - "react-currency-input-field": "^3.6.10", "react": "^18.2.0", + "react-currency-input-field": "^3.6.10", "react-date-range": "^1.4.0", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", diff --git a/projects/bp-gallery/src/components/views/search/SearchView.tsx b/projects/bp-gallery/src/components/views/search/SearchView.tsx index 8d86dc594..66ec9fbcb 100644 --- a/projects/bp-gallery/src/components/views/search/SearchView.tsx +++ b/projects/bp-gallery/src/components/views/search/SearchView.tsx @@ -12,11 +12,8 @@ import SearchBreadcrumbs from './SearchBreadcrumbs'; import SearchHub from './SearchHub'; import './SearchView.scss'; import { isValidYear } from './helpers/addNewParamToSearchPath'; -import { - SearchType, - convertSearchParamsToPictureFilters, - paramToTime, -} from './helpers/search-filters'; +import getSearchResultPictureIds from './helpers/getSearchResultPictureIds'; +import { SearchType, paramToTime } from './helpers/search-filters'; import { toURLSearchParam } from './helpers/url-search-params'; const isValidTimeSpecification = (searchRequest: string) => { @@ -47,25 +44,26 @@ const SearchView = () => { // Builds query from search params in the path const queryParams = useMemo(() => { - if (isAllSearchActive) { - const allSearchTerms = searchParams - .getAll(toURLSearchParam(SearchType.ALL)) - .map(decodeURIComponent); - const searchTimes: string[][] = []; - allSearchTerms.forEach(searchTerm => { - if (isValidTimeSpecification(searchTerm)) { - const { startTime, endTime } = paramToTime(searchTerm); - searchTimes.push([searchTerm, startTime, endTime]); - } - }); - return { - searchTerms: allSearchTerms.filter(searchTerm => !isValidTimeSpecification(searchTerm)), - searchTimes, - }; - } - return convertSearchParamsToPictureFilters(searchParams); - }, [isAllSearchActive, searchParams]); - + // if (isAllSearchActive) { + const allSearchTerms = searchParams + .getAll(toURLSearchParam(SearchType.ALL)) + .map(decodeURIComponent); + const searchTimes: string[][] = []; + allSearchTerms.forEach(searchTerm => { + if (isValidTimeSpecification(searchTerm)) { + const { startTime, endTime } = paramToTime(searchTerm); + searchTimes.push([searchTerm, startTime, endTime]); + } + }); + return { + searchTerms: allSearchTerms.filter(searchTerm => !isValidTimeSpecification(searchTerm)), + searchTimes, + }; + // } + // return convertSearchParamsToPictureFilters(searchParams); + }, [/*isAllSearchActive,*/ searchParams]); + if (import.meta.env.MODE === 'development') + getSearchResultPictureIds(queryParams, '').then(res => console.log('search results:', res)); const { linkToCollection, bulkEdit } = useBulkOperations(); return ( diff --git a/projects/bp-gallery/src/components/views/search/helpers/getSearchResultPictureIds.ts b/projects/bp-gallery/src/components/views/search/helpers/getSearchResultPictureIds.ts new file mode 100644 index 000000000..9492f44bf --- /dev/null +++ b/projects/bp-gallery/src/components/views/search/helpers/getSearchResultPictureIds.ts @@ -0,0 +1,48 @@ +import { MeiliSearch } from 'meilisearch'; + +const dateToTimeStamp = (date: string) => { + return Date.parse(date) / 1000; +}; + +const getSearchResultPictureIds = async ( + { searchTerms, searchTimes }: { searchTerms: string[]; searchTimes: string[][] }, + filter: string +) => { + const client = new MeiliSearch({ + host: 'localhost:7700', + apiKey: '', + }); + const index = client.index('picture'); + + const TIME_RANGE_START = 'time_range_tag_start'; + const TIME_RANGE_END = 'time_range_tag_end'; + // when building a filter for meilisearch the filtered attribute always + // has to come first i.e. time_range_start >= 0 works but 0 <= time_range_start does not work + if (searchTimes.length !== 0) { + const timeFilters = searchTimes.map( + searchTime => + `(${TIME_RANGE_START} >= ${dateToTimeStamp( + searchTime[1] + )} AND ${TIME_RANGE_END} <= ${dateToTimeStamp(searchTime[2])})` + ); + + const timeFilter = timeFilters.join(' OR '); + filter = filter === '' ? timeFilter : filter.concat(' AND ', timeFilter); + } + + const RESULT_LIMIT = 1000; + // this makes it so only documents that match all of the query terms are returned + const MATCHING_STRATEGY = 'all'; + + const settings = { + limit: RESULT_LIMIT, + showMatchesPosition: true, + matchingStrategy: MATCHING_STRATEGY, + filter: filter, + }; + const query = searchTerms.length !== 0 ? searchTerms.join(' ') : ''; + const searchResult = await index.search(query, settings); + return searchResult.hits; +}; + +export default getSearchResultPictureIds; diff --git a/projects/bp-gallery/vite.config.ts b/projects/bp-gallery/vite.config.ts index 96f6f6c02..5426b501e 100644 --- a/projects/bp-gallery/vite.config.ts +++ b/projects/bp-gallery/vite.config.ts @@ -1,7 +1,7 @@ import react from '@vitejs/plugin-react-swc'; -import { defineConfig } from 'vite'; import type { Plugin, PluginBuild } from 'esbuild'; import path from 'path'; +import { defineConfig } from 'vite'; import eslint from 'vite-plugin-eslint'; const splitPackages = ['@mui/icons-material', '@mui/material']; diff --git a/projects/bp-gallery/yarn.lock b/projects/bp-gallery/yarn.lock index d9883dd6f..1cdbf9f4e 100755 --- a/projects/bp-gallery/yarn.lock +++ b/projects/bp-gallery/yarn.lock @@ -3422,6 +3422,13 @@ cross-fetch@3.1.5, cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" +cross-fetch@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" + integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== + dependencies: + node-fetch "^2.6.11" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -5978,6 +5985,13 @@ map-stream@~0.1.0: resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== +meilisearch@^0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.33.0.tgz#25982b193cdd22e9ec534a022dbde89c42951dc4" + integrity sha512-bYPb9WyITnJfzf92e7QFK8Rc50DmshFWxypXCs3ILlpNh8pT15A7KSu9Xgnnk/K3G/4vb3wkxxtFS4sxNkWB8w== + dependencies: + cross-fetch "^3.1.6" + meow@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" @@ -6251,6 +6265,13 @@ node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.11: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + node-gyp@^8.4.1: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" diff --git a/projects/bp-strapi/config/plugins.js b/projects/bp-strapi/config/plugins.js index 38964047c..1346ce148 100755 --- a/projects/bp-strapi/config/plugins.js +++ b/projects/bp-strapi/config/plugins.js @@ -1,4 +1,9 @@ /* eslint-disable no-unused-vars */ + +const dateToTimeStamp = (date) => { + return Date.parse(date) / 1000; +}; + module.exports = ({ env }) => ({ // disable i18n (all content is explicitly german as it's a german photo archive) i18n: false, @@ -58,6 +63,120 @@ module.exports = ({ env }) => ({ }, }, }, + meilisearch: + env("MEILISEARCH_ENABLED", "false") === "true" + ? { + config: { + host: env("MEILISEARCH_HOST"), + apiKey: env("MEILISEARCH_API_KEY"), + picture: { + transformEntry({ entry }) { + const transformedEntry = { + id: entry.id, + likes: entry.likes, + descriptions: entry.descriptions.map( + (description) => description.text + ), + comments: entry.comments.map((comment) => comment.text), + keyword_tags: entry.keyword_tags + .map((tag) => tag.name) + .concat(entry.verified_keyword_tags.map((tag) => tag.name)), + person_tags: entry.person_tags + .map((tag) => tag.name) + .concat(entry.verified_person_tags.map((tag) => tag.name)), + location_tags: entry.location_tags + .map((tag) => tag.name) + .concat( + entry.verified_location_tags.map((tag) => tag.name) + ), + face_tags: entry.face_tags.map((tag) => tag.name), + collections: entry.collections.map((tag) => tag.name), + archive_tag: entry.archive_tag, + time_range_tag_start: entry?.time_range_tag + ? dateToTimeStamp(entry.time_range_tag.start) + : entry.verified_time_range_tag + ? dateToTimeStamp(entry.verified_time_range_tag.start) + : null, + time_range_tag_end: entry?.time_range_tag + ? dateToTimeStamp(entry.time_range_tag.end) + : entry.verified_time_range_tag + ? dateToTimeStamp(entry.verified_time_range_tag.end) + : null, + }; + + return transformedEntry; + }, + settings: { + displayedAttributes: ["id"], + // the order of the attributes in searchableAttributes determines the priorization + // of search results i.e. a match in the first searchable attribute will always outrank a match in any other searchable attribute + searchableAttributes: [ + "descriptions", + "keyword_tags", + "location_tags", + "time_range_tag_start", + "time_range_tag_end", + "face_tags", + "person_tags", + "collections", + "archive_tag", + "comments", + ], + filterableAttributes: [ + "keyword_tags", + "location_tags", + "time_range_tag_start", + "time_range_tag_end", + "face_tags", + "person_tags", + "descriptions", + "comments", + "collections", + "archive_tag", + "is_text", + ], + sortableAttributes: [ + "time_range_tag_start", + "time_range_tag_end", + "likes", + ], + rankingRules: [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness", + ], + // words that are ignored during searches, useful for common words + // that do not carry a meaning on their own like articles, pronomina etc. + // we do not use this setting, since our data on user searchers suggests, that + // users only search for proper names, people, locations and nouns in general + stopWords: [], + synonyms: {}, + // returned documents will always be unigue in this attribute + distinctAttribute: null, + typoTolerance: { + enabled: true, + minWordSizeForTypos: { oneTypo: 3, twoTypos: 4 }, + disableOnWords: [], + disableOnAttributes: [], + }, + // faceting is currently not in use + faceting: { + maxValuesPerFacet: 100, + }, + // maxtotalHits determines the maximal possible amount + // of search results and overrides any other settings + // like the result_limit of the search settings in this regard + pagination: { + maxTotalHits: 1000, + }, + }, + }, + }, + } + : null, upload: env("AWS_ENABLED", "false") === "true" ? { diff --git a/projects/bp-strapi/package.json b/projects/bp-strapi/package.json index 28f0e3dd1..bdcf8c4f0 100755 --- a/projects/bp-strapi/package.json +++ b/projects/bp-strapi/package.json @@ -34,6 +34,8 @@ "pg": "8.6.0", "pm2": "^5.2.0", "sendmail": "^1.6.1", + "sharp": "^0.32.1", + "strapi-plugin-meilisearch": "^0.9.2", "strapi-provider-upload-aws-s3-advanced": "^5.0.1", "xlsx": "^0.18.5" }, diff --git a/projects/bp-strapi/yarn.lock b/projects/bp-strapi/yarn.lock index 5648fd256..1d6954141 100755 --- a/projects/bp-strapi/yarn.lock +++ b/projects/bp-strapi/yarn.lock @@ -3582,6 +3582,18 @@ lodash "4.17.21" yup "0.32.9" +"@strapi/utils@^4.5.4": + version "4.10.7" + resolved "https://registry.yarnpkg.com/@strapi/utils/-/utils-4.10.7.tgz#2cf1aa5958c353e1a1b2e7d3de5e55eff1006cbc" + integrity sha512-LyTcpsdcJ6Dq/33eblSU4pN4Yb9VnARA6K5/66cmTTQH34C2bKqkozm7wjK+Y1smK5HA2nvHQiZfpoDtqI0NmA== + dependencies: + "@sindresorhus/slugify" "1.1.0" + date-fns "2.30.0" + http-errors "1.8.1" + lodash "4.17.21" + p-map "4.0.0" + yup "0.32.9" + "@swc/helpers@^0.4.14": version "0.4.14" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" @@ -5944,7 +5956,7 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" -cross-fetch@^3.1.6: +cross-fetch@^3.1.5, cross-fetch@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== @@ -9451,6 +9463,13 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +meilisearch@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.30.0.tgz#707f9a6b07440c496b965379616e084f112160ae" + integrity sha512-3y1hALOwTDpquYar+gDREqRasFPWKxkWAhk6h+RF+nKObPVf9N77wcTNvukGwOKbxRyJnKge0OPgAB1BkB9VVw== + dependencies: + cross-fetch "^3.1.5" + memfs@^3.4.1, memfs@^3.4.3: version "3.4.13" resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.13.tgz#248a8bd239b3c240175cd5ec548de5227fc4f345" @@ -9819,6 +9838,11 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -11878,6 +11902,13 @@ semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.5.0: + version "7.5.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" + integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ== + dependencies: + lru-cache "^6.0.0" + semver@~7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.2.3.tgz#3641217233c6382173c76bf2c7ecd1e1c16b0d8a" @@ -12007,6 +12038,20 @@ sharp@0.31.0: tar-fs "^2.1.1" tunnel-agent "^0.6.0" +sharp@^0.32.1: + version "0.32.1" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.1.tgz#41aa0d0b2048b2e0ee453d9fcb14ec1f408390fe" + integrity sha512-kQTFtj7ldpUqSe8kDxoGLZc1rnMFU0AO2pqbX6pLy3b7Oj8ivJIdoKNwxHVQG2HN6XpHPJqCSM2nsma2gOXvOg== + dependencies: + color "^4.2.3" + detect-libc "^2.0.1" + node-addon-api "^6.1.0" + prebuild-install "^7.1.1" + semver "^7.5.0" + simple-get "^4.0.1" + tar-fs "^2.1.1" + tunnel-agent "^0.6.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -12316,6 +12361,14 @@ std-env@^3.0.1: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.1.tgz#93a81835815e618c8aa75e7c8a4dc04f7c314e29" integrity sha512-3H20QlwQsSm2OvAxWIYhs+j01MzzqwMwGiiO1NQaJYZgJZFPuAbf95/DiKRBSTYIJ2FeGUc+B/6mPGcWP9dO3Q== +strapi-plugin-meilisearch@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/strapi-plugin-meilisearch/-/strapi-plugin-meilisearch-0.9.2.tgz#c8db12820b86640b1bdc850dde94da9c50e9b6e8" + integrity sha512-18rAUkQ5LTVYGKqe0bslKpMPC3nd/VpQNFAFkydTqhONkdPI/m1SkRnudaFujMspTB7+fqcAhrdGOeHTH/g3cQ== + dependencies: + "@strapi/utils" "^4.5.4" + meilisearch "^0.30.0" + strapi-provider-upload-aws-s3-advanced@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/strapi-provider-upload-aws-s3-advanced/-/strapi-provider-upload-aws-s3-advanced-5.0.1.tgz#8e0de23920ecb7478beeb4e2c62cf42dd2cd6296"