프로젝트를 한 문장으로
실제 판매 중인 상품이 있는데 AI는 “상품이 없다”고 답했습니다.
AI에게 직접 상품 URL이나 상품 코드를 주면 AI는 제대로 된 페이지 내용을 전달하기 때문에, 단순 크롤링 실패보다 일반 사용자 질문을 특정 상품·출발 Event에 연결하는 Retrieval 문제를 의심합니다.
1. 문제 발견 — AI False Negative
같은 상품을 질문 방식만 바꿔 확인했습니다. 일반 자연어에서는 실패했지만, 상품 식별 정보를 강하게 줄수록 실제 페이지 정보를 찾을 수 있었습니다.
| 테스트 | 질문 | 관찰 결과 | 해석 |
|---|---|---|---|
| T1 | 2027년 1월 15일 출발, 2박 3일 오사카 패키지 중 1인 기본 상품가 120만 원 이하이며 현재 예약 가능한 상품을 추천해줘. | 상품 없음 / 미오픈으로 판단 | 실패 |
| T2 | 2027년 1월 15일 출발해서 1월 17일 돌아오는 2박 3일 오사카 패키지여행 중, 노랑풍선에서 판매하는 상품을 찾아줘. 1인 기본 상품가 120만 원 이하이고 현재 예약 가능한 상품이 있다면 최대 3개 추천해줘. | 도메인을 좁혀도 실패 | 실패 |
| T3 | 상품코드 JOP1193인 오사카 패키지여행 상품을 찾아줘. 실제 존재 여부와 상품명·출발일·가격·예약 가능 여부를 알려줘. | 가격·날짜·예약 정보 확인 | 성공 |
| T4 | 이 상품 페이지의 상품명·출발일·가격·예약 가능 여부를 확인해줘. https://tourexpress.ybtour.co.kr/product/detailPackage?menu=PKG&dspSid=AAEC000&goodsCd=JOP1193&evCd=JOP1310-270115ZE01 | 페이지 정보 추출 가능 | 성공 |
“페이지를 못 읽는다”보다 Query → 정확한 상품/Event 매핑이 불안정하다는 쪽으로 문제 범위를 좁혔습니다.
2. 원인 좁히기
같은 상품군에도 출발일이 다른 여러 판매 Event가 존재합니다. 그래서 “상품을 찾았는가?”보다 한 단계 더 나아가 사용자가 말한 날짜와 정확히 같은 출발 Event를 찾는가를 확인했습니다.
Exact
질문한 날짜와 정확히 같은 Event가 직접 노출
13개 실제 판매일 중 정확한 출발 Event가 노출된 경우 1개
Wrong-date
같은 상품군이지만 다른 출발일 Event가 노출
13개 실제 판매일 중 다른 날짜의 Event가 노출된 경우 1개
Indirect / Miss
상품군·목록만 노출되거나 해당 Event를 찾지 못함
13개 실제 판매일 중 간접 노출 5개, 해당 Event 미노출 6개
관찰: JOP1190 상품군의 실제 판매일 13개를 탐색했을 때 Exact 1, Wrong-date 1, Indirect 5, Miss 6이 섞였습니다.
대표 Event 페이지에는 정확한 출발일 정보가 내부 데이터에 존재했지만, title·meta description·og:title 등 문서 수준의 식별 신호는 여러 Event 사이에서 유사했습니다. 그래서 날짜가 특정 Event를 구분하는 신호로 충분히 작동하는지를 통제 실험으로 분리했습니다.
3. 통제 실험 — V0 → V1 → V2
실제 Google 알고리즘을 재현한 실험이 아닙니다. 동일한 문서 조건에서 “날짜 신호를 텍스트로 추가하는 것”과 “날짜를 별도 속성으로 분리해 정확 일치시키는 것”의 차이를 보기 위해 로컬 검색 환경을 만들었습니다.
V0 / V1 — meta description의 날짜 신호만 변경
title, og:title, canonical, 본문, 내부 데이터는 동일하게 두고 meta description 맨 앞에 정확한 출발일을 넣었는지만 바꿨습니다.
<meta name="description"
content="[기존 상품 설명]">
V1 · 날짜 텍스트 추가
<meta name="description"
content="2026년 12월 9일 출발. [기존 상품 설명]">
로컬 검색은 이렇게 비교했습니다
# HTML에서 검색 신호 추출
fields = {
"title": title_text,
"meta": meta_description,
"body": body_text
}
# char n-gram TF-IDF + cosine similarity
# 가중치: title 3 / meta 2 / body 1
score = title_score * 3 + meta_score * 2 + body_score
이 검색기는 Google 복제품이 아니라 독립변수 하나가 lexical relevance에 어떤 차이를 만드는지 보기 위한 통제 장치였습니다.
| Query | V0 | V1 | 변화 |
|---|---|---|---|
| 오사카 패키지 | 0.1797 | 0.1791 | -0.3% |
| JOP1190 | 0.0178 | 0.0177 | -0.6% |
| 2026년 12월 9일 | 0.0883 | 0.1047 | +18.6% |
| JOP1190 + 날짜 | 0.1172 | 0.1312 | +11.9% |
| 오사카 + 날짜 + 출발 | 0.1943 | 0.2119 | +9.1% |
결과: 날짜가 없는 Query는 거의 변하지 않았고, 날짜가 포함된 Query에서만 관련도가 상승했습니다. 즉 “날짜 텍스트를 명시하면 날짜 Query와 더 비슷해진다”는 것은 확인했습니다.
4. 실패 케이스 — 날짜를 써도 정확한 Event는 틀릴 수 있다
단일 Event에서 관련도가 올라간 것만으로는 충분하지 않았습니다. 실제 상황처럼 출발일이 다른 4개 Event를 동시에 넣고 정확한 날짜의 Event가 1위가 되는지 다시 확인했습니다.
| Query 날짜 | V1 1위 | 판정 |
|---|---|---|
| 11/03 | 11/03 Event | Exact Hit |
| 11/07 | 11/29 Event | Wrong-date |
| 11/21 | 11/21 Event | Exact Hit |
| 11/29 | 11/29 Event | Exact Hit |
V1 Exact Event Resolution = 3/4 = 75%
11월 7일 Query에서도 V1 문서들의 점수는 전반적으로 올라갔지만, 정확한 11/07 Event가 아니라 11/29 Event가 1위였습니다. 날짜를 “문장 속 단어”로 넣는 것만으로는 정확한 일(day)이 필수 조건으로 작동하지 않았습니다.
5. V2 — 날짜를 Event의 속성으로 분리
V2에서는 날짜를 검색 문자열 안의 단어로만 다루지 않고, 각 여행 Event의 구조화된 출발일 속성으로 분리했습니다. HTML에는 schema.org의 TouristTrip과 departureTime을 사용하고, 로컬 Retrieval에서는 Query 날짜와 이 값을 정확히 비교했습니다.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TouristTrip",
"name": "오사카 패키지",
"departureTime": "2026-11-07"
}
</script>
query_date = normalize_date(query) # YYYY-MM-DD
event_date = document.departureTime
if query_date == event_date:
exact_match = True
priority = 1 # 먼저 후보로 선택
else:
exact_match = False
# exact date match 이후에 TF-IDF를 보조 순위로 사용
| Query 날짜 | 선택된 departureTime | 판정 |
|---|---|---|
| 2026-11-03 | 2026-11-03 | Exact Hit |
| 2026-11-07 | 2026-11-07 | Exact Hit |
| 2026-11-21 | 2026-11-21 | Exact Hit |
| 2026-11-29 | 2026-11-29 | Exact Hit |
V2 통제 후보군 Exact Event Resolution = 4/4 = 100%
하지만 “Schema를 넣었더니 검색 정확도가 100%가 됐다”는 뜻은 아닙니다. V2의 개선은 구조화된 departureTime을 읽고, Query 날짜와 정확히 일치하는 Event를 우선하도록 Retrieval 로직까지 직접 설계했기 때문에 발생했습니다. 실제 Google/AI Search 효과는 별도 실서비스 검증이 필요합니다.
6. 여기까지의 결론
False Negative→ 02
Retrieval 분리→ 03
Date-to-Event Gap→ 04
통제 실험
- 페이지에 정보가 존재하는 것과 일반 Query에서 해당 Event가 검색되는 것은 다른 문제였습니다.
- meta description의 정확한 날짜는 날짜 관련 Query의 lexical relevance를 높였지만, 다중 Event 환경에서 정확한 날짜 선택을 보장하지 못했습니다.
- 날짜를 Event의 별도 속성으로 읽고 필수 조건으로 먼저 비교하면, 통제 후보군에서는 정확한 Event Resolution이 가능했습니다.
- 따라서 개선 방향은 “더 많은 문장을 추가”하는 것보다 상품·Event·필수 조건의 관계를 명확하게 만들고 Retrieval에서 먼저 검증하는 것입니다.
7. 추가 실험 전 Baseline — 장소로 찾으면 JOP1193이 노출되는가?
날짜 실험 다음에는 상품 ↔ 관광지 관계를 따로 보기로 했습니다. 대상은 노랑풍선 오사카 상품군 JOP1193입니다. 실제 상세 페이지 본문에는 신사이바시와 도톤보리가 관광지로 존재하지만, 장소를 기준으로 AI에게 상품을 추천하게 했을 때 이 상품이 노출되는지는 별도 문제입니다.
Ground Truth로 확인한 JOP1193 상세 페이지 ↗
노출 판정 기준
- 각 Prompt는 새 대화에서 독립 실행
- 최대 3개 추천 결과 안에 JOP1193 상품군이 등장하면 노출 O
- 특정 출발일 Event가 아니라 상품군과 장소의 연결을 보는 실험이므로 evCd 일치 여부는 판정에서 제외
- P1~P4를 각각 5회, 총 20회 실행
| Prompt | 질문 조건 | 실행 | JOP1193 노출 |
|---|---|---|---|
| P1 | 도톤보리가 일정에 포함된 노랑풍선 오사카 패키지 | 5회 | 0회 |
| P2 | 신사이바시가 일정에 포함된 노랑풍선 오사카 패키지 | 5회 | 0회 |
| P3 | 신사이바시와 도톤보리가 모두 포함된 상품 | 5회 | 0회 |
| P4 | 신사이바시와 도톤보리를 관광하는 상품 | 5회 | 0회 |
Baseline Exposure Rate = 0 / 20 = 0%
현재 확정할 수 있는 사실은 여기까지입니다. 페이지에는 신사이바시·도톤보리 정보가 존재하지만, 설정한 4개의 장소 기반 추천 Prompt에서는 JOP1193이 20회 모두 노출되지 않았습니다.
아직 “Schema가 부족해서 0%였다”고 결론내리지 않습니다. 원인과 개선 효과는 다음 실험에서 분리해 검증합니다.
8. 1차 Place Schema 통제 실험 — 텍스트는 그대로, 관계만 구조화
장소 기반 AI 노출 Baseline이 0/20이었던 이유를 바로 “Schema 부족”이라고 결론내리지 않고, 먼저 보이는 본문은 완전히 같게 두고 JSON-LD만 추가했을 때 무엇이 달라지는지를 통제 환경에서 확인했습니다.
| 항목 | V0 | V1 |
|---|---|---|
| 보이는 HTML 본문 | 동일 | 동일 |
| 신사이바시·도톤보리 텍스트 | 있음 | 있음 |
| JSON-LD | 없음 | 추가 |
| 구조화 관계 | 없음 | TouristTrip → itinerary → Place |
V0 — 본문만 존재
<section>
<h2>관광지</h2>
<article>
<h3>신사이바시</h3>
<p>JOP1193 일정에 포함된 오사카의 관광지입니다.</p>
</article>
<article>
<h3>도톤보리</h3>
<p>JOP1193 일정에 포함된 오사카의 관광지입니다.</p>
</article>
</section>
V1 — 같은 본문 + 장소 관계 JSON-LD
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TouristTrip",
"name": "JOP1193 오사카 패키지",
"identifier": "JOP1193",
"itinerary": {
"@type": "ItemList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"item": {
"@type": "Place",
"name": "신사이바시"
}
},
{
"@type": "ListItem",
"position": 2,
"item": {
"@type": "Place",
"name": "도톤보리"
}
}
]
}
}
</script>
실험 A — 일반 텍스트 검색
<script>와 <style>을 제거한 뒤, P1~P4와 문서 본문을 char n-gram TF-IDF + cosine similarity로 비교했습니다. V0와 V1의 보이는 본문이 동일하기 때문에 4개 Prompt 모두 V0와 V1의 점수가 같았고 차이는 0이었습니다.
| Prompt | 조건 | V0 vs V1 | 결과 |
|---|---|---|---|
| P1 | 도톤보리 | 동일 | Δ 0 |
| P2 | 신사이바시 | 동일 | Δ 0 |
| P3 | 신사이바시 + 도톤보리 | 동일 | Δ 0 |
| P4 | 두 장소 관광 | 동일 | Δ 0 |
해석 1 · Schema 추가만으로 본문 lexical relevance가 올라가지는 않았습니다.
V1은 사용자에게 보이는 문장을 추가하지 않았기 때문에 텍스트 유사도 기준에서는 V0와 같은 문서입니다.
실험 B — 구조화 관계 파싱
다음에는 JSON-LD를 직접 파싱해 TouristTrip → itinerary → Place 관계가 명시적으로 추출되는지 확인했습니다.
| Prompt | 필요한 장소 관계 | V0 | V1 |
|---|---|---|---|
| P1 | 도톤보리 | 식별 X | 식별 O |
| P2 | 신사이바시 | 식별 X | 식별 O |
| P3 | 신사이바시 AND 도톤보리 | 식별 X | 식별 O |
| P4 | 신사이바시 AND 도톤보리 | 식별 X | 식별 O |
구조화 관계 식별: V0 0/4 → V1 4/4
이 결과는 “Schema를 넣으면 AI 노출이 100%가 된다”는 의미가 아닙니다. 같은 본문에서도 구조화 데이터를 읽는 시스템은 상품과 관광지의 관계를 별도 속성으로 명시적으로 식별할 수 있었다는 통제 실험 결과입니다.
9. 실험 — 실제 원본 구조를 보존해 다시 검증
1차 V0/V1 페이지는 관계를 통제하기 위해 본문에 “JOP1193 일정에 포함된 관광지”라는 설명을 명확하게 적었습니다. 따라서 실제 노랑풍선 원본보다 상품 ↔ 장소 관계가 본문에서도 더 쉽게 읽히는 조건입니다.
다음 단계에서는 실제 JOP1193 페이지의 HTML 구조와 문구를 최대한 보존한 V0를 만들고, V1에는 Place 관계 JSON-LD만 추가합니다. 다른 요소를 바꾸지 않은 상태에서 동일한 P1~P4로 다시 비교합니다.
원본 구조 복제→ 02
V0 고정→ 03
V1 Schema만 추가→ 04
동일 Query 재검증
비교 지표: lexical relevance · 구조화 관계 식별 · JOP1193 Exposure Rate · 추천 순위 · 출처 인용 여부
검증 범위와 한계
- 로컬 TF-IDF 실험은 Google 검색 알고리즘을 재현한 것이 아니라 원인 후보를 통제하기 위한 실험입니다.
- V2의 100%는 작은 4개 Event 후보군과 직접 설계한 exact date Retrieval에서의 결과입니다.
- 구조화 데이터 자체가 Google/AI Search 노출을 올렸다고 일반화하지 않습니다.
- 장소 Baseline 0/20 역시 사용한 모델·검색 환경·실행 시점에 한정된 관찰값입니다.
- 1차 Place Schema 실험의 0/4 → 4/4는 구조화 관계 파서의 식별 결과이며, 실제 AI 추천 노출률을 의미하지 않습니다.
- 1차 V0/V1 본문은 원본보다 상품과 장소의 관계를 더 명확하게 적은 통제용 페이지이므로, 실제 원본 구조를 보존한 재실험이 필요합니다.
- AI 추천 노출과 실제 클릭·예약 전환은 별도 지표로 검증해야 합니다.