From 1552683c07be619f770c5b79f9b0748039537c3f Mon Sep 17 00:00:00 2001 From: koomchang Date: Sat, 16 Nov 2024 22:22:54 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20elasticsearch=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=B9=98=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 4 +- backend/yarn.lock | 554 +++++++++++++++++++++++++------------------ 2 files changed, 323 insertions(+), 235 deletions(-) diff --git a/backend/package.json b/backend/package.json index 7fc80991..8dba54c0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,7 +32,9 @@ "nestjs-pino": "^4.1.0", "pino-http": "^10.3.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "@nestjs/elasticsearch": "^10.0.2", + "@elastic/elasticsearch": "^8.16.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/backend/yarn.lock b/backend/yarn.lock index b658696e..7f16d780 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -81,7 +81,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz" integrity sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": version "7.25.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz" integrity sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ== @@ -365,6 +365,28 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@elastic/elasticsearch@^8.16.0": + version "8.16.0" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.16.0.tgz#35c6729e15684154b8b3c391cfad34877d771cf0" + integrity sha512-uiPC6AePESl/jakx+ZLarLsLdxDHiCSrpAkkXJ5Q8A78vadEQsTGXRUVFwWWkS8gfGthnT3O+QK6BHXuszd0KQ== + dependencies: + "@elastic/transport" "^8.9.1" + apache-arrow "^18.0.0" + tslib "^2.4.0" + +"@elastic/transport@^8.9.1": + version "8.9.1" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.9.1.tgz#1acc090ac45903a3d5a8b7269f6ba6410108dc0a" + integrity sha512-jasKNQeOb1vNf9aEYg+8zXmetaFjApDTSCC4QTl6aTixvyiRiSLcCiB8P6Q0lY9JIII/BhqNl8WbpFnsKitntw== + dependencies: + "@opentelemetry/api" "1.x" + debug "^4.3.4" + hpagent "^1.0.0" + ms "^2.1.3" + secure-json-parse "^2.4.0" + tslib "^2.4.0" + undici "^6.12.0" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" @@ -608,7 +630,7 @@ jest-haste-map "^29.7.0" slash "^3.0.0" -"@jest/transform@^29.0.0", "@jest/transform@^29.7.0": +"@jest/transform@^29.7.0": version "29.7.0" resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== @@ -629,7 +651,7 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^29.0.0", "@jest/types@^29.6.3": +"@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== @@ -673,14 +695,6 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -689,6 +703,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@ljharb/through@^2.3.12": version "2.3.13" resolved "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz" @@ -726,14 +748,14 @@ webpack "5.94.0" webpack-node-externals "3.0.0" -"@nestjs/common@^10.0.0", "@nestjs/common@^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/common@^8.0.0 || ^9.0.0 || ^10.0.0": +"@nestjs/common@^10.0.0": version "10.4.6" resolved "https://registry.npmjs.org/@nestjs/common/-/common-10.4.6.tgz" integrity sha512-KkezkZvU9poWaNq4L+lNvx+386hpOxPJkfXBBeSMrcqBOx8kVr36TGN2uYkF4Ta4zNu1KbCjmZbc0rhHSg296g== dependencies: + uid "2.0.2" iterare "1.2.1" tslib "2.7.0" - uid "2.0.2" "@nestjs/config@^3.3.0": version "3.3.0" @@ -744,17 +766,22 @@ dotenv-expand "10.0.0" lodash "4.17.21" -"@nestjs/core@^10.0.0", "@nestjs/core@^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "@nestjs/core@^8.0.0 || ^9.0.0 || ^10.0.0": +"@nestjs/core@^10.0.0": version "10.4.6" resolved "https://registry.npmjs.org/@nestjs/core/-/core-10.4.6.tgz" integrity sha512-zXVPxCNRfO6gAy0yvEDjUxE/8gfZICJFpsl2lZAUH31bPb6m+tXuhUq2mVCTEltyMYQ+DYtRe+fEYM2v152N1g== dependencies: + uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" path-to-regexp "3.3.0" tslib "2.7.0" - uid "2.0.2" + +"@nestjs/elasticsearch@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@nestjs/elasticsearch/-/elasticsearch-10.0.2.tgz#b86c521464aed36f756efc054869ad13c070c9ce" + integrity sha512-G3aIU47Bc6y0CuC1hWEHItiy9aK9d1PpW20bo8bxbkIOZE58XwbUzLZ8En7VK88nTZKQjMEfaHgsXpyB1TqdAQ== "@nestjs/platform-express@^10.0.0": version "10.4.6" @@ -805,7 +832,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -827,6 +854,11 @@ consola "^2.15.0" node-fetch "^2.6.1" +"@opentelemetry/api@1.x": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -861,6 +893,13 @@ resolved "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz" integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== +"@swc/helpers@^0.5.11": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + "@testcontainers/mysql@^10.14.0": version "10.14.0" resolved "https://registry.npmjs.org/@testcontainers/mysql/-/mysql-10.14.0.tgz" @@ -929,6 +968,16 @@ "@types/connect" "*" "@types/node" "*" +"@types/command-line-args@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.3.tgz#553ce2fd5acf160b448d307649b38ffc60d39639" + integrity sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw== + +"@types/command-line-usage@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.4.tgz#374e4c62d78fbc5a670a0f36da10235af879a0d5" + integrity sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg== + "@types/connect@*": version "3.4.38" resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" @@ -1065,6 +1114,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20.13.0": + version "20.17.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.6.tgz#6e4073230c180d3579e8c60141f99efdf5df0081" + integrity sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ== + dependencies: + undici-types "~6.19.2" + "@types/qs@*": version "6.9.16" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz" @@ -1169,7 +1225,7 @@ natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^8.0.0 || ^8.0.0-alpha.0", "@typescript-eslint/parser@^8.11.0": +"@typescript-eslint/parser@^8.11.0": version "8.11.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz" integrity sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg== @@ -1240,7 +1296,7 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@1.12.1": +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz" integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== @@ -1341,7 +1397,7 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@^1.12.1", "@webassemblyjs/wasm-parser@1.12.1": +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz" integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== @@ -1403,7 +1459,7 @@ acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.11.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.11.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: version "8.13.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== @@ -1427,7 +1483,17 @@ ajv-keywords@^3.5.2: resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1: +ajv@8.12.0: + version "8.12.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1447,16 +1513,6 @@ ajv@^8.0.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" -ajv@8.12.0: - version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - ansi-colors@4.1.3: version "4.1.3" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" @@ -1516,6 +1572,21 @@ anymatch@^3.0.3, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +apache-arrow@^18.0.0: + version "18.0.0" + resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-18.0.0.tgz#a187bd55318a7a68b6ff09835d05e89e019eba2e" + integrity sha512-gFlPaqN9osetbB83zC29AbbZqGiCuFH1vyyPseJ+B7SIbfBtESV62mMT/CkiIt77W6ykC/nTWFzTXFs0Uldg4g== + dependencies: + "@swc/helpers" "^0.5.11" + "@types/command-line-args" "^5.2.3" + "@types/command-line-usage" "^5.0.4" + "@types/node" "^20.13.0" + command-line-args "^5.2.1" + command-line-usage "^7.0.1" + flatbuffers "^24.3.25" + json-bignum "^0.0.3" + tslib "^2.6.2" + app-root-path@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz" @@ -1569,6 +1640,16 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157" + integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw== + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" @@ -1621,7 +1702,7 @@ b4a@^1.6.4: resolved "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz" integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== -babel-jest@^29.0.0, babel-jest@^29.7.0: +babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== @@ -1793,7 +1874,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.21.10, browserslist@^4.24.0, "browserslist@>= 4.21.0": +browserslist@^4.21.10, browserslist@^4.24.0: version "4.24.2" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -1901,16 +1982,14 @@ caniuse-lite@^1.0.30001669: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz" integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== +chalk-template@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b" + integrity sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + chalk "^4.1.2" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2, chalk@4.1.2: +chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1918,6 +1997,15 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2, chalk@4.1. ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" @@ -1933,7 +2021,7 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chokidar@^3.5.2, chokidar@^3.5.3, chokidar@3.6.0: +chokidar@3.6.0, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -1968,12 +2056,12 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== -class-transformer@*, class-transformer@^0.5.1: +class-transformer@^0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz" integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== -class-validator@*, class-validator@^0.14.1: +class-validator@^0.14.1: version "0.14.1" resolved "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz" integrity sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ== @@ -2072,16 +2160,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colorette@^2.0.7: version "2.0.20" resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" @@ -2094,16 +2182,36 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +command-line-args@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-7.0.3.tgz#6bce992354f6af10ecea2b631bfdf0c8b3bfaea3" + integrity sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q== + dependencies: + array-back "^6.2.2" + chalk-template "^0.4.0" + table-layout "^4.1.0" + typical "^7.1.1" commander@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + comment-json@4.2.5: version "4.2.5" resolved "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz" @@ -2277,13 +2385,6 @@ dayjs@^1.11.9: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@4: - version "4.3.7" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== - dependencies: - ms "^2.1.3" - debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -2291,6 +2392,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + dedent@^1.0.0: version "1.5.3" resolved "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz" @@ -2403,7 +2511,7 @@ dotenv-expand@10.0.0: resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@^16.0.3, dotenv@16.4.5: +dotenv@16.4.5, dotenv@^16.0.3: version "16.4.5" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== @@ -2413,7 +2521,7 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -ecdsa-sig-formatter@^1.0.11, ecdsa-sig-formatter@1.0.11: +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== @@ -2526,7 +2634,7 @@ escape-string-regexp@^4.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@*, eslint-config-prettier@^9.0.0: +eslint-config-prettier@^9.0.0: version "9.1.0" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== @@ -2539,14 +2647,6 @@ eslint-plugin-prettier@^5.0.0: prettier-linter-helpers "^1.0.0" synckit "^0.9.1" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" @@ -2555,12 +2655,20 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -"eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@^8.42.0, "eslint@^8.57.0 || ^9.0.0", eslint@>=7.0.0, eslint@>=8.0.0: +eslint@^8.42.0: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -2775,7 +2883,7 @@ fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2790,7 +2898,7 @@ fast-redact@^3.1.1: resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz" integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== -fast-safe-stringify@^2.1.1, fast-safe-stringify@2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -2855,6 +2963,13 @@ finalhandler@1.3.1: statuses "2.0.1" unpipe "~1.0.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -2880,6 +2995,11 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" +flatbuffers@^24.3.25: + version "24.3.25" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-24.3.25.tgz#e2f92259ba8aa53acd0af7844afb7c7eb95e7089" + integrity sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ== + flatted@^3.2.9: version "3.3.1" resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz" @@ -3055,10 +3175,10 @@ glob-to-regexp@^0.4.1: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.0.0: - version "10.4.5" - resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== +glob@10.4.2: + version "10.4.2" + resolved "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz" + integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" @@ -3067,7 +3187,7 @@ glob@^10.0.0: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^10.3.10: +glob@^10.0.0, glob@^10.3.10: version "10.4.5" resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== @@ -3091,18 +3211,6 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@10.4.2: - version "10.4.2" - resolved "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz" - integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - globals@^11.1.0: version "11.12.0" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" @@ -3206,6 +3314,11 @@ highlight.js@^10.7.1: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +hpagent@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-1.2.0.tgz#0ae417895430eb3770c03443456b8d90ca464903" + integrity sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" @@ -3235,7 +3348,7 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@^0.4.24, iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3288,7 +3401,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3702,7 +3815,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@*, jest-resolve@^29.7.0: +jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -3855,7 +3968,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^29.0.0, jest@^29.5.0: +jest@^29.5.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -3902,6 +4015,11 @@ json-bigint@^1.0.0: dependencies: bignumber.js "^9.0.0" +json-bignum@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7" + integrity sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" @@ -4062,6 +4180,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" @@ -4107,7 +4230,7 @@ lodash.once@^4.0.0: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -lodash@^4.17.15, lodash@^4.17.21, lodash@4.17.21: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4247,14 +4370,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^5.1.0: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -4300,16 +4416,16 @@ mkdirp@^2.1.3: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz" integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== -ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multer@1.4.4-lts.1: version "1.4.4-lts.1" resolved "https://registry.npmjs.org/multer/-/multer-1.4.4-lts.1.tgz" @@ -4333,7 +4449,7 @@ mute-stream@1.0.0: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -"mysql2@^2.2.5 || ^3.0.1", mysql2@^3.11.3: +mysql2@^3.11.3: version "3.11.3" resolved "https://registry.npmjs.org/mysql2/-/mysql2-3.11.3.tgz" integrity sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ== @@ -4478,7 +4594,7 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" -ora@^5.4.1, ora@5.4.1: +ora@5.4.1, ora@^5.4.1: version "5.4.1" resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== @@ -4623,16 +4739,16 @@ picocolors@^1.0.0, picocolors@^1.1.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - picomatch@4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz" integrity sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + pino-abstract-transport@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz" @@ -4640,7 +4756,7 @@ pino-abstract-transport@^2.0.0: dependencies: split2 "^4.0.0" -pino-http@^10.3.0, "pino-http@^6.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0": +pino-http@^10.3.0: version "10.3.0" resolved "https://registry.npmjs.org/pino-http/-/pino-http-10.3.0.tgz" integrity sha512-kaHQqt1i5S9LXWmyuw6aPPqYW/TjoDPizPs4PnDW4hSpajz2Uo/oisNliLf7We1xzpiLacdntmw8yaZiEkppQQ== @@ -4720,7 +4836,7 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^3.0.0, prettier@>=3.0.0: +prettier@^3.0.0: version "3.3.3" resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz" integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== @@ -4799,7 +4915,7 @@ pure-rand@^6.0.0: resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -qs@^6.11.0, qs@6.13.0: +qs@6.13.0, qs@^6.11.0: version "6.13.0" resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== @@ -4861,25 +4977,7 @@ readable-stream@^2.0.5, readable-stream@^2.2.2: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.4.0: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^3.5.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -4918,7 +5016,7 @@ real-require@^0.2.0: resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== -"reflect-metadata@^0.1.12 || ^0.2.0", "reflect-metadata@^0.1.13 || ^0.2.0", reflect-metadata@^0.2.0, reflect-metadata@^0.2.1: +reflect-metadata@^0.2.0, reflect-metadata@^0.2.1: version "0.2.2" resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz" integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== @@ -5011,14 +5109,14 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.1, rxjs@7.8.1: +rxjs@7.8.1, rxjs@^7.5.5, rxjs@^7.8.1: version "7.8.1" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5052,12 +5150,7 @@ secure-json-parse@^2.4.0: resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== -semver@^6.3.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.1: +semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -5182,14 +5275,6 @@ sonic-boom@^4.0.1: dependencies: atomic-sleep "^1.0.0" -source-map-support@^0.5.21, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" @@ -5198,20 +5283,23 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map-support@^0.5.21, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" -source-map@^0.7.4: +source-map@0.7.4, source-map@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@0.7.4: - version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== split-ca@^1.0.1: version "1.0.1" @@ -5280,20 +5368,6 @@ streamx@^2.15.0, streamx@^2.20.0: optionalDependencies: bare-events "^2.2.0" -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -5302,16 +5376,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5329,14 +5394,21 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - ansi-regex "^5.0.1" + safe-buffer "~5.2.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5433,6 +5505,14 @@ synckit@^0.9.1: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +table-layout@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-4.1.1.tgz#0f72965de1a5c0c1419c9ba21cae4e73a2f73a42" + integrity sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA== + dependencies: + array-back "^6.2.2" + wordwrapjs "^5.1.0" + tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" @@ -5636,7 +5716,7 @@ ts-loader@^9.4.3: semver "^7.3.4" source-map "^0.7.4" -ts-node@^10.7.0, ts-node@^10.9.1, ts-node@>=9.0.0: +ts-node@^10.9.1: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -5664,7 +5744,7 @@ tsconfig-paths-webpack-plugin@4.1.0: enhanced-resolve "^5.7.0" tsconfig-paths "^4.1.2" -tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0, tsconfig-paths@4.2.0: +tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== @@ -5673,25 +5753,20 @@ tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0, tsconfig-paths@4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0: - version "2.8.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz" - integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== - -tslib@^2.5.0: - version "2.8.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz" - integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== -tslib@^2.6.2: +tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.2: version "2.8.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== -tslib@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tslib@^2.4.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tweetnacl@^0.14.3: version "0.14.5" @@ -5733,7 +5808,7 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typeorm@^0.3.0, typeorm@^0.3.20: +typeorm@^0.3.20: version "0.3.20" resolved "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz" integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q== @@ -5754,15 +5829,25 @@ typeorm@^0.3.0, typeorm@^0.3.20: uuid "^9.0.0" yargs "^17.6.2" -typescript@*, typescript@^5.1.3, typescript@>=2.7, typescript@>=4.2.0, "typescript@>=4.3 <6", typescript@>=4.8.2: +typescript@5.3.3: + version "5.3.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + +typescript@^5.1.3: version "5.6.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz" integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== -typescript@>=4.9.5, typescript@>3.6.0, typescript@5.3.3: - version "5.3.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^7.1.1: + version "7.3.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-7.3.0.tgz#930376be344228709f134613911fa22aa09617a4" + integrity sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw== uid@2.0.2: version "2.0.2" @@ -5788,12 +5873,17 @@ undici@^5.28.4: dependencies: "@fastify/busboy" "^2.0.0" +undici@^6.12.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.0.tgz#4b3d3afaef984e07b48e7620c34ed8a285ed4cd4" + integrity sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -5823,7 +5913,7 @@ utils-merge@1.0.1: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^9.0.0, uuid@^9.0.1, uuid@9.0.1: +uuid@9.0.1, uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -5889,7 +5979,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.0.0, webpack@^5.1.0, webpack@^5.11.0, webpack@5.94.0: +webpack@5.94.0: version "5.94.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz" integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== @@ -5938,7 +6028,12 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +wordwrapjs@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a" + integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5956,15 +6051,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -6007,16 +6093,16 @@ yaml@^2.2.2: resolved "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz" integrity sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ== +yargs-parser@21.1.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.1.1, yargs-parser@21.1.1: - version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - yargs@^16.0.0: version "16.2.0" resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" From bb64b5c257f03ba724df1eba16ed1f6dcf345d13 Mon Sep 17 00:00:00 2001 From: koomchang Date: Sun, 17 Nov 2024 02:12:50 +0900 Subject: [PATCH 02/27] =?UTF-8?q?config:=20elasticsearch=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20place=20index=20=EA=B5=AC=EC=84=B1=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch/index/place-index.json | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 backend/resources/elasticsearch/index/place-index.json diff --git a/backend/resources/elasticsearch/index/place-index.json b/backend/resources/elasticsearch/index/place-index.json new file mode 100644 index 00000000..fbd2804c --- /dev/null +++ b/backend/resources/elasticsearch/index/place-index.json @@ -0,0 +1,86 @@ +{ + "settings": { + "number_of_shards": 1, + "number_of_replicas": 0, + "analysis": { + "analyzer": { + "customKeywordAnalyzer": { + "type": "custom", + "tokenizer": "keyword", + "filter": [ + "lowercase" + ] + } + } + } + }, + "mappings": { + "properties": { + "id": { + "type": "integer" + }, + "googlePlaceId": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "analyzer": "customKeywordAnalyzer" + }, + "thumbnailUrl": { + "type": "text", + "index": false, + "null_value": null + }, + "rating": { + "type": "scaled_float", + "scaling_factor": 100, + "null_value": null + }, + "longitude": { + "type": "float", + "null_value": null + }, + "latitude": { + "type": "float", + "null_value": null + }, + "formattedAddress": { + "type": "text", + "index": true, + "null_value": null + }, + "category": { + "type": "keyword", + "index": true, + "null_value": null + }, + "description": { + "type": "text", + "null_value": null + }, + "detailPageUrl": { + "type": "text", + "index": false, + "null_value": null + }, + "createdAt": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "updatedAt": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + }, + "deletedAt": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis", + "null_value": null + } + } + } +} From 7c9e52d52287901de6420243d11127bb1dc0d29a Mon Sep 17 00:00:00 2001 From: koomchang Date: Sun, 17 Nov 2024 02:13:29 +0900 Subject: [PATCH 03/27] =?UTF-8?q?config:=20=EB=A1=9C=EC=BB=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20elasticsearch=EC=99=80=20kibana=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=97=86=EC=9D=B4=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch/docker-compose.yml | 41 +++++++++++++++++++ backend/resources/elasticsearch/init-index.sh | 22 ++++++++++ 2 files changed, 63 insertions(+) create mode 100644 backend/resources/elasticsearch/docker-compose.yml create mode 100755 backend/resources/elasticsearch/init-index.sh diff --git a/backend/resources/elasticsearch/docker-compose.yml b/backend/resources/elasticsearch/docker-compose.yml new file mode 100644 index 00000000..d9f13214 --- /dev/null +++ b/backend/resources/elasticsearch/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.8" + +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.16.0 + container_name: elasticsearch + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - xpack.security.enabled=false + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - esdata:/usr/share/elasticsearch/data + - ./index/place-index.json:/usr/share/elasticsearch/place-index.json + - ./init-index.sh:/usr/share/elasticsearch/init-index.sh + ports: + - "9200:9200" + command: > + /bin/bash -c " + chmod +x /usr/share/elasticsearch/init-index.sh && + /usr/share/elasticsearch/init-index.sh & + exec /usr/local/bin/docker-entrypoint.sh + " + + kibana: + image: docker.elastic.co/kibana/kibana:8.16.0 + container_name: kibana + depends_on: + - elasticsearch + environment: + - SERVER_NAME=kibana + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + ports: + - "5601:5601" + +volumes: + esdata: + driver: local \ No newline at end of file diff --git a/backend/resources/elasticsearch/init-index.sh b/backend/resources/elasticsearch/init-index.sh new file mode 100755 index 00000000..aaa5c276 --- /dev/null +++ b/backend/resources/elasticsearch/init-index.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +until curl -s -XGET "http://localhost:9200/_cluster/health" | grep '"status":"green"' > /dev/null; do + echo "Waiting for Elasticsearch to be ready..." + sleep 5 +done + +# Place 인덱스 확인 및 생성 +echo "Checking if 'place' index exists..." +response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place") + +if [ "$response" -eq 404 ]; then + echo "Index 'place' does not exist. Creating it..." + curl -X PUT "http://localhost:9200/place" \ + -H "Content-Type: application/json" \ + --data-binary @/usr/share/elasticsearch/place-index.json + echo "'place' index created successfully." +elif [ "$response" -eq 200 ]; then + echo "Index 'place' already exists." +else + echo "Unexpected response while checking for 'place' index: HTTP $response" +fi From cf4b02022cd288c40687c127ca404487f0620375 Mon Sep 17 00:00:00 2001 From: koomchang Date: Sun, 17 Nov 2024 02:14:28 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat:=20search=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=20ElasticSearch=EC=97=90=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/app.module.ts | 2 ++ backend/src/search/search.controller.ts | 17 +++++++++ backend/src/search/search.module.ts | 25 +++++++++++++ backend/src/search/search.service.ts | 48 +++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 backend/src/search/search.controller.ts create mode 100644 backend/src/search/search.module.ts create mode 100644 backend/src/search/search.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 861b5e76..514b41be 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,6 +17,7 @@ import { TimezoneInterceptor } from './config/TimezoneInterceptor'; import { StorageModule } from './storage/storage.module'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { CustomLoggerModule } from './common/log/CustomLoggerModule'; +import { SearchModule } from './search/search.module'; @Module({ imports: [ @@ -39,6 +40,7 @@ import { CustomLoggerModule } from './common/log/CustomLoggerModule'; BannerModule, AdminModule, StorageModule, + SearchModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/search/search.controller.ts b/backend/src/search/search.controller.ts new file mode 100644 index 00000000..9da01ffb --- /dev/null +++ b/backend/src/search/search.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { SearchService } from '@src/search/search.service'; + +@Controller('search') +export class SearchController { + constructor(private readonly searchService: SearchService) {} + + @Get() + async search(@Query('query') query: string) { + return await this.searchService.search(query); + } + + @Get('index/place') + async indexPlace() { + return await this.searchService.indexPlace(); + } +} diff --git a/backend/src/search/search.module.ts b/backend/src/search/search.module.ts new file mode 100644 index 00000000..0eee9d27 --- /dev/null +++ b/backend/src/search/search.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { SearchController } from './search.controller'; +import { SearchService } from './search.service'; +import { PlaceModule } from '@src/place/place.module'; + +@Module({ + imports: [ + PlaceModule, + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: + configService.get('ELASTICSEARCH_NODE') || + 'http://localhost:9200', + }), + inject: [ConfigService], + }), + ], + providers: [SearchService], + controllers: [SearchController], + exports: [], +}) +export class SearchModule {} diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts new file mode 100644 index 00000000..ac834427 --- /dev/null +++ b/backend/src/search/search.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { PlaceRepository } from '@src/place/place.repository'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { PinoLogger } from 'nestjs-pino'; + +@Injectable() +export class SearchService { + constructor( + private placeRepository: PlaceRepository, + private readonly logger: PinoLogger, + private readonly esService: ElasticsearchService, + ) {} + + // 데이터를 인덱스(엘라스틱서치 형태로 주입) + async indexData(indexName: string, data: any) { + return await this.esService.index({ + index: indexName, + document: data, + }); + } + + async indexPlace() { + const startTime = new Date().toISOString(); + this.logger.info(`Indexing 시작: ${startTime}`); + const places = await this.placeRepository.find(); + for (const place of places) { + await this.indexData('place', place); + } + const endTime = new Date().toISOString(); + this.logger.info(`Indexing 완료: ${endTime}`); + this.logger.info( + `소요 시간: ${new Date(endTime).getTime() - new Date(startTime).getTime()}ms`, + ); + } + + async search(query: string) { + const result = await this.esService.search({ + index: 'place', + query: { + multi_match: { + query: query, + fields: ['name'], + }, + }, + }); + return result.hits?.hits || []; + } +} From 43c2d2d1a047b7fcd9cedfb0d84fa17da3c5495b Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 18 Nov 2024 17:17:14 +0900 Subject: [PATCH 05/27] =?UTF-8?q?config:=20es=EC=97=90=20nori=20analyzer?= =?UTF-8?q?=20=EC=84=A4=EC=B9=98=ED=95=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/resources/elasticsearch/Dockerfile | 3 +++ backend/resources/elasticsearch/docker-compose.yml | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 backend/resources/elasticsearch/Dockerfile diff --git a/backend/resources/elasticsearch/Dockerfile b/backend/resources/elasticsearch/Dockerfile new file mode 100644 index 00000000..c4545348 --- /dev/null +++ b/backend/resources/elasticsearch/Dockerfile @@ -0,0 +1,3 @@ +FROM docker.elastic.co/elasticsearch/elasticsearch:8.16.0 + +RUN elasticsearch-plugin install --batch analysis-nori diff --git a/backend/resources/elasticsearch/docker-compose.yml b/backend/resources/elasticsearch/docker-compose.yml index d9f13214..07bf6df6 100644 --- a/backend/resources/elasticsearch/docker-compose.yml +++ b/backend/resources/elasticsearch/docker-compose.yml @@ -1,8 +1,8 @@ -version: "3.8" - services: elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:8.16.0 + build: + context: ./elasticsearch + dockerfile: Dockerfile container_name: elasticsearch environment: - discovery.type=single-node @@ -15,6 +15,7 @@ services: volumes: - esdata:/usr/share/elasticsearch/data - ./index/place-index.json:/usr/share/elasticsearch/place-index.json + - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt - ./init-index.sh:/usr/share/elasticsearch/init-index.sh ports: - "9200:9200" From a5a35d85b7d8591e24e21195e46ddfeed2518ace Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 18 Nov 2024 17:17:33 +0900 Subject: [PATCH 06/27] =?UTF-8?q?config:=20=EB=8C=80=ED=95=9C=EB=AF=BC?= =?UTF-8?q?=EA=B5=AD=20=EC=A7=80=EC=97=AD=20=EB=8F=99=EC=9D=8C=EC=9D=B4?= =?UTF-8?q?=EC=9D=98=EC=96=B4=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch/index/place-index.json | 43 +++++++++---------- .../elasticsearch/index/synonyms.txt | 17 ++++++++ 2 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 backend/resources/elasticsearch/index/synonyms.txt diff --git a/backend/resources/elasticsearch/index/place-index.json b/backend/resources/elasticsearch/index/place-index.json index fbd2804c..181f845f 100644 --- a/backend/resources/elasticsearch/index/place-index.json +++ b/backend/resources/elasticsearch/index/place-index.json @@ -4,13 +4,21 @@ "number_of_replicas": 0, "analysis": { "analyzer": { - "customKeywordAnalyzer": { + "nori_with_synonym": { "type": "custom", - "tokenizer": "keyword", + "tokenizer": "nori_tokenizer", "filter": [ - "lowercase" + "lowercase", + "nori_readingform", + "synonym_filter" ] } + }, + "filter": { + "synonym_filter": { + "type": "synonym", + "synonyms_path": "synonyms.txt" + } } } }, @@ -29,44 +37,33 @@ "type": "keyword" } }, - "analyzer": "customKeywordAnalyzer" + "analyzer": "nori_with_synonym" }, "thumbnailUrl": { "type": "text", - "index": false, - "null_value": null + "index": false }, "rating": { "type": "scaled_float", - "scaling_factor": 100, - "null_value": null - }, - "longitude": { - "type": "float", - "null_value": null + "scaling_factor": 100 }, - "latitude": { - "type": "float", - "null_value": null + "location": { + "type": "geo_point" }, "formattedAddress": { "type": "text", - "index": true, - "null_value": null + "analyzer": "nori_with_synonym" }, "category": { "type": "keyword", - "index": true, - "null_value": null + "index": true }, "description": { - "type": "text", - "null_value": null + "type": "text" }, "detailPageUrl": { "type": "text", - "index": false, - "null_value": null + "index": false }, "createdAt": { "type": "date", diff --git a/backend/resources/elasticsearch/index/synonyms.txt b/backend/resources/elasticsearch/index/synonyms.txt new file mode 100644 index 00000000..aea4ae3d --- /dev/null +++ b/backend/resources/elasticsearch/index/synonyms.txt @@ -0,0 +1,17 @@ +서울특별시, 서울, 서울시 +부산광역시, 부산, 부산시 +대구광역시, 대구, 대구시 +인천광역시, 인천, 인천시 +광주광역시, 광주, 광주시 +대전광역시, 대전, 대전시 +울산광역시, 울산, 울산시 +세종특별자치시, 세종, 세종시 +경기도, 경기, 경기도청 +강원도, 강원, 강원도청 +충청북도, 충북, 충청북도청 +충청남도, 충남, 충청남도청 +전라북도, 전북, 전북특별자치도, 전라북도청 +전라남도, 전남, 전라남도청 +경상북도, 경북, 경상북도청 +경상남도, 경남, 경상남도청 +제주특별자치도, 제주, 제주도 From 517156cf945528d8025d5cd34e34b2182d02294a Mon Sep 17 00:00:00 2001 From: koomchang Date: Mon, 18 Nov 2024 17:18:06 +0900 Subject: [PATCH 07/27] =?UTF-8?q?feat:=20es=EA=B0=80=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EB=90=9C=20=EC=9E=A5=EC=86=8C=20=EA=B2=80=EC=83=89=20api=20?= =?UTF-8?q?#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/search/search.controller.ts | 11 +- backend/src/search/search.service.ts | 139 +++++++++++++++++++++++- 2 files changed, 143 insertions(+), 7 deletions(-) diff --git a/backend/src/search/search.controller.ts b/backend/src/search/search.controller.ts index 9da01ffb..7ef2df66 100644 --- a/backend/src/search/search.controller.ts +++ b/backend/src/search/search.controller.ts @@ -1,13 +1,20 @@ import { Controller, Get, Query } from '@nestjs/common'; import { SearchService } from '@src/search/search.service'; +import { ParseOptionalNumberPipe } from '@src/common/pipe/ParseOptionalNumberPipe'; @Controller('search') export class SearchController { constructor(private readonly searchService: SearchService) {} @Get() - async search(@Query('query') query: string) { - return await this.searchService.search(query); + async search( + @Query('query') query: string, + @Query('lat') lat?: number, + @Query('lon') lon?: number, + @Query('page', new ParseOptionalNumberPipe(1)) page?: number, + @Query('limit', new ParseOptionalNumberPipe(10)) limit?: number, + ) { + return await this.searchService.search(query, lat, lon, page, limit); } @Get('index/place') diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index ac834427..3eef8b32 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -5,6 +5,9 @@ import { PinoLogger } from 'nestjs-pino'; @Injectable() export class SearchService { + private readonly PLACE_INDEX = 'place'; + private readonly PLACE_ANALYZER = 'nori_with_synonym'; + constructor( private placeRepository: PlaceRepository, private readonly logger: PinoLogger, @@ -24,7 +27,17 @@ export class SearchService { this.logger.info(`Indexing 시작: ${startTime}`); const places = await this.placeRepository.find(); for (const place of places) { - await this.indexData('place', place); + const data = { + ...place, + location: { + lat: place.latitude, + lon: place.longitude, + }, + }; + delete data.latitude; + delete data.longitude; + + await this.indexData(this.PLACE_INDEX, data); } const endTime = new Date().toISOString(); this.logger.info(`Indexing 완료: ${endTime}`); @@ -33,16 +46,132 @@ export class SearchService { ); } - async search(query: string) { + async search( + query: string, + lat?: number, + lon?: number, + page: number = 1, + size: number = 10, + ) { + const from = (page - 1) * size; + const tokens = await this.tokenizeQuery(query); + const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; + const result = await this.esService.search({ index: 'place', + from, + size, query: { - multi_match: { - query: query, - fields: ['name'], + function_score: { + query: { + bool: { + should: [ + // 완전 일치 + { + match: { + name: { + query: query, + fuzziness: 1, + }, + }, + }, + // name에 대한 토큰 매칭 + ...tokens.map((token) => ({ + match: { + name: { + query: token, + fuzziness: 1, + }, + }, + })), + // formattedAddress에 대한 토큰 매칭 + ...tokens.map((token) => ({ + match: { + formattedAddress: { + query: token, + fuzziness: 1, + }, + }, + })), + ], + }, + }, + boost_mode: 'sum', + score_mode: 'sum', + functions: [ + // 완전 일치에 가중치 조정 + { + filter: { term: { 'name.keyword': query } }, + weight: 20, + }, + // name의 토큰 매칭 가중치 + ...tokens.map((token) => ({ + filter: { match: { name: token } }, + weight: 15, + })), + // formattedAddress의 토큰 매칭 가중치 + ...tokens.map((token) => ({ + filter: { match: { formattedAddress: token } }, + weight: 10, + })), + // 위치 정보 가중치 + ...(location + ? [ + { + gauss: { + location: { + origin: `${location.lat},${location.lon}`, + scale: '10km', + offset: '2km', + decay: 0.5, + }, + }, + weight: 20, + }, + ] + : []), + ], }, }, }); + + // 결과가 없으면 prefix 검색 (name, formattedAddress) + if (result.hits?.hits.length === 0) { + const prefixResult = await this.esService.search({ + index: 'place', + query: { + bool: { + should: [ + { + prefix: { + name: { + value: query, + }, + }, + }, + { + prefix: { + formattedAddress: { + value: query, + }, + }, + }, + ], + }, + }, + }); + return prefixResult.hits?.hits || []; + } + return result.hits?.hits || []; } + + private async tokenizeQuery(query: string): Promise { + const analysis = await this.esService.indices.analyze({ + index: this.PLACE_INDEX, + analyzer: this.PLACE_ANALYZER, + text: query, + }); + return analysis.tokens?.map((token) => token.token) || []; + } } From 2cee7132a6ea10224dae30fe75e5418846f18ec0 Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 11:02:54 +0900 Subject: [PATCH 08/27] =?UTF-8?q?config:=20production=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20es=20=EC=84=9C=EB=B2=84=20=EC=84=A4=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker-compose.production.yml | 35 +++++++++++++++++++ backend/resources/elasticsearch/sample_env | 1 + backend/resources/nginx/nginx.conf | 20 +++++++++++ 3 files changed, 56 insertions(+) create mode 100644 backend/resources/elasticsearch/docker-compose.production.yml create mode 100644 backend/resources/elasticsearch/sample_env diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml new file mode 100644 index 00000000..79d9a03a --- /dev/null +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -0,0 +1,35 @@ +services: + elasticsearch: + build: + context: ./elasticsearch + dockerfile: Dockerfile + container_name: elasticsearch + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - xpack.security.enabled=true + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - esdata:/usr/share/elasticsearch/data + ports: + - "9200:9200" + + kibana: + image: docker.elastic.co/kibana/kibana:8.16.0 + container_name: kibana + environment: + - ELASTICSEARCH_USERNAME=elastic + - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD} + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + ports: + - "5601:5601" + depends_on: + - elasticsearch + +volumes: + esdata: + driver: local \ No newline at end of file diff --git a/backend/resources/elasticsearch/sample_env b/backend/resources/elasticsearch/sample_env new file mode 100644 index 00000000..307ea6a3 --- /dev/null +++ b/backend/resources/elasticsearch/sample_env @@ -0,0 +1 @@ +ELASTIC_PASSWORD= diff --git a/backend/resources/nginx/nginx.conf b/backend/resources/nginx/nginx.conf index 7fe2a9f4..1d2681c5 100644 --- a/backend/resources/nginx/nginx.conf +++ b/backend/resources/nginx/nginx.conf @@ -61,4 +61,24 @@ http { include /etc/nginx/proxy_headers.conf; } } + + server { + listen 443 ssl; + server_name es.dailyroad.site; + + location / { + proxy_pass http://:9200; + include /etc/nginx/proxy_headers.conf; + } + } + + server { + listen 443 ssl; + server_name search.dailyroad.site; + + location / { + proxy_pass http://:5601; + include /etc/nginx/proxy_headers.conf; + } + } } From 69195a2977d8df376762571208592fb701b95b1f Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 11:26:07 +0900 Subject: [PATCH 09/27] =?UTF-8?q?fix:=20es=20docker=20compose=20context=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/resources/elasticsearch/docker-compose.production.yml | 2 +- backend/resources/elasticsearch/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index 79d9a03a..7acb95cd 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -1,7 +1,7 @@ services: elasticsearch: build: - context: ./elasticsearch + context: . dockerfile: Dockerfile container_name: elasticsearch environment: diff --git a/backend/resources/elasticsearch/docker-compose.yml b/backend/resources/elasticsearch/docker-compose.yml index 07bf6df6..97aef724 100644 --- a/backend/resources/elasticsearch/docker-compose.yml +++ b/backend/resources/elasticsearch/docker-compose.yml @@ -1,7 +1,7 @@ services: elasticsearch: build: - context: ./elasticsearch + context: . dockerfile: Dockerfile container_name: elasticsearch environment: From 3a91e97bfa1dbbefbebd2c959fb2431c60f3742b Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 12:55:40 +0900 Subject: [PATCH 10/27] =?UTF-8?q?config:=20kibana=20=EC=84=B8=ED=8C=85=20?= =?UTF-8?q?=EC=A0=84=EC=97=90=20es=20password=20=EC=84=A4=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch/docker-compose.production.yml | 11 ++++++++++- backend/resources/elasticsearch/init-index.sh | 3 +++ backend/resources/nginx/nginx.conf | 8 +++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index 7acb95cd..acd4a30f 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -15,14 +15,23 @@ services: hard: -1 volumes: - esdata:/usr/share/elasticsearch/data + - ./index/place-index.json:/usr/share/elasticsearch/place-index.json + - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt + - ./init-index.sh:/usr/share/elasticsearch/init-index.sh ports: - "9200:9200" + command: > + /bin/bash -c " + chmod +x /usr/share/elasticsearch/init-index.sh && + /usr/share/elasticsearch/init-index.sh & + exec /usr/local/bin/docker-entrypoint.sh + " kibana: image: docker.elastic.co/kibana/kibana:8.16.0 container_name: kibana environment: - - ELASTICSEARCH_USERNAME=elastic + - ELASTICSEARCH_USERNAME=kibana_system - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD} - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: diff --git a/backend/resources/elasticsearch/init-index.sh b/backend/resources/elasticsearch/init-index.sh index aaa5c276..8d4f20d3 100755 --- a/backend/resources/elasticsearch/init-index.sh +++ b/backend/resources/elasticsearch/init-index.sh @@ -10,6 +10,9 @@ echo "Checking if 'place' index exists..." response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place") if [ "$response" -eq 404 ]; then + echo "Setting kibana_system password"; + until curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" http://elastic:9200/_security/user/kibana_system/_password -d "{\"password\":\"${ELASTIC_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; + echo "All done!"; echo "Index 'place' does not exist. Creating it..." curl -X PUT "http://localhost:9200/place" \ -H "Content-Type: application/json" \ diff --git a/backend/resources/nginx/nginx.conf b/backend/resources/nginx/nginx.conf index 1d2681c5..31d9f075 100644 --- a/backend/resources/nginx/nginx.conf +++ b/backend/resources/nginx/nginx.conf @@ -62,22 +62,24 @@ http { } } + # Elasticsearch 서브도메인 server { listen 443 ssl; server_name es.dailyroad.site; location / { - proxy_pass http://:9200; + proxy_pass http://:9200; include /etc/nginx/proxy_headers.conf; } } - server { + # Search(Kibana) 서브도메인 + server { listen 443 ssl; server_name search.dailyroad.site; location / { - proxy_pass http://:5601; + proxy_pass http://:5601; include /etc/nginx/proxy_headers.conf; } } From afeb6eb31e0a6cbfb3d57014ff7c0cf95cc58c89 Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 13:12:11 +0900 Subject: [PATCH 11/27] =?UTF-8?q?fix:=20es=20kibana=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=84=A4=EC=A0=95=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/resources/elasticsearch/init-index.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/resources/elasticsearch/init-index.sh b/backend/resources/elasticsearch/init-index.sh index 8d4f20d3..bbba6550 100755 --- a/backend/resources/elasticsearch/init-index.sh +++ b/backend/resources/elasticsearch/init-index.sh @@ -1,20 +1,26 @@ #!/bin/bash -until curl -s -XGET "http://localhost:9200/_cluster/health" | grep '"status":"green"' > /dev/null; do +# Elasticsearch 준비 상태 확인 +until curl -s -XGET "http://localhost:9200/_cluster/health" -u "elastic:${ELASTIC_PASSWORD}" | grep '"status":"green"' > /dev/null; do echo "Waiting for Elasticsearch to be ready..." sleep 5 done -# Place 인덱스 확인 및 생성 +# Kibana 시스템 계정 비밀번호 설정 +echo "Setting kibana_system password..." +curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" \ + -H "Content-Type: application/json" \ + -d "{\"password\":\"${ELASTIC_PASSWORD}\"}" \ + "http://localhost:9200/_security/user/kibana_system/_password" + +# "place" 인덱스 확인 및 생성 echo "Checking if 'place' index exists..." -response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place") +response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place" -u "elastic:${ELASTIC_PASSWORD}") if [ "$response" -eq 404 ]; then - echo "Setting kibana_system password"; - until curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" http://elastic:9200/_security/user/kibana_system/_password -d "{\"password\":\"${ELASTIC_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done; - echo "All done!"; echo "Index 'place' does not exist. Creating it..." curl -X PUT "http://localhost:9200/place" \ + -u "elastic:${ELASTIC_PASSWORD}" \ -H "Content-Type: application/json" \ --data-binary @/usr/share/elasticsearch/place-index.json echo "'place' index created successfully." From 297360cebdd21512e09dedd2d360119fe530097d Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 13:24:50 +0900 Subject: [PATCH 12/27] =?UTF-8?q?fix:=20es=20health=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=20=EC=9D=B4=ED=9B=84=20kibana=20=EB=8F=99=EC=9E=91=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elasticsearch/docker-compose.production.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index acd4a30f..657f244f 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -26,18 +26,25 @@ services: /usr/share/elasticsearch/init-index.sh & exec /usr/local/bin/docker-entrypoint.sh " + healthcheck: + test: [ "CMD", "curl", "-s", "-XGET", "http://localhost:9200/_cluster/health" ] + interval: 10s + timeout: 5s + retries: 5 kibana: image: docker.elastic.co/kibana/kibana:8.16.0 container_name: kibana + depends_on: + elasticsearch: + condition: service_healthy environment: + - SERVER_NAME=kibana - ELASTICSEARCH_USERNAME=kibana_system - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD} - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - "5601:5601" - depends_on: - - elasticsearch volumes: esdata: From a24df3f19383a6bff21696dc980dda9cff371be7 Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 20:16:32 +0900 Subject: [PATCH 13/27] =?UTF-8?q?fix:=20prod=20=ED=99=98=EA=B2=BD=20sh=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95=20#13?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker-compose.production.yml | 42 ++++++++++++------- .../elasticsearch/docker-compose.yml | 6 +-- .../{init-index.sh => init-es.local.sh} | 20 ++------- .../resources/elasticsearch/init-es.prod.sh | 15 +++++++ 4 files changed, 49 insertions(+), 34 deletions(-) rename backend/resources/elasticsearch/{init-index.sh => init-es.local.sh} (50%) create mode 100755 backend/resources/elasticsearch/init-es.prod.sh diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index 657f244f..e534aeae 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -1,4 +1,30 @@ services: + setup: + profiles: + - setup + build: + context: . + init: true + volumes: + - ./index/place-index.json:/usr/share/elasticsearch/place-index.json + - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt + - ./init-es.prod.sh:/usr/share/elasticsearch/init-es.prod.sh + command: > + /bin/bash -c ' + until curl -s -XGET "http://elasticsearch:9200/_cluster/health" -u "elastic:${ELASTIC_PASSWORD}" | grep "\"status\":\"green\"" > /dev/null; do + echo "Waiting for Elasticsearch to be ready..." + sleep 5 + done + chmod +x /usr/share/elasticsearch/init-es.prod.sh && + curl -X POST "http://elasticsearch:9200/_security/user/kibana_user" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{\"password\": \"${ELASTIC_PASSWORD}\", \"roles\": [\"kibana_system\", \"kibana_user\"]}" || echo "Kibana user already exists." && + curl -X POST "http://elasticsearch:9200/_security/user/kibana_system/_password" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{\"password\": \"${ELASTIC_PASSWORD}\"}" || echo "kibana_system password already set." && + /usr/share/elasticsearch/init-es.prod.sh + ' + environment: + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} + depends_on: + - elasticsearch + elasticsearch: build: context: . @@ -15,29 +41,15 @@ services: hard: -1 volumes: - esdata:/usr/share/elasticsearch/data - - ./index/place-index.json:/usr/share/elasticsearch/place-index.json - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt - - ./init-index.sh:/usr/share/elasticsearch/init-index.sh ports: - "9200:9200" - command: > - /bin/bash -c " - chmod +x /usr/share/elasticsearch/init-index.sh && - /usr/share/elasticsearch/init-index.sh & - exec /usr/local/bin/docker-entrypoint.sh - " - healthcheck: - test: [ "CMD", "curl", "-s", "-XGET", "http://localhost:9200/_cluster/health" ] - interval: 10s - timeout: 5s - retries: 5 kibana: image: docker.elastic.co/kibana/kibana:8.16.0 container_name: kibana depends_on: - elasticsearch: - condition: service_healthy + - elasticsearch environment: - SERVER_NAME=kibana - ELASTICSEARCH_USERNAME=kibana_system diff --git a/backend/resources/elasticsearch/docker-compose.yml b/backend/resources/elasticsearch/docker-compose.yml index 97aef724..72a121b4 100644 --- a/backend/resources/elasticsearch/docker-compose.yml +++ b/backend/resources/elasticsearch/docker-compose.yml @@ -16,13 +16,13 @@ services: - esdata:/usr/share/elasticsearch/data - ./index/place-index.json:/usr/share/elasticsearch/place-index.json - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt - - ./init-index.sh:/usr/share/elasticsearch/init-index.sh + - ./init-es.local.sh:/usr/share/elasticsearch/init-es.local.sh ports: - "9200:9200" command: > /bin/bash -c " - chmod +x /usr/share/elasticsearch/init-index.sh && - /usr/share/elasticsearch/init-index.sh & + chmod +x /usr/share/elasticsearch/init-es.local && + /usr/share/elasticsearch/init-es.local & exec /usr/local/bin/docker-entrypoint.sh " diff --git a/backend/resources/elasticsearch/init-index.sh b/backend/resources/elasticsearch/init-es.local.sh similarity index 50% rename from backend/resources/elasticsearch/init-index.sh rename to backend/resources/elasticsearch/init-es.local.sh index bbba6550..2bbb233d 100755 --- a/backend/resources/elasticsearch/init-index.sh +++ b/backend/resources/elasticsearch/init-es.local.sh @@ -1,26 +1,14 @@ #!/bin/bash - -# Elasticsearch 준비 상태 확인 -until curl -s -XGET "http://localhost:9200/_cluster/health" -u "elastic:${ELASTIC_PASSWORD}" | grep '"status":"green"' > /dev/null; do +until curl -s -XGET "http://localhost:9200/_cluster/health" | grep '"status":"green"' > /dev/null; do echo "Waiting for Elasticsearch to be ready..." sleep 5 done - -# Kibana 시스템 계정 비밀번호 설정 -echo "Setting kibana_system password..." -curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" \ - -H "Content-Type: application/json" \ - -d "{\"password\":\"${ELASTIC_PASSWORD}\"}" \ - "http://localhost:9200/_security/user/kibana_system/_password" - -# "place" 인덱스 확인 및 생성 +# Place 인덱스 확인 및 생성 echo "Checking if 'place' index exists..." -response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place" -u "elastic:${ELASTIC_PASSWORD}") - +response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://localhost:9200/place") if [ "$response" -eq 404 ]; then echo "Index 'place' does not exist. Creating it..." curl -X PUT "http://localhost:9200/place" \ - -u "elastic:${ELASTIC_PASSWORD}" \ -H "Content-Type: application/json" \ --data-binary @/usr/share/elasticsearch/place-index.json echo "'place' index created successfully." @@ -28,4 +16,4 @@ elif [ "$response" -eq 200 ]; then echo "Index 'place' already exists." else echo "Unexpected response while checking for 'place' index: HTTP $response" -fi +fi \ No newline at end of file diff --git a/backend/resources/elasticsearch/init-es.prod.sh b/backend/resources/elasticsearch/init-es.prod.sh new file mode 100755 index 00000000..70346d7a --- /dev/null +++ b/backend/resources/elasticsearch/init-es.prod.sh @@ -0,0 +1,15 @@ +#!/bin/bash +echo "Checking if 'place' index exists..." +response=$(curl -s -o /dev/null -w "%{http_code}" -XGET "http://elasticsearch:9200/place" -u "elastic:${ELASTIC_PASSWORD}") +if [ "$response" -eq 404 ]; then + echo "Index 'place' does not exist. Creating it..." + curl -X PUT "http://elasticsearch:9200/place" \ + -H "Content-Type: application/json" \ + -u "elastic:${ELASTIC_PASSWORD}" \ + --data-binary @/usr/share/elasticsearch/place-index.json + echo "'place' index created successfully." +elif [ "$response" -eq 200 ]; then + echo "Index 'place' already exists." +else + echo "Unexpected response while checking for 'place' index: HTTP $response" +fi \ No newline at end of file From 70654a9be3641863cb86d59270ede7eb68e7c2fe Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 21:58:43 +0900 Subject: [PATCH 14/27] =?UTF-8?q?fix:=20elastic=20=EB=B0=8F=20Kibana?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=88=98=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docker-compose.production.yml | 26 ++++++++++++++++--- backend/resources/elasticsearch/sample_env | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index e534aeae..ffb0d906 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -14,13 +14,31 @@ services: until curl -s -XGET "http://elasticsearch:9200/_cluster/health" -u "elastic:${ELASTIC_PASSWORD}" | grep "\"status\":\"green\"" > /dev/null; do echo "Waiting for Elasticsearch to be ready..." sleep 5 - done + done && + curl -X POST "http://elasticsearch:9200/_security/role/custom_role" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{ + \"cluster\": [\"all\"], + \"indices\": [ + { + \"names\": [\"*\"], + \"privileges\": [\"all\"] + } + ], + \"applications\": [ + { + \"application\": \"kibana-.kibana\", + \"privileges\": [\"all\"], + \"resources\": [\"*\"] + } + ], + \"run_as\": [\"*\"] + }" && + + curl -X POST "http://elasticsearch:9200/_security/user/${CUSTOM_USERNAME}" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{\"password\": \"${ELASTIC_PASSWORD}\", \"roles\": [\"custom_role\", \"kibana_system\"]}" && chmod +x /usr/share/elasticsearch/init-es.prod.sh && - curl -X POST "http://elasticsearch:9200/_security/user/kibana_user" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{\"password\": \"${ELASTIC_PASSWORD}\", \"roles\": [\"kibana_system\", \"kibana_user\"]}" || echo "Kibana user already exists." && - curl -X POST "http://elasticsearch:9200/_security/user/kibana_system/_password" -H "Content-Type: application/json" -u elastic:${ELASTIC_PASSWORD} -d "{\"password\": \"${ELASTIC_PASSWORD}\"}" || echo "kibana_system password already set." && /usr/share/elasticsearch/init-es.prod.sh ' environment: + - CUSTOM_USERNAME=${CUSTOM_USERNAME} - ELASTIC_PASSWORD=${ELASTIC_PASSWORD} depends_on: - elasticsearch @@ -52,7 +70,7 @@ services: - elasticsearch environment: - SERVER_NAME=kibana - - ELASTICSEARCH_USERNAME=kibana_system + - ELASTICSEARCH_USERNAME=${CUSTOM_USERNAME} - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD} - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: diff --git a/backend/resources/elasticsearch/sample_env b/backend/resources/elasticsearch/sample_env index 307ea6a3..823e7d94 100644 --- a/backend/resources/elasticsearch/sample_env +++ b/backend/resources/elasticsearch/sample_env @@ -1 +1,2 @@ +CUSTOM_USERNAME= ELASTIC_PASSWORD= From 598688612701209ba59568034b423352227a8426 Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 23:42:34 +0900 Subject: [PATCH 15/27] =?UTF-8?q?refactor:=20es=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20dto=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/search/dto/PlaceSearchResponse.ts | 17 ++++ backend/src/search/search.controller.ts | 5 -- backend/src/search/search.module.ts | 8 +- backend/src/search/search.service.ts | 84 +++++++++---------- backend/src/search/search.type.ts | 23 +++++ 5 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 backend/src/search/dto/PlaceSearchResponse.ts create mode 100644 backend/src/search/search.type.ts diff --git a/backend/src/search/dto/PlaceSearchResponse.ts b/backend/src/search/dto/PlaceSearchResponse.ts new file mode 100644 index 00000000..02be2d9d --- /dev/null +++ b/backend/src/search/dto/PlaceSearchResponse.ts @@ -0,0 +1,17 @@ +export class PlaceSearchResponse { + constructor( + readonly id: string, + readonly name: string, + readonly location: { + readonly latitude: number; + readonly longitude: number; + }, + readonly google_place_id: string, + readonly category?: string, + readonly description?: string, + readonly detail_page_url?: string, + readonly thumbnail_url?: string, + readonly rating?: number, + readonly formed_address?: string, + ) {} +} diff --git a/backend/src/search/search.controller.ts b/backend/src/search/search.controller.ts index 7ef2df66..7c3c8da7 100644 --- a/backend/src/search/search.controller.ts +++ b/backend/src/search/search.controller.ts @@ -16,9 +16,4 @@ export class SearchController { ) { return await this.searchService.search(query, lat, lon, page, limit); } - - @Get('index/place') - async indexPlace() { - return await this.searchService.indexPlace(); - } } diff --git a/backend/src/search/search.module.ts b/backend/src/search/search.module.ts index 0eee9d27..a711e1ee 100644 --- a/backend/src/search/search.module.ts +++ b/backend/src/search/search.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { ElasticsearchModule } from '@nestjs/elasticsearch'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { SearchController } from './search.controller'; -import { SearchService } from './search.service'; +import { SearchController } from '@src/search/search.controller'; +import { SearchService } from '@src/search/search.service'; import { PlaceModule } from '@src/place/place.module'; @Module({ @@ -14,6 +14,10 @@ import { PlaceModule } from '@src/place/place.module'; node: configService.get('ELASTICSEARCH_NODE') || 'http://localhost:9200', + auth: { + username: configService.get('ELASTICSEARCH_USERNAME') || '', + password: configService.get('ELASTICSEARCH_PASSWORD') || '', + }, }), inject: [ConfigService], }), diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index 3eef8b32..8e0cc8ac 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -1,50 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { PlaceRepository } from '@src/place/place.repository'; import { ElasticsearchService } from '@nestjs/elasticsearch'; -import { PinoLogger } from 'nestjs-pino'; +import { PlaceSearchResponse } from '@src/search/dto/PlaceSearchResponse'; +import { PlaceSearchHit } from '@src/search/search.type'; @Injectable() export class SearchService { private readonly PLACE_INDEX = 'place'; private readonly PLACE_ANALYZER = 'nori_with_synonym'; - constructor( - private placeRepository: PlaceRepository, - private readonly logger: PinoLogger, - private readonly esService: ElasticsearchService, - ) {} - - // 데이터를 인덱스(엘라스틱서치 형태로 주입) - async indexData(indexName: string, data: any) { - return await this.esService.index({ - index: indexName, - document: data, - }); - } - - async indexPlace() { - const startTime = new Date().toISOString(); - this.logger.info(`Indexing 시작: ${startTime}`); - const places = await this.placeRepository.find(); - for (const place of places) { - const data = { - ...place, - location: { - lat: place.latitude, - lon: place.longitude, - }, - }; - delete data.latitude; - delete data.longitude; - - await this.indexData(this.PLACE_INDEX, data); - } - const endTime = new Date().toISOString(); - this.logger.info(`Indexing 완료: ${endTime}`); - this.logger.info( - `소요 시간: ${new Date(endTime).getTime() - new Date(startTime).getTime()}ms`, - ); - } + constructor(private readonly esService: ElasticsearchService) {} async search( query: string, @@ -56,8 +20,9 @@ export class SearchService { const from = (page - 1) * size; const tokens = await this.tokenizeQuery(query); const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; + let result = []; - const result = await this.esService.search({ + const searched = await this.esService.search({ index: 'place', from, size, @@ -136,8 +101,8 @@ export class SearchService { }); // 결과가 없으면 prefix 검색 (name, formattedAddress) - if (result.hits?.hits.length === 0) { - const prefixResult = await this.esService.search({ + if (searched.hits?.hits.length === 0) { + const prefixSearched = await this.esService.search({ index: 'place', query: { bool: { @@ -160,10 +125,41 @@ export class SearchService { }, }, }); - return prefixResult.hits?.hits || []; + result = prefixSearched.hits?.hits || []; + } else { + result = searched.hits?.hits || []; } + return { + places: result.map((hit: PlaceSearchHit) => { + const { _source } = hit; + const { + id, + name, + location, + googlePlaceId, + category, + description, + detailPageUrl, + thumbnailUrl, + rating, + formattedAddress, + } = _source; - return result.hits?.hits || []; + return new PlaceSearchResponse( + id, + name, + location ? { latitude: location.lat, longitude: location.lon } : null, + googlePlaceId, + category, + description, + detailPageUrl, + thumbnailUrl, + rating, + formattedAddress, + ); + }), + total_count: result.length, + }; } private async tokenizeQuery(query: string): Promise { diff --git a/backend/src/search/search.type.ts b/backend/src/search/search.type.ts new file mode 100644 index 00000000..3ca2db5b --- /dev/null +++ b/backend/src/search/search.type.ts @@ -0,0 +1,23 @@ +export type PlaceSearchHit = { + _index: string; + _id: string; + _score: number; + _source: { + id: string; + name: string; + location: { + lat: number; + lon: number; + }; + googlePlaceId: string; + category: string | null; + description: string; + detailPageUrl: string; + thumbnailUrl: string; + rating: number; + formattedAddress: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + }; +}; From 2dfb6ca0fc4d292c346e801f8733f97add8e956b Mon Sep 17 00:00:00 2001 From: koomchang Date: Tue, 19 Nov 2024 23:53:40 +0900 Subject: [PATCH 16/27] =?UTF-8?q?config:=20=EB=B0=B0=ED=8F=AC=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EC=84=9C=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20root=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=84=A4=EC=A0=95=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/resources/elasticsearch/docker-compose.production.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/resources/elasticsearch/docker-compose.production.yml b/backend/resources/elasticsearch/docker-compose.production.yml index ffb0d906..7ffaf7d8 100644 --- a/backend/resources/elasticsearch/docker-compose.production.yml +++ b/backend/resources/elasticsearch/docker-compose.production.yml @@ -5,6 +5,7 @@ services: build: context: . init: true + user: root volumes: - ./index/place-index.json:/usr/share/elasticsearch/place-index.json - ./index/synonyms.txt:/usr/share/elasticsearch/config/synonyms.txt From 88a2156a3139056b95b40a20f1364c518b56f3d4 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 00:43:26 +0900 Subject: [PATCH 17/27] =?UTF-8?q?docs:=20es=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A6=AC=EB=93=9C=EB=AF=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/resources/elasticsearch/README.md | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 backend/resources/elasticsearch/README.md diff --git a/backend/resources/elasticsearch/README.md b/backend/resources/elasticsearch/README.md new file mode 100644 index 00000000..8aa338d3 --- /dev/null +++ b/backend/resources/elasticsearch/README.md @@ -0,0 +1,35 @@ +# Production 환경 ES 및 Kibana 설정 + +1. `.env` 설정 (`sample_env` 참고) +2. `docker compose -f docker-compose.production.yml up setup` +3. 2가 끝난 이후 `docker compose -f docker-compose.production.yml up -d` + +## 디렉토리 구조 + +- `index` : ES index 설정 파일 + - `place-index.json` : place index 설정 파일 + - `synonyms.txt` : 대한민국 지역 별 동의어 설정 파일 +- `Dockerfile`: `norianalyzer` 를 설치한 `elasticsearch:8.16.0` 이미지 +- `docker-compose.yml`: 회원 인증 없이 ES 및 Kibana를 실행하는 설정 파일 (Local 환경) + - `init-es.local.sh`: ES index 설정 파일을 적용하는 스크립트 +- `docker-compose.production.yml`: 회원 인증을 통해 ES 및 Kibana를 실행하는 설정 파일 (Production 환경) + - `init-es.production.sh`: ES index 설정 파일을 적용하는 스크립트 + +### 초기 json 데이터 bulk + +```shell +# place index 예시 +curl -H "Content-Type: application/json" -XPOST '127.0.0.1:9200/place/_bulk' -u "elastic:${ELASTIC_PASSWORD}" --data-binary @{이름}.json +``` + +참고 : [elastic - Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) + +## Kibana 배포 + +https://search.dailyroad.site + +--- + +### 수정 + +241120 초안 작성 \ No newline at end of file From a31c13131b79f6ed1434bff6e4e9973a2713a52a Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 13:23:18 +0900 Subject: [PATCH 18/27] =?UTF-8?q?refactor:=20ES=20index=20=EB=93=B1=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=83=81=EC=88=98=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/config/ElasticSearchConfig.ts | 4 + .../src/search/query/ElasticSearchQuery.ts | 130 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 backend/src/config/ElasticSearchConfig.ts create mode 100644 backend/src/search/query/ElasticSearchQuery.ts diff --git a/backend/src/config/ElasticSearchConfig.ts b/backend/src/config/ElasticSearchConfig.ts new file mode 100644 index 00000000..78488e5f --- /dev/null +++ b/backend/src/config/ElasticSearchConfig.ts @@ -0,0 +1,4 @@ +export const ElasticSearchConfig = { + PLACE_INDEX: 'place', + PLACE_ANALYZER: 'nori_with_synonym', +}; diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts new file mode 100644 index 00000000..6a160888 --- /dev/null +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -0,0 +1,130 @@ +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { ESconfig } from '@src/config/ESconfig'; + +export class ElasticSearchQuery { + constructor(private readonly esService: ElasticsearchService) {} + + async searchPlace( + query: string, + lat?: number, + lon?: number, + page: number = 1, + size: number = 10, + ) { + const tokens = await this.tokenizeQuery(query); + const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; + const from = (page - 1) * size; + return await this.esService.search({ + index: ESconfig.PLACE_INDEX, + from, + size, + query: { + function_score: { + query: { + bool: { + should: [ + // 완전 일치 + { + match: { + name: { + query: query, + fuzziness: 1, + }, + }, + }, + // name에 대한 토큰 매칭 + ...tokens.map((token) => ({ + match: { + name: { + query: token, + fuzziness: 1, + }, + }, + })), + // formattedAddress에 대한 토큰 매칭 + ...tokens.map((token) => ({ + match: { + formattedAddress: { + query: token, + fuzziness: 1, + }, + }, + })), + ], + }, + }, + boost_mode: 'sum', + score_mode: 'sum', + functions: [ + // 완전 일치에 가중치 조정 + { + filter: { term: { 'name.keyword': query } }, + weight: 20, + }, + // name의 토큰 매칭 가중치 + ...tokens.map((token) => ({ + filter: { match: { name: token } }, + weight: 15, + })), + // formattedAddress의 토큰 매칭 가중치 + ...tokens.map((token) => ({ + filter: { match: { formattedAddress: token } }, + weight: 10, + })), + // 위치 정보 가중치 + ...(location + ? [ + { + gauss: { + location: { + origin: `${location.lat},${location.lon}`, + scale: '10km', + offset: '2km', + decay: 0.5, + }, + }, + weight: 20, + }, + ] + : []), + ], + }, + }, + }); + } + + async searchPlaceWithPrefix(query: string) { + return await this.esService.search({ + index: ESconfig.PLACE_INDEX, + query: { + bool: { + should: [ + { + prefix: { + name: { + value: query, + }, + }, + }, + { + prefix: { + formattedAddress: { + value: query, + }, + }, + }, + ], + }, + }, + }); + } + + private async tokenizeQuery(query: string): Promise { + const analysis = await this.esService.indices.analyze({ + index: ESconfig.PLACE_INDEX, + analyzer: ESconfig.PLACE_ANALYZER, + text: query, + }); + return analysis.tokens?.map((token) => token.token) || []; + } +} From 8de59fcbc74d0d4576454fbe297edf492cfad405 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 13:23:41 +0900 Subject: [PATCH 19/27] =?UTF-8?q?refactor:=20ES=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=EB=A5=BC=20service=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/search/query/ElasticSearchQuery.ts | 10 +- backend/src/search/search.service.ts | 173 +++--------------- 2 files changed, 32 insertions(+), 151 deletions(-) diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index 6a160888..bbbbe829 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -1,5 +1,5 @@ import { ElasticsearchService } from '@nestjs/elasticsearch'; -import { ESconfig } from '@src/config/ESconfig'; +import { ElasticSearchConfig } from '@src/config/ElasticSearchConfig'; export class ElasticSearchQuery { constructor(private readonly esService: ElasticsearchService) {} @@ -15,7 +15,7 @@ export class ElasticSearchQuery { const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; const from = (page - 1) * size; return await this.esService.search({ - index: ESconfig.PLACE_INDEX, + index: ElasticSearchConfig.PLACE_INDEX, from, size, query: { @@ -95,7 +95,7 @@ export class ElasticSearchQuery { async searchPlaceWithPrefix(query: string) { return await this.esService.search({ - index: ESconfig.PLACE_INDEX, + index: ElasticSearchConfig.PLACE_INDEX, query: { bool: { should: [ @@ -121,8 +121,8 @@ export class ElasticSearchQuery { private async tokenizeQuery(query: string): Promise { const analysis = await this.esService.indices.analyze({ - index: ESconfig.PLACE_INDEX, - analyzer: ESconfig.PLACE_ANALYZER, + index: ElasticSearchConfig.PLACE_INDEX, + analyzer: ElasticSearchConfig.PLACE_ANALYZER, text: query, }); return analysis.tokens?.map((token) => token.token) || []; diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index 8e0cc8ac..375cdbb0 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -1,14 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { ElasticsearchService } from '@nestjs/elasticsearch'; import { PlaceSearchResponse } from '@src/search/dto/PlaceSearchResponse'; import { PlaceSearchHit } from '@src/search/search.type'; +import { ElasticSearchQuery } from '@src/search/query/ElasticSearchQuery'; @Injectable() export class SearchService { - private readonly PLACE_INDEX = 'place'; - private readonly PLACE_ANALYZER = 'nori_with_synonym'; - - constructor(private readonly esService: ElasticsearchService) {} + constructor(private readonly elasticSearchQuery: ElasticSearchQuery) {} async search( query: string, @@ -17,157 +14,41 @@ export class SearchService { page: number = 1, size: number = 10, ) { - const from = (page - 1) * size; - const tokens = await this.tokenizeQuery(query); - const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; - let result = []; - - const searched = await this.esService.search({ - index: 'place', - from, + const searched = await this.elasticSearchQuery.searchPlace( + query, + lat, + lon, + page, size, - query: { - function_score: { - query: { - bool: { - should: [ - // 완전 일치 - { - match: { - name: { - query: query, - fuzziness: 1, - }, - }, - }, - // name에 대한 토큰 매칭 - ...tokens.map((token) => ({ - match: { - name: { - query: token, - fuzziness: 1, - }, - }, - })), - // formattedAddress에 대한 토큰 매칭 - ...tokens.map((token) => ({ - match: { - formattedAddress: { - query: token, - fuzziness: 1, - }, - }, - })), - ], - }, - }, - boost_mode: 'sum', - score_mode: 'sum', - functions: [ - // 완전 일치에 가중치 조정 - { - filter: { term: { 'name.keyword': query } }, - weight: 20, - }, - // name의 토큰 매칭 가중치 - ...tokens.map((token) => ({ - filter: { match: { name: token } }, - weight: 15, - })), - // formattedAddress의 토큰 매칭 가중치 - ...tokens.map((token) => ({ - filter: { match: { formattedAddress: token } }, - weight: 10, - })), - // 위치 정보 가중치 - ...(location - ? [ - { - gauss: { - location: { - origin: `${location.lat},${location.lon}`, - scale: '10km', - offset: '2km', - decay: 0.5, - }, - }, - weight: 20, - }, - ] - : []), - ], - }, - }, - }); - - // 결과가 없으면 prefix 검색 (name, formattedAddress) - if (searched.hits?.hits.length === 0) { - const prefixSearched = await this.esService.search({ - index: 'place', - query: { - bool: { - should: [ - { - prefix: { - name: { - value: query, - }, - }, - }, - { - prefix: { - formattedAddress: { - value: query, - }, - }, - }, - ], - }, - }, - }); + ); + let result = searched.hits?.hits || []; + if (result.length === 0) { + const prefixSearched = + await this.elasticSearchQuery.searchPlaceWithPrefix(query); result = prefixSearched.hits?.hits || []; - } else { - result = searched.hits?.hits || []; } return { places: result.map((hit: PlaceSearchHit) => { const { _source } = hit; - const { - id, - name, - location, - googlePlaceId, - category, - description, - detailPageUrl, - thumbnailUrl, - rating, - formattedAddress, - } = _source; - return new PlaceSearchResponse( - id, - name, - location ? { latitude: location.lat, longitude: location.lon } : null, - googlePlaceId, - category, - description, - detailPageUrl, - thumbnailUrl, - rating, - formattedAddress, + _source.id, + _source.name, + _source.location + ? { + latitude: _source.location.lat, + longitude: _source.location.lon, + } + : null, + _source.googlePlaceId, + _source.category, + _source.description, + _source.detailPageUrl, + _source.thumbnailUrl, + _source.rating, + _source.formattedAddress, ); }), total_count: result.length, }; } - - private async tokenizeQuery(query: string): Promise { - const analysis = await this.esService.indices.analyze({ - index: this.PLACE_INDEX, - analyzer: this.PLACE_ANALYZER, - text: query, - }); - return analysis.tokens?.map((token) => token.token) || []; - } } From 95d0d529b390ace47bbaea56686c145765c1c9b5 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 13:46:56 +0900 Subject: [PATCH 20/27] =?UTF-8?q?fix:=20ESQuery=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20injectable=EB=A1=9C=20=EB=91=90=EC=96=B4?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=EC=97=90=20=EC=B6=94=EA=B0=80=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/search/query/ElasticSearchQuery.ts | 2 ++ backend/src/search/search.module.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index bbbbe829..420c2664 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -1,6 +1,8 @@ import { ElasticsearchService } from '@nestjs/elasticsearch'; import { ElasticSearchConfig } from '@src/config/ElasticSearchConfig'; +import { Injectable } from '@nestjs/common'; +@Injectable() export class ElasticSearchQuery { constructor(private readonly esService: ElasticsearchService) {} diff --git a/backend/src/search/search.module.ts b/backend/src/search/search.module.ts index a711e1ee..fa0cd8bb 100644 --- a/backend/src/search/search.module.ts +++ b/backend/src/search/search.module.ts @@ -4,6 +4,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { SearchController } from '@src/search/search.controller'; import { SearchService } from '@src/search/search.service'; import { PlaceModule } from '@src/place/place.module'; +import { ElasticSearchQuery } from '@src/search/query/ElasticSearchQuery'; @Module({ imports: [ @@ -22,7 +23,7 @@ import { PlaceModule } from '@src/place/place.module'; inject: [ConfigService], }), ], - providers: [SearchService], + providers: [SearchService, ElasticSearchQuery], controllers: [SearchController], exports: [], }) From 5006896b6c5a01dedfc47cd5ff71be1cd16c82e1 Mon Sep 17 00:00:00 2001 From: Soap Date: Wed, 20 Nov 2024 18:28:36 +0900 Subject: [PATCH 21/27] =?UTF-8?q?docs:=20ELK=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elk/elasticsearch/log_index_template.json | 47 ++++++++ .../scripts/elk/logstash/logstash.conf | 34 ++++++ backend/resources/scripts/elk/setup-elk.sh | 79 +++++++++---- .../elasticsearch/log_index_template.json | 47 ++++++++ .../kibana/level.painless | 26 +++++ .../kibana/log-message.painless | 26 +++++ .../kibana/request.painless | 46 ++++++++ .../logstash/logstash.conf | 34 ++++++ .../ubuntu@dailyroad.site/setup-elk.sh | 110 ++++++++++++++++++ 9 files changed, 424 insertions(+), 25 deletions(-) create mode 100644 backend/resources/scripts/elk/elasticsearch/log_index_template.json create mode 100644 backend/resources/scripts/elk/logstash/logstash.conf create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/elasticsearch/log_index_template.json create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/kibana/level.painless create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/kibana/log-message.painless create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/kibana/request.painless create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/logstash/logstash.conf create mode 100644 backend/resources/scripts/ubuntu@dailyroad.site/setup-elk.sh diff --git a/backend/resources/scripts/elk/elasticsearch/log_index_template.json b/backend/resources/scripts/elk/elasticsearch/log_index_template.json new file mode 100644 index 00000000..519f124e --- /dev/null +++ b/backend/resources/scripts/elk/elasticsearch/log_index_template.json @@ -0,0 +1,47 @@ +{ + "index_patterns": [ + "dailyroad-*" + ], + "template": { + "settings": { + "number_of_shards": 1, + "number_of_replicas": 1 + }, + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "log_level": { + "type": "keyword" + }, + "log_message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "req": { + "properties": { + "id": { + "type": "integer" + }, + "method": { + "type": "keyword" + }, + "url": { + "type": "keyword" + }, + "headers": { + "type": "object", + "enabled": false + } + } + } + } + } + } +} diff --git a/backend/resources/scripts/elk/logstash/logstash.conf b/backend/resources/scripts/elk/logstash/logstash.conf new file mode 100644 index 00000000..d16ffdd9 --- /dev/null +++ b/backend/resources/scripts/elk/logstash/logstash.conf @@ -0,0 +1,34 @@ +input { + beats { + port => 5044 + } + + tcp { + port => 50000 + codec => json_lines # JSON 형태의 데이터를 처리 + } +} + +filter { + json { + source => "message" + target => "parsed_message" + } + + mutate { + rename => { "[parsed_message][level]" => "log_level" } + rename => { "[parsed_message][msg]" => "log_message" } + rename => { "[parsed_message][time]" => "@timestamp" } + + remove_field => ["message", "parsed_message"] + } +} + +output { + elasticsearch { + index => "%{[@metadata][service_name]}-%{+YYYY.MM.dd}" + hosts => "elasticsearch:9200" + user => "logstash_internal" + password => "${LOGSTASH_INTERNAL_PASSWORD}" + } +} diff --git a/backend/resources/scripts/elk/setup-elk.sh b/backend/resources/scripts/elk/setup-elk.sh index 5fc88990..ff05800c 100644 --- a/backend/resources/scripts/elk/setup-elk.sh +++ b/backend/resources/scripts/elk/setup-elk.sh @@ -3,14 +3,20 @@ # ELK 스택 설정 초기화 스크립트 # 환경 설정 -export ELASTIC_PASSWORD="example" # 변경해 사용 +export ELASTIC_PASSWORD="example" # 변경 필요 export LOGSTASH_PASSWORD="example" export KIBANA_PASSWORD="example" export ELASTIC_VERSION="8.15.3" -LOGSTASH_CONFIG="logstash/pipeline/logstash.conf" -SERVICE_NAME="dailyroad" -LOG_INDEX="$SERVICE_NAME-%{+YYYY.MM.dd}" +export SERVICE_NAME="dailyroad" + +# 기본 경로 설정 +SETUP_DIR=$(dirname "$(readlink -f "$0")") +DOCKER_ELK_DIR="$SETUP_DIR/../docker-elk" # ELK 스택 클론 경로 + +LOGSTASH_CONFIG="$SETUP_DIR/logstash/logstash.conf" +TEMPLATE_FILE="$SETUP_DIR/elasticsearch/log_index_template.json" +KIBANA_SCRIPTS_DIR="$SETUP_DIR/kibana" # 필요 패키지 업데이트 및 설치 echo "Updating packages..." @@ -24,31 +30,32 @@ if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; exit 1 fi -if [ ! -d "docker-elk" ]; then - echo "Cloning ELK stack repository..." - git clone https://github.com/deviantony/docker-elk.git +# Docker-ELK 클론 또는 디렉토리로 이동 +if [ ! -d "$DOCKER_ELK_DIR" ]; then + echo "Cloning ELK stack repository to $DOCKER_ELK_DIR..." + git clone https://github.com/deviantony/docker-elk.git "$DOCKER_ELK_DIR" fi -cd docker-elk || exit +cd "$DOCKER_ELK_DIR" || exit -# .env 파일에서 버전 설정 및 초기 패스워드 변경 +# .env 파일에서 버전 및 비밀번호 설정 echo "Setting up environment file..." sed -i "s/^ELASTIC_VERSION=.*/ELASTIC_VERSION=${ELASTIC_VERSION}/" .env sed -i "s/^ELASTIC_PASSWORD=.*/ELASTIC_PASSWORD=${ELASTIC_PASSWORD}/" .env sed -i "s/^LOGSTASH_PASSWORD=.*/LOGSTASH_PASSWORD=${LOGSTASH_PASSWORD}/" .env sed -i "s/^KIBANA_PASSWORD=.*/KIBANA_PASSWORD=${KIBANA_PASSWORD}/" .env +sed -i "s/^SERVICE_NAME=.*/SERVICE_NAME=${SERVICE_NAME}/" .env sed -i "s/'//g" .env # 작은 따옴표 제거 -# 로그 인덱스 설정 +# Logstash 구성 복사 if [ -f "$LOGSTASH_CONFIG" ]; then - echo "Configuring Logstash to use a custom index..." - sed -i "/elasticsearch {/a \ index => \"${LOG_INDEX}\"" "$LOGSTASH_CONFIG" + echo "Copying Logstash configuration..." + cp "$LOGSTASH_CONFIG" logstash/pipeline/logstash.conf else echo "Logstash configuration file not found: $LOGSTASH_CONFIG" exit 1 fi - # 초기 사용자 및 권한 설정 echo "Setting up initial users and permissions..." docker-compose up setup @@ -57,21 +64,43 @@ docker-compose up setup echo "Starting ELK stack..." docker-compose up -d -# Kibana 초기화 대기 -echo "Waiting for Kibana to initialize..." -sleep 60 +echo "Waiting for Elasticsearch to be ready..." +until curl -s -o /dev/null -w "%{http_code}" -u elastic:"${ELASTIC_PASSWORD}" http://localhost:9200 | grep -q "200"; do + sleep 10 + echo "Waiting for Elasticsearch..." +done +echo "Elasticsearch is ready." + # Logstash 사용자에게 writer 권한 부여 echo "Assigning writer role to Logstash user..." -curl -u elastic:"${ELASTIC_PASSWORD}" -X PUT "http://localhost:9200/_security/role/logstash_writer" -H "Content-Type: application/json" -d "{ - \"cluster\": [\"manage_index_templates\", \"monitor\", \"manage_ilm\"], - \"indices\": [ - { - \"names\": [\"${SERVICE_NAME}-*\"], - \"privileges\": [\"write\", \"create_index\"] - } - ] -}" +if ! curl -u elastic:"${ELASTIC_PASSWORD}" -X PUT "http://localhost:9200/_security/role/logstash_writer" \ + -H "Content-Type: application/json" \ + -d "{\"cluster\": [\"manage_index_templates\", \"monitor\", \"manage_ilm\"], \"indices\": [{\"names\": [\"${SERVICE_NAME}-*\"], \"privileges\": [\"write\", \"create_index\"]}]}"; then + echo "Failed to assign writer role to Logstash user." + exit 1 +fi + + +# Elasticsearch 인덱스 템플릿 등록 +if [ -f "$TEMPLATE_FILE" ]; then + echo "Registering Elasticsearch index template..." + curl -u elastic:"${ELASTIC_PASSWORD}" -X PUT "http://localhost:9200/_index_template/${SERVICE_NAME}-template" \ + -H "Content-Type: application/json" \ + -d @"$TEMPLATE_FILE" +else + echo "Index template file not found: $TEMPLATE_FILE" + exit 1 +fi + +# Kibana 초기화 대기 +echo "Waiting for Kibana to initialize..." +until curl -s -o /dev/null -w "%{http_code}" -X GET http://localhost:5601/api/status | grep -q "200"; do + sleep 10 + echo "Kibana is still initializing..." +done +echo "Kibana is ready." + # ELK 스택 상태 확인 docker-compose ps diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/elasticsearch/log_index_template.json b/backend/resources/scripts/ubuntu@dailyroad.site/elasticsearch/log_index_template.json new file mode 100644 index 00000000..519f124e --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/elasticsearch/log_index_template.json @@ -0,0 +1,47 @@ +{ + "index_patterns": [ + "dailyroad-*" + ], + "template": { + "settings": { + "number_of_shards": 1, + "number_of_replicas": 1 + }, + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "log_level": { + "type": "keyword" + }, + "log_message": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "req": { + "properties": { + "id": { + "type": "integer" + }, + "method": { + "type": "keyword" + }, + "url": { + "type": "keyword" + }, + "headers": { + "type": "object", + "enabled": false + } + } + } + } + } + } +} diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/kibana/level.painless b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/level.painless new file mode 100644 index 00000000..09217398 --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/level.painless @@ -0,0 +1,26 @@ +if (!params['_source'].containsKey('message')) { + emit("no message"); + return; +} + +String message = params['_source']['message']; +if (message == null) { + emit("message is null"); + return; +} + +// "level":" 를 기준으로 값 추출 +int startIndex = message.indexOf('"level":"'); +if (startIndex == -1) { + emit("no msg in message"); + return; +} + +startIndex += 9; // "level":"의 길이만큼 이동 +int endIndex = message.indexOf('"', startIndex); +if (endIndex == -1) { + emit("no end of msg"); + return; +} + +emit(message.substring(startIndex, endIndex)); diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/kibana/log-message.painless b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/log-message.painless new file mode 100644 index 00000000..824f65dc --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/log-message.painless @@ -0,0 +1,26 @@ +if (!params['_source'].containsKey('message')) { + emit("no message"); + return; +} + +String message = params['_source']['message']; +if (message == null) { + emit("message is null"); + return; +} + +// "msg":" 를 기준으로 값 추출 +int startIndex = message.indexOf('"msg":"'); +if (startIndex == -1) { + emit("no msg in message"); + return; +} + +startIndex += 7; // "msg":"의 길이만큼 이동 +int endIndex = message.indexOf('"', startIndex); +if (endIndex == -1) { + emit("no end of msg"); + return; +} + +emit(message.substring(startIndex, endIndex)); diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/kibana/request.painless b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/request.painless new file mode 100644 index 00000000..bb2dfcf9 --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/kibana/request.painless @@ -0,0 +1,46 @@ +if (!params['_source'].containsKey('message')) { + emit("no message"); + return; +} + +String message = params['_source']['message']; +if (message == null) { + emit("message is null"); + return; +} + +// "method":" 를 기준으로 method 값 추출 +int methodStartIndex = message.indexOf('"method":"'); +if (methodStartIndex == -1) { + emit("no method in message"); + return; +} + +methodStartIndex += 10; // "method":"의 길이만큼 이동 +int methodEndIndex = message.indexOf('"', methodStartIndex); +if (methodEndIndex == -1) { + emit("no end of method"); + return; +} + +String method = message.substring(methodStartIndex, methodEndIndex); + +// "url":" 를 기준으로 url 값 추출 +int urlStartIndex = message.indexOf('"url":"'); +if (urlStartIndex == -1) { + emit("no url in message"); + return; +} + +urlStartIndex += 7; // "url":"의 길이만큼 이동 +int urlEndIndex = message.indexOf('"', urlStartIndex); +if (urlEndIndex == -1) { + emit("no end of url"); + return; +} + +String url = message.substring(urlStartIndex, urlEndIndex); + +// method와 url을 합쳐 request 생성 +String request = method + " " + url; +emit(request); diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/logstash/logstash.conf b/backend/resources/scripts/ubuntu@dailyroad.site/logstash/logstash.conf new file mode 100644 index 00000000..d16ffdd9 --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/logstash/logstash.conf @@ -0,0 +1,34 @@ +input { + beats { + port => 5044 + } + + tcp { + port => 50000 + codec => json_lines # JSON 형태의 데이터를 처리 + } +} + +filter { + json { + source => "message" + target => "parsed_message" + } + + mutate { + rename => { "[parsed_message][level]" => "log_level" } + rename => { "[parsed_message][msg]" => "log_message" } + rename => { "[parsed_message][time]" => "@timestamp" } + + remove_field => ["message", "parsed_message"] + } +} + +output { + elasticsearch { + index => "%{[@metadata][service_name]}-%{+YYYY.MM.dd}" + hosts => "elasticsearch:9200" + user => "logstash_internal" + password => "${LOGSTASH_INTERNAL_PASSWORD}" + } +} diff --git a/backend/resources/scripts/ubuntu@dailyroad.site/setup-elk.sh b/backend/resources/scripts/ubuntu@dailyroad.site/setup-elk.sh new file mode 100644 index 00000000..a635086c --- /dev/null +++ b/backend/resources/scripts/ubuntu@dailyroad.site/setup-elk.sh @@ -0,0 +1,110 @@ +#!/bin/bash + +# ELK 스택 설정 초기화 스크립트 + +# 환경 설정 +export ELASTIC_PASSWORD="example" # 변경 필요 +export LOGSTASH_PASSWORD="example" +export KIBANA_PASSWORD="example" +export ELASTIC_VERSION="8.15.3" + +export SERVICE_NAME="dailyroad" + +# 기본 경로 설정 +SETUP_DIR=$(dirname "$(readlink -f "$0")") +DOCKER_ELK_DIR="$SETUP_DIR/../docker-elk" # ELK 스택 클론 경로 + +LOGSTASH_CONFIG="$SETUP_DIR/logstash/logstash.conf" +TEMPLATE_FILE="$SETUP_DIR/elasticsearch/log_index_template.json" +KIBANA_SCRIPTS_DIR="$SETUP_DIR/kibana" + +# 필요 패키지 업데이트 및 설치 +echo "Updating packages..." +sudo apt update -y && sudo apt upgrade -y +echo "Installing Docker and Docker Compose..." +sudo apt install -y docker.io docker-compose + +# Docker와 Docker Compose 설치 확인 +if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then + echo "Docker or Docker Compose installation failed. Please check your setup." + exit 1 +fi + +# Docker-ELK 클론 또는 디렉토리로 이동 +if [ ! -d "$DOCKER_ELK_DIR" ]; then + echo "Cloning ELK stack repository to $DOCKER_ELK_DIR..." + git clone https://github.com/deviantony/docker-elk.git "$DOCKER_ELK_DIR" +fi + +cd "$DOCKER_ELK_DIR" || exit + +# .env 파일에서 버전 및 비밀번호 설정 +echo "Setting up environment file..." +sed -i "s/^ELASTIC_VERSION=.*/ELASTIC_VERSION=${ELASTIC_VERSION}/" .env +sed -i "s/^ELASTIC_PASSWORD=.*/ELASTIC_PASSWORD=${ELASTIC_PASSWORD}/" .env +sed -i "s/^LOGSTASH_PASSWORD=.*/LOGSTASH_PASSWORD=${LOGSTASH_PASSWORD}/" .env +sed -i "s/^KIBANA_PASSWORD=.*/KIBANA_PASSWORD=${KIBANA_PASSWORD}/" .env +sed -i "s/^SERVICE_NAME=.*/SERVICE_NAME=${SERVICE_NAME}/" .env +sed -i "s/'//g" .env # 작은 따옴표 제거 + +# Logstash 구성 복사 +if [ -f "$LOGSTASH_CONFIG" ]; then + echo "Copying Logstash configuration..." + cp "$LOGSTASH_CONFIG" logstash/pipeline/logstash.conf +else + echo "Logstash configuration file not found: $LOGSTASH_CONFIG" + exit 1 +fi + +# Elasticsearch 인덱스 템플릿 등록 +if [ -f "$TEMPLATE_FILE" ]; then + echo "Registering Elasticsearch index template..." + curl -u elastic:"${ELASTIC_PASSWORD}" -X PUT "http://localhost:9200/_index_template/${SERVICE_NAME}-template" \ + -H "Content-Type: application/json" \ + -d @"$TEMPLATE_FILE" +else + echo "Index template file not found: $TEMPLATE_FILE" + exit 1 +fi + +# Kibana Painless 스크립트 설정 +if [ -d "$KIBANA_SCRIPTS_DIR" ]; then + for SCRIPT in "$KIBANA_SCRIPTS_DIR"/*.painless; do + SCRIPT_NAME=$(basename "$SCRIPT" .painless) + echo "Adding Kibana Painless script: $SCRIPT_NAME" + curl -u elastic:"${ELASTIC_PASSWORD}" -X POST "http://localhost:9200/_scripts/$SCRIPT_NAME" \ + -H "Content-Type: application/json" \ + -d @"$SCRIPT" + done +else + echo "Kibana scripts directory not found: $KIBANA_SCRIPTS_DIR" +fi + +# 초기 사용자 및 권한 설정 +echo "Setting up initial users and permissions..." +docker-compose up setup + +# ELK 스택 시작 +echo "Starting ELK stack..." +docker-compose up -d + +# Kibana 초기화 대기 +echo "Waiting for Kibana to initialize..." +sleep 60 + +# Logstash 사용자에게 writer 권한 부여 +echo "Assigning writer role to Logstash user..." +curl -u elastic:"${ELASTIC_PASSWORD}" -X PUT "http://localhost:9200/_security/role/logstash_writer" -H "Content-Type: application/json" -d "{ + \"cluster\": [\"manage_index_templates\", \"monitor\", \"manage_ilm\"], + \"indices\": [ + { + \"names\": [\"${SERVICE_NAME}-*\"], + \"privileges\": [\"write\", \"create_index\"] + } + ] +}" + +# ELK 스택 상태 확인 +docker-compose ps + +echo "ELK stack setup completed. Access Kibana at http://localhost:5601 with the default user credentials." From 9c257eba37dd35a21cdb6d9889e24bc34f255230 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 15:51:53 +0900 Subject: [PATCH 22/27] =?UTF-8?q?refactor:=20es=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {backend/resources => docs}/elasticsearch/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {backend/resources => docs}/elasticsearch/README.md (100%) diff --git a/backend/resources/elasticsearch/README.md b/docs/elasticsearch/README.md similarity index 100% rename from backend/resources/elasticsearch/README.md rename to docs/elasticsearch/README.md From 0ff0ce8201a12171bdb4bb88ee7ea137fbf692ff Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 15:53:55 +0900 Subject: [PATCH 23/27] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A1=9C=EB=A5=BC?= =?UTF-8?q?=20=EB=82=98=ED=83=80=EB=82=B4=EB=8A=94=20lon=EC=9D=84=20long?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/search/query/ElasticSearchQuery.ts | 6 +++--- backend/src/search/search.controller.ts | 4 ++-- backend/src/search/search.service.ts | 6 +++--- backend/src/search/search.type.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index 420c2664..a2d8727f 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -9,12 +9,12 @@ export class ElasticSearchQuery { async searchPlace( query: string, lat?: number, - lon?: number, + long?: number, page: number = 1, size: number = 10, ) { const tokens = await this.tokenizeQuery(query); - const location = !isNaN(lat) && !isNaN(lon) ? { lat, lon } : null; + const location = !isNaN(lat) && !isNaN(long) ? { lat, long: long } : null; const from = (page - 1) * size; return await this.esService.search({ index: ElasticSearchConfig.PLACE_INDEX, @@ -79,7 +79,7 @@ export class ElasticSearchQuery { { gauss: { location: { - origin: `${location.lat},${location.lon}`, + origin: `${location.lat},${location.long}`, scale: '10km', offset: '2km', decay: 0.5, diff --git a/backend/src/search/search.controller.ts b/backend/src/search/search.controller.ts index 7c3c8da7..cd5dc660 100644 --- a/backend/src/search/search.controller.ts +++ b/backend/src/search/search.controller.ts @@ -10,10 +10,10 @@ export class SearchController { async search( @Query('query') query: string, @Query('lat') lat?: number, - @Query('lon') lon?: number, + @Query('long') long?: number, @Query('page', new ParseOptionalNumberPipe(1)) page?: number, @Query('limit', new ParseOptionalNumberPipe(10)) limit?: number, ) { - return await this.searchService.search(query, lat, lon, page, limit); + return await this.searchService.search(query, lat, long, page, limit); } } diff --git a/backend/src/search/search.service.ts b/backend/src/search/search.service.ts index 375cdbb0..1837afd3 100644 --- a/backend/src/search/search.service.ts +++ b/backend/src/search/search.service.ts @@ -10,14 +10,14 @@ export class SearchService { async search( query: string, lat?: number, - lon?: number, + long?: number, page: number = 1, size: number = 10, ) { const searched = await this.elasticSearchQuery.searchPlace( query, lat, - lon, + long, page, size, ); @@ -36,7 +36,7 @@ export class SearchService { _source.location ? { latitude: _source.location.lat, - longitude: _source.location.lon, + longitude: _source.location.long, } : null, _source.googlePlaceId, diff --git a/backend/src/search/search.type.ts b/backend/src/search/search.type.ts index 3ca2db5b..a4be0802 100644 --- a/backend/src/search/search.type.ts +++ b/backend/src/search/search.type.ts @@ -7,7 +7,7 @@ export type PlaceSearchHit = { name: string; location: { lat: number; - lon: number; + long: number; }; googlePlaceId: string; category: string | null; From 2bcc48ddcd77699b4c0db065ac0443f533e51f42 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 16:51:02 +0900 Subject: [PATCH 24/27] =?UTF-8?q?refactor:=20es=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20match=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EB=B6=84=EB=A6=AC=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/search/query/ElasticSearchQuery.ts | 32 +++++-------------- .../builder/ElasticSearchQueryBuilder.ts | 5 +++ .../src/search/query/builder/MatchBuilder.ts | 18 +++++++++++ 3 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 backend/src/search/query/builder/ElasticSearchQueryBuilder.ts create mode 100644 backend/src/search/query/builder/MatchBuilder.ts diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index a2d8727f..9642d933 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -1,6 +1,7 @@ import { ElasticsearchService } from '@nestjs/elasticsearch'; import { ElasticSearchConfig } from '@src/config/ElasticSearchConfig'; import { Injectable } from '@nestjs/common'; +import { ElasticSearchQueryBuilder } from '@src/search/query/builder/ElasticSearchQueryBuilder'; @Injectable() export class ElasticSearchQuery { @@ -26,32 +27,15 @@ export class ElasticSearchQuery { bool: { should: [ // 완전 일치 - { - match: { - name: { - query: query, - fuzziness: 1, - }, - }, - }, + ElasticSearchQueryBuilder.MATCH_NAME(query), // name에 대한 토큰 매칭 - ...tokens.map((token) => ({ - match: { - name: { - query: token, - fuzziness: 1, - }, - }, - })), + ...tokens.map((token) => + ElasticSearchQueryBuilder.MATCH_NAME(token), + ), // formattedAddress에 대한 토큰 매칭 - ...tokens.map((token) => ({ - match: { - formattedAddress: { - query: token, - fuzziness: 1, - }, - }, - })), + ...tokens.map((token) => + ElasticSearchQueryBuilder.MATCH_FORMATTED_ADDRESS(token), + ), ], }, }, diff --git a/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts new file mode 100644 index 00000000..3ed4ab1f --- /dev/null +++ b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts @@ -0,0 +1,5 @@ +import { MatchBuilders } from '@src/search/query/builder/MatchBuilder'; + +export const ElasticSearchQueryBuilder = { + ...MatchBuilders, +}; diff --git a/backend/src/search/query/builder/MatchBuilder.ts b/backend/src/search/query/builder/MatchBuilder.ts new file mode 100644 index 00000000..995b41c9 --- /dev/null +++ b/backend/src/search/query/builder/MatchBuilder.ts @@ -0,0 +1,18 @@ +const createMatchQuery = ( + field: string, + query: string, + fuzziness: number = 1, +) => ({ + match: { + [field]: { + query, + fuzziness, + }, + }, +}); + +export const MatchBuilders = { + MATCH_NAME: (query: string) => createMatchQuery('name', query), + MATCH_FORMATTED_ADDRESS: (query: string) => + createMatchQuery('formattedAddress', query), +}; From 605ade44c12d4b1bf26b578ef1b5545d5146edf4 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 20:23:59 +0900 Subject: [PATCH 25/27] =?UTF-8?q?refactor:=20es=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20gauss=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=B6=84=EB=A6=AC=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/search/query/ElasticSearchQuery.ts | 15 ++++---------- .../builder/ElasticSearchQueryBuilder.ts | 2 ++ .../search/query/builder/LocationBuilder.ts | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 backend/src/search/query/builder/LocationBuilder.ts diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index 9642d933..655d195c 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -60,17 +60,10 @@ export class ElasticSearchQuery { // 위치 정보 가중치 ...(location ? [ - { - gauss: { - location: { - origin: `${location.lat},${location.long}`, - scale: '10km', - offset: '2km', - decay: 0.5, - }, - }, - weight: 20, - }, + ElasticSearchQueryBuilder.GAUSS_LOCATION( + location.lat, + location.long, + ), ] : []), ], diff --git a/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts index 3ed4ab1f..88e63be0 100644 --- a/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts +++ b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts @@ -1,5 +1,7 @@ import { MatchBuilders } from '@src/search/query/builder/MatchBuilder'; +import { LocationQueryBuilder } from '@src/search/query/builder/LocationBuilder'; export const ElasticSearchQueryBuilder = { ...MatchBuilders, + ...LocationQueryBuilder, }; diff --git a/backend/src/search/query/builder/LocationBuilder.ts b/backend/src/search/query/builder/LocationBuilder.ts new file mode 100644 index 00000000..916bddf5 --- /dev/null +++ b/backend/src/search/query/builder/LocationBuilder.ts @@ -0,0 +1,20 @@ +export const LocationQueryBuilder = { + GAUSS_LOCATION: ( + lat: number, + long: number, + scale = '10km', + offset = '2km', + decay = 0.5, + weight = 20, + ) => ({ + gauss: { + location: { + origin: `${lat},${long}`, + scale: scale, + offset: offset, + decay: decay, + }, + }, + weight: weight, + }), +}; From 1de007eb00718423daa2b9296549ba3323ddf941 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 20:32:22 +0900 Subject: [PATCH 26/27] =?UTF-8?q?refactor:=20es=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20filter=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=B6=84=EB=A6=AC=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../query/builder/FilterQueryBuilder.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 backend/src/search/query/builder/FilterQueryBuilder.ts diff --git a/backend/src/search/query/builder/FilterQueryBuilder.ts b/backend/src/search/query/builder/FilterQueryBuilder.ts new file mode 100644 index 00000000..0fb2d257 --- /dev/null +++ b/backend/src/search/query/builder/FilterQueryBuilder.ts @@ -0,0 +1,26 @@ +const createTermFilter = (field: string, value: string, weight: number) => ({ + filter: { + term: { + [field]: value, + }, + }, + weight, +}); + +const createMatchFilter = (field: string, value: string, weight: number) => ({ + filter: { + match: { + [field]: value, + }, + }, + weight, +}); + +export const FilterBuilders = { + FILTER_TERM_NAME_KEYWORD: (query: string, weight: number = 20) => + createTermFilter('name.keyword', query, weight), + FILTER_MATCH_NAME: (token: string, weight: number = 15) => + createMatchFilter('name', token, weight), + FILTER_MATCH_FORMATTED_ADDRESS: (token: string, weight: number = 10) => + createMatchFilter('formattedAddress', token, weight), +}; From 10b536c172a836fdba8a4e7f8e4268dfbaac1b40 Mon Sep 17 00:00:00 2001 From: koomchang Date: Wed, 20 Nov 2024 20:32:55 +0900 Subject: [PATCH 27/27] =?UTF-8?q?refactor:=20es=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20prefix=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=B6=84=EB=A6=AC=20#138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/search/query/ElasticSearchQuery.ts | 35 +++++-------------- .../builder/ElasticSearchQueryBuilder.ts | 8 +++-- .../search/query/builder/LocationBuilder.ts | 2 +- .../src/search/query/builder/PrefixBuilder.ts | 13 +++++++ 4 files changed, 29 insertions(+), 29 deletions(-) create mode 100644 backend/src/search/query/builder/PrefixBuilder.ts diff --git a/backend/src/search/query/ElasticSearchQuery.ts b/backend/src/search/query/ElasticSearchQuery.ts index 655d195c..94191c20 100644 --- a/backend/src/search/query/ElasticSearchQuery.ts +++ b/backend/src/search/query/ElasticSearchQuery.ts @@ -43,20 +43,15 @@ export class ElasticSearchQuery { score_mode: 'sum', functions: [ // 완전 일치에 가중치 조정 - { - filter: { term: { 'name.keyword': query } }, - weight: 20, - }, + ElasticSearchQueryBuilder.FILTER_TERM_NAME_KEYWORD(query), // name의 토큰 매칭 가중치 - ...tokens.map((token) => ({ - filter: { match: { name: token } }, - weight: 15, - })), + ...tokens.map((token) => + ElasticSearchQueryBuilder.FILTER_MATCH_NAME(token), + ), // formattedAddress의 토큰 매칭 가중치 - ...tokens.map((token) => ({ - filter: { match: { formattedAddress: token } }, - weight: 10, - })), + ...tokens.map((token) => + ElasticSearchQueryBuilder.FILTER_MATCH_FORMATTED_ADDRESS(token), + ), // 위치 정보 가중치 ...(location ? [ @@ -78,20 +73,8 @@ export class ElasticSearchQuery { query: { bool: { should: [ - { - prefix: { - name: { - value: query, - }, - }, - }, - { - prefix: { - formattedAddress: { - value: query, - }, - }, - }, + ElasticSearchQueryBuilder.PREFIX_NAME(query), + ElasticSearchQueryBuilder.PREFIX_FORMATTED_ADDRESS(query), ], }, }, diff --git a/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts index 88e63be0..bff42736 100644 --- a/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts +++ b/backend/src/search/query/builder/ElasticSearchQueryBuilder.ts @@ -1,7 +1,11 @@ import { MatchBuilders } from '@src/search/query/builder/MatchBuilder'; -import { LocationQueryBuilder } from '@src/search/query/builder/LocationBuilder'; +import { LocationQueryBuilders } from '@src/search/query/builder/LocationBuilder'; +import { PrefixBuilders } from '@src/search/query/builder/PrefixBuilder'; +import { FilterBuilders } from '@src/search/query/builder/FilterQueryBuilder'; export const ElasticSearchQueryBuilder = { ...MatchBuilders, - ...LocationQueryBuilder, + ...LocationQueryBuilders, + ...PrefixBuilders, + ...FilterBuilders, }; diff --git a/backend/src/search/query/builder/LocationBuilder.ts b/backend/src/search/query/builder/LocationBuilder.ts index 916bddf5..cdb77ef3 100644 --- a/backend/src/search/query/builder/LocationBuilder.ts +++ b/backend/src/search/query/builder/LocationBuilder.ts @@ -1,4 +1,4 @@ -export const LocationQueryBuilder = { +export const LocationQueryBuilders = { GAUSS_LOCATION: ( lat: number, long: number, diff --git a/backend/src/search/query/builder/PrefixBuilder.ts b/backend/src/search/query/builder/PrefixBuilder.ts new file mode 100644 index 00000000..18ccc1fa --- /dev/null +++ b/backend/src/search/query/builder/PrefixBuilder.ts @@ -0,0 +1,13 @@ +const createPrefixQuery = (field: string, value: string) => ({ + prefix: { + [field]: { + value, + }, + }, +}); + +export const PrefixBuilders = { + PREFIX_NAME: (query: string) => createPrefixQuery('name', query), + PREFIX_FORMATTED_ADDRESS: (query: string) => + createPrefixQuery('formattedAddress', query), +};