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 @@