From f7485aab3287b878aad1376bb3165986afde8bec Mon Sep 17 00:00:00 2001 From: hbyang Date: Wed, 9 Sep 2026 16:00:00 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=A4=EC=B9=AD=EC=9D=84=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EB=B3=84=20=EB=8B=A4=EC=A4=91=20=EC=A7=88=EC=9D=98=20?= =?UTF-8?q?+=20=EA=B0=80=EC=A4=91=20RRF=20+=20=EC=82=AC=EC=8B=A4=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=ED=95=84=ED=84=B0=EB=A1=9C=20=EC=9E=AC?= =?UTF-8?q?=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로필을 통짜로 한 벡터에 넣으면 속성이 희석된다. 실측: 통짜는 점수 폭 0.0076, 속성별로 쪼개면 0.0624 (8배). - src/serving/match.rules.ts — 레인 빌더, 권역 판정, 수용 인원/시설 필터, 시설 통제 어휘 정규화 - src/serving/match.service.ts — 레인별 검색 → 가중 RRF 융합 → 필터 → 레인별 그룹(byLane) 출력. 근거(어느 레인 몇 위)를 함께 반환 - POST /v1/match 에 mode=fusion(기본) / single(기존 통짜, 비교용) - 데모 페이지: 모드 토글, 레인 카드, 융합/레인별/배제됨 탭 레인 설계에서 실측으로 고친 것 - 브랜드 레인 제거 — 상호는 사전에 없어 generic '군산 펜션 ~예약'만 끌어왔다 - 권역/인근 레인 병합 — '신흥동' 토큰이 겹쳐 위치 키워드가 상위를 쓸어갔다 - RRF 상수 60 → 20 — 60은 1위/40위 기여도 차이가 1.6배뿐이라 generic이 유리했다 사실 기반 필터는 벡터가 못 거르는 모순을 배제한다. 스테이머뭄(최대 4인, 원도심) 기준 61건 배제. 미확인 시설은 배제하지 않고 '보류'로 표시한다 — 없음이 아니라 모름이므로. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 56 +++++++- public/demo.html | 172 ++++++++++++++++++----- src/serving/match.rules.ts | 164 ++++++++++++++++++++++ src/serving/match.service.ts | 225 ++++++++++++++++++++++++++++++ src/serving/serving.controller.ts | 19 ++- src/serving/serving.module.ts | 3 +- src/serving/serving.service.ts | 1 + 7 files changed, 597 insertions(+), 43 deletions(-) create mode 100644 src/serving/match.rules.ts create mode 100644 src/serving/match.service.ts diff --git a/README.md b/README.md index 44be926..00a1ccd 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ OPENAI_EMBEDDING_MODEL=text-embedding-3-small | `GET` | `/v1/merchants` `/v1/merchants/:id` | 조회 | | `GET` | `/v1/sites/:id/seo?limit=20` | **발행 사이트가 렌더링 시 호출** — title/description/keywords/tags | | `GET` | `/v1/sites/:id/aeo?limit=10` | 답변엔진용 topics/FAQ/structuredDataHints | -| `POST` | `/v1/match` | **업체명 또는 문장 → 사전에서 잘 맞는 키워드** (데모 콘솔이 쓰는 API) | +| `POST` | `/v1/match` | **업체명 또는 문장 → 사전에서 잘 맞는 키워드.** `mode=fusion`(기본) / `single`(통짜, 비교용) | | `POST` | `/v1/keywords/search` | 의미 기반 키워드 검색 (어드민) | | `GET` | `/demo` | 매칭 콘솔 (로컬 확인용) | | `POST` | `/v1/sites/:id/performance` | Search Console·유입 로그 피드백 → 저성과 키워드 강등 | @@ -174,6 +174,60 @@ curl 'http://localhost:3100/v1/sites/site-1001/seo?limit=8' } ``` +### 매칭 — 속성별 다중 질의 + 사실 기반 필터 + +프로필을 통짜로 한 벡터에 넣으면 속성이 희석된다. 실측: + +| 방식 | 점수 범위 | 폭 | +|---|---|---| +| 통짜 질의문 하나 | 0.8761 ~ 0.8837 | 0.0076 | +| 속성별로 쪼갠 질의 | 0.8552 ~ 0.9176 | **0.0624** | + +976건이 전부 0.87 언저리에 뭉쳐 순위는 매기지만 변별하지 못하는 상태였다. +그래서 프로필을 레인으로 쪼개 각각 임베딩하고 가중 RRF 로 융합한다. + +| 레인 | 가중치 | 질의문 예시 | +|---|---|---| +| 유형 | 1.0 | `군산 펜션 독채 감성숙소` | +| 위치 | 0.7 | `원도심 신흥동 말랭이마을 동국사 근처` | +| 동반자 | 0.6 | `커플 친구 가족 혼자` | +| 시설 | 0.6 | `프라이빗` | + +레인 설계에서 실측으로 배운 것 세 가지. + +- **브랜드 레인을 두면 안 된다.** 상호는 사전에 없으므로 결국 `군산 펜션` 만 남아 + 가장 generic 한 것들을 끌어온다. 넣었더니 상위 6개가 전부 `~예약` 으로 도배됐다. +- **레인끼리 겹치면 안 된다.** 권역과 인근을 따로 두었더니 `신흥동` 토큰이 양쪽에 걸려 + 위치 키워드가 상위를 쓸어갔고, 정작 핵심인 `군산 펜션 독채` 가 8위로 밀렸다. 한 레인으로 합쳤다. +- **RRF 상수는 관례값 60 이 아니라 20.** 60 이면 1위와 40위의 기여도 차이가 1.6배뿐이라 + 깊은 순위의 generic 키워드가 여러 레인에서 조금씩 쌓아 올라온다. 20 이면 2.9배로 벌어진다. + +#### 사실 기반 필터 — 벡터가 못 거르는 것 + +임베딩은 "비슷함"만 알지 "최대 4인 < 단체"를 모른다. 그래서 코드 조건으로 배제한다. + +| 규칙 | 예시 | +|---|---| +| 수용 인원 | 최대 4인 → `군산 단체 독채펜션`, `군산 독채 세미나실 펜션` 배제 | +| 권역 불일치 | 원도심 업체 → `선유도`·`오션뷰` 계열 배제 | +| 미보유 시설 | `수영장` 없음 → `군산 독채 온수풀 펜션` 배제 | +| **미확인 시설** | `바베큐` 가 `unverified` → 배제하지 않고 **보류** 표시 | + +마지막 항목이 중요하다. 사업자가 확인해주지 않은 항목은 "없음"이 아니라 "모름"이다. +스테이머뭄 기준 61건이 배제됐고, 배제 사유는 응답의 `excluded` 로 함께 내려준다. + +#### 레인별 출력 = SEO 페이지 배분 + +응답의 `byLane` 은 레인별 상위 8건이다. 평평한 순위보다 이쪽이 실무에 쓰인다 — +한 페이지의 주력 키워드는 1개여야 하므로, **레인 1위가 그 페이지의 주력**이 된다. + +| 레인 | → 페이지 | 주력 | +|---|---|---| +| 유형 | 메인 | 군산 펜션 독채 | +| 위치 | 주변 여행 | 신흥동 일본식가옥 근처 숙소 | +| 동반자 | 객실 | 군산 커플 프라이빗 펜션 | +| 시설 | 시설 | 군산 프라이빗 펜션 | + ### 매칭 예시 ```bash diff --git a/public/demo.html b/public/demo.html index 3d6bb30..cff0648 100644 --- a/public/demo.html +++ b/public/demo.html @@ -85,6 +85,31 @@ .err{background:var(--surface);border:1px solid var(--stop);color:var(--stop); border-radius:6px;padding:14px 16px;font-size:13.5px;margin-bottom:16px} .tblwrap{overflow-x:auto} + .modes{display:flex;gap:0;border:1px solid var(--line);border-radius:5px;overflow:hidden} + .modes button{border:0;border-radius:0;background:var(--surface);color:var(--muted);font-size:13px;padding:12px 16px;font-weight:500} + .modes button[aria-pressed=true]{background:var(--accent);color:#fff} + .lanes{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin-bottom:16px} + .lane{background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:11px 13px} + .lane b{font-size:12.5px} + .lane .w{font-family:var(--mono);font-size:10.5px;color:var(--accent);margin-left:5px} + .lane .q{font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:5px;line-height:1.55;word-break:break-all} + .tabs{display:flex;gap:6px;margin-bottom:12px} + .tabs button{font-size:12.5px;padding:6px 13px;border-radius:20px;border:1px solid var(--line); + background:var(--surface);color:var(--muted);font-weight:400} + .tabs button[aria-pressed=true]{background:var(--accent);color:#fff;border-color:var(--accent)} + .prov{display:inline-flex;gap:4px;flex-wrap:wrap} + .prov span{font-family:var(--mono);font-size:10px;background:var(--surface2);color:var(--muted); + padding:1px 5px;border-radius:3px} + .hold{font-size:10.5px;font-family:var(--mono);color:var(--warn);background:var(--warn-bg); + padding:1px 6px;border-radius:3px;white-space:nowrap} + .facts{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px} + .facts span{font-family:var(--mono);font-size:11px;background:var(--surface2);color:var(--ink-soft); + padding:3px 8px;border-radius:3px} + .lanegroup{margin-bottom:20px} + .lanegroup h3{margin:0 0 4px;font-size:13.5px} + .lanegroup .q{font-family:var(--mono);font-size:11px;color:var(--muted);margin:0 0 8px} + .exrow td{color:var(--muted)} + .exwhy{font-family:var(--mono);font-size:11px;color:var(--stop)} @@ -98,6 +123,10 @@
+
+ + +