https://prototype.p-picker.com/
์ด ์๋น์ค๋ ํ
์คํธ ์
๋ ฅ์ ํตํด ์ํ๋ ์คํ์ผ์ ์ท์ ์ ์ํ๋ Vibe Searching Prototype์
๋๋ค.
Fastapi + React + Postrgresql ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํ๋์ด ์์ผ๋ฉฐ, ์ ์ ๋ก๊ทธ์ธ ๋ฑ์ ์ธํฐ๋์
์์ด ๊ธฐ๋ฅ์ ์ ์ธํ ํ๋กํ ํ์
์
๋๋ค.
ํ์ฌ๋ top - 3 related items๋ฅผ ์ ์ํฉ๋๋ค.
ํ
์คํธ ์
๋ ฅ์ฐฝ์ ์ํ๋ ์คํ์ผ์ ๊ฐ๋จํ ์์ฑํฉ๋๋ค.

์ ๋ ฅ: "์คํธ๋ผ์ดํ ํจํด์ ๋ฏธ๋๋ฉํ ๋ฌด๋์ ์ ์ธ "
์์ ๊ฐ์ด Top-3 ๊ด๋ จ ์ํ์ด ์นด๋ ํํ๋ก ํ์๋ฉ๋๋ค.
ํด๋ฆญ ์ ์์ธ ์ ๋ณด(์ด๋ฏธ์ง, ๊ฐ๊ฒฉ, ์ค๋ช , ๊ตฌ๋งค ๋งํฌ ๋ฑ)๋ฅผ ์ ๊ณตํฉ๋๋ค.
์์ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ๋ก ๋ณํํ๊ณ , ์นดํ ๊ณ ๋ฆฌ ํํฐ๋ง๊ณผ ๋ฒกํฐ ์ ์ฌ๋ ๊ฒ์์ ๊ฒฐํฉํ ํ์ด๋ธ๋ฆฌ๋ ๊ฒ์ ์์คํ ์ ๋๋ค.
์
๋ ฅ: "๋์ฟ ๋นํฐ์ง ์ํฌ์จ์ด ์์ผ"
# ๋ ํจ์๋ฅผ ๋์์ ์คํํ์ฌ ์๋ต์๊ฐ ๋จ์ถ
query_json, category = await asyncio.gather(
parse_fashion_query(q), # ๊ตฌ์กฐํ๋ JSON ์์ฑ
query_categorizer(q) # ์นดํ
๊ณ ๋ฆฌ ๋ถ๋ฅ
)์
๋ ฅ: "๋์ฟ ๋นํฐ์ง ์ํฌ์จ์ด ์์ผ"
์ถ๋ ฅ:
{
"์์ฌ": null,
"์ฅ๋ฅด": "์ํฌ์จ์ด",
"์นดํ
๊ณ ๋ฆฌ": "outer",
"์ค๋ฃจ์ฃ": null,
"์์ ๋ฐ ํจํด": null,
"์ธ๋ถ ์นดํ
๊ณ ๋ฆฌ": "์์ผ",
"๋ถ์๊ธฐ ๋ฐ ์งํฅ์ ": "๋์ฟ ๋นํฐ์ง ์ ๋๋"
}์
๋ ฅ: "๋์ฟ ๋นํฐ์ง ์ํฌ์จ์ด ์์ผ"
์ถ๋ ฅ: "jacket"
q_emb = embedder.embed(query_json) # ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฒกํฐ๋ก ๋ณํSELECT path::text AS path, nlevel(path) AS depth
FROM category WHERE name = 'jacket'
-- ๊ฒฐ๊ณผ: path='outer.jacket', depth=2SELECT p.id, p.name, p.original_price AS price,
p.url AS link, p.brand, p.thumbnail_key,
p.category_path
FROM products AS p
WHERE p.category_path <@ 'outer.jacket'::ltree -- ์นดํ
๊ณ ๋ฆฌ + ํ์ ํํฐ
AND p.genre && ARRAY['์ํฌ์จ์ด'] -- ์ฅ๋ฅด ๋ฐฐ์ด ๊ฒน์นจ ํํฐ
ORDER BY p.embedding <#> query_embedding -- ๋ฒกํฐ ์ ์ฌ๋ ์ ๋ ฌ
LIMIT 40- Presigned URL ์์ฑ (S3 ์ด๋ฏธ์ง ๋งํฌ)
- ProductResponse ๊ฐ์ฒด ๋ณํ
- ์์ ๊ธฐ๋ฐ ํ์ต์ผ๋ก ์ผ๊ด๋ JSON ๊ตฌ์กฐ ์ถ๋ ฅ
- 11๊ฐ ํจ์ ์ฅ๋ฅด ๋ถ๋ฅ ์ง์
- ์นดํ ๊ณ ๋ฆฌ๋ณ ์ธ๋ถ ๋ถ๋ฅ (top/bottom/outer/accessory)
-- outer.jacket ๊ฒ์ ์
WHERE category_path <@ 'outer.jacket'::ltree
-- ํฌํจ๋๋ ํญ๋ชฉ๋ค:
-- โ
outer.jacket (์ ํํ ๋งค์นญ)
-- โ
outer.jacket.denim (ํ์)
-- โ
outer.jacket.bomber (ํ์)
-- โ outer.coat (๋ค๋ฅธ ์นดํ
๊ณ ๋ฆฌ)ORDER BY p.embedding <#> query_embedding- ๊ตฌ์กฐํ๋ ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์๋ฒ ๋ฉ์ผ๋ก ๋ณํ
- ์ํ ์๋ฒ ๋ฉ๊ณผ ์ ์ฌ๋ ๊ณ์ฐ
- ์๋ฏธ์ ์ ์ฌ์ฑ ๊ธฐ๋ฐ ์ ๋ ฌ
- ์นดํ ๊ณ ๋ฆฌ ํํฐ (๊ณ์ธต์ ): ๊ด๋ จ ์นดํ ๊ณ ๋ฆฌ + ํ์ ํญ๋ชฉ๋ค
- ์ฅ๋ฅด ํํฐ (๋ฐฐ์ด ๊ฒน์นจ): ์ฌ์ฉ์ ์๋์ ์ผ์นํ๋ ์คํ์ผ
- ๋ฒกํฐ ์ ์ฌ๋ (์ ๋ ฌ): ์๋ฏธ์ ๊ด๋ จ์ฑ ์์
- ์นดํ ๊ณ ๋ฆฌ "None": ์ ์ฒด ๊ฒ์์ผ๋ก ๋์ฒด
- API ์คํจ: ์๋ ์ฌ์๋ + ํค ๋กํ ์ด์
- ๋น ๊ฒฐ๊ณผ: ์ ์ง์ ํํฐ ์ํ
"๋ฏธ๋๋ฉํ ์ค๋ฒ์ฌ์ด์ฆ ๋ํธ"
-
ํ์ฑ ๊ฒฐ๊ณผ:
- ์ฅ๋ฅด: "๋ฏธ๋๋ฉ๋ฆฌ์ฆ"
- ์นดํ ๊ณ ๋ฆฌ: "top"
- ์ค๋ฃจ์ฃ: "์ค๋ฒ์ฌ์ด์ฆ"
- ์ธ๋ถ ์นดํ ๊ณ ๋ฆฌ: "๋ํธ"
-
์นดํ ๊ณ ๋ฆฌ ๋ถ๋ฅ:
"knit" -
๊ฒ์ ์กฐ๊ฑด:
WHERE category_path <@ 'top.knit'::ltree AND genre && ARRAY['๋ฏธ๋๋ฉ๋ฆฌ์ฆ'] ORDER BY embedding <#> query_embedding
-
๊ฒฐ๊ณผ: ๋ฏธ๋๋ฉ๋ฆฌ์ฆ ์คํ์ผ์ ๋ํธ ์ํ๋ค์ ์ ์ฌ๋ ์์ผ๋ก ๋ฐํ
- ์น ํ๋ ์์ํฌ: FastAPI (๋น๋๊ธฐ ์ฒ๋ฆฌ)
- AI ๋ชจ๋ธ: Google Gemini 2.5 Flash (์ฟผ๋ฆฌ ํ์ฑ/๋ถ๋ฅ/Embedding)
- ๋ฒกํฐ DB: PostgreSQL + pgvector
- ๊ณ์ธต ๊ด๋ฆฌ: ltree ํ์ฅ
- ์ด๋ฏธ์ง ์ ์ฅ: AWS S3
- ๋์์ฑ: asyncio.gather() (๋ณ๋ ฌ ์ฒ๋ฆฌ)
- ์ฟผ๋ฆฌ ํ์ฑ๊ณผ ์นดํ ๊ณ ๋ฆฌ ๋ถ๋ฅ๋ฅผ ๋์ ์คํ
- ์ ์ฒด ์๋ต ์๊ฐ ~50% ๋จ์ถ
- ์ฅ์ ๋ฐ์ ์ ์๋ failover
- ltree ์ธ๋ฑ์ค ํ์ฉ์ผ๋ก ๋น ๋ฅธ ์นดํ ๊ณ ๋ฆฌ ํํฐ๋ง
- ๋ฒกํฐ ๊ฒ์๊ณผ ๊ฒฐํฉํ์ฌ ์ ํ๋/์ฑ๋ฅ ๊ท ํ



